dsh-code-review-graph 0.0.0-stage → 0.1.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.
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: code-review-graph
3
+ name: dsh-code-review-graph
@@ -0,0 +1,215 @@
1
+ // src/backend.ts
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
+ import { spawn } from "node:child_process";
5
+
6
+ // src/safety.ts
7
+ import { realpath, stat, lstat, readdir } from "node:fs/promises";
8
+ import { isAbsolute, relative, resolve, sep, dirname, basename, join } from "node:path";
9
+ function contained(root, path) {
10
+ const part = relative(root, path);
11
+ return part === "" || !isAbsolute(part) && part !== ".." && !part.startsWith(`..${sep}`);
12
+ }
13
+ async function checkedPath(root, value, allowMissing = false) {
14
+ const candidate = resolve(root, value);
15
+ if (!contained(root, candidate)) throw new Error("Path is outside the authorized repository");
16
+ let current = candidate;
17
+ const suffix = [];
18
+ while (allowMissing && !await lstat(current).then(() => true, () => false)) {
19
+ suffix.unshift(basename(current));
20
+ current = dirname(current);
21
+ }
22
+ const canonical = join(await realpath(current), ...suffix);
23
+ if (!contained(root, canonical)) throw new Error("Symlink escapes the authorized repository");
24
+ return canonical;
25
+ }
26
+ function scrubEnv(env) {
27
+ return { ...Object.fromEntries(Object.entries(env).filter(([key, value]) => value !== void 0 && !/KEY|PASSWORD|SECRET|TOKEN|^(DSH|CRG|OPENAI|GOOGLE|GEMINI|MINIMAX|VOYAGE|HF)_/i.test(key))), HF_HUB_OFFLINE: "1", TRANSFORMERS_OFFLINE: "1", HF_HUB_DISABLE_TELEMETRY: "1" };
28
+ }
29
+ async function checkedData(root) {
30
+ const dir = resolve(root, ".code-review-graph");
31
+ if (!await lstat(dir).then(() => true, () => false)) return;
32
+ await checkedPath(root, dir);
33
+ const entries = await readdir(dir, { withFileTypes: true, recursive: true });
34
+ for (const entry of entries) {
35
+ if (entry.isSymbolicLink()) await checkedPath(root, join(entry.parentPath, entry.name));
36
+ }
37
+ }
38
+
39
+ // src/backend.ts
40
+ var environment = (engine) => ({ ...scrubEnv(process.env), ...engine.env });
41
+ function valueOf(result) {
42
+ const r = result;
43
+ const text = r.content?.filter((x) => x.type === "text").map((x) => x.text).join("\n") ?? "";
44
+ if (r.isError) throw new Error(text || "Engine tool failed");
45
+ const value = r.structuredContent ?? (() => {
46
+ try {
47
+ return JSON.parse(text);
48
+ } catch {
49
+ return { text };
50
+ }
51
+ })();
52
+ if (value.status === "error") throw new Error(String(value.error));
53
+ return value;
54
+ }
55
+ async function connect(engine, cwd) {
56
+ await checkedData(cwd);
57
+ const transport = new StdioClientTransport({ command: engine.command, args: [...engine.args, "serve", "--repo", cwd], cwd, env: environment(engine), stderr: "pipe" });
58
+ transport.stderr?.on("data", () => {
59
+ });
60
+ const client = new Client({ name: "dsh-code-review-graph", version: "0.1.1" });
61
+ try {
62
+ await client.connect(transport);
63
+ return client;
64
+ } catch (error) {
65
+ await transport.close();
66
+ throw new Error(`CRG engine unavailable. Run dsh-crg prepare explicitly. ${String(error)}`);
67
+ }
68
+ }
69
+ async function stop(child) {
70
+ if (!child || child.exitCode !== null || child.signalCode !== null) return;
71
+ await new Promise((resolve2) => {
72
+ const timer = setTimeout(() => {
73
+ child.kill("SIGKILL");
74
+ resolve2();
75
+ }, 3e3);
76
+ child.once("exit", () => {
77
+ clearTimeout(timer);
78
+ resolve2();
79
+ });
80
+ child.kill();
81
+ });
82
+ }
83
+ var BackendPool = class {
84
+ constructor(engine) {
85
+ this.engine = engine;
86
+ }
87
+ entries = /* @__PURE__ */ new Map();
88
+ retired = /* @__PURE__ */ new Set();
89
+ closed = false;
90
+ activate(owner) {
91
+ this.retired.delete(owner);
92
+ }
93
+ async acquire(root, owner) {
94
+ if (this.closed || this.retired.has(owner)) throw new Error("Session has been released");
95
+ let promise = this.entries.get(root);
96
+ if (!promise) {
97
+ promise = connect(this.engine, root).then((client) => ({ client, owners: /* @__PURE__ */ new Set(), queue: Promise.resolve() }));
98
+ this.entries.set(root, promise);
99
+ promise.catch(() => {
100
+ if (this.entries.get(root) === promise) this.entries.delete(root);
101
+ });
102
+ }
103
+ const entry = await promise;
104
+ if (this.closed || this.retired.has(owner)) throw new Error("Session has been released");
105
+ entry.owners.add(owner);
106
+ return entry;
107
+ }
108
+ async call(root, owner, name, args, signal) {
109
+ const entry = await this.acquire(root, owner);
110
+ const work = entry.queue.catch(() => {
111
+ }).then(async () => {
112
+ signal.throwIfAborted();
113
+ const cancel = () => {
114
+ this.entries.delete(root);
115
+ void stop(entry.watcher);
116
+ void entry.client.close();
117
+ };
118
+ signal.addEventListener("abort", cancel, { once: true });
119
+ let result;
120
+ try {
121
+ await checkedData(root);
122
+ if (entry.watchError) throw new Error(`Incremental watcher stopped: ${entry.watchError}`);
123
+ result = await entry.client.callTool({ name, arguments: args }, void 0, { signal, timeout: this.engine.timeoutMs });
124
+ } catch (error) {
125
+ this.entries.delete(root);
126
+ await stop(entry.watcher);
127
+ await entry.client.close();
128
+ throw error;
129
+ } finally {
130
+ signal.removeEventListener("abort", cancel);
131
+ }
132
+ valueOf(result);
133
+ return result;
134
+ });
135
+ entry.queue = work;
136
+ return work;
137
+ }
138
+ async ensure(root, owner, signal) {
139
+ const entry = await this.acquire(root, owner);
140
+ entry.ready ??= (async () => {
141
+ const update = valueOf(await this.call(root, owner, "build_or_update_graph_tool", { repo_root: root }, signal));
142
+ if (update.status === "partial" || Array.isArray(update.warnings) && update.warnings.length) entry.update = update;
143
+ signal.throwIfAborted();
144
+ const watcher = spawn(this.engine.command, [...this.engine.args, "watch", "--repo", root], { cwd: root, env: environment(this.engine), stdio: "ignore", windowsHide: true });
145
+ entry.watcher = watcher;
146
+ await new Promise((resolve2, reject) => {
147
+ watcher.once("spawn", resolve2);
148
+ watcher.once("error", reject);
149
+ });
150
+ watcher.on("error", (error) => {
151
+ entry.watchError = String(error);
152
+ });
153
+ watcher.on("exit", (code, reason) => {
154
+ if (entry.watcher === watcher) entry.watchError = `exit ${code ?? reason}`;
155
+ });
156
+ })().catch((error) => {
157
+ entry.ready = void 0;
158
+ throw error;
159
+ });
160
+ await entry.ready;
161
+ if (entry.watchError) throw new Error(`Incremental watcher stopped: ${entry.watchError}`);
162
+ return entry.update;
163
+ }
164
+ async release(owner) {
165
+ this.retired.add(owner);
166
+ for (const [root, promise] of this.entries) {
167
+ const entry = await promise.catch(() => void 0);
168
+ if (!entry) continue;
169
+ entry.owners.delete(owner);
170
+ if (!entry.owners.size) {
171
+ this.entries.delete(root);
172
+ const watcher = entry.watcher;
173
+ entry.watcher = void 0;
174
+ await stop(watcher);
175
+ await entry.client.close();
176
+ }
177
+ }
178
+ }
179
+ async dispose() {
180
+ this.closed = true;
181
+ const entries = [...this.entries.values()];
182
+ this.entries.clear();
183
+ for (const promise of entries) {
184
+ const entry = await promise.catch(() => void 0);
185
+ if (entry) {
186
+ const watcher = entry.watcher;
187
+ entry.watcher = void 0;
188
+ await stop(watcher);
189
+ await entry.client.close();
190
+ }
191
+ }
192
+ }
193
+ };
194
+ async function run(engine, args, cwd, signal) {
195
+ await checkedData(cwd);
196
+ signal = AbortSignal.any([signal, AbortSignal.timeout(engine.timeoutMs)]);
197
+ return new Promise((resolve2, reject) => {
198
+ let log = "";
199
+ const child = spawn(engine.command, [...engine.args, ...args], { cwd, env: environment(engine), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, signal });
200
+ const capture = (bytes) => {
201
+ log = (log + bytes.toString()).slice(-16e3);
202
+ };
203
+ child.stdout?.on("data", capture);
204
+ child.stderr?.on("data", capture);
205
+ child.on("error", reject);
206
+ child.on("exit", (code) => code === 0 ? resolve2({ log }) : reject(new Error(`Engine command exited ${code}: ${log}`)));
207
+ });
208
+ }
209
+ export {
210
+ BackendPool,
211
+ connect,
212
+ run,
213
+ valueOf
214
+ };
215
+ //# sourceMappingURL=backend.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/backend.ts", "../src/safety.ts"],
4
+ "sourcesContent": ["import { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';\nimport { spawn, type ChildProcess } from 'node:child_process';\nimport { checkedData, scrubEnv } from './safety.ts';\n\nexport interface Engine { command: string; args: string[]; timeoutMs: number; env?: Record<string, string> }\nconst environment = (engine: Engine) => ({ ...scrubEnv(process.env), ...engine.env });\nexport function valueOf(result: unknown): Record<string, unknown> {\n const r = result as { isError?: boolean; structuredContent?: Record<string, unknown>; content?: { type: string; text?: string }[] };\n const text = r.content?.filter(x => x.type === 'text').map(x => x.text).join('\\n') ?? '';\n if (r.isError) throw new Error(text || 'Engine tool failed');\n const value = r.structuredContent ?? (() => { try { return JSON.parse(text); } catch { return { text }; } })();\n if (value.status === 'error') throw new Error(String(value.error));\n return value;\n}\n\nexport async function connect(engine: Engine, cwd: string): Promise<Client> {\n await checkedData(cwd);\n const transport = new StdioClientTransport({ command: engine.command, args: [...engine.args, 'serve', '--repo', cwd], cwd, env: environment(engine), stderr: 'pipe' });\n transport.stderr?.on('data', () => {});\n const client = new Client({ name: 'dsh-code-review-graph', version: '0.1.1' });\n try { await client.connect(transport); return client; }\n catch (error) { await transport.close(); throw new Error(`CRG engine unavailable. Run dsh-crg prepare explicitly. ${String(error)}`); }\n}\n\nasync function stop(child?: ChildProcess): Promise<void> {\n if (!child || child.exitCode !== null || child.signalCode !== null) return;\n await new Promise<void>(resolve => {\n const timer = setTimeout(() => { child.kill('SIGKILL'); resolve(); }, 3000);\n child.once('exit', () => { clearTimeout(timer); resolve(); });\n child.kill();\n });\n}\ninterface Entry { client: Client; owners: Set<string>; ready?: Promise<void>; watcher?: ChildProcess; watchError?: string; queue: Promise<unknown>; update?: Record<string, unknown> }\nexport class BackendPool {\n private entries = new Map<string, Promise<Entry>>();\n private retired = new Set<string>();\n private closed = false;\n constructor(readonly engine: Engine) {}\n activate(owner: string): void { this.retired.delete(owner); }\n\n async acquire(root: string, owner: string): Promise<Entry> {\n if (this.closed || this.retired.has(owner)) throw new Error('Session has been released');\n let promise = this.entries.get(root);\n if (!promise) {\n promise = connect(this.engine, root).then(client => ({ client, owners: new Set<string>(), queue: Promise.resolve() }));\n this.entries.set(root, promise);\n promise.catch(() => { if (this.entries.get(root) === promise) this.entries.delete(root); });\n }\n const entry = await promise;\n if (this.closed || this.retired.has(owner)) throw new Error('Session has been released');\n entry.owners.add(owner);\n return entry;\n }\n\n async call(root: string, owner: string, name: string, args: Record<string, unknown>, signal: AbortSignal) {\n const entry = await this.acquire(root, owner);\n // One mutation at a time per SQLite graph; cancellation restarts this repository's shared worker.\n const work = entry.queue.catch(() => {}).then(async () => {\n signal.throwIfAborted();\n const cancel = () => { this.entries.delete(root); void stop(entry.watcher); void entry.client.close(); };\n signal.addEventListener('abort', cancel, { once: true });\n let result: unknown;\n try {\n await checkedData(root);\n if (entry.watchError) throw new Error(`Incremental watcher stopped: ${entry.watchError}`);\n result = await entry.client.callTool({ name, arguments: args }, undefined, { signal, timeout: this.engine.timeoutMs });\n } catch (error) {\n // A timed-out Python thread cannot safely be left mutating the graph.\n this.entries.delete(root); await stop(entry.watcher); await entry.client.close();\n throw error;\n } finally { signal.removeEventListener('abort', cancel); }\n // A normal tool error must not discard another call's pending refactor preview.\n valueOf(result);\n return result;\n });\n entry.queue = work;\n return work;\n }\n\n async ensure(root: string, owner: string, signal: AbortSignal): Promise<Record<string, unknown> | undefined> {\n const entry = await this.acquire(root, owner);\n entry.ready ??= (async () => {\n // An existing database can be stale after all sessions were closed.\n const update = valueOf(await this.call(root, owner, 'build_or_update_graph_tool', { repo_root: root }, signal));\n if (update.status === 'partial' || Array.isArray(update.warnings) && update.warnings.length) entry.update = update;\n signal.throwIfAborted();\n const watcher = spawn(this.engine.command, [...this.engine.args, 'watch', '--repo', root], { cwd: root, env: environment(this.engine), stdio: 'ignore', windowsHide: true });\n entry.watcher = watcher;\n await new Promise<void>((resolve, reject) => { watcher.once('spawn', resolve); watcher.once('error', reject); });\n watcher.on('error', error => { entry.watchError = String(error); });\n watcher.on('exit', (code, reason) => { if (entry.watcher === watcher) entry.watchError = `exit ${code ?? reason}`; });\n })().catch(error => { entry.ready = undefined; throw error; });\n await entry.ready;\n if (entry.watchError) throw new Error(`Incremental watcher stopped: ${entry.watchError}`);\n return entry.update;\n }\n\n async release(owner: string): Promise<void> {\n this.retired.add(owner);\n for (const [root, promise] of this.entries) {\n const entry = await promise.catch(() => undefined);\n if (!entry) continue;\n entry.owners.delete(owner);\n if (!entry.owners.size) { this.entries.delete(root); const watcher = entry.watcher; entry.watcher = undefined; await stop(watcher); await entry.client.close(); }\n }\n }\n\n async dispose(): Promise<void> {\n this.closed = true;\n const entries = [...this.entries.values()]; this.entries.clear();\n for (const promise of entries) {\n const entry = await promise.catch(() => undefined);\n if (entry) { const watcher = entry.watcher; entry.watcher = undefined; await stop(watcher); await entry.client.close(); }\n }\n }\n}\n\nexport async function run(engine: Engine, args: string[], cwd: string, signal: AbortSignal): Promise<{ log: string }> {\n await checkedData(cwd);\n signal = AbortSignal.any([signal, AbortSignal.timeout(engine.timeoutMs)]);\n return new Promise((resolve, reject) => {\n let log = '';\n const child = spawn(engine.command, [...engine.args, ...args], { cwd, env: environment(engine), stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, signal });\n const capture = (bytes: Buffer) => { log = (log + bytes.toString()).slice(-16000); };\n child.stdout?.on('data', capture); child.stderr?.on('data', capture);\n child.on('error', reject);\n child.on('exit', code => code === 0 ? resolve({ log }) : reject(new Error(`Engine command exited ${code}: ${log}`)));\n });\n}\n", "import { realpath, stat, lstat, readdir } from 'node:fs/promises';\nimport { isAbsolute, relative, resolve, sep, dirname, basename, join } from 'node:path';\nimport { createHash } from 'node:crypto';\n\nexport const sha256 = (bytes: Uint8Array | string): string => createHash('sha256').update(bytes).digest('hex');\n\nexport function contained(root: string, path: string): boolean {\n const part = relative(root, path);\n return part === '' || (!isAbsolute(part) && part !== '..' && !part.startsWith(`..${sep}`));\n}\n\nexport async function checkedPath(root: string, value: string, allowMissing = false): Promise<string> {\n const candidate = resolve(root, value);\n if (!contained(root, candidate)) throw new Error('Path is outside the authorized repository');\n let current = candidate;\n const suffix: string[] = [];\n while (allowMissing && !await lstat(current).then(() => true, () => false)) {\n suffix.unshift(basename(current)); current = dirname(current);\n }\n const canonical = join(await realpath(current), ...suffix);\n if (!contained(root, canonical)) throw new Error('Symlink escapes the authorized repository');\n return canonical;\n}\n\nexport async function repository(cwd: string | undefined): Promise<string> {\n if (!cwd || !isAbsolute(cwd)) throw new Error('This session has no local absolute workspace');\n const root = await realpath(cwd);\n if (!(await stat(root)).isDirectory()) throw new Error('Workspace is not a directory');\n for (const marker of ['.git', '.svn']) {\n if (await stat(resolve(root, marker)).then(() => true, () => false)) return root;\n }\n throw new Error('Select a Git or SVN repository before using the graph');\n}\n\nexport function scrubEnv(env: NodeJS.ProcessEnv): Record<string, string> {\n return { ...Object.fromEntries(Object.entries(env).filter(([key, value]) => value !== undefined && !/KEY|PASSWORD|SECRET|TOKEN|^(DSH|CRG|OPENAI|GOOGLE|GEMINI|MINIMAX|VOYAGE|HF)_/i.test(key))), HF_HUB_OFFLINE: '1', TRANSFORMERS_OFFLINE: '1', HF_HUB_DISABLE_TELEMETRY: '1' } as Record<string, string>;\n}\n\nexport async function checkedData(root: string): Promise<void> {\n const dir = resolve(root, '.code-review-graph');\n if (!await lstat(dir).then(() => true, () => false)) return;\n await checkedPath(root, dir);\n const entries = await readdir(dir, { withFileTypes: true, recursive: true });\n for (const entry of entries) {\n if (entry.isSymbolicLink()) await checkedPath(root, join(entry.parentPath, entry.name));\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["resolve"]
7
+ }
package/dist/client.js ADDED
@@ -0,0 +1,103 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-code-review-graph", factory: (require) => { var module = { exports: {} }; var exports = module.exports;
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name2 in all)
11
+ __defProp(target, name2, { get: all[name2], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/client.tsx
32
+ var client_exports = {};
33
+ __export(client_exports, {
34
+ apply: () => apply,
35
+ inject: () => inject,
36
+ name: () => name,
37
+ readAsset: () => readAsset
38
+ });
39
+ module.exports = __toCommonJS(client_exports);
40
+ var import_react = __toESM(require("react"), 1);
41
+
42
+ // src/html.ts
43
+ async function inlineGraph(html, read) {
44
+ html = html.replace(/<script>\s*window\.d3\s*\|\|\s*document\.write\([\s\S]*?\);\s*<\/script>/gi, "");
45
+ const scripts = [...html.matchAll(/<script\b([^>]*?)\bsrc=["']([^"']+)["']([^>]*)>[\s\S]*?<\/script>/gi)];
46
+ for (const match of scripts) {
47
+ const path = match[2];
48
+ if (/^(?:https?:|data:|\/|\\)|\.\./i.test(path)) throw new Error("Graph contains a nonlocal script");
49
+ const script = await read(path);
50
+ html = html.replace(match[0], () => `<script>${script.replace(/<\/script/gi, "<\\/script")}<\/script>`);
51
+ }
52
+ 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'">`;
53
+ return html.replace(/<head>/i, `<head>${policy}`);
54
+ }
55
+
56
+ // src/client.tsx
57
+ var inject = ["slots", "sidebarRightTabs", "remote", "remote.workspaceFiles"];
58
+ var name = "code-review-graph-client";
59
+ async function readAsset(reader, session, path, signal, base) {
60
+ const result = reader.readBytes ? await reader.readBytes(session, path, base ? { baseFile: base } : {}, signal) : base && reader.readRelated ? await reader.readRelated(session, base, path, signal) : reader.readAll ? await reader.readAll(session, path, signal) : (() => {
61
+ throw new Error("Unsupported DSH file reader");
62
+ })();
63
+ if (!result.ok) throw new Error(result.error.message);
64
+ if (result.value.data instanceof Uint8Array) return new TextDecoder().decode(result.value.data);
65
+ const base64 = result.value.data;
66
+ if (typeof base64 !== "string") throw new Error("Invalid graph asset response");
67
+ return new TextDecoder().decode(Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)));
68
+ }
69
+ function apply(ctx) {
70
+ const id = "dsh-code-review-graph";
71
+ 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." }] }));
72
+ function Graph({ sessionId, useTabInfo }) {
73
+ const { tab } = useTabInfo();
74
+ const [html, setHtml] = (0, import_react.useState)("");
75
+ const [error, setError] = (0, import_react.useState)("");
76
+ const [generation, refresh] = (0, import_react.useState)(0);
77
+ const download = () => {
78
+ const url = URL.createObjectURL(new Blob([html], { type: "text/html;charset=utf-8" }));
79
+ const link = document.createElement("a");
80
+ link.href = url;
81
+ link.download = "code-review-graph.html";
82
+ link.click();
83
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
84
+ };
85
+ (0, import_react.useEffect)(() => {
86
+ const life = new AbortController();
87
+ const signal = AbortSignal.any([life.signal, tab.signal]);
88
+ const base = ".code-review-graph/graph.html";
89
+ const reader = ctx.remote.workspaceFiles;
90
+ setError("");
91
+ void readAsset(reader, sessionId, base, signal).then((html2) => inlineGraph(html2, (path) => readAsset(reader, sessionId, path, signal, base))).then((result) => {
92
+ if (!signal.aborted) setHtml(result);
93
+ }).catch((error2) => {
94
+ if (!signal.aborted) setError(String(error2));
95
+ });
96
+ return () => life.abort();
97
+ }, [sessionId, generation, tab.signal]);
98
+ return /* @__PURE__ */ import_react.default.createElement("section", { style: { height: "100%", display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("button", { type: "button", onClick: () => refresh((n) => n + 1) }, "Refresh / \u5237\u65B0"), " ", /* @__PURE__ */ import_react.default.createElement("button", { type: "button", disabled: !html, onClick: download }, "Export HTML / \u5BFC\u51FA HTML")), error && /* @__PURE__ */ import_react.default.createElement("p", { role: "status" }, "Generate the graph with crg_visualize, then refresh. / \u4F7F\u7528 crg_visualize \u751F\u6210\u56FE\u8C31\u540E\u5237\u65B0\u3002", /* @__PURE__ */ import_react.default.createElement("br", null), error), html && /* @__PURE__ */ import_react.default.createElement("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 } }));
99
+ }
100
+ ctx.effect(() => ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({ name: "sidebar.right.pane.tab", key: id }, Graph)));
101
+ }
102
+ return module.exports; } });
103
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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><button type=\"button\" onClick={() => refresh(n => n + 1)}>Refresh / \u5237\u65B0</button> <button type=\"button\" disabled={!html} onClick={download}>Export HTML / \u5BFC\u51FA HTML</button></div>\n {error && <p role=\"status\">Generate the graph with crg_visualize, then refresh. / \u4F7F\u7528 crg_visualize \u751F\u6210\u56FE\u8C31\u540E\u5237\u65B0\u3002<br />{error}</p>}\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": ["html", "error", "React"]
7
+ }
package/dist/html.js ADDED
@@ -0,0 +1,17 @@
1
+ // src/html.ts
2
+ async function inlineGraph(html, read) {
3
+ html = html.replace(/<script>\s*window\.d3\s*\|\|\s*document\.write\([\s\S]*?\);\s*<\/script>/gi, "");
4
+ const scripts = [...html.matchAll(/<script\b([^>]*?)\bsrc=["']([^"']+)["']([^>]*)>[\s\S]*?<\/script>/gi)];
5
+ for (const match of scripts) {
6
+ const path = match[2];
7
+ if (/^(?:https?:|data:|\/|\\)|\.\./i.test(path)) throw new Error("Graph contains a nonlocal script");
8
+ const script = await read(path);
9
+ html = html.replace(match[0], () => `<script>${script.replace(/<\/script/gi, "<\\/script")}</script>`);
10
+ }
11
+ 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'">`;
12
+ return html.replace(/<head>/i, `<head>${policy}`);
13
+ }
14
+ export {
15
+ inlineGraph
16
+ };
17
+ //# sourceMappingURL=html.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/html.ts"],
4
+ "sourcesContent": ["/** 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": ";AACA,eAAsB,YAAY,MAAc,MAA0D;AAGxG,SAAO,KAAK,QAAQ,8EAA8E,EAAE;AACpG,QAAM,UAAU,CAAC,GAAG,KAAK,SAAS,qEAAqE,CAAC;AACxG,aAAW,SAAS,SAAS;AAC3B,UAAM,OAAO,MAAM,CAAC;AACpB,QAAI,iCAAiC,KAAK,IAAI,EAAG,OAAM,IAAI,MAAM,kCAAkC;AACnG,UAAM,SAAS,MAAM,KAAK,IAAI;AAC9B,WAAO,KAAK,QAAQ,MAAM,CAAC,GAAG,MAAM,WAAW,OAAO,QAAQ,eAAe,YAAY,CAAC,WAAW;AAAA,EACvG;AACA,QAAM,SAAS;AACf,SAAO,KAAK,QAAQ,WAAW,SAAS,MAAM,EAAE;AAClD;",
6
+ "names": []
7
+ }