dsh-gitnexus-plus 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.
Files changed (78) hide show
  1. package/LICENSE +38 -0
  2. package/README.md +483 -2
  3. package/cordis.patch.yml +18 -0
  4. package/lib/bin/gitnexus-coverage.d.ts +14 -0
  5. package/lib/bin/gitnexus-coverage.d.ts.map +1 -0
  6. package/lib/bin/gitnexus-coverage.js +171 -0
  7. package/lib/bin/gitnexus-coverage.js.map +1 -0
  8. package/lib/client.js +1642 -0
  9. package/lib/gitnexus/analyze.d.ts +148 -0
  10. package/lib/gitnexus/analyze.d.ts.map +1 -0
  11. package/lib/gitnexus/analyze.js +283 -0
  12. package/lib/gitnexus/analyze.js.map +1 -0
  13. package/lib/gitnexus/commands.d.ts +96 -0
  14. package/lib/gitnexus/commands.d.ts.map +1 -0
  15. package/lib/gitnexus/commands.js +132 -0
  16. package/lib/gitnexus/commands.js.map +1 -0
  17. package/lib/gitnexus/coverage.d.ts +340 -0
  18. package/lib/gitnexus/coverage.d.ts.map +1 -0
  19. package/lib/gitnexus/coverage.js +552 -0
  20. package/lib/gitnexus/coverage.js.map +1 -0
  21. package/lib/gitnexus/format.d.ts +16 -0
  22. package/lib/gitnexus/format.d.ts.map +1 -0
  23. package/lib/gitnexus/format.js +178 -0
  24. package/lib/gitnexus/format.js.map +1 -0
  25. package/lib/gitnexus/git-activity.d.ts +136 -0
  26. package/lib/gitnexus/git-activity.d.ts.map +1 -0
  27. package/lib/gitnexus/git-activity.js +345 -0
  28. package/lib/gitnexus/git-activity.js.map +1 -0
  29. package/lib/gitnexus/git.d.ts +70 -0
  30. package/lib/gitnexus/git.d.ts.map +1 -0
  31. package/lib/gitnexus/git.js +121 -0
  32. package/lib/gitnexus/git.js.map +1 -0
  33. package/lib/gitnexus/index-meta.d.ts +150 -0
  34. package/lib/gitnexus/index-meta.d.ts.map +1 -0
  35. package/lib/gitnexus/index-meta.js +156 -0
  36. package/lib/gitnexus/index-meta.js.map +1 -0
  37. package/lib/gitnexus/install.d.ts +97 -0
  38. package/lib/gitnexus/install.d.ts.map +1 -0
  39. package/lib/gitnexus/install.js +228 -0
  40. package/lib/gitnexus/install.js.map +1 -0
  41. package/lib/gitnexus/languages.d.ts +73 -0
  42. package/lib/gitnexus/languages.d.ts.map +1 -0
  43. package/lib/gitnexus/languages.js +217 -0
  44. package/lib/gitnexus/languages.js.map +1 -0
  45. package/lib/gitnexus/root.d.ts +68 -0
  46. package/lib/gitnexus/root.d.ts.map +1 -0
  47. package/lib/gitnexus/root.js +162 -0
  48. package/lib/gitnexus/root.js.map +1 -0
  49. package/lib/gitnexus/scan.d.ts +67 -0
  50. package/lib/gitnexus/scan.d.ts.map +1 -0
  51. package/lib/gitnexus/scan.js +247 -0
  52. package/lib/gitnexus/scan.js.map +1 -0
  53. package/lib/gitnexus/toolchain.d.ts +136 -0
  54. package/lib/gitnexus/toolchain.d.ts.map +1 -0
  55. package/lib/gitnexus/toolchain.js +251 -0
  56. package/lib/gitnexus/toolchain.js.map +1 -0
  57. package/lib/index.d.ts +79 -0
  58. package/lib/index.d.ts.map +1 -0
  59. package/lib/index.js +211 -0
  60. package/lib/index.js.map +1 -0
  61. package/lib/routes/analyze.d.ts +35 -0
  62. package/lib/routes/analyze.d.ts.map +1 -0
  63. package/lib/routes/analyze.js +71 -0
  64. package/lib/routes/analyze.js.map +1 -0
  65. package/lib/routes/coverage.d.ts +44 -0
  66. package/lib/routes/coverage.d.ts.map +1 -0
  67. package/lib/routes/coverage.js +68 -0
  68. package/lib/routes/coverage.js.map +1 -0
  69. package/lib/routes/guard.d.ts +92 -0
  70. package/lib/routes/guard.d.ts.map +1 -0
  71. package/lib/routes/guard.js +187 -0
  72. package/lib/routes/guard.js.map +1 -0
  73. package/lib/routes/install.d.ts +27 -0
  74. package/lib/routes/install.d.ts.map +1 -0
  75. package/lib/routes/install.js +46 -0
  76. package/lib/routes/install.js.map +1 -0
  77. package/package.json +87 -4
  78. 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"}
package/package.json CHANGED
@@ -1,6 +1,89 @@
1
1
  {
2
2
  "name": "dsh-gitnexus-plus",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "DSH plugin: visualize a GitNexus project index, 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();