dsh-gitnexus-plus 0.0.0-stage → 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/LICENSE +38 -0
- package/README.md +631 -2
- package/cordis.patch.yml +18 -0
- package/lib/bin/gitnexus-coverage.d.ts +14 -0
- package/lib/bin/gitnexus-coverage.d.ts.map +1 -0
- package/lib/bin/gitnexus-coverage.js +171 -0
- package/lib/bin/gitnexus-coverage.js.map +1 -0
- package/lib/bin/search-helper.d.ts +22 -0
- package/lib/bin/search-helper.d.ts.map +1 -0
- package/lib/bin/search-helper.js +57 -0
- package/lib/bin/search-helper.js.map +1 -0
- package/lib/client.js +2131 -0
- package/lib/gitnexus/analyze.d.ts +173 -0
- package/lib/gitnexus/analyze.d.ts.map +1 -0
- package/lib/gitnexus/analyze.js +308 -0
- package/lib/gitnexus/analyze.js.map +1 -0
- package/lib/gitnexus/commands.d.ts +129 -0
- package/lib/gitnexus/commands.d.ts.map +1 -0
- package/lib/gitnexus/commands.js +185 -0
- package/lib/gitnexus/commands.js.map +1 -0
- package/lib/gitnexus/coverage.d.ts +340 -0
- package/lib/gitnexus/coverage.d.ts.map +1 -0
- package/lib/gitnexus/coverage.js +554 -0
- package/lib/gitnexus/coverage.js.map +1 -0
- package/lib/gitnexus/format.d.ts +16 -0
- package/lib/gitnexus/format.d.ts.map +1 -0
- package/lib/gitnexus/format.js +178 -0
- package/lib/gitnexus/format.js.map +1 -0
- package/lib/gitnexus/git-activity.d.ts +136 -0
- package/lib/gitnexus/git-activity.d.ts.map +1 -0
- package/lib/gitnexus/git-activity.js +345 -0
- package/lib/gitnexus/git-activity.js.map +1 -0
- package/lib/gitnexus/git.d.ts +70 -0
- package/lib/gitnexus/git.d.ts.map +1 -0
- package/lib/gitnexus/git.js +121 -0
- package/lib/gitnexus/git.js.map +1 -0
- package/lib/gitnexus/index-meta.d.ts +160 -0
- package/lib/gitnexus/index-meta.d.ts.map +1 -0
- package/lib/gitnexus/index-meta.js +176 -0
- package/lib/gitnexus/index-meta.js.map +1 -0
- package/lib/gitnexus/install.d.ts +97 -0
- package/lib/gitnexus/install.d.ts.map +1 -0
- package/lib/gitnexus/install.js +228 -0
- package/lib/gitnexus/install.js.map +1 -0
- package/lib/gitnexus/languages.d.ts +80 -0
- package/lib/gitnexus/languages.d.ts.map +1 -0
- package/lib/gitnexus/languages.js +297 -0
- package/lib/gitnexus/languages.js.map +1 -0
- package/lib/gitnexus/root.d.ts +74 -0
- package/lib/gitnexus/root.d.ts.map +1 -0
- package/lib/gitnexus/root.js +168 -0
- package/lib/gitnexus/root.js.map +1 -0
- package/lib/gitnexus/scan.d.ts +68 -0
- package/lib/gitnexus/scan.d.ts.map +1 -0
- package/lib/gitnexus/scan.js +249 -0
- package/lib/gitnexus/scan.js.map +1 -0
- package/lib/gitnexus/search.d.ts +167 -0
- package/lib/gitnexus/search.d.ts.map +1 -0
- package/lib/gitnexus/search.js +517 -0
- package/lib/gitnexus/search.js.map +1 -0
- package/lib/gitnexus/toolchain.d.ts +136 -0
- package/lib/gitnexus/toolchain.d.ts.map +1 -0
- package/lib/gitnexus/toolchain.js +251 -0
- package/lib/gitnexus/toolchain.js.map +1 -0
- package/lib/index.d.ts +98 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +252 -0
- package/lib/index.js.map +1 -0
- package/lib/routes/analyze.d.ts +41 -0
- package/lib/routes/analyze.d.ts.map +1 -0
- package/lib/routes/analyze.js +75 -0
- package/lib/routes/analyze.js.map +1 -0
- package/lib/routes/coverage.d.ts +52 -0
- package/lib/routes/coverage.d.ts.map +1 -0
- package/lib/routes/coverage.js +81 -0
- package/lib/routes/coverage.js.map +1 -0
- package/lib/routes/guard.d.ts +92 -0
- package/lib/routes/guard.d.ts.map +1 -0
- package/lib/routes/guard.js +187 -0
- package/lib/routes/guard.js.map +1 -0
- package/lib/routes/install.d.ts +27 -0
- package/lib/routes/install.d.ts.map +1 -0
- package/lib/routes/install.js +46 -0
- package/lib/routes/install.js.map +1 -0
- package/lib/routes/search.d.ts +27 -0
- package/lib/routes/search.d.ts.map +1 -0
- package/lib/routes/search.js +77 -0
- package/lib/routes/search.js.map +1 -0
- package/package.json +87 -4
- package/scripts/link-runtime.mjs +146 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared guards for this plugin's HTTP routes.
|
|
3
|
+
*
|
|
4
|
+
* Extracted because two routes now need identical treatment, and a guard that is
|
|
5
|
+
* copy-pasted stops being a guard the first time one copy is edited.
|
|
6
|
+
*
|
|
7
|
+
* WHY THE GUARD EXISTS AT ALL
|
|
8
|
+
* ---------------------------
|
|
9
|
+
* On a bare `dsh web` profile a third-party named route is unauthenticated: the
|
|
10
|
+
* carrier owns no TLS, authentication, or Origin policy, and the Host/Origin
|
|
11
|
+
* checks that do exist belong to `client/connection` and cover only the routes it
|
|
12
|
+
* registers itself. The Desktop carrier adds its own gate (an
|
|
13
|
+
* `x-dsh-desktop-renderer` token; unauthenticated requests get 403), but a route
|
|
14
|
+
* that is safe only on one carrier is not safe.
|
|
15
|
+
*/
|
|
16
|
+
/** This plugin only ever answers reads. */
|
|
17
|
+
export const ALLOWED_METHODS = new Set(['GET', 'HEAD']);
|
|
18
|
+
/**
|
|
19
|
+
* @param {import('node:http').IncomingMessage} req
|
|
20
|
+
* @returns {boolean} whether the peer is this machine
|
|
21
|
+
*/
|
|
22
|
+
export function isLoopback(req) {
|
|
23
|
+
const address = req.socket?.remoteAddress ?? '';
|
|
24
|
+
return (address === '127.0.0.1' ||
|
|
25
|
+
address === '::1' ||
|
|
26
|
+
address === '::ffff:127.0.0.1' ||
|
|
27
|
+
// Node reports an absent family as an empty string for unix sockets.
|
|
28
|
+
address === '');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* @param {import('node:http').ServerResponse} res
|
|
32
|
+
* @param {number} status
|
|
33
|
+
* @param {unknown} body
|
|
34
|
+
* @param {Record<string, string>} [headers]
|
|
35
|
+
*/
|
|
36
|
+
export function sendJson(res, status, body, headers = {}) {
|
|
37
|
+
const payload = JSON.stringify(body, null, 2);
|
|
38
|
+
res.statusCode = status;
|
|
39
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
40
|
+
res.setHeader('cache-control', 'no-store');
|
|
41
|
+
res.setHeader('x-content-type-options', 'nosniff');
|
|
42
|
+
res.setHeader('referrer-policy', 'no-referrer');
|
|
43
|
+
for (const [name, value] of Object.entries(headers))
|
|
44
|
+
res.setHeader(name, value);
|
|
45
|
+
res.end(payload);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Run the shared pre-checks, answering the request when one fails.
|
|
49
|
+
*
|
|
50
|
+
* `methods` exists because the default set is the read-only one: the coverage route
|
|
51
|
+
* answers GET and HEAD and nothing else, and giving it POST would widen it by accident.
|
|
52
|
+
* A route that legitimately mutates passes its own set, so the method policy is stated
|
|
53
|
+
* at the route rather than inferred from what the guard happens to allow.
|
|
54
|
+
*
|
|
55
|
+
* @param {import('node:http').IncomingMessage} req
|
|
56
|
+
* @param {import('node:http').ServerResponse} res
|
|
57
|
+
* @param methods the methods this route accepts
|
|
58
|
+
* @returns `true` when the caller may continue handling the request
|
|
59
|
+
*/
|
|
60
|
+
export function passGuard(req, res, methods = ALLOWED_METHODS) {
|
|
61
|
+
if (!isLoopback(req)) {
|
|
62
|
+
sendJson(res, 403, { ok: false, code: 'forbidden', message: 'loopback only' });
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
if (!methods.has(req.method ?? '')) {
|
|
66
|
+
sendJson(res, 405, { ok: false, code: 'method-not-allowed' }, { allow: [...methods].join(', ') });
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Answer a HEAD request with headers only, or report that the caller should
|
|
73
|
+
* continue and send the body.
|
|
74
|
+
*
|
|
75
|
+
* @returns `true` when the request was fully answered
|
|
76
|
+
*/
|
|
77
|
+
export function answerHead(req, res) {
|
|
78
|
+
if (req.method !== 'HEAD')
|
|
79
|
+
return false;
|
|
80
|
+
res.statusCode = 200;
|
|
81
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
82
|
+
res.end();
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Wrap a handler so no failure escapes as an unhandled rejection on the carrier.
|
|
87
|
+
*
|
|
88
|
+
* `Promise.resolve` around the call is not decoration: `RequestHandler` may return
|
|
89
|
+
* `void`, and calling `.catch` on `void` is a runtime `TypeError`. The shape the
|
|
90
|
+
* carrier actually uses is "sync throw or async rejection", and only the resolved
|
|
91
|
+
* form covers both.
|
|
92
|
+
*/
|
|
93
|
+
export function guardHandler(handler) {
|
|
94
|
+
return (req, res) => {
|
|
95
|
+
Promise.resolve(handler(req, res)).catch((error) => {
|
|
96
|
+
if (res.headersSent) {
|
|
97
|
+
res.end();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
sendJson(res, 500, {
|
|
101
|
+
ok: false,
|
|
102
|
+
code: 'internal-error',
|
|
103
|
+
message: error instanceof Error ? error.message : String(error),
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Extra guard for routes that *change* something.
|
|
110
|
+
*
|
|
111
|
+
* The loopback check alone is not enough for a mutation: any page the operator
|
|
112
|
+
* visits can issue a request to `127.0.0.1`, and a cross-origin `POST` needs no
|
|
113
|
+
* preflight when it uses a simple content type. `Origin` is the discriminator —
|
|
114
|
+
* a malicious page always sends its own origin, and it will not match this host.
|
|
115
|
+
*
|
|
116
|
+
* A request with NO `Origin` is allowed through, and that is deliberate rather
|
|
117
|
+
* than lazy: a same-origin `fetch` from the desktop shell, and any local tool
|
|
118
|
+
* (including this repository's own tests), legitimately send none. The remaining
|
|
119
|
+
* gates — loopback peer, and the Desktop carrier's renderer token — still apply,
|
|
120
|
+
* so an absent header is not by itself a way in.
|
|
121
|
+
*
|
|
122
|
+
* @param {import('node:http').IncomingMessage} req
|
|
123
|
+
* @param {import('node:http').ServerResponse} res
|
|
124
|
+
* @returns {boolean} `true` when the caller may proceed
|
|
125
|
+
*/
|
|
126
|
+
export function passWriteGuard(req, res) {
|
|
127
|
+
const origin = req.headers.origin;
|
|
128
|
+
if (typeof origin !== 'string' || origin === '')
|
|
129
|
+
return true;
|
|
130
|
+
const host = req.headers.host;
|
|
131
|
+
if (typeof host === 'string' && host !== '') {
|
|
132
|
+
try {
|
|
133
|
+
if (new URL(origin).host === host)
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
/* malformed origin falls through to the refusal below */
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
sendJson(res, 403, {
|
|
141
|
+
ok: false,
|
|
142
|
+
code: 'cross-origin-write',
|
|
143
|
+
message: 'this endpoint changes state and refuses cross-origin requests',
|
|
144
|
+
});
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Read a JSON body with a hard byte cap.
|
|
149
|
+
*
|
|
150
|
+
* Bounded because the cap is the only thing standing between a malformed client
|
|
151
|
+
* and unbounded host memory — and this plugin's bodies are at most a few fields.
|
|
152
|
+
*
|
|
153
|
+
* @param {import('node:http').IncomingMessage} req
|
|
154
|
+
* @param {number} [maxBytes]
|
|
155
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
156
|
+
*/
|
|
157
|
+
export async function readJsonBody(req, maxBytes = 8 * 1024) {
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
let size = 0;
|
|
160
|
+
const chunks = [];
|
|
161
|
+
req.on('data', (chunk) => {
|
|
162
|
+
size += chunk.length;
|
|
163
|
+
if (size > maxBytes) {
|
|
164
|
+
reject(new Error('request body too large'));
|
|
165
|
+
req.destroy();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
chunks.push(chunk);
|
|
169
|
+
});
|
|
170
|
+
req.on('end', () => {
|
|
171
|
+
const raw = Buffer.concat(chunks).toString('utf8').trim();
|
|
172
|
+
if (raw === '') {
|
|
173
|
+
resolve({});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const parsed = JSON.parse(raw);
|
|
178
|
+
resolve(parsed !== null && typeof parsed === 'object' ? parsed : {});
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
reject(new Error(`invalid JSON body: ${error instanceof Error ? error.message : String(error)}`));
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
req.on('error', reject);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../../src/routes/guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The endpoint that installs GitNexus on this machine.
|
|
3
|
+
*
|
|
4
|
+
* Its own route rather than a branch in `analyze.ts`, because it is a different kind of
|
|
5
|
+
* action: `analyze` reads and writes an index that already exists, while this downloads
|
|
6
|
+
* several hundred megabytes and runs the package's build scripts. Separating them keeps
|
|
7
|
+
* the cheap, frequent operation from being reviewed alongside the expensive, rare one.
|
|
8
|
+
*
|
|
9
|
+
* `POST` starts, `GET` reports, `DELETE` cancels — the same shape as the analyze route,
|
|
10
|
+
* for the same reason: the panel polls the URL it started.
|
|
11
|
+
*/
|
|
12
|
+
import type { WebServerService } from '@deepseek-ai/cordis';
|
|
13
|
+
import type { InstallStatus } from '../gitnexus/install.js';
|
|
14
|
+
export interface InstallRouteDeps {
|
|
15
|
+
start: () => InstallStatus;
|
|
16
|
+
status: () => InstallStatus | null;
|
|
17
|
+
cancel: () => boolean;
|
|
18
|
+
/** Where a successful install puts the CLI, so the panel can show it. */
|
|
19
|
+
cliPath: () => string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* @returns disposer that unregisters the route
|
|
23
|
+
*/
|
|
24
|
+
export declare function mountInstallRoute(webServer: WebServerService, deps: InstallRouteDeps): () => void;
|
|
25
|
+
/** Narrow a repo path for the panel's benefit; unused by the route itself. */
|
|
26
|
+
export declare function projectName(repoPath: string): string;
|
|
27
|
+
//# sourceMappingURL=install.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/routes/install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAI5D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,aAAa,CAAC;IAC3B,MAAM,EAAE,MAAM,aAAa,GAAG,IAAI,CAAC;IACnC,MAAM,EAAE,MAAM,OAAO,CAAC;IACtB,yEAAyE;IACzE,OAAO,EAAE,MAAM,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,gBAAgB,EAAE,IAAI,EAAE,gBAAgB,GAAG,MAAM,IAAI,CAuBjG;AAED,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpD"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The endpoint that installs GitNexus on this machine.
|
|
3
|
+
*
|
|
4
|
+
* Its own route rather than a branch in `analyze.ts`, because it is a different kind of
|
|
5
|
+
* action: `analyze` reads and writes an index that already exists, while this downloads
|
|
6
|
+
* several hundred megabytes and runs the package's build scripts. Separating them keeps
|
|
7
|
+
* the cheap, frequent operation from being reviewed alongside the expensive, rare one.
|
|
8
|
+
*
|
|
9
|
+
* `POST` starts, `GET` reports, `DELETE` cancels — the same shape as the analyze route,
|
|
10
|
+
* for the same reason: the panel polls the URL it started.
|
|
11
|
+
*/
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { guardHandler, passGuard, passWriteGuard, answerHead, sendJson } from './guard.js';
|
|
14
|
+
const INSTALL_METHODS = new Set(['GET', 'HEAD', 'POST', 'DELETE']);
|
|
15
|
+
/**
|
|
16
|
+
* @returns disposer that unregisters the route
|
|
17
|
+
*/
|
|
18
|
+
export function mountInstallRoute(webServer, deps) {
|
|
19
|
+
const handler = guardHandler(async (req, res) => {
|
|
20
|
+
if (!passGuard(req, res, INSTALL_METHODS))
|
|
21
|
+
return;
|
|
22
|
+
if (answerHead(req, res))
|
|
23
|
+
return;
|
|
24
|
+
if (req.method === 'POST') {
|
|
25
|
+
// Installing runs arbitrary build scripts from the dependency tree, so this is the
|
|
26
|
+
// most privileged verb this plugin exposes and takes the cross-origin write guard.
|
|
27
|
+
if (!passWriteGuard(req, res))
|
|
28
|
+
return;
|
|
29
|
+
sendJson(res, 200, { ok: true, run: deps.start(), cliPath: deps.cliPath() });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (req.method === 'DELETE') {
|
|
33
|
+
if (!passWriteGuard(req, res))
|
|
34
|
+
return;
|
|
35
|
+
sendJson(res, 200, { ok: true, cancelled: deps.cancel(), run: deps.status() });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
sendJson(res, 200, { ok: true, run: deps.status(), cliPath: deps.cliPath() });
|
|
39
|
+
});
|
|
40
|
+
return webServer.register({ kind: 'prefix', path: '/api/gitnexus/install', handler });
|
|
41
|
+
}
|
|
42
|
+
/** Narrow a repo path for the panel's benefit; unused by the route itself. */
|
|
43
|
+
export function projectName(repoPath) {
|
|
44
|
+
return path.basename(repoPath);
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/routes/install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAI3F,MAAM,eAAe,GAAwB,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;AAUxF;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAA2B,EAAE,IAAsB;IACnF,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QAC9C,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,eAAe,CAAC;YAAE,OAAO;QAClD,IAAI,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;YAAE,OAAO;QAEjC,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC1B,mFAAmF;YACnF,mFAAmF;YACnF,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC;gBAAE,OAAO;YACtC,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;YAC7E,OAAO;QACT,CAAC;QAED,IAAI,GAAG,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC5B,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC;gBAAE,OAAO;YACtC,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YAC/E,OAAO;QACT,CAAC;QAED,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;IAEH,OAAO,SAAS,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,uBAAuB,EAAE,OAAO,EAAE,CAAC,CAAC;AACxF,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /api/gitnexus/search?repo=<project>&q=<keyword>` — keyword search.
|
|
3
|
+
*
|
|
4
|
+
* The fourth route, and the first that is neither a reading of the index manifest
|
|
5
|
+
* nor a maintenance action: it queries the graph's symbol names and file paths.
|
|
6
|
+
* It is read-only in the strongest sense this plugin can offer — the engine opens
|
|
7
|
+
* LadybugDB with `readOnly: true` and takes no write lock — so the method policy is
|
|
8
|
+
* the read-only one (GET/HEAD), not the analyze route's wider set.
|
|
9
|
+
*
|
|
10
|
+
* The bound is the same project-root check the coverage route uses: a directory
|
|
11
|
+
* that is neither indexed nor a `.git` root is refused, so the route cannot be used
|
|
12
|
+
* to enumerate a filesystem. Search itself additionally requires a complete index
|
|
13
|
+
* store and returns a typed error when the graph is missing.
|
|
14
|
+
*/
|
|
15
|
+
import type { WebServerService } from '@deepseek-ai/cordis';
|
|
16
|
+
import { type SearchReport } from '../gitnexus/search.js';
|
|
17
|
+
export interface SearchRouteDeps {
|
|
18
|
+
/** Run one keyword search. Errors are `SearchError` instances when typed. */
|
|
19
|
+
search: (repoPath: string, query: string, options?: {
|
|
20
|
+
limit?: number;
|
|
21
|
+
}) => Promise<SearchReport>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* @returns disposer that unregisters the route
|
|
25
|
+
*/
|
|
26
|
+
export declare function mountSearchRoute(webServer: WebServerService, deps: SearchRouteDeps): () => void;
|
|
27
|
+
//# sourceMappingURL=search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/routes/search.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAIvE,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;CAClG;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,gBAAgB,EAAE,IAAI,EAAE,eAAe,GAAG,MAAM,IAAI,CAwD/F"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /api/gitnexus/search?repo=<project>&q=<keyword>` — keyword search.
|
|
3
|
+
*
|
|
4
|
+
* The fourth route, and the first that is neither a reading of the index manifest
|
|
5
|
+
* nor a maintenance action: it queries the graph's symbol names and file paths.
|
|
6
|
+
* It is read-only in the strongest sense this plugin can offer — the engine opens
|
|
7
|
+
* LadybugDB with `readOnly: true` and takes no write lock — so the method policy is
|
|
8
|
+
* the read-only one (GET/HEAD), not the analyze route's wider set.
|
|
9
|
+
*
|
|
10
|
+
* The bound is the same project-root check the coverage route uses: a directory
|
|
11
|
+
* that is neither indexed nor a `.git` root is refused, so the route cannot be used
|
|
12
|
+
* to enumerate a filesystem. Search itself additionally requires a complete index
|
|
13
|
+
* store and returns a typed error when the graph is missing.
|
|
14
|
+
*/
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { guardHandler, passGuard, answerHead, sendJson } from './guard.js';
|
|
17
|
+
import { isProjectRoot } from '../gitnexus/index-meta.js';
|
|
18
|
+
import { SearchError } from '../gitnexus/search.js';
|
|
19
|
+
const SEARCH_METHODS = new Set(['GET', 'HEAD']);
|
|
20
|
+
/**
|
|
21
|
+
* @returns disposer that unregisters the route
|
|
22
|
+
*/
|
|
23
|
+
export function mountSearchRoute(webServer, deps) {
|
|
24
|
+
const handler = guardHandler(async (req, res) => {
|
|
25
|
+
if (!passGuard(req, res, SEARCH_METHODS))
|
|
26
|
+
return;
|
|
27
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
28
|
+
const repo = url.searchParams.get('repo');
|
|
29
|
+
if (repo === null || repo.trim() === '') {
|
|
30
|
+
sendJson(res, 400, { ok: false, code: 'missing-repo', message: 'repo query parameter is required' });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const query = url.searchParams.get('q');
|
|
34
|
+
if (query === null || query.trim() === '') {
|
|
35
|
+
sendJson(res, 400, { ok: false, code: 'missing-query', message: 'q query parameter is required' });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const absRepo = path.resolve(repo);
|
|
39
|
+
// The same boundary as the coverage route: a search must not become a way to
|
|
40
|
+
// ask whether an arbitrary directory exists. The engine repeats this check for
|
|
41
|
+
// direct callers; at the HTTP edge it saves opening a database to say no.
|
|
42
|
+
if (!(await isProjectRoot(absRepo))) {
|
|
43
|
+
sendJson(res, 404, {
|
|
44
|
+
ok: false,
|
|
45
|
+
code: 'not-a-project',
|
|
46
|
+
message: 'directory is neither a GitNexus index nor a git repository root',
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (answerHead(req, res))
|
|
51
|
+
return;
|
|
52
|
+
// `limit` is optional; the engine clamps it. A malformed value is treated as
|
|
53
|
+
// absent rather than as an error — the search itself does not depend on it.
|
|
54
|
+
const rawLimit = url.searchParams.get('limit');
|
|
55
|
+
const parsed = rawLimit === null ? Number.NaN : Number.parseInt(rawLimit, 10);
|
|
56
|
+
const limit = Number.isFinite(parsed) ? parsed : undefined;
|
|
57
|
+
try {
|
|
58
|
+
const report = await deps.search(absRepo, query, { limit });
|
|
59
|
+
sendJson(res, 200, {
|
|
60
|
+
ok: true,
|
|
61
|
+
...report,
|
|
62
|
+
readAt: new Date().toISOString(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error instanceof SearchError) {
|
|
67
|
+
sendJson(res, error.status, { ok: false, code: error.code, message: error.message });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// `guardHandler` turns anything else into a 500; rethrow so its single error
|
|
71
|
+
// path stays the only place that formats an unexpected failure.
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return webServer.register({ kind: 'prefix', path: '/api/gitnexus/search', handler });
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/routes/search.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,89 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-gitnexus-plus",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "DSH plugin: visualize a GitNexus project index, search its symbols by keyword, report what it left out, and run its incremental refresh from a sidebar panel — including detecting a missing GitNexus and installing one on request.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Moowningstar/gitnexus-plus.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Moowningstar/gitnexus-plus#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Moowningstar/gitnexus-plus/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"dsh",
|
|
17
|
+
"dsh-plugin",
|
|
18
|
+
"gitnexus",
|
|
19
|
+
"code-index",
|
|
20
|
+
"code-graph",
|
|
21
|
+
"coverage"
|
|
22
|
+
],
|
|
23
|
+
"main": "./lib/index.js",
|
|
24
|
+
"types": "./lib/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./lib/index.d.ts",
|
|
28
|
+
"default": "./lib/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./client": "./lib/client.js",
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"bin": {
|
|
34
|
+
"dsh-gitnexus-coverage": "./lib/bin/gitnexus-coverage.js"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"lib",
|
|
38
|
+
"scripts/link-runtime.mjs",
|
|
39
|
+
"cordis.patch.yml",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=22",
|
|
45
|
+
"dsh": ">=0.2.0-rc.2 <0.3.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"clean": "node scripts/clean.mjs",
|
|
49
|
+
"build": "npm run clean && tsc -p tsconfig.host.json && node scripts/build-client.mjs",
|
|
50
|
+
"prepare": "npm run build",
|
|
51
|
+
"postinstall": "node scripts/link-runtime.mjs",
|
|
52
|
+
"check": "tsc -p tsconfig.json && tsc -p tsconfig.scripts.json && tsc -p tsconfig.client.json",
|
|
53
|
+
"coverage": "node lib/bin/gitnexus-coverage.js",
|
|
54
|
+
"test": "npm run build && node test/run.mjs && node test/plugin.mjs && node test/client.mjs"
|
|
55
|
+
},
|
|
56
|
+
"dsh": {
|
|
57
|
+
"bundle": {
|
|
58
|
+
"patch": "./cordis.patch.yml"
|
|
59
|
+
},
|
|
60
|
+
"client": {
|
|
61
|
+
"platform": "web"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
66
|
+
"@deepseek-ai/dsh-tools": ">=0.2.0-rc.2 <0.3.0-0",
|
|
67
|
+
"@deepseek-ai/schemastery": ">=3.0.0 <4.0.0",
|
|
68
|
+
"gitnexus": ">=1.6.0 <2.0.0"
|
|
69
|
+
},
|
|
70
|
+
"peerDependenciesMeta": {
|
|
71
|
+
"@deepseek-ai/cordis": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"@deepseek-ai/dsh-tools": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"@deepseek-ai/schemastery": {
|
|
78
|
+
"optional": true
|
|
79
|
+
},
|
|
80
|
+
"gitnexus": {
|
|
81
|
+
"optional": true
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"devDependencies": {
|
|
85
|
+
"@types/node": "^26.6.5",
|
|
86
|
+
"@types/react": "^19.3.0",
|
|
87
|
+
"typescript": "^5.9.3"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Re-link the DSH runtime packages into `node_modules`.
|
|
4
|
+
*
|
|
5
|
+
* The host half imports `@deepseek-ai/schemastery`, `@deepseek-ai/dsh-tools` and
|
|
6
|
+
* `@deepseek-ai/cordis`; the client half is built against `react` and
|
|
7
|
+
* `@deepseek-ai/dsh-client-ui-primitives`. In a real install these arrive as
|
|
8
|
+
* ordinary dependencies, but during development they are taken from the DSH
|
|
9
|
+
* installation that is already on this machine rather than fetched from a registry
|
|
10
|
+
* — the primitives package in particular ships no `lib/` on npm; its JavaScript
|
|
11
|
+
* lives inside the web app bundle, and the published README states that plugins
|
|
12
|
+
* must share it rather than bundle their own copy.
|
|
13
|
+
*
|
|
14
|
+
* WHY THIS IS A SCRIPT AND NOT A ONE-OFF
|
|
15
|
+
* --------------------------------------
|
|
16
|
+
* `npm install` prunes anything in `node_modules` it does not recognise, so a
|
|
17
|
+
* hand-made junction survives exactly until the next dependency change. Twice
|
|
18
|
+
* during development that silently broke the test suite — the failure looked like
|
|
19
|
+
* a missing module, not like a pruned link. Wiring this to `postinstall` makes the
|
|
20
|
+
* links a property of the project instead of a property of the machine.
|
|
21
|
+
*
|
|
22
|
+
* It never fails the install: on a machine without DSH, or on a published package
|
|
23
|
+
* where the real dependencies are present, there is nothing to do.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { existsSync, mkdirSync, rmSync, symlinkSync, statSync } from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import process from 'node:process';
|
|
29
|
+
|
|
30
|
+
/** Packages the project needs, mapped to their location under a DSH install. */
|
|
31
|
+
const PACKAGES = [
|
|
32
|
+
'@deepseek-ai/schemastery',
|
|
33
|
+
'@deepseek-ai/dsh-tools',
|
|
34
|
+
'@deepseek-ai/cordis',
|
|
35
|
+
'@deepseek-ai/dsh-client-ui-primitives',
|
|
36
|
+
'react',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** Where a DSH installation keeps its `node_modules`, most specific first. */
|
|
40
|
+
function candidateRoots() {
|
|
41
|
+
const roots = [];
|
|
42
|
+
const fromEnv = process.env.DSH_RUNTIME_ROOT;
|
|
43
|
+
if (typeof fromEnv === 'string' && fromEnv !== '') roots.push(fromEnv);
|
|
44
|
+
|
|
45
|
+
if (process.platform === 'win32') {
|
|
46
|
+
const local = process.env.LOCALAPPDATA;
|
|
47
|
+
if (typeof local === 'string' && local !== '') {
|
|
48
|
+
roots.push(path.join(local, 'Programs', 'DSH Desktop', 'resources', 'app', 'node_modules'));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// A source checkout, for developers running DSH from a repository.
|
|
52
|
+
roots.push(path.resolve(import.meta.dirname, '..', '..', 'deepseek-harness', 'node_modules'));
|
|
53
|
+
return roots;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function findRuntimeRoot() {
|
|
57
|
+
for (const root of candidateRoots()) {
|
|
58
|
+
if (existsSync(path.join(root, '@deepseek-ai', 'dsh-tools'))) return root;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether this run is the project's own checkout rather than an installed copy.
|
|
65
|
+
*
|
|
66
|
+
* `postinstall` runs on **every** install, including when this package is a
|
|
67
|
+
* dependency inside someone else's tree. Linking the host's runtime packages into
|
|
68
|
+
* that tree is not ours to do, and a reviewer reading a plugin for "surprising
|
|
69
|
+
* install-time behaviour" would be right to flag a postinstall that walks PATH,
|
|
70
|
+
* probes for a DSH installation and rewrites `node_modules`. Installed copies live
|
|
71
|
+
* under `node_modules/`, so that is the test.
|
|
72
|
+
*/
|
|
73
|
+
function isOwnCheckout() {
|
|
74
|
+
return !/(^|[\\/])node_modules([\\/]|$)/.test(process.cwd());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function main() {
|
|
78
|
+
if (!isOwnCheckout()) return;
|
|
79
|
+
|
|
80
|
+
const runtime = findRuntimeRoot();
|
|
81
|
+
if (runtime === null) {
|
|
82
|
+
// Expected on a machine without DSH: nothing to link from.
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const linked = [];
|
|
87
|
+
const skipped = [];
|
|
88
|
+
for (const name of PACKAGES) {
|
|
89
|
+
const target = path.join(runtime, name);
|
|
90
|
+
if (!existsSync(target)) {
|
|
91
|
+
skipped.push(name);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const link = path.join(process.cwd(), 'node_modules', name);
|
|
96
|
+
// A real installation already satisfies this; leave it alone.
|
|
97
|
+
if (existsSync(link)) {
|
|
98
|
+
let isLink = false;
|
|
99
|
+
try {
|
|
100
|
+
isLink = statSync(link).isDirectory() && existsSync(path.join(link, 'package.json'));
|
|
101
|
+
} catch {
|
|
102
|
+
isLink = false;
|
|
103
|
+
}
|
|
104
|
+
const pointsHere = (() => {
|
|
105
|
+
try {
|
|
106
|
+
return path.resolve(link, '..', '..') === path.resolve(target, '..', '..');
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
if (isLink && !pointsHere) {
|
|
112
|
+
skipped.push(`${name} (already present)`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
mkdirSync(path.dirname(link), { recursive: true });
|
|
118
|
+
if (existsSync(link)) rmSync(link, { recursive: true, force: true });
|
|
119
|
+
try {
|
|
120
|
+
symlinkSync(target, link, process.platform === 'win32' ? 'junction' : 'dir');
|
|
121
|
+
linked.push(name);
|
|
122
|
+
} catch (cause) {
|
|
123
|
+
// `cause` is `unknown` at this boundary. `ErrnoException` rather than `Error`
|
|
124
|
+
// because `code` lives on the former — an fs failure is what is expected here,
|
|
125
|
+
// and reading `.code` off a plain `Error` is not a thing that exists.
|
|
126
|
+
/** @type {string} */
|
|
127
|
+
let detail;
|
|
128
|
+
if (cause instanceof Error) {
|
|
129
|
+
const code = /** @type {NodeJS.ErrnoException} */ (cause).code;
|
|
130
|
+
detail = typeof code === 'string' ? code : cause.message;
|
|
131
|
+
} else {
|
|
132
|
+
detail = String(cause);
|
|
133
|
+
}
|
|
134
|
+
skipped.push(`${name} (${detail})`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (linked.length > 0) {
|
|
139
|
+
console.log(`[link-runtime] linked ${linked.length} package(s) from ${runtime}`);
|
|
140
|
+
}
|
|
141
|
+
if (skipped.length > 0 && process.env.DSH_LINK_VERBOSE === '1') {
|
|
142
|
+
console.log(`[link-runtime] skipped: ${skipped.join(', ')}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
main();
|