soup-chop 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +901 -0
- package/dist/index.js +2056 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2056 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
|
|
4
|
+
// src/lib/clientConfig.ts
|
|
5
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
var CLIENT_ALIASES = {
|
|
9
|
+
"claude": "claude-desktop",
|
|
10
|
+
"claude_desktop": "claude-desktop",
|
|
11
|
+
"claude-desktop": "claude-desktop",
|
|
12
|
+
"claudecode": "claude-code",
|
|
13
|
+
"claude_code": "claude-code",
|
|
14
|
+
"claude-code": "claude-code",
|
|
15
|
+
"vscode": "vscode-copilot",
|
|
16
|
+
"vscode_copilot": "vscode-copilot",
|
|
17
|
+
"vscode-copilot": "vscode-copilot",
|
|
18
|
+
"copilot": "vscode-copilot",
|
|
19
|
+
"gemini": "gemini-cli",
|
|
20
|
+
"geminicli": "gemini-cli",
|
|
21
|
+
"gemini_cli": "gemini-cli",
|
|
22
|
+
"gemini-code-assist": "gemini-code-assist",
|
|
23
|
+
"gemini_code_assist": "gemini-code-assist",
|
|
24
|
+
"firebase": "firebase-studio",
|
|
25
|
+
"firebase_studio": "firebase-studio",
|
|
26
|
+
"firebase-studio": "firebase-studio"
|
|
27
|
+
};
|
|
28
|
+
var DEFAULT_SOUP_CHOP_SERVER = {
|
|
29
|
+
command: "npx",
|
|
30
|
+
args: ["soup-chop", "serve"]
|
|
31
|
+
};
|
|
32
|
+
function buildSoupChopServer(options = {}) {
|
|
33
|
+
if (!options.localBuild) {
|
|
34
|
+
return DEFAULT_SOUP_CHOP_SERVER;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
command: "node",
|
|
38
|
+
args: [resolve(options.cwd ?? process.cwd(), "dist", "index.js"), "serve"]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
var CLIENTS = [
|
|
42
|
+
{
|
|
43
|
+
id: "windsurf",
|
|
44
|
+
name: "Windsurf",
|
|
45
|
+
status: "implemented",
|
|
46
|
+
configPath: "~/.codeium/windsurf/mcp_config.json",
|
|
47
|
+
scope: "global",
|
|
48
|
+
notes: "Official Windsurf docs use mcp_config.json with an mcpServers object.",
|
|
49
|
+
format: "mcpServers"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "cursor",
|
|
53
|
+
name: "Cursor",
|
|
54
|
+
status: "implemented",
|
|
55
|
+
configPath: "~/.cursor/mcp.json",
|
|
56
|
+
projectConfigPath: ".cursor/mcp.json",
|
|
57
|
+
scope: "global",
|
|
58
|
+
notes: "Official docs also support per-project .cursor/mcp.json.",
|
|
59
|
+
format: "mcpServers"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "claude-desktop",
|
|
63
|
+
name: "Claude Desktop",
|
|
64
|
+
status: "implemented",
|
|
65
|
+
scope: "global",
|
|
66
|
+
notes: "Official docs expose this file via Claude > Settings > Developer > Edit Config on macOS and Windows.",
|
|
67
|
+
format: "mcpServers"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "claude-code",
|
|
71
|
+
name: "Claude Code",
|
|
72
|
+
status: "documented",
|
|
73
|
+
scope: "ui",
|
|
74
|
+
notes: "Official docs recommend claude mcp add or plugin installation rather than direct file editing.",
|
|
75
|
+
format: "manual"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "antigravity",
|
|
79
|
+
name: "Antigravity",
|
|
80
|
+
status: "documented",
|
|
81
|
+
scope: "ui",
|
|
82
|
+
notes: "Official docs describe MCP Store / View raw config flow, but do not expose a stable file path.",
|
|
83
|
+
format: "manual"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "cline",
|
|
87
|
+
name: "Cline",
|
|
88
|
+
status: "documented",
|
|
89
|
+
scope: "ui",
|
|
90
|
+
notes: "Official docs expose cline_mcp_settings.json from the MCP Servers UI, but path varies by install.",
|
|
91
|
+
format: "manual"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "vscode-copilot",
|
|
95
|
+
name: "VS Code Copilot",
|
|
96
|
+
status: "implemented",
|
|
97
|
+
configPath: ".vscode/mcp.json",
|
|
98
|
+
scope: "project",
|
|
99
|
+
notes: "Official docs support project .vscode/mcp.json or user settings under mcp.servers.",
|
|
100
|
+
format: "servers"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "gemini-cli",
|
|
104
|
+
name: "Gemini CLI",
|
|
105
|
+
status: "documented",
|
|
106
|
+
configPath: "~/.gemini/settings.json",
|
|
107
|
+
scope: "global",
|
|
108
|
+
notes: "Official docs also support project .gemini/settings.json.",
|
|
109
|
+
format: "mcpServers"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "gemini-code-assist",
|
|
113
|
+
name: "Gemini Code Assist",
|
|
114
|
+
status: "documented",
|
|
115
|
+
configPath: "~/.gemini/settings.json",
|
|
116
|
+
scope: "global",
|
|
117
|
+
notes: "Official docs share the Gemini settings format with Gemini CLI.",
|
|
118
|
+
format: "mcpServers"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "firebase-studio",
|
|
122
|
+
name: "Firebase Studio",
|
|
123
|
+
status: "implemented",
|
|
124
|
+
configPath: ".idx/mcp.json",
|
|
125
|
+
scope: "project",
|
|
126
|
+
notes: "Official docs use a project-local .idx/mcp.json file.",
|
|
127
|
+
format: "mcpServers"
|
|
128
|
+
}
|
|
129
|
+
];
|
|
130
|
+
function expandHome(path) {
|
|
131
|
+
return path.startsWith("~/") ? join(homedir(), path.slice(2)) : path;
|
|
132
|
+
}
|
|
133
|
+
function resolvePathForScope(path, scope, cwd) {
|
|
134
|
+
if (path.startsWith("~/")) {
|
|
135
|
+
return expandHome(path);
|
|
136
|
+
}
|
|
137
|
+
if (scope === "project" && !isAbsolute(path)) {
|
|
138
|
+
return join(cwd, path);
|
|
139
|
+
}
|
|
140
|
+
return path;
|
|
141
|
+
}
|
|
142
|
+
function resolveClaudeDesktopConfigPath(scope) {
|
|
143
|
+
if (scope !== "global") {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
if (process.platform === "darwin") {
|
|
147
|
+
return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
148
|
+
}
|
|
149
|
+
if (process.platform === "win32") {
|
|
150
|
+
const appData = process.env["APPDATA"];
|
|
151
|
+
return appData ? join(appData, "Claude", "claude_desktop_config.json") : null;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
function getClientConfigScopes(client) {
|
|
156
|
+
const scopes = /* @__PURE__ */ new Set();
|
|
157
|
+
if (client.scope === "global" && client.configPath) {
|
|
158
|
+
scopes.add("global");
|
|
159
|
+
}
|
|
160
|
+
if (client.scope === "project" && client.configPath) {
|
|
161
|
+
scopes.add("project");
|
|
162
|
+
}
|
|
163
|
+
if (client.globalConfigPath) {
|
|
164
|
+
scopes.add("global");
|
|
165
|
+
}
|
|
166
|
+
if (client.projectConfigPath) {
|
|
167
|
+
scopes.add("project");
|
|
168
|
+
}
|
|
169
|
+
if (client.id === "claude-desktop") {
|
|
170
|
+
scopes.add("global");
|
|
171
|
+
}
|
|
172
|
+
return [...scopes];
|
|
173
|
+
}
|
|
174
|
+
function getDefaultClientConfigScope(client) {
|
|
175
|
+
if (client.scope === "global" || client.scope === "project") {
|
|
176
|
+
return client.scope;
|
|
177
|
+
}
|
|
178
|
+
const [first] = getClientConfigScopes(client);
|
|
179
|
+
return first ?? null;
|
|
180
|
+
}
|
|
181
|
+
function getDocumentedClientTargets(client) {
|
|
182
|
+
const targets = /* @__PURE__ */ new Set();
|
|
183
|
+
if (client.configPath) {
|
|
184
|
+
targets.add(client.configPath);
|
|
185
|
+
}
|
|
186
|
+
if (client.globalConfigPath) {
|
|
187
|
+
targets.add(client.globalConfigPath);
|
|
188
|
+
}
|
|
189
|
+
if (client.projectConfigPath) {
|
|
190
|
+
targets.add(client.projectConfigPath);
|
|
191
|
+
}
|
|
192
|
+
return [...targets];
|
|
193
|
+
}
|
|
194
|
+
function getClientDisplayPath(client, options = {}) {
|
|
195
|
+
return resolveClientConfigPath(client, options) ?? getDocumentedClientTargets(client)[0] ?? "UI-managed";
|
|
196
|
+
}
|
|
197
|
+
function getClientMode(client, options = {}) {
|
|
198
|
+
if (client.status === "implemented" && resolveClientConfigPath(client, options) !== null) {
|
|
199
|
+
return "auto-config";
|
|
200
|
+
}
|
|
201
|
+
if (client.status === "implemented") {
|
|
202
|
+
return "implemented elsewhere";
|
|
203
|
+
}
|
|
204
|
+
return client.status;
|
|
205
|
+
}
|
|
206
|
+
function listClientEntries(options = {}) {
|
|
207
|
+
return CLIENTS.map((client) => ({
|
|
208
|
+
id: client.id,
|
|
209
|
+
name: client.name,
|
|
210
|
+
status: client.status,
|
|
211
|
+
mode: getClientMode(client, options),
|
|
212
|
+
format: client.format,
|
|
213
|
+
supportedScopes: getClientConfigScopes(client),
|
|
214
|
+
defaultScope: getDefaultClientConfigScope(client),
|
|
215
|
+
displayPath: getClientDisplayPath(client, options),
|
|
216
|
+
resolvedPath: resolveClientConfigPath(client, options),
|
|
217
|
+
notes: client.notes
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
function getClientSetupGuidance(client, options = {}) {
|
|
221
|
+
const entry = listClientEntries(options).find((candidate) => candidate.id === client.id);
|
|
222
|
+
const targets = getDocumentedClientTargets(client);
|
|
223
|
+
const targetText = targets.length > 0 ? targets.join(", ") : entry?.displayPath ?? "UI-managed";
|
|
224
|
+
return `Documented target: ${targetText}. ${client.notes} See \`soup-chop clients\` for the full client matrix.`;
|
|
225
|
+
}
|
|
226
|
+
function resolveClientConfigPath(client, options = {}) {
|
|
227
|
+
const scope = options.scope ?? getDefaultClientConfigScope(client);
|
|
228
|
+
if (!scope) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
if (client.id === "claude-desktop") {
|
|
232
|
+
return resolveClaudeDesktopConfigPath(scope);
|
|
233
|
+
}
|
|
234
|
+
const template = scope === "global" ? client.globalConfigPath ?? (client.scope === "global" ? client.configPath : void 0) : client.projectConfigPath ?? (client.scope === "project" ? client.configPath : void 0);
|
|
235
|
+
if (!template) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
return resolvePathForScope(template, scope, options.cwd ?? process.cwd());
|
|
239
|
+
}
|
|
240
|
+
function normalizeClientId(input) {
|
|
241
|
+
const normalized = input.trim().toLowerCase();
|
|
242
|
+
return CLIENT_ALIASES[normalized] ?? normalized;
|
|
243
|
+
}
|
|
244
|
+
function resolveClient(input) {
|
|
245
|
+
const id = normalizeClientId(input);
|
|
246
|
+
const client = CLIENTS.find((entry) => entry.id === id);
|
|
247
|
+
if (!client) {
|
|
248
|
+
throw new Error(`Unknown client: "${input}". Run \`soup-chop clients\` to see supported client IDs.`);
|
|
249
|
+
}
|
|
250
|
+
return client;
|
|
251
|
+
}
|
|
252
|
+
function getSupportedScopeOrThrow(client, requestedScope) {
|
|
253
|
+
if (!requestedScope) {
|
|
254
|
+
const defaultScope = getDefaultClientConfigScope(client);
|
|
255
|
+
if (defaultScope) {
|
|
256
|
+
return defaultScope;
|
|
257
|
+
}
|
|
258
|
+
if (client.format === "manual") {
|
|
259
|
+
throw new Error(`Client ${client.name} is documented but not file-configurable automatically. ${getClientSetupGuidance(client)}`);
|
|
260
|
+
}
|
|
261
|
+
throw new Error(`Client ${client.name} is not auto-configurable on this platform. ${getClientSetupGuidance(client)}`);
|
|
262
|
+
}
|
|
263
|
+
const supportedScopes = getClientConfigScopes(client);
|
|
264
|
+
if (supportedScopes.includes(requestedScope)) {
|
|
265
|
+
return requestedScope;
|
|
266
|
+
}
|
|
267
|
+
const supported = supportedScopes.length > 0 ? supportedScopes.join(", ") : "none";
|
|
268
|
+
const suggestedScope = getDefaultClientConfigScope(client);
|
|
269
|
+
const suggestion = suggestedScope ? ` Try \`soup-chop configure ${client.id}${suggestedScope === "project" ? " --project" : " --global"}\` or omit the scope.` : "";
|
|
270
|
+
throw new Error(`Client ${client.name} does not support ${requestedScope} configuration. Supported scopes: ${supported}.${suggestion} ${getClientSetupGuidance(client)}`);
|
|
271
|
+
}
|
|
272
|
+
function parseJsonObject(content) {
|
|
273
|
+
const parsed = JSON.parse(content);
|
|
274
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
275
|
+
throw new Error("Expected a JSON object at config root");
|
|
276
|
+
}
|
|
277
|
+
return parsed;
|
|
278
|
+
}
|
|
279
|
+
async function configureClient(id, options = {}) {
|
|
280
|
+
const prepared = await prepareClientConfig(id, options);
|
|
281
|
+
await mkdir(dirname(prepared.path), { recursive: true });
|
|
282
|
+
await writeFile(prepared.path, prepared.content, "utf8");
|
|
283
|
+
return {
|
|
284
|
+
client: prepared.client,
|
|
285
|
+
path: prepared.path,
|
|
286
|
+
created: prepared.created
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
async function prepareClientConfig(id, options = {}) {
|
|
290
|
+
const client = resolveClient(id);
|
|
291
|
+
const scope = getSupportedScopeOrThrow(client, options.scope);
|
|
292
|
+
const path = resolveClientConfigPath(client, { ...options, scope });
|
|
293
|
+
if (!path) {
|
|
294
|
+
throw new Error(`Client ${client.name} does not expose a stable file path for automatic configuration`);
|
|
295
|
+
}
|
|
296
|
+
const { root, existed } = await readConfig(path);
|
|
297
|
+
const updated = buildConfiguredRoot(root, client, options);
|
|
298
|
+
return {
|
|
299
|
+
client,
|
|
300
|
+
path,
|
|
301
|
+
created: !existed,
|
|
302
|
+
root: updated,
|
|
303
|
+
content: `${JSON.stringify(updated, null, 2)}
|
|
304
|
+
`
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function buildConfigSnippet(client, options = {}) {
|
|
308
|
+
return buildConfiguredRoot({}, client, options);
|
|
309
|
+
}
|
|
310
|
+
function buildConfiguredRoot(root, client, options = {}) {
|
|
311
|
+
if (client.format === "manual") {
|
|
312
|
+
throw new Error(`Client ${client.name} is documented but not file-configurable automatically. ${getClientSetupGuidance(client, options)}`);
|
|
313
|
+
}
|
|
314
|
+
const key = client.format;
|
|
315
|
+
const existing = root[key];
|
|
316
|
+
const servers = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
|
|
317
|
+
servers["soup-chop"] = buildSoupChopServer(options);
|
|
318
|
+
return { ...root, [key]: servers };
|
|
319
|
+
}
|
|
320
|
+
async function readConfig(path) {
|
|
321
|
+
try {
|
|
322
|
+
const content = await readFile(path, "utf8");
|
|
323
|
+
return { root: parseJsonObject(content), existed: true };
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (error.code === "ENOENT") {
|
|
326
|
+
return { root: {}, existed: false };
|
|
327
|
+
}
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function getSupportedAutoConfigClients(options = {}) {
|
|
332
|
+
return CLIENTS.filter((client) => getClientMode(client, options) === "auto-config");
|
|
333
|
+
}
|
|
334
|
+
function getSupportedAutoConfigClientIds(options = {}) {
|
|
335
|
+
return getSupportedAutoConfigClients(options).map((client) => client.id);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/lib/configureArgs.ts
|
|
339
|
+
function parseConfigureArgs(args, supportedClientIds) {
|
|
340
|
+
const [client, ...flags] = args;
|
|
341
|
+
if (!client) {
|
|
342
|
+
throw new Error(`Missing client name.
|
|
343
|
+
|
|
344
|
+
Usage: soup-chop configure <client> [--project|--global] [--dry-run|--print] [--local-build]
|
|
345
|
+
Supported clients: ${supportedClientIds.join(", ")}`);
|
|
346
|
+
}
|
|
347
|
+
let scope;
|
|
348
|
+
let localBuild = false;
|
|
349
|
+
let outputMode = "write";
|
|
350
|
+
for (const flag of flags) {
|
|
351
|
+
if (flag === "--project") {
|
|
352
|
+
if (scope && scope !== "project") {
|
|
353
|
+
throw new Error("Choose only one configure scope: --project or --global");
|
|
354
|
+
}
|
|
355
|
+
scope = "project";
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (flag === "--global") {
|
|
359
|
+
if (scope && scope !== "global") {
|
|
360
|
+
throw new Error("Choose only one configure scope: --project or --global");
|
|
361
|
+
}
|
|
362
|
+
scope = "global";
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (flag === "--dry-run") {
|
|
366
|
+
if (outputMode !== "write") {
|
|
367
|
+
throw new Error("Choose only one configure output mode: --dry-run or --print");
|
|
368
|
+
}
|
|
369
|
+
outputMode = "dry-run";
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (flag === "--print") {
|
|
373
|
+
if (outputMode !== "write") {
|
|
374
|
+
throw new Error("Choose only one configure output mode: --dry-run or --print");
|
|
375
|
+
}
|
|
376
|
+
outputMode = "print";
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (flag === "--local-build") {
|
|
380
|
+
localBuild = true;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
throw new Error(`Unknown configure option: "${flag}"
|
|
384
|
+
|
|
385
|
+
Usage: soup-chop configure <client> [--project|--global] [--dry-run|--print] [--local-build]`);
|
|
386
|
+
}
|
|
387
|
+
return { client, scope, localBuild, outputMode };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/server.ts
|
|
391
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
392
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
393
|
+
import { z } from "zod";
|
|
394
|
+
|
|
395
|
+
// src/lib/cache.ts
|
|
396
|
+
import envPaths from "env-paths";
|
|
397
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
398
|
+
import { join as join2 } from "node:path";
|
|
399
|
+
var paths = envPaths("soup-chop");
|
|
400
|
+
function cachePath(pkg, version, filename) {
|
|
401
|
+
return join2(paths.cache, pkg, version, filename);
|
|
402
|
+
}
|
|
403
|
+
async function readCached(pkg, version, filename) {
|
|
404
|
+
try {
|
|
405
|
+
return await readFile2(cachePath(pkg, version, filename), "utf-8");
|
|
406
|
+
} catch {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function writeCache(pkg, version, filename, content) {
|
|
411
|
+
const dir = join2(paths.cache, pkg, version);
|
|
412
|
+
await mkdir2(dir, { recursive: true });
|
|
413
|
+
await writeFile2(join2(dir, filename), content, "utf-8");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/lib/isExactVersion.ts
|
|
417
|
+
var EXACT_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
418
|
+
function isExactVersion(version) {
|
|
419
|
+
return EXACT_VERSION_RE.test(version);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/lib/fetchReadme.ts
|
|
423
|
+
var UNPKG_BASE = "https://unpkg.com";
|
|
424
|
+
function normalizeMarkdownOrigin(origin = "README.md") {
|
|
425
|
+
const normalized = origin.replace(/^\/+/, "");
|
|
426
|
+
return normalized.length > 0 ? normalized : "README.md";
|
|
427
|
+
}
|
|
428
|
+
function markdownCacheFile(origin = "README.md") {
|
|
429
|
+
const normalized = normalizeMarkdownOrigin(origin);
|
|
430
|
+
return normalized === "README.md" ? normalized : `source__${normalized.replace(/\//g, "__")}`;
|
|
431
|
+
}
|
|
432
|
+
async function fetchMarkdownFile(pkg, version, origin = "README.md") {
|
|
433
|
+
const normalizedOrigin = normalizeMarkdownOrigin(origin);
|
|
434
|
+
const cacheFile = markdownCacheFile(normalizedOrigin);
|
|
435
|
+
const canCache = isExactVersion(version);
|
|
436
|
+
if (canCache) {
|
|
437
|
+
const cached = await readCached(pkg, version, cacheFile);
|
|
438
|
+
if (cached !== null) return cached;
|
|
439
|
+
}
|
|
440
|
+
const url = `${UNPKG_BASE}/${pkg}@${version}/${normalizedOrigin}`;
|
|
441
|
+
const res = await fetch(url);
|
|
442
|
+
if (!res.ok) {
|
|
443
|
+
throw new Error(`Failed to fetch markdown file ${normalizedOrigin} for ${pkg}@${version}: ${res.status} ${res.statusText}`);
|
|
444
|
+
}
|
|
445
|
+
const content = await res.text();
|
|
446
|
+
if (canCache) {
|
|
447
|
+
await writeCache(pkg, version, cacheFile, content);
|
|
448
|
+
}
|
|
449
|
+
return content;
|
|
450
|
+
}
|
|
451
|
+
async function fetchReadme(pkg, version) {
|
|
452
|
+
return fetchMarkdownFile(pkg, version, "README.md");
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/lib/discoverSources.ts
|
|
456
|
+
import { posix } from "node:path";
|
|
457
|
+
var UNPKG_BASE2 = "https://unpkg.com";
|
|
458
|
+
var SOURCE_MANIFEST_VERSION = 1;
|
|
459
|
+
var TOP_LEVEL_DOC_ALLOWLIST = /* @__PURE__ */ new Set(["API.MD", "FAQ.MD", "MIGRATING.MD", "UPGRADING.MD", "CHANGELOG.MD", "CONTRIBUTING.MD"]);
|
|
460
|
+
function normalizeOrigin(origin) {
|
|
461
|
+
return origin.replace(/^\//, "");
|
|
462
|
+
}
|
|
463
|
+
function canonicalTopLevelDocName(path) {
|
|
464
|
+
return path.replace(/\.md$/i, ".md").toUpperCase();
|
|
465
|
+
}
|
|
466
|
+
function isSearchSource(source) {
|
|
467
|
+
return source !== null;
|
|
468
|
+
}
|
|
469
|
+
function cacheFileForOrigin(origin) {
|
|
470
|
+
const normalized = normalizeOrigin(origin);
|
|
471
|
+
if (normalized === "README.md") {
|
|
472
|
+
return normalized;
|
|
473
|
+
}
|
|
474
|
+
return `source__${normalized.replace(/\//g, "__")}`;
|
|
475
|
+
}
|
|
476
|
+
function createSourceId(origin) {
|
|
477
|
+
return normalizeOrigin(origin).replace(/[^a-zA-Z0-9]+/g, "__").replace(/^__+|__+$/g, "").toLowerCase();
|
|
478
|
+
}
|
|
479
|
+
function createSource(sourceKind, origin, content) {
|
|
480
|
+
const normalized = normalizeOrigin(origin);
|
|
481
|
+
return {
|
|
482
|
+
sourceId: createSourceId(normalized),
|
|
483
|
+
sourceKind,
|
|
484
|
+
origin: normalized,
|
|
485
|
+
title: posix.basename(normalized),
|
|
486
|
+
content
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
async function readManifestSources(pkg, version) {
|
|
490
|
+
const raw = await readCached(pkg, version, "sources-manifest.json");
|
|
491
|
+
if (raw === null) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
let manifest;
|
|
495
|
+
try {
|
|
496
|
+
manifest = JSON.parse(raw);
|
|
497
|
+
} catch {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
if (manifest.version !== SOURCE_MANIFEST_VERSION) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
const sources = await Promise.all(
|
|
504
|
+
manifest.sources.map(async (entry) => {
|
|
505
|
+
const content = await readCached(pkg, version, entry.cacheFile);
|
|
506
|
+
if (content === null) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
sourceId: entry.sourceId,
|
|
511
|
+
sourceKind: entry.sourceKind,
|
|
512
|
+
origin: entry.origin,
|
|
513
|
+
title: entry.title,
|
|
514
|
+
content
|
|
515
|
+
};
|
|
516
|
+
})
|
|
517
|
+
);
|
|
518
|
+
return sources.every(isSearchSource) ? sources : null;
|
|
519
|
+
}
|
|
520
|
+
async function writeManifestSources(pkg, version, sources) {
|
|
521
|
+
await Promise.all(
|
|
522
|
+
sources.map((source) => writeCache(pkg, version, cacheFileForOrigin(source.origin), source.content))
|
|
523
|
+
);
|
|
524
|
+
const manifest = {
|
|
525
|
+
version: SOURCE_MANIFEST_VERSION,
|
|
526
|
+
sources: sources.map((source) => ({
|
|
527
|
+
sourceId: source.sourceId,
|
|
528
|
+
sourceKind: source.sourceKind,
|
|
529
|
+
origin: source.origin,
|
|
530
|
+
title: source.title,
|
|
531
|
+
cacheFile: cacheFileForOrigin(source.origin)
|
|
532
|
+
}))
|
|
533
|
+
};
|
|
534
|
+
await writeCache(pkg, version, "sources-manifest.json", JSON.stringify(manifest, null, 2));
|
|
535
|
+
}
|
|
536
|
+
async function fetchMetaFiles(pkg, version, dir, canCache) {
|
|
537
|
+
const cacheKey = `meta__${dir}.json`;
|
|
538
|
+
if (canCache) {
|
|
539
|
+
const cached = await readCached(pkg, version, cacheKey);
|
|
540
|
+
if (cached !== null) {
|
|
541
|
+
const parsed = JSON.parse(cached);
|
|
542
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const response = await fetch(`${UNPKG_BASE2}/${pkg}@${version}/${dir}/?meta`);
|
|
546
|
+
if (response.status === 404) {
|
|
547
|
+
return [];
|
|
548
|
+
}
|
|
549
|
+
if (!response.ok) {
|
|
550
|
+
throw new Error(`Failed to fetch package docs metadata for ${pkg}@${version} in ${dir}/: ${response.status} ${response.statusText}`);
|
|
551
|
+
}
|
|
552
|
+
const meta = await response.json();
|
|
553
|
+
const paths2 = selectPackageDocPaths(
|
|
554
|
+
(meta.files ?? []).flatMap((file) => typeof file.path === "string" ? [file.path] : [])
|
|
555
|
+
);
|
|
556
|
+
if (canCache) {
|
|
557
|
+
await writeCache(pkg, version, cacheKey, JSON.stringify(paths2));
|
|
558
|
+
}
|
|
559
|
+
return paths2;
|
|
560
|
+
}
|
|
561
|
+
async function fetchRootMetaFiles(pkg, version, canCache) {
|
|
562
|
+
const cacheKey = "meta__root.json";
|
|
563
|
+
if (canCache) {
|
|
564
|
+
const cached = await readCached(pkg, version, cacheKey);
|
|
565
|
+
if (cached !== null) {
|
|
566
|
+
const parsed = JSON.parse(cached);
|
|
567
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const response = await fetch(`${UNPKG_BASE2}/${pkg}@${version}/?meta`);
|
|
571
|
+
if (!response.ok) {
|
|
572
|
+
throw new Error(`Failed to fetch package root metadata for ${pkg}@${version}: ${response.status} ${response.statusText}`);
|
|
573
|
+
}
|
|
574
|
+
const meta = await response.json();
|
|
575
|
+
const paths2 = selectTopLevelDocPaths(
|
|
576
|
+
(meta.files ?? []).flatMap((file) => typeof file.path === "string" ? [file.path] : [])
|
|
577
|
+
);
|
|
578
|
+
if (canCache) {
|
|
579
|
+
await writeCache(pkg, version, cacheKey, JSON.stringify(paths2));
|
|
580
|
+
}
|
|
581
|
+
return paths2;
|
|
582
|
+
}
|
|
583
|
+
async function fetchSource(pkg, version, sourceKind, origin) {
|
|
584
|
+
const normalized = normalizeOrigin(origin);
|
|
585
|
+
const content = await fetchMarkdownFile(pkg, version, normalized);
|
|
586
|
+
return createSource(sourceKind, normalized, content);
|
|
587
|
+
}
|
|
588
|
+
function selectPackageDocPaths(paths2) {
|
|
589
|
+
return [...new Set(
|
|
590
|
+
paths2.map((path) => normalizeOrigin(path)).filter((path) => /^(?:docs|doc)\//.test(path)).filter((path) => /\.md$/i.test(path))
|
|
591
|
+
)].sort((left, right) => left.localeCompare(right));
|
|
592
|
+
}
|
|
593
|
+
function selectTopLevelDocPaths(paths2) {
|
|
594
|
+
return [...new Set(
|
|
595
|
+
paths2.map((path) => normalizeOrigin(path)).filter((path) => !path.includes("/")).filter((path) => /\.md$/i.test(path)).filter((path) => TOP_LEVEL_DOC_ALLOWLIST.has(canonicalTopLevelDocName(path)))
|
|
596
|
+
)].sort((left, right) => left.localeCompare(right));
|
|
597
|
+
}
|
|
598
|
+
async function discoverSources(pkg, version) {
|
|
599
|
+
const canCache = isExactVersion(version);
|
|
600
|
+
if (canCache) {
|
|
601
|
+
const cached = await readManifestSources(pkg, version);
|
|
602
|
+
if (cached !== null) {
|
|
603
|
+
return cached;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const readme = await fetchReadme(pkg, version);
|
|
607
|
+
const [docPaths, docsPaths, topLevelDocPaths] = await Promise.all([
|
|
608
|
+
fetchMetaFiles(pkg, version, "doc", canCache),
|
|
609
|
+
fetchMetaFiles(pkg, version, "docs", canCache),
|
|
610
|
+
fetchRootMetaFiles(pkg, version, canCache)
|
|
611
|
+
]);
|
|
612
|
+
const packageDocOrigins = [.../* @__PURE__ */ new Set([...docPaths, ...docsPaths])];
|
|
613
|
+
const sources = [
|
|
614
|
+
createSource("readme", "README.md", readme),
|
|
615
|
+
...await Promise.all(packageDocOrigins.map((origin) => fetchSource(pkg, version, "package_doc", origin))),
|
|
616
|
+
...await Promise.all(topLevelDocPaths.map((origin) => fetchSource(pkg, version, "top_level_doc", origin)))
|
|
617
|
+
];
|
|
618
|
+
if (canCache) {
|
|
619
|
+
await writeManifestSources(pkg, version, sources);
|
|
620
|
+
}
|
|
621
|
+
return sources;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/lib/extractToc.ts
|
|
625
|
+
import { unified } from "unified";
|
|
626
|
+
import remarkParse from "remark-parse";
|
|
627
|
+
function hasPhrasingChildren(node) {
|
|
628
|
+
return "children" in node && Array.isArray(node.children);
|
|
629
|
+
}
|
|
630
|
+
function phrasingText(node) {
|
|
631
|
+
if (node.type === "text" || node.type === "inlineCode") {
|
|
632
|
+
return node.value;
|
|
633
|
+
}
|
|
634
|
+
if (hasPhrasingChildren(node)) {
|
|
635
|
+
return node.children.map((child) => phrasingText(child)).join("");
|
|
636
|
+
}
|
|
637
|
+
return "";
|
|
638
|
+
}
|
|
639
|
+
function headingText(node) {
|
|
640
|
+
return node.children.map((child) => phrasingText(child)).join("");
|
|
641
|
+
}
|
|
642
|
+
function buildPath(stack, current) {
|
|
643
|
+
const ancestors = stack.filter((s) => s.depth < current.depth);
|
|
644
|
+
return [...ancestors.map((s) => s.title), current.title].join("/");
|
|
645
|
+
}
|
|
646
|
+
function extractToc(markdown) {
|
|
647
|
+
const tree = unified().use(remarkParse).parse(markdown);
|
|
648
|
+
const totalLines = markdown.split("\n").length;
|
|
649
|
+
const rawHeadings = [];
|
|
650
|
+
for (const node of tree.children) {
|
|
651
|
+
if (node.type === "heading" && node.position) {
|
|
652
|
+
rawHeadings.push({
|
|
653
|
+
depth: node.depth,
|
|
654
|
+
title: headingText(node),
|
|
655
|
+
startLine: node.position.start.line
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const entries = [];
|
|
660
|
+
const stack = [];
|
|
661
|
+
for (let i = 0; i < rawHeadings.length; i++) {
|
|
662
|
+
const h = rawHeadings[i];
|
|
663
|
+
const endLine = i + 1 < rawHeadings.length ? rawHeadings[i + 1].startLine - 1 : totalLines;
|
|
664
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= h.depth) {
|
|
665
|
+
stack.pop();
|
|
666
|
+
}
|
|
667
|
+
const path = buildPath(stack, h);
|
|
668
|
+
stack.push({ depth: h.depth, title: h.title });
|
|
669
|
+
entries.push({ depth: h.depth, title: h.title, path, startLine: h.startLine, endLine });
|
|
670
|
+
}
|
|
671
|
+
return entries;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/lib/getTopic.ts
|
|
675
|
+
function getTopic(markdown, path, documentPath) {
|
|
676
|
+
if (documentPath === path) {
|
|
677
|
+
return markdown.trim();
|
|
678
|
+
}
|
|
679
|
+
const entries = extractToc(markdown);
|
|
680
|
+
const entry = entries.find((e) => e.path === path);
|
|
681
|
+
if (!entry) {
|
|
682
|
+
const available = entries.map((e) => e.path).join("\n ");
|
|
683
|
+
throw new Error(`Topic not found: "${path}"
|
|
684
|
+
Available paths:
|
|
685
|
+
${available}`);
|
|
686
|
+
}
|
|
687
|
+
return sliceLines(markdown, entry);
|
|
688
|
+
}
|
|
689
|
+
function sliceLines(markdown, entry) {
|
|
690
|
+
const lines = markdown.split("\n");
|
|
691
|
+
return lines.slice(entry.startLine - 1, entry.endLine).join("\n").trim();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/lib/extractChunks.ts
|
|
695
|
+
function extractMarkdownChunks(source) {
|
|
696
|
+
const entries = extractToc(source.content);
|
|
697
|
+
if (entries.length === 0) {
|
|
698
|
+
const content = source.content.trim();
|
|
699
|
+
if (content.length === 0) {
|
|
700
|
+
return [];
|
|
701
|
+
}
|
|
702
|
+
const totalLines = source.content.split("\n").length;
|
|
703
|
+
return [{
|
|
704
|
+
chunkId: `${source.sourceId}#1`,
|
|
705
|
+
sourceId: source.sourceId,
|
|
706
|
+
sourceKind: source.sourceKind,
|
|
707
|
+
sourceTitle: source.title,
|
|
708
|
+
origin: source.origin,
|
|
709
|
+
title: source.title,
|
|
710
|
+
path: source.origin,
|
|
711
|
+
startLine: 1,
|
|
712
|
+
endLine: totalLines,
|
|
713
|
+
content
|
|
714
|
+
}];
|
|
715
|
+
}
|
|
716
|
+
return entries.map((entry, index) => ({
|
|
717
|
+
chunkId: `${source.sourceId}#${index + 1}`,
|
|
718
|
+
sourceId: source.sourceId,
|
|
719
|
+
sourceKind: source.sourceKind,
|
|
720
|
+
sourceTitle: source.title,
|
|
721
|
+
origin: source.origin,
|
|
722
|
+
title: entry.title,
|
|
723
|
+
path: entry.path,
|
|
724
|
+
startLine: entry.startLine,
|
|
725
|
+
endLine: entry.endLine,
|
|
726
|
+
content: sliceLines(source.content, entry)
|
|
727
|
+
}));
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/lib/tocUtils.ts
|
|
731
|
+
import { posix as posix2 } from "node:path";
|
|
732
|
+
function buildTopicId(origin, path) {
|
|
733
|
+
const normalizedOrigin = normalizeMarkdownOrigin(origin);
|
|
734
|
+
if (path === void 0 || path === normalizedOrigin) {
|
|
735
|
+
return normalizedOrigin;
|
|
736
|
+
}
|
|
737
|
+
return `${normalizedOrigin}#${path}`;
|
|
738
|
+
}
|
|
739
|
+
function parseTopicId(topicId) {
|
|
740
|
+
const trimmed = topicId.trim();
|
|
741
|
+
if (trimmed.length === 0) {
|
|
742
|
+
throw new Error("Topic id must not be empty.");
|
|
743
|
+
}
|
|
744
|
+
const separatorIndex = trimmed.indexOf("#");
|
|
745
|
+
if (separatorIndex === -1) {
|
|
746
|
+
const origin2 = normalizeMarkdownOrigin(trimmed);
|
|
747
|
+
return { origin: origin2, path: origin2, topicId: buildTopicId(origin2) };
|
|
748
|
+
}
|
|
749
|
+
const origin = normalizeMarkdownOrigin(trimmed.slice(0, separatorIndex));
|
|
750
|
+
const path = trimmed.slice(separatorIndex + 1).trim();
|
|
751
|
+
if (path.length === 0) {
|
|
752
|
+
throw new Error(`Topic id "${topicId}" is missing a path after "#".`);
|
|
753
|
+
}
|
|
754
|
+
return { origin, path, topicId: buildTopicId(origin, path) };
|
|
755
|
+
}
|
|
756
|
+
function resolveTopicSelection(path, origin, topicId) {
|
|
757
|
+
if (topicId !== void 0) {
|
|
758
|
+
const selection = parseTopicId(topicId);
|
|
759
|
+
if (origin !== void 0 && normalizeMarkdownOrigin(origin) !== selection.origin) {
|
|
760
|
+
throw new Error(`Provided origin "${origin}" does not match topic_id origin "${selection.origin}".`);
|
|
761
|
+
}
|
|
762
|
+
if (path !== void 0 && path !== selection.path) {
|
|
763
|
+
throw new Error(`Provided path "${path}" does not match topic_id path "${selection.path}".`);
|
|
764
|
+
}
|
|
765
|
+
return selection;
|
|
766
|
+
}
|
|
767
|
+
if (path === void 0) {
|
|
768
|
+
if (origin === void 0) {
|
|
769
|
+
throw new Error("Either `topic_id` or `path` must be provided.");
|
|
770
|
+
}
|
|
771
|
+
const normalizedOrigin2 = normalizeMarkdownOrigin(origin);
|
|
772
|
+
return { origin: normalizedOrigin2, path: normalizedOrigin2, topicId: buildTopicId(normalizedOrigin2) };
|
|
773
|
+
}
|
|
774
|
+
const normalizedOrigin = normalizeMarkdownOrigin(origin);
|
|
775
|
+
return { origin: normalizedOrigin, path, topicId: buildTopicId(normalizedOrigin, path) };
|
|
776
|
+
}
|
|
777
|
+
function buildDocumentTocEntries(markdown, origin = "README.md", title = posix2.basename(normalizeMarkdownOrigin(origin))) {
|
|
778
|
+
const normalizedOrigin = normalizeMarkdownOrigin(origin);
|
|
779
|
+
const entries = extractToc(markdown);
|
|
780
|
+
if (entries.length === 0) {
|
|
781
|
+
return [{
|
|
782
|
+
depth: 0,
|
|
783
|
+
title,
|
|
784
|
+
path: normalizedOrigin,
|
|
785
|
+
startLine: 1,
|
|
786
|
+
endLine: markdown.split("\n").length,
|
|
787
|
+
origin: normalizedOrigin,
|
|
788
|
+
topicId: buildTopicId(normalizedOrigin)
|
|
789
|
+
}];
|
|
790
|
+
}
|
|
791
|
+
return entries.map((entry) => ({
|
|
792
|
+
...entry,
|
|
793
|
+
origin: normalizedOrigin,
|
|
794
|
+
topicId: buildTopicId(normalizedOrigin, entry.path)
|
|
795
|
+
}));
|
|
796
|
+
}
|
|
797
|
+
function buildPackageTocEntries(sources) {
|
|
798
|
+
return sources.flatMap((source) => {
|
|
799
|
+
const totalLines = source.content.split("\n").length;
|
|
800
|
+
const documentEntry = {
|
|
801
|
+
depth: 0,
|
|
802
|
+
title: source.title,
|
|
803
|
+
path: source.origin,
|
|
804
|
+
startLine: 1,
|
|
805
|
+
endLine: totalLines,
|
|
806
|
+
origin: source.origin,
|
|
807
|
+
topicId: buildTopicId(source.origin)
|
|
808
|
+
};
|
|
809
|
+
const entries = extractToc(source.content);
|
|
810
|
+
if (entries.length === 0) {
|
|
811
|
+
return [documentEntry];
|
|
812
|
+
}
|
|
813
|
+
return [
|
|
814
|
+
documentEntry,
|
|
815
|
+
...entries.map((entry) => ({
|
|
816
|
+
...entry,
|
|
817
|
+
origin: source.origin,
|
|
818
|
+
topicId: buildTopicId(source.origin, entry.path)
|
|
819
|
+
}))
|
|
820
|
+
];
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/lib/searchDocs.ts
|
|
825
|
+
var STOPWORDS = /* @__PURE__ */ new Set(["a", "an", "and", "are", "as", "at", "be", "by", "for", "from", "how", "in", "is", "it", "of", "on", "or", "that", "the", "to", "with"]);
|
|
826
|
+
var FIELD_WEIGHTS = { title: 5, path: 3, body: 1 };
|
|
827
|
+
var SEARCH_INDEX_VERSION = 1;
|
|
828
|
+
function searchIndexCacheFile() {
|
|
829
|
+
return "search-index.json";
|
|
830
|
+
}
|
|
831
|
+
function normalizeText(text) {
|
|
832
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
833
|
+
}
|
|
834
|
+
function tokenize(text) {
|
|
835
|
+
return normalizeText(text).split(/\s+/).filter((token) => token.length > 0).filter((token) => !STOPWORDS.has(token));
|
|
836
|
+
}
|
|
837
|
+
function countTokens(tokens) {
|
|
838
|
+
const frequencies = /* @__PURE__ */ new Map();
|
|
839
|
+
for (const token of tokens) {
|
|
840
|
+
frequencies.set(token, (frequencies.get(token) ?? 0) + 1);
|
|
841
|
+
}
|
|
842
|
+
return frequencies;
|
|
843
|
+
}
|
|
844
|
+
function buildSearchIndex(chunks) {
|
|
845
|
+
const documentFrequencies = /* @__PURE__ */ new Map();
|
|
846
|
+
const indexedChunks = chunks.map((chunk) => {
|
|
847
|
+
const titleTokens = tokenize(chunk.title);
|
|
848
|
+
const pathTokens = tokenize(chunk.path);
|
|
849
|
+
const bodyTokens = tokenize(chunk.content);
|
|
850
|
+
const seenTokens = /* @__PURE__ */ new Set([...titleTokens, ...pathTokens, ...bodyTokens]);
|
|
851
|
+
for (const token of seenTokens) {
|
|
852
|
+
documentFrequencies.set(token, (documentFrequencies.get(token) ?? 0) + 1);
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
chunk,
|
|
856
|
+
normalizedTitle: normalizeText(chunk.title),
|
|
857
|
+
normalizedPath: normalizeText(chunk.path),
|
|
858
|
+
normalizedBody: normalizeText(chunk.content),
|
|
859
|
+
titleFrequencies: countTokens(titleTokens),
|
|
860
|
+
pathFrequencies: countTokens(pathTokens),
|
|
861
|
+
bodyFrequencies: countTokens(bodyTokens)
|
|
862
|
+
};
|
|
863
|
+
});
|
|
864
|
+
return { chunks: indexedChunks, documentFrequencies };
|
|
865
|
+
}
|
|
866
|
+
function serializeSearchIndex(index) {
|
|
867
|
+
const serialized = {
|
|
868
|
+
version: SEARCH_INDEX_VERSION,
|
|
869
|
+
chunks: index.chunks.map((entry) => ({
|
|
870
|
+
chunk: entry.chunk,
|
|
871
|
+
normalizedTitle: entry.normalizedTitle,
|
|
872
|
+
normalizedPath: entry.normalizedPath,
|
|
873
|
+
normalizedBody: entry.normalizedBody,
|
|
874
|
+
titleFrequencies: [...entry.titleFrequencies.entries()],
|
|
875
|
+
pathFrequencies: [...entry.pathFrequencies.entries()],
|
|
876
|
+
bodyFrequencies: [...entry.bodyFrequencies.entries()]
|
|
877
|
+
})),
|
|
878
|
+
documentFrequencies: [...index.documentFrequencies.entries()]
|
|
879
|
+
};
|
|
880
|
+
return JSON.stringify(serialized);
|
|
881
|
+
}
|
|
882
|
+
function deserializeSearchIndex(raw) {
|
|
883
|
+
let parsed;
|
|
884
|
+
try {
|
|
885
|
+
parsed = JSON.parse(raw);
|
|
886
|
+
} catch {
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
if (parsed.version !== SEARCH_INDEX_VERSION) {
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
chunks: parsed.chunks.map((entry) => ({
|
|
894
|
+
chunk: entry.chunk,
|
|
895
|
+
normalizedTitle: entry.normalizedTitle,
|
|
896
|
+
normalizedPath: entry.normalizedPath,
|
|
897
|
+
normalizedBody: entry.normalizedBody,
|
|
898
|
+
titleFrequencies: new Map(entry.titleFrequencies),
|
|
899
|
+
pathFrequencies: new Map(entry.pathFrequencies),
|
|
900
|
+
bodyFrequencies: new Map(entry.bodyFrequencies)
|
|
901
|
+
})),
|
|
902
|
+
documentFrequencies: new Map(parsed.documentFrequencies)
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
async function readCachedSearchIndex(pkg, version) {
|
|
906
|
+
const raw = await readCached(pkg, version, searchIndexCacheFile());
|
|
907
|
+
if (raw === null) {
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
return deserializeSearchIndex(raw);
|
|
911
|
+
}
|
|
912
|
+
async function writeCachedSearchIndex(pkg, version, index) {
|
|
913
|
+
await writeCache(pkg, version, searchIndexCacheFile(), serializeSearchIndex(index));
|
|
914
|
+
}
|
|
915
|
+
async function resolveSearchIndex(pkg, version) {
|
|
916
|
+
const canCache = isExactVersion(version);
|
|
917
|
+
if (canCache) {
|
|
918
|
+
const cached = await readCachedSearchIndex(pkg, version);
|
|
919
|
+
if (cached !== null) {
|
|
920
|
+
return cached;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
const sources = await discoverSources(pkg, version);
|
|
924
|
+
const chunks = sources.flatMap((source) => extractMarkdownChunks(source));
|
|
925
|
+
const index = buildSearchIndex(chunks);
|
|
926
|
+
if (canCache) {
|
|
927
|
+
await writeCachedSearchIndex(pkg, version, index);
|
|
928
|
+
}
|
|
929
|
+
return index;
|
|
930
|
+
}
|
|
931
|
+
function inverseDocumentFrequency(documentCount, documentFrequency) {
|
|
932
|
+
return Math.log((documentCount + 1) / (documentFrequency + 1)) + 1;
|
|
933
|
+
}
|
|
934
|
+
function createPreview(content, queryTokens, maxLength = 220) {
|
|
935
|
+
const collapsed = content.replace(/\s+/g, " ").trim();
|
|
936
|
+
if (collapsed.length <= maxLength) {
|
|
937
|
+
return collapsed;
|
|
938
|
+
}
|
|
939
|
+
const lowerCollapsed = collapsed.toLowerCase();
|
|
940
|
+
const matchIndex = queryTokens.reduce((best, token) => {
|
|
941
|
+
const index = lowerCollapsed.indexOf(token.toLowerCase());
|
|
942
|
+
if (index === -1) {
|
|
943
|
+
return best;
|
|
944
|
+
}
|
|
945
|
+
return best === -1 ? index : Math.min(best, index);
|
|
946
|
+
}, -1);
|
|
947
|
+
if (matchIndex === -1) {
|
|
948
|
+
return `${collapsed.slice(0, maxLength - 3).trimEnd()}...`;
|
|
949
|
+
}
|
|
950
|
+
const start = Math.max(0, matchIndex - Math.floor((maxLength - 3) / 2));
|
|
951
|
+
const snippet = collapsed.slice(start, start + maxLength - 3).trim();
|
|
952
|
+
return start === 0 ? `${snippet}...` : `...${snippet}`;
|
|
953
|
+
}
|
|
954
|
+
function searchIndex(index, query, limit = 5) {
|
|
955
|
+
const queryTokens = [...new Set(tokenize(query))];
|
|
956
|
+
if (queryTokens.length === 0) {
|
|
957
|
+
return [];
|
|
958
|
+
}
|
|
959
|
+
const normalizedQuery = normalizeText(query);
|
|
960
|
+
const documentCount = index.chunks.length;
|
|
961
|
+
const ranked = index.chunks.map((entry, indexPosition) => {
|
|
962
|
+
let score = 0;
|
|
963
|
+
let matchedTokens = 0;
|
|
964
|
+
for (const token of queryTokens) {
|
|
965
|
+
const titleFrequency = entry.titleFrequencies.get(token) ?? 0;
|
|
966
|
+
const pathFrequency = entry.pathFrequencies.get(token) ?? 0;
|
|
967
|
+
const bodyFrequency = entry.bodyFrequencies.get(token) ?? 0;
|
|
968
|
+
const weightedFrequency = titleFrequency * FIELD_WEIGHTS.title + pathFrequency * FIELD_WEIGHTS.path + bodyFrequency * FIELD_WEIGHTS.body;
|
|
969
|
+
if (weightedFrequency === 0) {
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
matchedTokens += 1;
|
|
973
|
+
score += weightedFrequency * inverseDocumentFrequency(documentCount, index.documentFrequencies.get(token) ?? 0);
|
|
974
|
+
}
|
|
975
|
+
if (matchedTokens === 0) {
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
const coverage = matchedTokens / queryTokens.length;
|
|
979
|
+
score += coverage * 2;
|
|
980
|
+
if (normalizedQuery.length > 0) {
|
|
981
|
+
if (entry.normalizedTitle.includes(normalizedQuery) || entry.normalizedPath.includes(normalizedQuery)) {
|
|
982
|
+
score += 3;
|
|
983
|
+
} else if (entry.normalizedBody.includes(normalizedQuery)) {
|
|
984
|
+
score += 1.5;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
indexPosition,
|
|
989
|
+
score,
|
|
990
|
+
result: {
|
|
991
|
+
sourceKind: entry.chunk.sourceKind,
|
|
992
|
+
sourceTitle: entry.chunk.sourceTitle,
|
|
993
|
+
path: entry.chunk.path,
|
|
994
|
+
score,
|
|
995
|
+
startLine: entry.chunk.startLine,
|
|
996
|
+
endLine: entry.chunk.endLine,
|
|
997
|
+
preview: createPreview(entry.chunk.content, queryTokens),
|
|
998
|
+
origin: entry.chunk.origin,
|
|
999
|
+
topicId: buildTopicId(entry.chunk.origin, entry.chunk.path)
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
}).filter((entry) => entry !== null).sort((left, right) => {
|
|
1003
|
+
if (right.score !== left.score) {
|
|
1004
|
+
return right.score - left.score;
|
|
1005
|
+
}
|
|
1006
|
+
if (left.result.origin !== right.result.origin) {
|
|
1007
|
+
return left.result.origin.localeCompare(right.result.origin);
|
|
1008
|
+
}
|
|
1009
|
+
if (left.result.path !== right.result.path) {
|
|
1010
|
+
return left.result.path.localeCompare(right.result.path);
|
|
1011
|
+
}
|
|
1012
|
+
return left.indexPosition - right.indexPosition;
|
|
1013
|
+
}).slice(0, Math.max(1, limit));
|
|
1014
|
+
const maxScore = ranked[0]?.score ?? 1;
|
|
1015
|
+
return ranked.map(({ result, score }) => ({
|
|
1016
|
+
...result,
|
|
1017
|
+
score: Number((score / maxScore).toFixed(3))
|
|
1018
|
+
}));
|
|
1019
|
+
}
|
|
1020
|
+
async function searchDocs(pkg, version, query, limit = 5) {
|
|
1021
|
+
const index = await resolveSearchIndex(pkg, version);
|
|
1022
|
+
return {
|
|
1023
|
+
package: pkg,
|
|
1024
|
+
version,
|
|
1025
|
+
query,
|
|
1026
|
+
results: searchIndex(index, query, limit)
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/lib/fetchDts.ts
|
|
1031
|
+
import { posix as posix3 } from "node:path";
|
|
1032
|
+
var UNPKG_BASE3 = "https://unpkg.com";
|
|
1033
|
+
var EXPORT_FROM_RE_SOURCE = `^export\\s+(?:\\*|\\{[^}]*\\})\\s+from\\s+['"]([^'"]+)['"]`;
|
|
1034
|
+
var IMPORT_STAR_RE_SOURCE = `^import\\s+\\*\\s+as\\s+\\w+\\s+from\\s+['"]([^'"]+)['"]`;
|
|
1035
|
+
function createSpecifierRegexes() {
|
|
1036
|
+
return [
|
|
1037
|
+
new RegExp(EXPORT_FROM_RE_SOURCE, "gm"),
|
|
1038
|
+
new RegExp(IMPORT_STAR_RE_SOURCE, "gm")
|
|
1039
|
+
];
|
|
1040
|
+
}
|
|
1041
|
+
async function fetchText(url, cacheKey) {
|
|
1042
|
+
if (cacheKey) {
|
|
1043
|
+
const cached = await readCached(...cacheKey);
|
|
1044
|
+
if (cached !== null) return cached;
|
|
1045
|
+
}
|
|
1046
|
+
const res = await fetch(url);
|
|
1047
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText} \u2014 ${url}`);
|
|
1048
|
+
const content = await res.text();
|
|
1049
|
+
if (cacheKey) {
|
|
1050
|
+
await writeCache(...cacheKey, content);
|
|
1051
|
+
}
|
|
1052
|
+
return content;
|
|
1053
|
+
}
|
|
1054
|
+
async function resolveTypesPath(pkg, version) {
|
|
1055
|
+
const cacheVersion = isExactVersion(version) ? version : null;
|
|
1056
|
+
const pkgJsonUrl = `${UNPKG_BASE3}/${pkg}@${version}/package.json`;
|
|
1057
|
+
const raw = await fetchText(pkgJsonUrl, cacheVersion ? [pkg, cacheVersion, "package.json"] : null);
|
|
1058
|
+
const meta = JSON.parse(raw);
|
|
1059
|
+
const typesField = meta["types"] ?? meta["typings"];
|
|
1060
|
+
if (!typesField) throw new Error(`Package ${pkg}@${version} does not declare a "types" or "typings" field`);
|
|
1061
|
+
return typesField.startsWith("./") ? typesField.slice(2) : typesField;
|
|
1062
|
+
}
|
|
1063
|
+
function resolveRelativePath(currentFile, specifier) {
|
|
1064
|
+
const dir = posix3.dirname(currentFile);
|
|
1065
|
+
return posix3.normalize(posix3.join(dir, specifier));
|
|
1066
|
+
}
|
|
1067
|
+
function normalizeToDts(path) {
|
|
1068
|
+
if (path.endsWith(".d.ts")) return path;
|
|
1069
|
+
if (path.endsWith(".d.cts")) return path;
|
|
1070
|
+
if (path.endsWith(".d.mts")) return path;
|
|
1071
|
+
if (path.endsWith(".cjs")) return path.slice(0, -4) + ".d.cts";
|
|
1072
|
+
if (path.endsWith(".mjs")) return path.slice(0, -4) + ".d.mts";
|
|
1073
|
+
if (path.endsWith(".js")) return path.slice(0, -3) + ".d.ts";
|
|
1074
|
+
if (path.endsWith(".ts")) return path;
|
|
1075
|
+
if (path.endsWith(".cts")) return path;
|
|
1076
|
+
if (path.endsWith(".mts")) return path;
|
|
1077
|
+
return path + ".d.ts";
|
|
1078
|
+
}
|
|
1079
|
+
function candidateDtsPaths(filePath) {
|
|
1080
|
+
const normalized = normalizeToDts(filePath);
|
|
1081
|
+
const candidates = [normalized];
|
|
1082
|
+
if (!/\.(?:d\.ts|d\.cts|d\.mts|ts|cts|mts)$/.test(filePath) && !/\.(?:js|cjs|mjs)$/.test(filePath)) {
|
|
1083
|
+
candidates.push(posix3.join(filePath, "index.d.ts"));
|
|
1084
|
+
candidates.push(posix3.join(filePath, "index.d.cts"));
|
|
1085
|
+
candidates.push(posix3.join(filePath, "index.d.mts"));
|
|
1086
|
+
}
|
|
1087
|
+
return [...new Set(candidates)];
|
|
1088
|
+
}
|
|
1089
|
+
async function collectDts(pkg, version, cacheVersion, filePath, visited, chunks) {
|
|
1090
|
+
let resolvedPath = null;
|
|
1091
|
+
let content = null;
|
|
1092
|
+
for (const candidate of candidateDtsPaths(filePath)) {
|
|
1093
|
+
if (visited.has(candidate)) return;
|
|
1094
|
+
const url = `${UNPKG_BASE3}/${pkg}@${version}/${candidate}`;
|
|
1095
|
+
const cacheKey = `dts__${candidate.replace(/\//g, "__")}`;
|
|
1096
|
+
try {
|
|
1097
|
+
content = await fetchText(url, cacheVersion ? [pkg, cacheVersion, cacheKey] : null);
|
|
1098
|
+
resolvedPath = candidate;
|
|
1099
|
+
break;
|
|
1100
|
+
} catch {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (!resolvedPath || content === null) return;
|
|
1105
|
+
visited.add(resolvedPath);
|
|
1106
|
+
chunks.push(content);
|
|
1107
|
+
const specifiers = /* @__PURE__ */ new Set();
|
|
1108
|
+
for (const re of createSpecifierRegexes()) {
|
|
1109
|
+
re.lastIndex = 0;
|
|
1110
|
+
let m;
|
|
1111
|
+
while ((m = re.exec(content)) !== null) {
|
|
1112
|
+
const spec = m[1];
|
|
1113
|
+
if (spec.startsWith(".")) specifiers.add(spec);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
await Promise.all(
|
|
1117
|
+
[...specifiers].map((spec) => {
|
|
1118
|
+
const resolved = resolveRelativePath(resolvedPath, spec);
|
|
1119
|
+
return collectDts(pkg, version, cacheVersion, resolved, visited, chunks);
|
|
1120
|
+
})
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
async function fetchDts(pkg, version) {
|
|
1124
|
+
const typesPath = await resolveTypesPath(pkg, version);
|
|
1125
|
+
const cacheVersion = isExactVersion(version) ? version : null;
|
|
1126
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1127
|
+
const chunks = [];
|
|
1128
|
+
await collectDts(pkg, version, cacheVersion, typesPath, visited, chunks);
|
|
1129
|
+
return chunks.join("\n");
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/lib/extractApiRef.ts
|
|
1133
|
+
var PATTERNS = [
|
|
1134
|
+
{ kind: "function", re: /^export\s+(?:declare\s+)?function\s+(\w+)[^;{]*/gm },
|
|
1135
|
+
{ kind: "class", re: /^export\s+(?:declare\s+)?(?:abstract\s+)?class\s+(\w+)[^{]*/gm },
|
|
1136
|
+
{ kind: "interface", re: /^export\s+(?:declare\s+)?interface\s+(\w+)[^{]*/gm },
|
|
1137
|
+
{ kind: "type", re: /^export\s+(?:declare\s+)?type\s+(\w+)\s*(?:<[^=]*>)?\s*=[^;]*/gm },
|
|
1138
|
+
{ kind: "const", re: /^export\s+(?:declare\s+)?const\s+(\w+)\s*:[^;]*/gm },
|
|
1139
|
+
{ kind: "enum", re: /^export\s+(?:declare\s+)?(?:const\s+)?enum\s+(\w+)/gm }
|
|
1140
|
+
];
|
|
1141
|
+
function extractApiRef(dts, pkg, version) {
|
|
1142
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1143
|
+
for (const { kind, re } of PATTERNS) {
|
|
1144
|
+
re.lastIndex = 0;
|
|
1145
|
+
let m;
|
|
1146
|
+
while ((m = re.exec(dts)) !== null) {
|
|
1147
|
+
const name = m[1];
|
|
1148
|
+
const kinds = grouped.get(name) ?? /* @__PURE__ */ new Set();
|
|
1149
|
+
kinds.add(kind);
|
|
1150
|
+
grouped.set(name, kinds);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
const entries = [...grouped.entries()].map(([name, kinds]) => ({
|
|
1154
|
+
name,
|
|
1155
|
+
kinds: [...kinds].sort()
|
|
1156
|
+
}));
|
|
1157
|
+
entries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
|
1158
|
+
return { package: pkg, version, entries };
|
|
1159
|
+
}
|
|
1160
|
+
function collectMatches(dts, pattern) {
|
|
1161
|
+
pattern.lastIndex = 0;
|
|
1162
|
+
const matches = [];
|
|
1163
|
+
let match;
|
|
1164
|
+
while ((match = pattern.exec(dts)) !== null) {
|
|
1165
|
+
matches.push(match[0].trim());
|
|
1166
|
+
}
|
|
1167
|
+
return matches;
|
|
1168
|
+
}
|
|
1169
|
+
function blockPattern(prefix, name) {
|
|
1170
|
+
return new RegExp(`^export\\s+(?:declare\\s+)?${prefix}\\s+${name}(?:[^\\n{]*)\\{[\\s\\S]*?^\\}`, "gm");
|
|
1171
|
+
}
|
|
1172
|
+
function linePattern(prefix, name) {
|
|
1173
|
+
return new RegExp(`^export\\s+(?:declare\\s+)?${prefix}\\s+${name}[^;]*;`, "gm");
|
|
1174
|
+
}
|
|
1175
|
+
function extractDeclaration(dts, name) {
|
|
1176
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1177
|
+
const declarations = [
|
|
1178
|
+
...collectMatches(dts, blockPattern("(?:abstract\\s+)?class", escaped)),
|
|
1179
|
+
...collectMatches(dts, blockPattern("interface", escaped)),
|
|
1180
|
+
...collectMatches(dts, blockPattern("(?:const\\s+)?enum", escaped)),
|
|
1181
|
+
...collectMatches(dts, linePattern("function", escaped)),
|
|
1182
|
+
...collectMatches(dts, linePattern("type", escaped)),
|
|
1183
|
+
...collectMatches(dts, linePattern("const", escaped))
|
|
1184
|
+
];
|
|
1185
|
+
const unique = [...new Set(declarations)];
|
|
1186
|
+
if (unique.length === 0) {
|
|
1187
|
+
throw new Error(`Declaration not found: "${name}"`);
|
|
1188
|
+
}
|
|
1189
|
+
return unique.join("\n\n");
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// src/lib/compareVersions.ts
|
|
1193
|
+
function compareTocs(oldMd, newMd) {
|
|
1194
|
+
const oldEntries = extractToc(oldMd);
|
|
1195
|
+
const newEntries = extractToc(newMd);
|
|
1196
|
+
const oldPaths = new Set(oldEntries.map((e) => e.path));
|
|
1197
|
+
const newPaths = new Set(newEntries.map((e) => e.path));
|
|
1198
|
+
return {
|
|
1199
|
+
added: newEntries.filter((e) => !oldPaths.has(e.path)),
|
|
1200
|
+
removed: oldEntries.filter((e) => !newPaths.has(e.path)),
|
|
1201
|
+
unchanged: newEntries.filter((e) => oldPaths.has(e.path))
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/lib/mincer/cache.ts
|
|
1206
|
+
var MINCER_CACHE_VERSION = 9;
|
|
1207
|
+
function mincerCacheFile() {
|
|
1208
|
+
return "mincer-findings.json";
|
|
1209
|
+
}
|
|
1210
|
+
function serializeMincerCache(findings) {
|
|
1211
|
+
const serialized = {
|
|
1212
|
+
findings,
|
|
1213
|
+
version: MINCER_CACHE_VERSION
|
|
1214
|
+
};
|
|
1215
|
+
return JSON.stringify(serialized);
|
|
1216
|
+
}
|
|
1217
|
+
function deserializeMincerCache(raw) {
|
|
1218
|
+
let parsed;
|
|
1219
|
+
try {
|
|
1220
|
+
parsed = JSON.parse(raw);
|
|
1221
|
+
} catch {
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
if (parsed.version !== MINCER_CACHE_VERSION) {
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
return parsed.findings;
|
|
1228
|
+
}
|
|
1229
|
+
async function readCachedFindings(pkg, version) {
|
|
1230
|
+
const raw = await readCached(pkg, version, mincerCacheFile());
|
|
1231
|
+
if (raw === null) {
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
return deserializeMincerCache(raw);
|
|
1235
|
+
}
|
|
1236
|
+
async function writeCachedFindings(pkg, version, findings) {
|
|
1237
|
+
await writeCache(pkg, version, mincerCacheFile(), serializeMincerCache(findings));
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/lib/mincer/profiles.ts
|
|
1241
|
+
var PROFILES = {
|
|
1242
|
+
traps: {
|
|
1243
|
+
defaultTitle: "Potential trap or caveat",
|
|
1244
|
+
kind: "traps",
|
|
1245
|
+
mergeWindowLines: 1,
|
|
1246
|
+
rules: [
|
|
1247
|
+
{ confidence: "high", detector: "trap.warning", kind: "traps", pattern: /\bwarning(?:s)?\b/i, severity: "warning", title: "Warning noted in documentation" },
|
|
1248
|
+
{ confidence: "high", detector: "trap.important", kind: "traps", pattern: /\b(?:important|caveat(?:s)?|important limitation(?:s)?|known limitation(?:s)?)\b/i, severity: "warning", title: "Important caveat noted in documentation" },
|
|
1249
|
+
{ confidence: "high", detector: "trap.operational_risk", kind: "traps", pattern: /\b(?:may fail|less robust|not thread-safe|not safe for concurrent access|modif(?:y|ies) [^.]* globally|only enable if|context leaks?|context leak|monkey-patching|monkey patching|keep an eye on|crippling bugs?|race conditions? (?:can|may|might|could|will)|risk of race conditions?)\b/i, severity: "warning", title: "Operational risk noted in documentation" },
|
|
1250
|
+
{ confidence: "high", detector: "trap.do_not", kind: "traps", pattern: /\bdo not (?:use|call|pass|mutate|rely|assume|expect|forget|mix)\b/i, severity: "warning", title: "Explicit do-not guidance detected" },
|
|
1251
|
+
{ confidence: "medium", detector: "trap.avoid", kind: "traps", pattern: /\bavoid(?: using| calling)?\b/i, severity: "warning", title: "Avoidance guidance detected" },
|
|
1252
|
+
{ confidence: "medium", detector: "trap.careful", kind: "traps", pattern: /\b(?:be careful|careful when|careful with)\b/i, severity: "warning", title: "Careful-use guidance detected" },
|
|
1253
|
+
{ confidence: "medium", detector: "trap.pitfall", kind: "traps", pattern: /\bpitfall(?:s)?\b/i, severity: "warning", title: "Pitfall detected in documentation" },
|
|
1254
|
+
{ confidence: "medium", detector: "trap.gotcha", kind: "traps", pattern: /\bgotcha(?:s)?\b/i, severity: "warning", title: "Gotcha detected in documentation" }
|
|
1255
|
+
],
|
|
1256
|
+
sectionSignalPattern: /(?:^|\/)(?:warnings?|caveats?|gotchas?|pitfalls?|important|limitations?)(?:$|\/)/i
|
|
1257
|
+
},
|
|
1258
|
+
performance: {
|
|
1259
|
+
defaultTitle: "Performance-sensitive behavior",
|
|
1260
|
+
kind: "performance",
|
|
1261
|
+
mergeWindowLines: 1,
|
|
1262
|
+
rules: [
|
|
1263
|
+
{ confidence: "high", detector: "performance.big_o", kind: "performance", pattern: /\bO\([^\n]+\)/, severity: "warning", title: "Algorithmic complexity mentioned" },
|
|
1264
|
+
{ confidence: "high", detector: "performance.expensive", kind: "performance", pattern: /\bexpensive\b/i, severity: "warning", title: "Expensive operation noted" },
|
|
1265
|
+
{ confidence: "medium", detector: "performance.performance", kind: "performance", pattern: /\bperformance\b/i, severity: "info", title: "Performance-sensitive behavior noted" },
|
|
1266
|
+
{ confidence: "medium", detector: "performance.slow", kind: "performance", pattern: /\bslow(?:er|ly)?\b/i, severity: "info", title: "Slow behavior noted" },
|
|
1267
|
+
{ confidence: "medium", detector: "performance.memory", kind: "performance", pattern: /\bmemory(?: usage| overhead| leak)?\b/i, severity: "info", title: "Memory behavior noted" },
|
|
1268
|
+
{ confidence: "medium", detector: "performance.cache", kind: "performance", pattern: /\bcach(?:e|ed|ing)\b/i, severity: "info", title: "Cache-related behavior noted" },
|
|
1269
|
+
{ confidence: "medium", detector: "performance.hot_path", kind: "performance", pattern: /\bhot path\b/i, severity: "warning", title: "Hot path behavior noted" }
|
|
1270
|
+
],
|
|
1271
|
+
sectionSignalPattern: /(?:^|\/)(?:performance|complexity|optimization|optimizations|memory|cache|caching|hot path)(?:$|\/)/i
|
|
1272
|
+
},
|
|
1273
|
+
deprecations: {
|
|
1274
|
+
defaultTitle: "Deprecation or replacement guidance",
|
|
1275
|
+
kind: "deprecations",
|
|
1276
|
+
mergeWindowLines: 2,
|
|
1277
|
+
rules: [
|
|
1278
|
+
{ confidence: "high", detector: "deprecation.deprecated", kind: "deprecations", pattern: /\bdeprecated\b/i, severity: "warning", title: "Deprecated behavior or API noted" },
|
|
1279
|
+
{ confidence: "high", detector: "deprecation.use_instead", kind: "deprecations", pattern: /\b(?:deprecated|legacy|obsolete|removed)\b.+\buse\b.+\binstead\b|\buse\b.+\binstead\b.+\b(?:deprecated|legacy|obsolete|removed)\b/i, severity: "warning", title: "Replacement guidance detected" },
|
|
1280
|
+
{ confidence: "high", detector: "deprecation.removed", kind: "deprecations", pattern: /\b(?:will be removed|has been removed|was removed in|removed in v?\d|removed from (?:the )?(?:api|public api|surface)|deprecated and removed|no longer available|no longer supported)\b/i, severity: "warning", title: "Removal noted in documentation" },
|
|
1281
|
+
{ confidence: "medium", detector: "deprecation.legacy", kind: "deprecations", pattern: /\blegacy\b/i, severity: "warning", title: "Legacy status noted" },
|
|
1282
|
+
{ confidence: "medium", detector: "deprecation.obsolete", kind: "deprecations", pattern: /\bobsolete\b/i, severity: "warning", title: "Obsolete behavior or API noted" }
|
|
1283
|
+
],
|
|
1284
|
+
sectionSignalPattern: /(?:^|\/)(?:deprecated|deprecations?|legacy|obsolete|migration|migrating|upgrade|upgrading|breaking changes?)(?:$|\/)/i
|
|
1285
|
+
},
|
|
1286
|
+
constraints: {
|
|
1287
|
+
defaultTitle: "Operational constraint",
|
|
1288
|
+
kind: "constraints",
|
|
1289
|
+
mergeWindowLines: 1,
|
|
1290
|
+
rules: [
|
|
1291
|
+
{ confidence: "high", detector: "constraint.requires", kind: "constraints", pattern: /\b(?:requires?|required)\b/i, severity: "info", title: "Required precondition noted" },
|
|
1292
|
+
{ confidence: "high", detector: "constraint.only_works", kind: "constraints", pattern: /\bonly works\b/i, severity: "warning", title: "Limited support constraint detected" },
|
|
1293
|
+
{ confidence: "high", detector: "constraint.not_supported", kind: "constraints", pattern: /\bnot supported\b/i, severity: "warning", title: "Unsupported scenario detected" },
|
|
1294
|
+
{ confidence: "high", detector: "constraint.must_be_called", kind: "constraints", pattern: /\bmust be called\b/i, severity: "warning", title: "Required call ordering detected" },
|
|
1295
|
+
{ confidence: "medium", detector: "constraint.before_use", kind: "constraints", pattern: /\bbefore (?:using|calling|invoking|initializing|setup|setup is complete)\b/i, severity: "info", title: "Before-ordering constraint noted" },
|
|
1296
|
+
{ confidence: "medium", detector: "constraint.after_setup", kind: "constraints", pattern: /\bafter (?:setup|initialization|calling|mounting|creation)\b/i, severity: "info", title: "After-ordering constraint noted" }
|
|
1297
|
+
],
|
|
1298
|
+
sectionSignalPattern: /(?:^|\/)(?:requirements?|constraints?|supported|support|compatibility|prerequisites?|setup|limitations?)(?:$|\/)/i
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
var PROFILE_ORDER = ["traps", "performance", "deprecations", "constraints"];
|
|
1302
|
+
function getProfile(kind) {
|
|
1303
|
+
return PROFILES[kind];
|
|
1304
|
+
}
|
|
1305
|
+
function listProfiles() {
|
|
1306
|
+
return PROFILE_ORDER.map((kind) => PROFILES[kind]);
|
|
1307
|
+
}
|
|
1308
|
+
function hasSectionSignal(kind, path) {
|
|
1309
|
+
return getProfile(kind).sectionSignalPattern.test(path);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// src/lib/mincer/detectors.ts
|
|
1313
|
+
function isFenceLine(line) {
|
|
1314
|
+
return /^\s*(?:```|~~~)/.test(line);
|
|
1315
|
+
}
|
|
1316
|
+
function isCodeLikeLine(trimmed) {
|
|
1317
|
+
return /^['"`][^'"`]+['"`]\s*:/.test(trimmed) || /^(?:export|import|const|let|var|function|class|interface|type)\b/.test(trimmed) || /=>/.test(trimmed) || /^[A-Za-z_$][\w$]*\s*:\s*(?:true|false|null|undefined|['"`[{\d-])/.test(trimmed);
|
|
1318
|
+
}
|
|
1319
|
+
function collectRuleHits(chunk, kind) {
|
|
1320
|
+
const { rules } = getProfile(kind);
|
|
1321
|
+
let insideCodeFence = false;
|
|
1322
|
+
return chunk.content.split("\n").flatMap((line, index) => {
|
|
1323
|
+
if (isFenceLine(line)) {
|
|
1324
|
+
insideCodeFence = !insideCodeFence;
|
|
1325
|
+
return [];
|
|
1326
|
+
}
|
|
1327
|
+
const trimmed = line.trim();
|
|
1328
|
+
if (insideCodeFence || trimmed.length === 0 || isCodeLikeLine(trimmed)) {
|
|
1329
|
+
return [];
|
|
1330
|
+
}
|
|
1331
|
+
return rules.filter((rule) => rule.pattern.test(line)).map((rule) => ({
|
|
1332
|
+
detector: rule.detector,
|
|
1333
|
+
kind: rule.kind,
|
|
1334
|
+
severity: rule.severity,
|
|
1335
|
+
confidence: rule.confidence,
|
|
1336
|
+
title: rule.title,
|
|
1337
|
+
sourceId: chunk.sourceId,
|
|
1338
|
+
sourceKind: chunk.sourceKind,
|
|
1339
|
+
sourceTitle: chunk.sourceTitle,
|
|
1340
|
+
origin: chunk.origin,
|
|
1341
|
+
path: chunk.path,
|
|
1342
|
+
startLine: chunk.startLine + index,
|
|
1343
|
+
endLine: chunk.startLine + index,
|
|
1344
|
+
snippet: trimmed
|
|
1345
|
+
}));
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
function detectFindingsByKind(chunk, kind) {
|
|
1349
|
+
return collectRuleHits(chunk, kind).sort((left, right) => {
|
|
1350
|
+
if (left.startLine !== right.startLine) {
|
|
1351
|
+
return left.startLine - right.startLine;
|
|
1352
|
+
}
|
|
1353
|
+
return left.detector.localeCompare(right.detector);
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
function detectFindings(chunk) {
|
|
1357
|
+
return listProfiles().flatMap((profile) => detectFindingsByKind(chunk, profile.kind)).sort((left, right) => {
|
|
1358
|
+
if (left.startLine !== right.startLine) {
|
|
1359
|
+
return left.startLine - right.startLine;
|
|
1360
|
+
}
|
|
1361
|
+
if (left.kind !== right.kind) {
|
|
1362
|
+
return left.kind.localeCompare(right.kind);
|
|
1363
|
+
}
|
|
1364
|
+
return left.detector.localeCompare(right.detector);
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// src/lib/mincer/extractFindings.ts
|
|
1369
|
+
var CONFIDENCE_RANK = {
|
|
1370
|
+
high: 3,
|
|
1371
|
+
medium: 2,
|
|
1372
|
+
low: 1
|
|
1373
|
+
};
|
|
1374
|
+
var SEVERITY_RANK = {
|
|
1375
|
+
critical: 3,
|
|
1376
|
+
warning: 2,
|
|
1377
|
+
info: 1
|
|
1378
|
+
};
|
|
1379
|
+
function mergeTitleFromDetectors(detectors) {
|
|
1380
|
+
const [first] = detectors;
|
|
1381
|
+
if (first === void 0) {
|
|
1382
|
+
return "Documentation finding";
|
|
1383
|
+
}
|
|
1384
|
+
const prefix = first.split(".", 1)[0];
|
|
1385
|
+
if (prefix === "trap") {
|
|
1386
|
+
return getProfile("traps").defaultTitle;
|
|
1387
|
+
}
|
|
1388
|
+
if (prefix === "performance") {
|
|
1389
|
+
return getProfile("performance").defaultTitle;
|
|
1390
|
+
}
|
|
1391
|
+
if (prefix === "deprecation") {
|
|
1392
|
+
return getProfile("deprecations").defaultTitle;
|
|
1393
|
+
}
|
|
1394
|
+
if (prefix === "constraint") {
|
|
1395
|
+
return getProfile("constraints").defaultTitle;
|
|
1396
|
+
}
|
|
1397
|
+
return "Documentation finding";
|
|
1398
|
+
}
|
|
1399
|
+
function buildSummary(hit) {
|
|
1400
|
+
return hit.snippet;
|
|
1401
|
+
}
|
|
1402
|
+
function compareDetectors(left, right) {
|
|
1403
|
+
return left.localeCompare(right);
|
|
1404
|
+
}
|
|
1405
|
+
function normalizeSnippet(snippet) {
|
|
1406
|
+
return snippet.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
1407
|
+
}
|
|
1408
|
+
function promoteConfidence(confidence, kind, path) {
|
|
1409
|
+
if (!hasSectionSignal(kind, path)) {
|
|
1410
|
+
return confidence;
|
|
1411
|
+
}
|
|
1412
|
+
if (confidence === "low") {
|
|
1413
|
+
return "medium";
|
|
1414
|
+
}
|
|
1415
|
+
return "high";
|
|
1416
|
+
}
|
|
1417
|
+
function deriveTitle(kind, fallbackTitle, evidence) {
|
|
1418
|
+
const detectors = [...new Set(evidence.flatMap((item) => item.detectors))].sort(compareDetectors);
|
|
1419
|
+
if (detectors.length <= 1) {
|
|
1420
|
+
return fallbackTitle;
|
|
1421
|
+
}
|
|
1422
|
+
return getProfile(kind).defaultTitle;
|
|
1423
|
+
}
|
|
1424
|
+
function sortEvidence(evidence) {
|
|
1425
|
+
return [...evidence].sort((left, right) => {
|
|
1426
|
+
if (left.origin !== right.origin) {
|
|
1427
|
+
return left.origin.localeCompare(right.origin);
|
|
1428
|
+
}
|
|
1429
|
+
if (left.path !== right.path) {
|
|
1430
|
+
return left.path.localeCompare(right.path);
|
|
1431
|
+
}
|
|
1432
|
+
if (left.startLine !== right.startLine) {
|
|
1433
|
+
return left.startLine - right.startLine;
|
|
1434
|
+
}
|
|
1435
|
+
if (left.endLine !== right.endLine) {
|
|
1436
|
+
return left.endLine - right.endLine;
|
|
1437
|
+
}
|
|
1438
|
+
return left.detectors.join(",").localeCompare(right.detectors.join(","));
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
function mergeEvidence(existing, hit) {
|
|
1442
|
+
const match = existing.find((item) => item.origin === hit.origin && item.path === hit.path && item.startLine === hit.startLine && item.endLine === hit.endLine && item.snippet === hit.snippet);
|
|
1443
|
+
if (match === void 0) {
|
|
1444
|
+
return sortEvidence([
|
|
1445
|
+
...existing,
|
|
1446
|
+
{
|
|
1447
|
+
sourceId: hit.sourceId,
|
|
1448
|
+
sourceKind: hit.sourceKind,
|
|
1449
|
+
sourceTitle: hit.sourceTitle,
|
|
1450
|
+
origin: hit.origin,
|
|
1451
|
+
path: hit.path,
|
|
1452
|
+
startLine: hit.startLine,
|
|
1453
|
+
endLine: hit.endLine,
|
|
1454
|
+
snippet: hit.snippet,
|
|
1455
|
+
detectors: [hit.detector]
|
|
1456
|
+
}
|
|
1457
|
+
]);
|
|
1458
|
+
}
|
|
1459
|
+
match.detectors = [.../* @__PURE__ */ new Set([...match.detectors, hit.detector])].sort(compareDetectors);
|
|
1460
|
+
return sortEvidence(existing);
|
|
1461
|
+
}
|
|
1462
|
+
function isSameFindingWindow(left, right) {
|
|
1463
|
+
if (left.kind !== right.kind || left.origin !== right.origin || left.path !== right.path) {
|
|
1464
|
+
return false;
|
|
1465
|
+
}
|
|
1466
|
+
const profile = getProfile(left.kind);
|
|
1467
|
+
const distance = Math.abs(left.startLine - right.startLine);
|
|
1468
|
+
if (distance === 0) {
|
|
1469
|
+
return true;
|
|
1470
|
+
}
|
|
1471
|
+
if (distance > profile.mergeWindowLines) {
|
|
1472
|
+
return false;
|
|
1473
|
+
}
|
|
1474
|
+
if (normalizeSnippet(left.snippet) === normalizeSnippet(right.snippet)) {
|
|
1475
|
+
return true;
|
|
1476
|
+
}
|
|
1477
|
+
return left.kind === "deprecations";
|
|
1478
|
+
}
|
|
1479
|
+
function selectHigherSeverity(left, right) {
|
|
1480
|
+
return SEVERITY_RANK[left] >= SEVERITY_RANK[right] ? left : right;
|
|
1481
|
+
}
|
|
1482
|
+
function selectHigherConfidence(left, right) {
|
|
1483
|
+
return CONFIDENCE_RANK[left] >= CONFIDENCE_RANK[right] ? left : right;
|
|
1484
|
+
}
|
|
1485
|
+
function normalizeHits(hits) {
|
|
1486
|
+
const sortedHits = [...hits].sort((left, right) => {
|
|
1487
|
+
if (left.origin !== right.origin) {
|
|
1488
|
+
return left.origin.localeCompare(right.origin);
|
|
1489
|
+
}
|
|
1490
|
+
if (left.path !== right.path) {
|
|
1491
|
+
return left.path.localeCompare(right.path);
|
|
1492
|
+
}
|
|
1493
|
+
if (left.startLine !== right.startLine) {
|
|
1494
|
+
return left.startLine - right.startLine;
|
|
1495
|
+
}
|
|
1496
|
+
if (left.kind !== right.kind) {
|
|
1497
|
+
return left.kind.localeCompare(right.kind);
|
|
1498
|
+
}
|
|
1499
|
+
return left.detector.localeCompare(right.detector);
|
|
1500
|
+
});
|
|
1501
|
+
const drafts = [];
|
|
1502
|
+
for (const hit of sortedHits) {
|
|
1503
|
+
const existing = drafts.find((draft) => isSameFindingWindow(draft.anchor, hit));
|
|
1504
|
+
if (existing === void 0) {
|
|
1505
|
+
drafts.push({
|
|
1506
|
+
anchor: hit,
|
|
1507
|
+
finding: {
|
|
1508
|
+
confidence: promoteConfidence(hit.confidence, hit.kind, hit.path),
|
|
1509
|
+
evidence: [{
|
|
1510
|
+
sourceId: hit.sourceId,
|
|
1511
|
+
sourceKind: hit.sourceKind,
|
|
1512
|
+
sourceTitle: hit.sourceTitle,
|
|
1513
|
+
origin: hit.origin,
|
|
1514
|
+
path: hit.path,
|
|
1515
|
+
startLine: hit.startLine,
|
|
1516
|
+
endLine: hit.endLine,
|
|
1517
|
+
snippet: hit.snippet,
|
|
1518
|
+
detectors: [hit.detector]
|
|
1519
|
+
}],
|
|
1520
|
+
kind: hit.kind,
|
|
1521
|
+
severity: hit.severity,
|
|
1522
|
+
summary: buildSummary(hit),
|
|
1523
|
+
title: deriveTitle(hit.kind, hit.title, [{
|
|
1524
|
+
sourceId: hit.sourceId,
|
|
1525
|
+
sourceKind: hit.sourceKind,
|
|
1526
|
+
sourceTitle: hit.sourceTitle,
|
|
1527
|
+
origin: hit.origin,
|
|
1528
|
+
path: hit.path,
|
|
1529
|
+
startLine: hit.startLine,
|
|
1530
|
+
endLine: hit.endLine,
|
|
1531
|
+
snippet: hit.snippet,
|
|
1532
|
+
detectors: [hit.detector]
|
|
1533
|
+
}])
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
existing.finding.severity = selectHigherSeverity(existing.finding.severity, hit.severity);
|
|
1539
|
+
existing.finding.confidence = selectHigherConfidence(existing.finding.confidence, promoteConfidence(hit.confidence, hit.kind, hit.path));
|
|
1540
|
+
existing.finding.evidence = mergeEvidence(existing.finding.evidence, hit);
|
|
1541
|
+
const mergedDetectors = existing.finding.evidence.flatMap((item) => item.detectors).sort(compareDetectors);
|
|
1542
|
+
existing.finding.title = mergedDetectors.length <= 1 ? hit.title : mergeTitleFromDetectors(mergedDetectors);
|
|
1543
|
+
}
|
|
1544
|
+
return drafts.map((draft) => ({
|
|
1545
|
+
...draft.finding,
|
|
1546
|
+
evidence: sortEvidence(draft.finding.evidence)
|
|
1547
|
+
}));
|
|
1548
|
+
}
|
|
1549
|
+
function extractFindingsFromHits(pkg, version, hits) {
|
|
1550
|
+
return normalizeHits(hits).map((finding) => ({
|
|
1551
|
+
package: pkg,
|
|
1552
|
+
version,
|
|
1553
|
+
kind: finding.kind,
|
|
1554
|
+
severity: finding.severity,
|
|
1555
|
+
confidence: finding.confidence,
|
|
1556
|
+
title: finding.title,
|
|
1557
|
+
summary: finding.summary,
|
|
1558
|
+
evidence: finding.evidence
|
|
1559
|
+
}));
|
|
1560
|
+
}
|
|
1561
|
+
function extractFindings(pkg, version, chunks) {
|
|
1562
|
+
return extractFindingsFromHits(
|
|
1563
|
+
pkg,
|
|
1564
|
+
version,
|
|
1565
|
+
chunks.flatMap((chunk) => detectFindings(chunk))
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// src/lib/mincer/rankFindings.ts
|
|
1570
|
+
var CONFIDENCE_RANK2 = {
|
|
1571
|
+
high: 3,
|
|
1572
|
+
medium: 2,
|
|
1573
|
+
low: 1
|
|
1574
|
+
};
|
|
1575
|
+
var SEVERITY_RANK2 = {
|
|
1576
|
+
critical: 3,
|
|
1577
|
+
warning: 2,
|
|
1578
|
+
info: 1
|
|
1579
|
+
};
|
|
1580
|
+
function sectionSignalScore(finding) {
|
|
1581
|
+
return finding.evidence.some((evidence) => hasSectionSignal(finding.kind, evidence.path)) ? 1 : 0;
|
|
1582
|
+
}
|
|
1583
|
+
function isHeadingOnlySnippet(snippet) {
|
|
1584
|
+
return /^#{1,6}\s+/.test(snippet) || /^>\s*\[!(?:WARNING|IMPORTANT)\]\s*$/i.test(snippet) || /^\*\*Important Note:?\*\*$/i.test(snippet) || /^Important notes? about .+:$/i.test(snippet) || /^Important notes?$/i.test(snippet);
|
|
1585
|
+
}
|
|
1586
|
+
function headingOnlyScore(finding) {
|
|
1587
|
+
return finding.evidence.every((evidence) => isHeadingOnlySnippet(evidence.snippet)) ? 1 : 0;
|
|
1588
|
+
}
|
|
1589
|
+
function operationalRiskScore(finding) {
|
|
1590
|
+
return finding.evidence.some((evidence) => evidence.detectors.includes("trap.operational_risk")) ? 1 : 0;
|
|
1591
|
+
}
|
|
1592
|
+
function rankFindings(findings) {
|
|
1593
|
+
return [...findings].sort((left, right) => {
|
|
1594
|
+
if (SEVERITY_RANK2[right.severity] !== SEVERITY_RANK2[left.severity]) {
|
|
1595
|
+
return SEVERITY_RANK2[right.severity] - SEVERITY_RANK2[left.severity];
|
|
1596
|
+
}
|
|
1597
|
+
if (CONFIDENCE_RANK2[right.confidence] !== CONFIDENCE_RANK2[left.confidence]) {
|
|
1598
|
+
return CONFIDENCE_RANK2[right.confidence] - CONFIDENCE_RANK2[left.confidence];
|
|
1599
|
+
}
|
|
1600
|
+
if (operationalRiskScore(right) !== operationalRiskScore(left)) {
|
|
1601
|
+
return operationalRiskScore(right) - operationalRiskScore(left);
|
|
1602
|
+
}
|
|
1603
|
+
if (headingOnlyScore(left) !== headingOnlyScore(right)) {
|
|
1604
|
+
return headingOnlyScore(left) - headingOnlyScore(right);
|
|
1605
|
+
}
|
|
1606
|
+
if (sectionSignalScore(right) !== sectionSignalScore(left)) {
|
|
1607
|
+
return sectionSignalScore(right) - sectionSignalScore(left);
|
|
1608
|
+
}
|
|
1609
|
+
const leftEvidence = left.evidence[0];
|
|
1610
|
+
const rightEvidence = right.evidence[0];
|
|
1611
|
+
if (leftEvidence !== void 0 && rightEvidence !== void 0) {
|
|
1612
|
+
if (leftEvidence.origin !== rightEvidence.origin) {
|
|
1613
|
+
return leftEvidence.origin.localeCompare(rightEvidence.origin);
|
|
1614
|
+
}
|
|
1615
|
+
if (leftEvidence.path !== rightEvidence.path) {
|
|
1616
|
+
return leftEvidence.path.localeCompare(rightEvidence.path);
|
|
1617
|
+
}
|
|
1618
|
+
if (leftEvidence.startLine !== rightEvidence.startLine) {
|
|
1619
|
+
return leftEvidence.startLine - rightEvidence.startLine;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (left.kind !== right.kind) {
|
|
1623
|
+
return left.kind.localeCompare(right.kind);
|
|
1624
|
+
}
|
|
1625
|
+
return left.title.localeCompare(right.title);
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// src/lib/mincer/analyzeFindings.ts
|
|
1630
|
+
function selectFindingsByKind(findings, kind) {
|
|
1631
|
+
return findings.filter((finding) => finding.kind === kind);
|
|
1632
|
+
}
|
|
1633
|
+
async function analyzeFindings(pkg, version) {
|
|
1634
|
+
const canCache = isExactVersion(version);
|
|
1635
|
+
if (canCache) {
|
|
1636
|
+
const cached = await readCachedFindings(pkg, version);
|
|
1637
|
+
if (cached !== null) {
|
|
1638
|
+
return cached;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
const sources = await discoverSources(pkg, version);
|
|
1642
|
+
const chunks = sources.flatMap((source) => extractMarkdownChunks(source));
|
|
1643
|
+
const findings = rankFindings(extractFindings(pkg, version, chunks));
|
|
1644
|
+
if (canCache) {
|
|
1645
|
+
await writeCachedFindings(pkg, version, findings);
|
|
1646
|
+
}
|
|
1647
|
+
return findings;
|
|
1648
|
+
}
|
|
1649
|
+
async function analyzeFindingsByKind(pkg, version, kind, limit = 10) {
|
|
1650
|
+
const findings = await analyzeFindings(pkg, version);
|
|
1651
|
+
return {
|
|
1652
|
+
package: pkg,
|
|
1653
|
+
version,
|
|
1654
|
+
kind,
|
|
1655
|
+
findings: selectFindingsByKind(findings, kind).slice(0, Math.max(1, limit))
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// src/lib/mincer/getConstraints.ts
|
|
1660
|
+
async function getConstraints(pkg, version, limit = 10) {
|
|
1661
|
+
return analyzeFindingsByKind(pkg, version, "constraints", limit);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// src/lib/mincer/getDeprecations.ts
|
|
1665
|
+
async function getDeprecations(pkg, version, limit = 10) {
|
|
1666
|
+
return analyzeFindingsByKind(pkg, version, "deprecations", limit);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// src/lib/mincer/getPerformanceNotes.ts
|
|
1670
|
+
async function getPerformanceNotes(pkg, version, limit = 10) {
|
|
1671
|
+
return analyzeFindingsByKind(pkg, version, "performance", limit);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// src/lib/mincer/getTraps.ts
|
|
1675
|
+
async function getTraps(pkg, version, limit = 10) {
|
|
1676
|
+
return analyzeFindingsByKind(pkg, version, "traps", limit);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// src/server.ts
|
|
1680
|
+
function createServer() {
|
|
1681
|
+
const server = new McpServer({
|
|
1682
|
+
name: "soup-chop",
|
|
1683
|
+
version: "1.0.0"
|
|
1684
|
+
});
|
|
1685
|
+
server.registerResource(
|
|
1686
|
+
"capabilities",
|
|
1687
|
+
"soup-chop://capabilities",
|
|
1688
|
+
{
|
|
1689
|
+
title: "soup-chop capabilities",
|
|
1690
|
+
description: "Overview of the soup-chop MCP surface and the recommended tool-first workflow.",
|
|
1691
|
+
mimeType: "text/plain"
|
|
1692
|
+
},
|
|
1693
|
+
async () => ({
|
|
1694
|
+
contents: [
|
|
1695
|
+
{
|
|
1696
|
+
uri: "soup-chop://capabilities",
|
|
1697
|
+
text: [
|
|
1698
|
+
"soup-chop is a tool-first MCP server for version-accurate NPM documentation.",
|
|
1699
|
+
"",
|
|
1700
|
+
"Main tools:",
|
|
1701
|
+
"- search_docs: ranked cross-document discovery over README.md, doc/, docs/, and a narrow top-level markdown allowlist.",
|
|
1702
|
+
"- get_toc: package-wide TOC when origin is omitted, or single-document TOC when origin is provided.",
|
|
1703
|
+
"- get_topic: fetch a section or whole document via topic_id, or via path plus optional origin.",
|
|
1704
|
+
"- get_traps / get_deprecations / get_constraints / get_performance_notes: evidence-backed findings mined deterministically from markdown docs.",
|
|
1705
|
+
"- get_api_ref / get_declaration: inspect published TypeScript declarations.",
|
|
1706
|
+
"- compare_versions: diff documentation structure between two package versions.",
|
|
1707
|
+
"",
|
|
1708
|
+
"Recommended flow:",
|
|
1709
|
+
"1. Call search_docs when you do not know the exact section yet.",
|
|
1710
|
+
"2. Use the returned topicId directly with get_topic, or inspect the document with get_toc(origin).",
|
|
1711
|
+
"3. Use get_api_ref / get_declaration for exact type-level answers.",
|
|
1712
|
+
"",
|
|
1713
|
+
"Current search behavior: lexical ranking is optimized for English tokenization today."
|
|
1714
|
+
].join("\n")
|
|
1715
|
+
}
|
|
1716
|
+
]
|
|
1717
|
+
})
|
|
1718
|
+
);
|
|
1719
|
+
server.registerTool(
|
|
1720
|
+
"get_toc",
|
|
1721
|
+
{
|
|
1722
|
+
description: "Returns a structured table of contents for an NPM package markdown corpus or a specific markdown doc. Omit origin for a package-wide multi-document TOC, or pass origin to inspect one non-README doc directly.",
|
|
1723
|
+
inputSchema: {
|
|
1724
|
+
package: z.string().describe('NPM package name (e.g., "zod", "express")'),
|
|
1725
|
+
version: z.string().describe('NPM version string: exact version or published dist-tag (e.g., "3.23.8", "latest")'),
|
|
1726
|
+
origin: z.string().optional().describe('Optional markdown source path from search_docs (e.g., "docs/faq.md" or "CHANGELOG.md"). Omit it for a package-wide TOC.')
|
|
1727
|
+
}
|
|
1728
|
+
},
|
|
1729
|
+
async ({ package: pkg, version, origin }) => {
|
|
1730
|
+
const response = origin ? {
|
|
1731
|
+
package: pkg,
|
|
1732
|
+
version,
|
|
1733
|
+
origin,
|
|
1734
|
+
entries: buildDocumentTocEntries(await fetchMarkdownFile(pkg, version, origin), origin)
|
|
1735
|
+
} : {
|
|
1736
|
+
package: pkg,
|
|
1737
|
+
version,
|
|
1738
|
+
entries: buildPackageTocEntries(await discoverSources(pkg, version))
|
|
1739
|
+
};
|
|
1740
|
+
return {
|
|
1741
|
+
content: [
|
|
1742
|
+
{
|
|
1743
|
+
type: "text",
|
|
1744
|
+
text: JSON.stringify(response, null, 2)
|
|
1745
|
+
}
|
|
1746
|
+
]
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
);
|
|
1750
|
+
server.registerTool(
|
|
1751
|
+
"get_topic",
|
|
1752
|
+
{
|
|
1753
|
+
description: "Returns the raw markdown content for a specific section of an NPM package markdown doc. Use get_toc or search_docs first to discover the exact section path, and pass origin or topic_id when the section comes from a non-README doc.",
|
|
1754
|
+
inputSchema: {
|
|
1755
|
+
package: z.string().describe('NPM package name (e.g., "zod", "express")'),
|
|
1756
|
+
version: z.string().describe('NPM version string: exact version or published dist-tag (e.g., "3.23.8", "latest")'),
|
|
1757
|
+
path: z.string().optional().describe('Section path from get_toc (e.g., "Installation" or "API/Methods/parse")'),
|
|
1758
|
+
origin: z.string().optional().describe('Optional markdown source path from search_docs (e.g., "docs/faq.md" or "API.md"). Defaults to README.md'),
|
|
1759
|
+
topic_id: z.string().optional().describe('Stable topic identifier from search_docs or aggregated get_toc (e.g., "README.md#Installation" or "CHANGELOG.md").')
|
|
1760
|
+
}
|
|
1761
|
+
},
|
|
1762
|
+
async ({ package: pkg, version, path, origin, topic_id }) => {
|
|
1763
|
+
const selection = resolveTopicSelection(path, origin, topic_id);
|
|
1764
|
+
const markdown = await fetchMarkdownFile(pkg, version, selection.origin);
|
|
1765
|
+
const content = getTopic(markdown, selection.path, selection.origin);
|
|
1766
|
+
return {
|
|
1767
|
+
content: [
|
|
1768
|
+
{
|
|
1769
|
+
type: "text",
|
|
1770
|
+
text: content
|
|
1771
|
+
}
|
|
1772
|
+
]
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
);
|
|
1776
|
+
server.registerTool(
|
|
1777
|
+
"search_docs",
|
|
1778
|
+
{
|
|
1779
|
+
description: "Searches README and package-local markdown docs for an NPM package using local lexical ranking. Use this when you have keywords or a natural-language question but do not know the exact section path yet.",
|
|
1780
|
+
inputSchema: {
|
|
1781
|
+
package: z.string().describe('NPM package name (e.g., "zod", "express")'),
|
|
1782
|
+
version: z.string().describe('NPM version string: exact version or published dist-tag (e.g., "3.23.8", "latest")'),
|
|
1783
|
+
query: z.string().describe('Keywords or natural-language search query (e.g., "track state changes")'),
|
|
1784
|
+
limit: z.number().int().min(1).max(20).optional().describe("Maximum number of ranked results to return (default: 5)")
|
|
1785
|
+
}
|
|
1786
|
+
},
|
|
1787
|
+
async ({ package: pkg, version, query, limit }) => {
|
|
1788
|
+
const results = await searchDocs(pkg, version, query, limit ?? 5);
|
|
1789
|
+
return {
|
|
1790
|
+
content: [
|
|
1791
|
+
{
|
|
1792
|
+
type: "text",
|
|
1793
|
+
text: JSON.stringify(results, null, 2)
|
|
1794
|
+
}
|
|
1795
|
+
]
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
);
|
|
1799
|
+
server.registerTool(
|
|
1800
|
+
"get_traps",
|
|
1801
|
+
{
|
|
1802
|
+
description: "Returns compact, evidence-backed trap findings extracted deterministically from package markdown docs. Use this to surface warnings, caveats, and operational gotchas with source lines and origin paths.",
|
|
1803
|
+
inputSchema: {
|
|
1804
|
+
package: z.string().describe('NPM package name (e.g., "zod", "express")'),
|
|
1805
|
+
version: z.string().describe('NPM version string: exact version or published dist-tag (e.g., "3.23.8", "latest")'),
|
|
1806
|
+
limit: z.number().int().min(1).max(50).optional().describe("Maximum number of ranked trap findings to return (default: 10)")
|
|
1807
|
+
}
|
|
1808
|
+
},
|
|
1809
|
+
async ({ package: pkg, version, limit }) => {
|
|
1810
|
+
const results = await getTraps(pkg, version, limit ?? 10);
|
|
1811
|
+
return {
|
|
1812
|
+
content: [
|
|
1813
|
+
{
|
|
1814
|
+
type: "text",
|
|
1815
|
+
text: JSON.stringify(results, null, 2)
|
|
1816
|
+
}
|
|
1817
|
+
]
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
);
|
|
1821
|
+
server.registerTool(
|
|
1822
|
+
"get_deprecations",
|
|
1823
|
+
{
|
|
1824
|
+
description: "Returns evidence-backed deprecation findings extracted deterministically from package markdown docs. Use this to surface deprecated APIs, legacy guidance, removals, and suggested replacements.",
|
|
1825
|
+
inputSchema: {
|
|
1826
|
+
package: z.string().describe('NPM package name (e.g., "zod", "express")'),
|
|
1827
|
+
version: z.string().describe('NPM version string: exact version or published dist-tag (e.g., "3.23.8", "latest")'),
|
|
1828
|
+
limit: z.number().int().min(1).max(50).optional().describe("Maximum number of ranked deprecation findings to return (default: 10)")
|
|
1829
|
+
}
|
|
1830
|
+
},
|
|
1831
|
+
async ({ package: pkg, version, limit }) => {
|
|
1832
|
+
const results = await getDeprecations(pkg, version, limit ?? 10);
|
|
1833
|
+
return {
|
|
1834
|
+
content: [
|
|
1835
|
+
{
|
|
1836
|
+
type: "text",
|
|
1837
|
+
text: JSON.stringify(results, null, 2)
|
|
1838
|
+
}
|
|
1839
|
+
]
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
);
|
|
1843
|
+
server.registerTool(
|
|
1844
|
+
"get_constraints",
|
|
1845
|
+
{
|
|
1846
|
+
description: "Returns evidence-backed operational constraints extracted deterministically from package markdown docs. Use this to surface prerequisites, unsupported scenarios, and ordering requirements.",
|
|
1847
|
+
inputSchema: {
|
|
1848
|
+
package: z.string().describe('NPM package name (e.g., "zod", "express")'),
|
|
1849
|
+
version: z.string().describe('NPM version string: exact version or published dist-tag (e.g., "3.23.8", "latest")'),
|
|
1850
|
+
limit: z.number().int().min(1).max(50).optional().describe("Maximum number of ranked constraint findings to return (default: 10)")
|
|
1851
|
+
}
|
|
1852
|
+
},
|
|
1853
|
+
async ({ package: pkg, version, limit }) => {
|
|
1854
|
+
const results = await getConstraints(pkg, version, limit ?? 10);
|
|
1855
|
+
return {
|
|
1856
|
+
content: [
|
|
1857
|
+
{
|
|
1858
|
+
type: "text",
|
|
1859
|
+
text: JSON.stringify(results, null, 2)
|
|
1860
|
+
}
|
|
1861
|
+
]
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
);
|
|
1865
|
+
server.registerTool(
|
|
1866
|
+
"get_performance_notes",
|
|
1867
|
+
{
|
|
1868
|
+
description: "Returns evidence-backed performance findings extracted deterministically from package markdown docs. Use this to surface complexity notes, expensive operations, cache behavior, and hot paths.",
|
|
1869
|
+
inputSchema: {
|
|
1870
|
+
package: z.string().describe('NPM package name (e.g., "zod", "express")'),
|
|
1871
|
+
version: z.string().describe('NPM version string: exact version or published dist-tag (e.g., "3.23.8", "latest")'),
|
|
1872
|
+
limit: z.number().int().min(1).max(50).optional().describe("Maximum number of ranked performance findings to return (default: 10)")
|
|
1873
|
+
}
|
|
1874
|
+
},
|
|
1875
|
+
async ({ package: pkg, version, limit }) => {
|
|
1876
|
+
const results = await getPerformanceNotes(pkg, version, limit ?? 10);
|
|
1877
|
+
return {
|
|
1878
|
+
content: [
|
|
1879
|
+
{
|
|
1880
|
+
type: "text",
|
|
1881
|
+
text: JSON.stringify(results, null, 2)
|
|
1882
|
+
}
|
|
1883
|
+
]
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
);
|
|
1887
|
+
server.registerTool(
|
|
1888
|
+
"get_api_ref",
|
|
1889
|
+
{
|
|
1890
|
+
description: "Returns a compact TypeScript API index grouped by identifier name. Each entry lists the compile-time and runtime kinds associated with that name.",
|
|
1891
|
+
inputSchema: {
|
|
1892
|
+
package: z.string().describe('NPM package name (e.g., "zod", "express")'),
|
|
1893
|
+
version: z.string().describe('NPM version string: exact version or published dist-tag (e.g., "3.23.8", "latest")')
|
|
1894
|
+
}
|
|
1895
|
+
},
|
|
1896
|
+
async ({ package: pkg, version }) => {
|
|
1897
|
+
const dts = await fetchDts(pkg, version);
|
|
1898
|
+
const apiRef = extractApiRef(dts, pkg, version);
|
|
1899
|
+
return {
|
|
1900
|
+
content: [
|
|
1901
|
+
{
|
|
1902
|
+
type: "text",
|
|
1903
|
+
text: JSON.stringify(apiRef, null, 2)
|
|
1904
|
+
}
|
|
1905
|
+
]
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
);
|
|
1909
|
+
server.registerTool(
|
|
1910
|
+
"get_declaration",
|
|
1911
|
+
{
|
|
1912
|
+
description: "Returns the explicit TypeScript declaration text for a given exported identifier name. Use get_api_ref first to discover names and kinds, then call this for the full declaration body.",
|
|
1913
|
+
inputSchema: {
|
|
1914
|
+
package: z.string().describe('NPM package name (e.g., "zod", "express")'),
|
|
1915
|
+
version: z.string().describe('NPM version string: exact version or published dist-tag (e.g., "3.23.8", "latest")'),
|
|
1916
|
+
name: z.string().describe('Exported identifier name (e.g., "ZodSchema", "parse", "ZodTypeAny")')
|
|
1917
|
+
}
|
|
1918
|
+
},
|
|
1919
|
+
async ({ package: pkg, version, name }) => {
|
|
1920
|
+
const dts = await fetchDts(pkg, version);
|
|
1921
|
+
const declaration = extractDeclaration(dts, name);
|
|
1922
|
+
return {
|
|
1923
|
+
content: [
|
|
1924
|
+
{
|
|
1925
|
+
type: "text",
|
|
1926
|
+
text: declaration
|
|
1927
|
+
}
|
|
1928
|
+
]
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
);
|
|
1932
|
+
server.registerTool(
|
|
1933
|
+
"compare_versions",
|
|
1934
|
+
{
|
|
1935
|
+
description: "Returns a structural diff of documentation headings between two versions of an NPM package. Shows which sections were added, removed, or unchanged \u2014 useful for spotting breaking changes.",
|
|
1936
|
+
inputSchema: {
|
|
1937
|
+
package: z.string().describe('NPM package name (e.g., "zod", "express")'),
|
|
1938
|
+
v_old: z.string().describe('Older version to compare from (e.g., "3.22.0")'),
|
|
1939
|
+
v_new: z.string().describe('Newer version to compare to (e.g., "3.23.8")')
|
|
1940
|
+
}
|
|
1941
|
+
},
|
|
1942
|
+
async ({ package: pkg, v_old, v_new }) => {
|
|
1943
|
+
const [oldReadme, newReadme] = await Promise.all([
|
|
1944
|
+
fetchReadme(pkg, v_old),
|
|
1945
|
+
fetchReadme(pkg, v_new)
|
|
1946
|
+
]);
|
|
1947
|
+
const diff = compareTocs(oldReadme, newReadme);
|
|
1948
|
+
return {
|
|
1949
|
+
content: [
|
|
1950
|
+
{
|
|
1951
|
+
type: "text",
|
|
1952
|
+
text: JSON.stringify({ package: pkg, v_old, v_new, diff }, null, 2)
|
|
1953
|
+
}
|
|
1954
|
+
]
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
);
|
|
1958
|
+
return server;
|
|
1959
|
+
}
|
|
1960
|
+
async function runStdioServer() {
|
|
1961
|
+
const server = createServer();
|
|
1962
|
+
const transport = new StdioServerTransport();
|
|
1963
|
+
await server.connect(transport);
|
|
1964
|
+
process.stderr.write("[soup-chop] MCP server running on stdio \u2014 connect your client via stdio transport\n");
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// src/index.ts
|
|
1968
|
+
function parseClientsArgs(args) {
|
|
1969
|
+
let json = false;
|
|
1970
|
+
for (const flag of args) {
|
|
1971
|
+
if (flag === "--json") {
|
|
1972
|
+
json = true;
|
|
1973
|
+
continue;
|
|
1974
|
+
}
|
|
1975
|
+
throw new Error(`Unknown clients option: "${flag}"
|
|
1976
|
+
|
|
1977
|
+
Usage: soup-chop clients [--json]`);
|
|
1978
|
+
}
|
|
1979
|
+
return { json };
|
|
1980
|
+
}
|
|
1981
|
+
function usage() {
|
|
1982
|
+
const supported = getSupportedAutoConfigClientIds().join(", ");
|
|
1983
|
+
return [
|
|
1984
|
+
"soup-chop <command>",
|
|
1985
|
+
"",
|
|
1986
|
+
"Commands:",
|
|
1987
|
+
" serve Run the MCP server over stdio",
|
|
1988
|
+
" configure <client> [--project|--global] [--dry-run|--print] [--local-build]",
|
|
1989
|
+
" Write soup-chop into a supported client config",
|
|
1990
|
+
" clients [--json] List documented clients and auto-config support",
|
|
1991
|
+
" help Show this help message",
|
|
1992
|
+
"",
|
|
1993
|
+
`Auto-config clients: ${supported}`
|
|
1994
|
+
].join("\n");
|
|
1995
|
+
}
|
|
1996
|
+
function printClients(json) {
|
|
1997
|
+
const entries = listClientEntries();
|
|
1998
|
+
if (json) {
|
|
1999
|
+
process.stdout.write(`${JSON.stringify(entries, null, 2)}
|
|
2000
|
+
`);
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
const lines = entries.map((entry) => {
|
|
2004
|
+
const suffix = entry.mode === "implemented elsewhere" ? " (not auto-configurable on this platform)" : "";
|
|
2005
|
+
return `- ${entry.name} [${entry.mode}] \u2014 ${entry.displayPath}${suffix}`;
|
|
2006
|
+
});
|
|
2007
|
+
process.stdout.write(`${lines.join("\n")}
|
|
2008
|
+
`);
|
|
2009
|
+
}
|
|
2010
|
+
async function main() {
|
|
2011
|
+
const [command = "serve", ...rest] = process.argv.slice(2);
|
|
2012
|
+
if (command === "serve") {
|
|
2013
|
+
await runStdioServer();
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
if (command === "configure") {
|
|
2017
|
+
const { client, scope, localBuild, outputMode } = parseConfigureArgs(rest, getSupportedAutoConfigClientIds());
|
|
2018
|
+
if (outputMode === "print") {
|
|
2019
|
+
const resolvedClient = resolveClient(client);
|
|
2020
|
+
const snippet = buildConfigSnippet(resolvedClient, { scope, localBuild });
|
|
2021
|
+
process.stdout.write(`${JSON.stringify(snippet, null, 2)}
|
|
2022
|
+
`);
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
if (outputMode === "dry-run") {
|
|
2026
|
+
const prepared = await prepareClientConfig(client, { scope, localBuild });
|
|
2027
|
+
process.stdout.write(`Would configure ${prepared.client.name} at ${prepared.path}
|
|
2028
|
+
`);
|
|
2029
|
+
process.stdout.write(prepared.content);
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
const result = await configureClient(client, { scope, localBuild });
|
|
2033
|
+
process.stdout.write(`Configured ${result.client.name} at ${result.path}
|
|
2034
|
+
`);
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
if (command === "clients") {
|
|
2038
|
+
const { json } = parseClientsArgs(rest);
|
|
2039
|
+
printClients(json);
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
2043
|
+
process.stdout.write(`${usage()}
|
|
2044
|
+
`);
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
throw new Error(`Unknown command: "${command}"
|
|
2048
|
+
|
|
2049
|
+
${usage()}`);
|
|
2050
|
+
}
|
|
2051
|
+
await main().catch((error) => {
|
|
2052
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2053
|
+
process.stderr.write(`${message}
|
|
2054
|
+
`);
|
|
2055
|
+
process.exitCode = 1;
|
|
2056
|
+
});
|