codebase-onboarder 0.7.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -12
- package/bin/onboarder.js +0 -0
- package/cli/agent/command.d.ts +71 -0
- package/cli/agent/command.js +177 -0
- package/cli/agent/command.js.map +1 -0
- package/cli/agent/config.d.ts +31 -0
- package/cli/agent/config.js +98 -0
- package/cli/agent/config.js.map +1 -0
- package/cli/agent/contracts.d.ts +77 -0
- package/cli/agent/contracts.js +2 -0
- package/cli/agent/contracts.js.map +1 -0
- package/cli/agent/files.d.ts +19 -0
- package/cli/agent/files.js +109 -0
- package/cli/agent/files.js.map +1 -0
- package/cli/agent/gitSource.d.ts +18 -0
- package/cli/agent/gitSource.js +69 -0
- package/cli/agent/gitSource.js.map +1 -0
- package/cli/agent/github.d.ts +21 -0
- package/cli/agent/github.js +94 -0
- package/cli/agent/github.js.map +1 -0
- package/cli/agent/mcp.d.ts +15 -0
- package/cli/agent/mcp.js +78 -0
- package/cli/agent/mcp.js.map +1 -0
- package/cli/agent/process.d.ts +18 -0
- package/cli/agent/process.js +118 -0
- package/cli/agent/process.js.map +1 -0
- package/cli/agent/protocol.d.ts +3 -0
- package/cli/agent/protocol.js +26 -0
- package/cli/agent/protocol.js.map +1 -0
- package/cli/agent/runner.d.ts +34 -0
- package/cli/agent/runner.js +152 -0
- package/cli/agent/runner.js.map +1 -0
- package/cli/agent/stdio.d.ts +1 -0
- package/cli/agent/stdio.js +62 -0
- package/cli/agent/stdio.js.map +1 -0
- package/cli/agent/tools.d.ts +7 -0
- package/cli/agent/tools.js +225 -0
- package/cli/agent/tools.js.map +1 -0
- package/cli/chat/analysis.d.ts +26 -0
- package/cli/chat/analysis.js +188 -0
- package/cli/chat/analysis.js.map +1 -0
- package/cli/chat/app.d.ts +16 -0
- package/cli/chat/app.js +363 -0
- package/cli/chat/app.js.map +1 -0
- package/cli/chat/commands.d.ts +11 -0
- package/cli/chat/commands.js +122 -0
- package/cli/chat/commands.js.map +1 -0
- package/cli/chat/composer.d.ts +75 -0
- package/cli/chat/composer.js +467 -0
- package/cli/chat/composer.js.map +1 -0
- package/cli/chat/controller.d.ts +64 -0
- package/cli/chat/controller.js +402 -0
- package/cli/chat/controller.js.map +1 -0
- package/cli/chat/logo.d.ts +3 -0
- package/cli/chat/logo.js +14 -0
- package/cli/chat/logo.js.map +1 -0
- package/cli/chat/models.d.ts +18 -0
- package/cli/chat/models.js +88 -0
- package/cli/chat/models.js.map +1 -0
- package/cli/chat/profile.d.ts +5 -0
- package/cli/chat/profile.js +98 -0
- package/cli/chat/profile.js.map +1 -0
- package/cli/chat/render.d.ts +40 -0
- package/cli/chat/render.js +259 -0
- package/cli/chat/render.js.map +1 -0
- package/cli/chat/screen.d.ts +26 -0
- package/cli/chat/screen.js +117 -0
- package/cli/chat/screen.js.map +1 -0
- package/cli/chat/source.d.ts +29 -0
- package/cli/chat/source.js +116 -0
- package/cli/chat/source.js.map +1 -0
- package/cli/chat/store.d.ts +43 -0
- package/cli/chat/store.js +94 -0
- package/cli/chat/store.js.map +1 -0
- package/cli/chat/text.d.ts +4 -0
- package/cli/chat/text.js +27 -0
- package/cli/chat/text.js.map +1 -0
- package/cli/explorer/app.d.ts +1 -0
- package/cli/explorer/app.js +20 -1
- package/cli/explorer/app.js.map +1 -1
- package/cli/explorer/commands.js +5 -0
- package/cli/explorer/commands.js.map +1 -1
- package/cli/explorer/featureViews.d.ts +1 -0
- package/cli/explorer/featureViews.js +4 -0
- package/cli/explorer/featureViews.js.map +1 -1
- package/cli/main.js +47 -13
- package/cli/main.js.map +1 -1
- package/docs/HERMES_RESEARCH.md +51 -0
- package/package.json +2 -1
- package/public/app.js +98 -24
- package/public/app.js.map +1 -1
- package/public/assets/onboarder-mark.svg +11 -0
- package/public/browser.d.ts +3 -0
- package/public/browser.js.map +1 -1
- package/public/index.html +24 -7
- package/public/js/about.js +2 -2
- package/public/js/about.js.map +1 -1
- package/public/js/aiDraft.d.ts +1 -0
- package/public/js/aiDraft.js +66 -17
- package/public/js/aiDraft.js.map +1 -1
- package/public/js/api.d.ts +8 -5
- package/public/js/api.js +45 -22
- package/public/js/api.js.map +1 -1
- package/public/js/deepAnalysisView.js +2 -2
- package/public/js/deepAnalysisView.js.map +1 -1
- package/public/js/diagramPane.d.ts +1 -0
- package/public/js/diagramPane.js +5 -0
- package/public/js/diagramPane.js.map +1 -1
- package/public/js/docsView.js +6 -6
- package/public/js/docsView.js.map +1 -1
- package/public/js/dom.d.ts +8 -0
- package/public/js/dom.js.map +1 -1
- package/public/js/llm.d.ts +3 -1
- package/public/js/llm.js +15 -8
- package/public/js/llm.js.map +1 -1
- package/public/js/repoFiles.js +1 -1
- package/public/js/repoFiles.js.map +1 -1
- package/public/js/reviewView.js +1 -1
- package/public/js/reviewView.js.map +1 -1
- package/public/styles.css +25 -0
- package/server/gitDiff.d.ts +1 -1
- package/server/hermesChat.d.ts +13 -0
- package/server/hermesChat.js +145 -0
- package/server/hermesChat.js.map +1 -0
- package/server/http.d.ts +5 -4
- package/server/http.js +0 -4
- package/server/http.js.map +1 -1
- package/server/mcp/server.d.ts +1 -1
- package/server/mcp/server.js +2 -2
- package/server/mcp/server.js.map +1 -1
- package/server/router.js +7 -3
- package/server/router.js.map +1 -1
- package/server/tools/scan.js +16 -2
- package/server/tools/scan.js.map +1 -1
- package/server/tools.d.ts +1 -0
- package/server/tools.js +46 -6
- package/server/tools.js.map +1 -1
- package/shared/diagram/aiMermaid.js +16 -13
- package/shared/diagram/aiMermaid.js.map +1 -1
package/server/mcp/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server/mcp/server.ts"],"names":[],"mappings":"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","sourcesContent":["// The MCP server: JSON-RPC 2.0 over stdio, one JSON object per line.\n//\n// Why no SDK: the project has no dependencies, and `package.json` says so. MCP's\n// stdio transport is a framing convention (one line-delimited JSON-RPC message\n// per message, on the child's stdin/stdout), not a wire protocol with magic — so\n// implementing it directly is a few dozen lines and keeps the promise that this\n// repo installs with nothing. If the SDK ever becomes worth it, this file is the\n// only thing that changes; `tools.js` and `analysis.js` know nothing about JSON.\n//\n// The rules that matter for correctness:\n// * stdout belongs to the protocol. Anything else printed there corrupts the\n// stream, so the logger is redirected to stderr for the lifetime of the\n// child (see `runner.js`).\n// * Requests are answered in order. MCP clients pipeline, and answering a\n// `tools/call` out of order makes some clients discard the later result.\n// * A tool that throws is answered with a *result* carrying `isError: true`,\n// not a JSON-RPC error. That is what puts the message in front of the model\n// so it can correct itself; a protocol error just fails the turn.\n\nimport { toolDefinitions, callTool, findTool } from './tools.js';\nimport { ToolError } from './analysis.js';\n\nexport const PROTOCOL_VERSION = '2025-06-18';\nexport const SERVER_INFO = {\n name: 'onboarder',\n version: '1.0.0',\n title: 'Onboarder',\n instructions:\n 'Onboarder explains a codebase. Start with onboarder_scan on a repository path; '\n + 'it returns a written overview, the entry points, the most depended-upon files, and '\n + 'any dependency cycles. Then use onboarder_tour for a reading order, onboarder_search '\n + 'to find things by name, onboarder_explain_file for one file, and onboarder_read_file '\n + 'for raw source. Every tool also takes `path`; omit it to keep working on the last '\n + 'repository scanned. The repository is read, never written.',\n};\n\n// JSON-RPC error codes. Only the first two are protocol-level; the rest are\n// conventional in MCP and keep a misbehaving client from parsing a server bug as\n// a server bug it caused.\nconst PARSE_ERROR = -32700;\nconst INVALID_REQUEST = -32600;\nconst METHOD_NOT_FOUND = -32601;\nconst INVALID_PARAMS = -32602;\n\nfunction result(id, value) {\n return { jsonrpc: '2.0', id, result: value };\n}\n\nfunction failure(id, code, message, data?: unknown) {\n const error: { code: number; message: string; data?: unknown } = { code, message };\n if (data !== undefined) error.data = data;\n return { jsonrpc: '2.0', id, error };\n}\n\n// A tool error and an internal error are different things. A `ToolError` is a\n// sentence the model can act on — \"that path is outside the repo\" — so it becomes\n// an isError result with that sentence intact. Anything else is a bug, and saying\n// so plainly beats pretending it was the caller's fault.\nasync function callOne(name, args) {\n const tool = findTool(name);\n if (!tool) {\n return {\n isError: true,\n content: [{ type: 'text', text: `No tool called \"${name}\". Call tools/list to see what is available.` }],\n };\n }\n try {\n const value = await tool.run(args || {});\n // MCP content is a list of blocks. A tool that returns structured data sends\n // it as JSON text plus the native `structuredContent` field, which is what\n // typed clients read; the text block is what everything else falls back to.\n return {\n content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],\n structuredContent: value,\n isError: false,\n };\n } catch (err) {\n if (err instanceof ToolError) {\n return { isError: true, content: [{ type: 'text', text: err.message }] };\n }\n // Unexpected. Report the message, not the stack: the caller is a model on the\n // other end of a pipe, and a stack trace is noise. The full error is on the\n // child process's stderr, which is where a human debugging this will look.\n console.error('[onboarder-mcp] tool failed:', err);\n return {\n isError: true,\n content: [{ type: 'text', text: `The tool failed unexpectedly: ${err?.message || String(err)}` }],\n };\n }\n}\n\n// The dispatcher is exported because the Streamable HTTP transport in `./http.js`\n// needs to answer the same messages without going through a line of stdio. The\n// protocol logic lives in exactly one place; a second transport must not become a\n// second implementation of `tools/list`.\nexport { handle };\n\nasync function handle(msg) {\n // A request must name a method and must declare the protocol version. Anything\n // else is not a request at all, and replying to it would invent an id to answer\n // under — so this returns nothing and the line is dropped.\n //\n // `null` rather than `undefined` is the contract with the loop below: both mean\n // \"say nothing\", but only one of them survives being awaited and compared.\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0' || typeof msg.method !== 'string') {\n return null;\n }\n const { method, id, params } = msg;\n\n // A notification has no id and expects no answer. Returning `null` is how the\n // loop knows to stay silent, which matters: replying to a notification is\n // itself a protocol violation.\n const isNotification = id === undefined || id === null;\n\n switch (method) {\n case 'initialize':\n return isNotification ? undefined : result(id, {\n protocolVersion: PROTOCOL_VERSION,\n capabilities: { tools: { listChanged: false } },\n serverInfo: SERVER_INFO,\n });\n\n // The client's post-handshake ping. Answering it is what tells the client the\n // handshake completed and the tool loop is live.\n case 'notifications/initialized':\n return null;\n\n case 'ping':\n return isNotification ? undefined : result(id, {});\n\n case 'tools/list':\n return isNotification ? undefined : result(id, { tools: toolDefinitions() });\n\n case 'tools/call':\n if (isNotification) return null;\n if (!params?.name || typeof params.name !== 'string') {\n return failure(id, INVALID_PARAMS, 'tools/call needs a tool name.');\n }\n return result(id, await callOne(params.name, params.arguments));\n\n // A client asking for resources or prompts is not an error — this server has\n // none, and saying so is more useful than METHOD_NOT_FOUND. A harness that\n // probes both at startup gets a clean answer instead of a warning.\n case 'resources/list':\n return result(id, { resources: [] });\n\n case 'prompts/list':\n return result(id, { prompts: [] });\n\n case 'resources/templates/list':\n return result(id, { resourceTemplates: [] });\n\n default:\n if (isNotification) return null;\n return failure(id, METHOD_NOT_FOUND, `No method called \"${method}\".`);\n }\n}\n\n// One line in, one line out. A message that will not parse gets a parse error\n// with `id: null`, which is what the spec requires — the client cannot know which\n// request the bad line belonged to.\nfunction parseLine(line) {\n try {\n const msg = JSON.parse(line);\n if (!msg || typeof msg !== 'object' || Array.isArray(msg)) {\n return { bad: failure(null, INVALID_REQUEST, 'A message must be a JSON-RPC object.') };\n }\n return { msg };\n } catch (err) {\n return { bad: failure(null, PARSE_ERROR, `Could not parse the message: ${err.message}`) };\n }\n}\n\n/**\n * Run the server until stdin closes. `input` and `output` are parameters so tests\n * can drive a whole session through in-memory streams.\n */\nexport function serveMcp({ input = process.stdin, output = process.stdout }: Record<string, any> = {}) {\n let buffer = '';\n // Messages are handled strictly in order, but not awaited inline: a long scan\n // must not block the event loop from reading the next line, or a client that\n // pipelines would sit with a full buffer. The promise chain is what preserves\n // the ordering while letting reads continue.\n let chain = Promise.resolve();\n let closed = false;\n\n const send = (message) => {\n if (closed) return;\n output.write(JSON.stringify(message) + '\\n');\n };\n\n const enqueue = (line) => {\n chain = chain.then(async () => {\n const { msg, bad } = parseLine(line);\n if (bad) { send(bad); return; }\n const response = await handle(msg);\n if (response) send(response);\n }).catch((err) => {\n // `handle` already turns tool failures into results. Reaching here means\n // something threw outside that, so the only honest move is to log it and\n // keep serving — a dead server helps nobody recover.\n console.error('[onboarder-mcp] dispatch failed:', err);\n });\n };\n\n input.setEncoding?.('utf8');\n input.on('data', (chunk) => {\n buffer += chunk;\n let nl;\n // A partial line at the end of a chunk stays in the buffer for the next one.\n while ((nl = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, nl).trim();\n buffer = buffer.slice(nl + 1);\n if (line) enqueue(line);\n }\n });\n\n input.on('end', () => {\n // A last line without a trailing newline is still a message. Dropping it\n // would lose the final request of a session — which is often the only one.\n const line = buffer.trim();\n buffer = '';\n if (line) enqueue(line);\n // Let the queue finish before the process exits, so a reply written just\n // after stdin closed still reaches the client.\n chain.finally(() => {\n if (output === process.stdout) process.exitCode = 0;\n });\n });\n\n input.on('error', (err) => {\n console.error('[onboarder-mcp] stdin error:', err);\n });\n\n return {\n stop() { closed = true; },\n // Awaiting the queue is how a test knows every reply has been written, and\n // how a shutdown can drain in-flight work instead of cutting it off.\n settled() { return chain; },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server/mcp/server.ts"],"names":[],"mappings":"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","sourcesContent":["// The MCP server: JSON-RPC 2.0 over stdio, one JSON object per line.\n//\n// Why no SDK: the project has no dependencies, and `package.json` says so. MCP's\n// stdio transport is a framing convention (one line-delimited JSON-RPC message\n// per message, on the child's stdin/stdout), not a wire protocol with magic — so\n// implementing it directly is a few dozen lines and keeps the promise that this\n// repo installs with nothing. If the SDK ever becomes worth it, this file is the\n// only thing that changes; `tools.js` and `analysis.js` know nothing about JSON.\n//\n// The rules that matter for correctness:\n// * stdout belongs to the protocol. Anything else printed there corrupts the\n// stream, so the logger is redirected to stderr for the lifetime of the\n// child (see `runner.js`).\n// * Requests are answered in order. MCP clients pipeline, and answering a\n// `tools/call` out of order makes some clients discard the later result.\n// * A tool that throws is answered with a *result* carrying `isError: true`,\n// not a JSON-RPC error. That is what puts the message in front of the model\n// so it can correct itself; a protocol error just fails the turn.\n\nimport { toolDefinitions, callTool, findTool } from './tools.js';\nimport { ToolError } from './analysis.js';\n\nexport const PROTOCOL_VERSION = '2025-06-18';\nexport const SERVER_INFO = {\n name: 'onboarder',\n version: '1.0.0',\n title: 'Onboarder',\n instructions:\n 'Onboarder explains a codebase. Start with onboarder_scan on a repository path; '\n + 'it returns a written overview, the entry points, the most depended-upon files, and '\n + 'any dependency cycles. Then use onboarder_tour for a reading order, onboarder_search '\n + 'to find things by name, onboarder_explain_file for one file, and onboarder_read_file '\n + 'for raw source. Every tool also takes `path`; omit it to keep working on the last '\n + 'repository scanned. The repository is read, never written.',\n};\n\n// JSON-RPC error codes. Only the first two are protocol-level; the rest are\n// conventional in MCP and keep a misbehaving client from parsing a server bug as\n// a server bug it caused.\nconst PARSE_ERROR = -32700;\nconst INVALID_REQUEST = -32600;\nconst METHOD_NOT_FOUND = -32601;\nconst INVALID_PARAMS = -32602;\n\nfunction result(id, value) {\n return { jsonrpc: '2.0', id, result: value };\n}\n\nfunction failure(id, code, message, data?: unknown) {\n const error: { code: number; message: string; data?: unknown } = { code, message };\n if (data !== undefined) error.data = data;\n return { jsonrpc: '2.0', id, error };\n}\n\n// A tool error and an internal error are different things. A `ToolError` is a\n// sentence the model can act on — \"that path is outside the repo\" — so it becomes\n// an isError result with that sentence intact. Anything else is a bug, and saying\n// so plainly beats pretending it was the caller's fault.\nasync function callOne(name, args) {\n const tool = findTool(name);\n if (!tool) {\n return {\n isError: true,\n content: [{ type: 'text', text: `No tool called \"${name}\". Call tools/list to see what is available.` }],\n };\n }\n try {\n const value = await tool.run(args || {});\n // MCP content is a list of blocks. A tool that returns structured data sends\n // it as JSON text plus the native `structuredContent` field, which is what\n // typed clients read; the text block is what everything else falls back to.\n return {\n content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],\n structuredContent: value,\n isError: false,\n };\n } catch (err) {\n if (err instanceof ToolError) {\n return { isError: true, content: [{ type: 'text', text: err.message }] };\n }\n // Unexpected. Report the message, not the stack: the caller is a model on the\n // other end of a pipe, and a stack trace is noise. The full error is on the\n // child process's stderr, which is where a human debugging this will look.\n console.error('[onboarder-mcp] tool failed:', err);\n return {\n isError: true,\n content: [{ type: 'text', text: `The tool failed unexpectedly: ${err?.message || String(err)}` }],\n };\n }\n}\n\n// The dispatcher is exported because the Streamable HTTP transport in `./http.js`\n// needs to answer the same messages without going through a line of stdio. The\n// protocol logic lives in exactly one place; a second transport must not become a\n// second implementation of `tools/list`.\nexport { handle };\n\nasync function handle(msg) {\n // A request must name a method and must declare the protocol version. Anything\n // else is not a request at all, and replying to it would invent an id to answer\n // under — so this returns nothing and the line is dropped.\n //\n // `null` rather than `undefined` is the contract with the loop below: both mean\n // \"say nothing\", but only one of them survives being awaited and compared.\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0' || typeof msg.method !== 'string') {\n return null;\n }\n const { method, id, params } = msg;\n\n // A notification has no id and expects no answer. Returning `null` is how the\n // loop knows to stay silent, which matters: replying to a notification is\n // itself a protocol violation.\n const isNotification = id === undefined || id === null;\n\n switch (method) {\n case 'initialize':\n return isNotification ? undefined : result(id, {\n protocolVersion: PROTOCOL_VERSION,\n capabilities: { tools: { listChanged: false } },\n serverInfo: SERVER_INFO,\n });\n\n // The client's post-handshake ping. Answering it is what tells the client the\n // handshake completed and the tool loop is live.\n case 'notifications/initialized':\n return null;\n\n case 'ping':\n return isNotification ? undefined : result(id, {});\n\n case 'tools/list':\n return isNotification ? undefined : result(id, { tools: toolDefinitions() });\n\n case 'tools/call':\n if (isNotification) return null;\n if (!params?.name || typeof params.name !== 'string') {\n return failure(id, INVALID_PARAMS, 'tools/call needs a tool name.');\n }\n return result(id, await callOne(params.name, params.arguments));\n\n // A client asking for resources or prompts is not an error — this server has\n // none, and saying so is more useful than METHOD_NOT_FOUND. A harness that\n // probes both at startup gets a clean answer instead of a warning.\n case 'resources/list':\n return result(id, { resources: [] });\n\n case 'prompts/list':\n return result(id, { prompts: [] });\n\n case 'resources/templates/list':\n return result(id, { resourceTemplates: [] });\n\n default:\n if (isNotification) return null;\n return failure(id, METHOD_NOT_FOUND, `No method called \"${method}\".`);\n }\n}\n\n// One line in, one line out. A message that will not parse gets a parse error\n// with `id: null`, which is what the spec requires — the client cannot know which\n// request the bad line belonged to.\nfunction parseLine(line) {\n try {\n const msg = JSON.parse(line);\n if (!msg || typeof msg !== 'object' || Array.isArray(msg)) {\n return { bad: failure(null, INVALID_REQUEST, 'A message must be a JSON-RPC object.') };\n }\n return { msg };\n } catch (err) {\n return { bad: failure(null, PARSE_ERROR, `Could not parse the message: ${err.message}`) };\n }\n}\n\n/**\n * Run the server until stdin closes. `input` and `output` are parameters so tests\n * can drive a whole session through in-memory streams.\n */\nexport function serveMcp({ input = process.stdin, output = process.stdout, dispatch = handle }: Record<string, any> = {}) {\n let buffer = '';\n // Messages are handled strictly in order, but not awaited inline: a long scan\n // must not block the event loop from reading the next line, or a client that\n // pipelines would sit with a full buffer. The promise chain is what preserves\n // the ordering while letting reads continue.\n let chain = Promise.resolve();\n let closed = false;\n\n const send = (message) => {\n if (closed) return;\n output.write(JSON.stringify(message) + '\\n');\n };\n\n const enqueue = (line) => {\n chain = chain.then(async () => {\n const { msg, bad } = parseLine(line);\n if (bad) { send(bad); return; }\n const response = await dispatch(msg);\n if (response) send(response);\n }).catch((err) => {\n // `handle` already turns tool failures into results. Reaching here means\n // something threw outside that, so the only honest move is to log it and\n // keep serving — a dead server helps nobody recover.\n console.error('[onboarder-mcp] dispatch failed:', err);\n });\n };\n\n input.setEncoding?.('utf8');\n input.on('data', (chunk) => {\n buffer += chunk;\n let nl;\n // A partial line at the end of a chunk stays in the buffer for the next one.\n while ((nl = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, nl).trim();\n buffer = buffer.slice(nl + 1);\n if (line) enqueue(line);\n }\n });\n\n input.on('end', () => {\n // A last line without a trailing newline is still a message. Dropping it\n // would lose the final request of a session — which is often the only one.\n const line = buffer.trim();\n buffer = '';\n if (line) enqueue(line);\n // Let the queue finish before the process exits, so a reply written just\n // after stdin closed still reaches the client.\n chain.finally(() => {\n if (output === process.stdout) process.exitCode = 0;\n });\n });\n\n input.on('error', (err) => {\n console.error('[onboarder-mcp] stdin error:', err);\n });\n\n return {\n stop() { closed = true; },\n // Awaiting the queue is how a test knows every reply has been written, and\n // how a shutdown can drain in-flight work instead of cutting it off.\n settled() { return chain; },\n };\n}\n"]}
|
package/server/router.js
CHANGED
|
@@ -15,6 +15,7 @@ import { handleCleanup, handleScan } from './apiScan.js';
|
|
|
15
15
|
import { sendError, sendJSON, readBody } from './http.js';
|
|
16
16
|
import { crossOriginReason, rebindingReason } from './httpGuards.js';
|
|
17
17
|
import { proxyChat } from './llmProxy.js';
|
|
18
|
+
import { hermesWeb } from './hermesChat.js';
|
|
18
19
|
import { serveStatic } from './static.js';
|
|
19
20
|
import { handleSearch } from './apiSearch.js';
|
|
20
21
|
import { handleBlame } from './apiGitBlame.js';
|
|
@@ -57,11 +58,14 @@ const ROUTES = [
|
|
|
57
58
|
run: ({ res, url }) => handleFile(res, url.searchParams.get('scan'), url.searchParams.get('path')),
|
|
58
59
|
},
|
|
59
60
|
{
|
|
60
|
-
//
|
|
61
|
-
//
|
|
61
|
+
// Hermes owns its server-side login. API mode forwards the browser's key
|
|
62
|
+
// to its chosen endpoint. Both share the same streaming response contract.
|
|
62
63
|
method: 'POST', path: '/api/explain', body: true,
|
|
63
|
-
run: ({ res, body }) =>
|
|
64
|
+
run: ({ res, body, config }) => body?.connection === 'hermes'
|
|
65
|
+
? (config.hermes || hermesWeb).chat(res, body) : proxyChat(res, body),
|
|
64
66
|
},
|
|
67
|
+
{ method: 'GET', path: '/api/hermes', sameOrigin: true,
|
|
68
|
+
run: ({ res, config }) => (config.hermes || hermesWeb).status(res) },
|
|
65
69
|
{
|
|
66
70
|
method: 'POST', path: '/api/doc', body: true,
|
|
67
71
|
run: ({ res, body }) => handleDocs(res, body),
|
package/server/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/server/router.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { Settings } from '../shared/contracts.js';\n\ninterface RouteContext { req: IncomingMessage; res: ServerResponse; url: URL; body: any; rest: string; config: any; settings: Settings }\ninterface Route { method: string; path?: string; prefix?: string; body?: boolean; sameOrigin?: boolean; run(context: RouteContext): unknown }\n// The route table and the gates in front of it.\n//\n// Everything arrives here: one function decides whether a request is allowed to\n// be answered at all, then which handler answers it. The routes are a list rather\n// than a ladder of `if` statements so that the whole surface of the server is\n// visible in one screen, and everything else is a static file.\n//\n// There are three concerns. Local mode relies on `Host`, `Origin`, and\n// `Sec-Fetch-Site` to stop another page and DNS rebinding from driving it.\n// Self-hosted mode adds real access-key authentication: local browser requests\n// stay open, remote browsers get a signed session, and API clients use Bearer.\n\nimport { handleDocs } from './apiDocs.js';\nimport { handleFile } from './apiFile.js';\nimport { handleCleanup, handleScan } from './apiScan.js';\nimport { sendError, sendJSON, readBody } from './http.js';\nimport { crossOriginReason, rebindingReason } from './httpGuards.js';\nimport { proxyChat } from './llmProxy.js';\nimport { serveStatic } from './static.js';\nimport { handleSearch } from './apiSearch.js';\nimport { handleBlame } from './apiGitBlame.js';\nimport { handleDiff, handleDiffRefs } from './apiDiff.js';\nimport { handleReview } from './apiReview.js';\nimport { handleToolsInstall, handleToolsRun, handleToolsStatus } from './apiTools.js';\nimport { handleMcpStart, handleMcpStatus, handleMcpStop, handleMcpCommand } from './apiMcp.js';\nimport { handleGetSettings, handleRotateAccessKey, handleUpdateSettings } from './apiSettings.js';\nimport { handleAuthStatus, handleLogin, handleLogout } from './apiAuth.js';\nimport { accessKeysMatch, allowedHosts, authReason, bearerToken, DEFAULT_SETTINGS } from './config.js';\nimport { hasValidSession } from './auth.js';\n\nconst ROUTES: Route[] = [\n { method: 'POST', path: '/api/review', body: true, run: ({ res, body }) => handleReview(res, body) },\n {\n method: 'GET', path: '/api/diff/refs', sameOrigin: true,\n run: ({ res, url }) => handleDiffRefs(res, url.searchParams.get('scan')),\n },\n {\n method: 'GET', path: '/api/diff', sameOrigin: true,\n run: ({ res, url }) => handleDiff(res, url.searchParams.get('scan'), url.searchParams),\n },\n {\n method: 'POST', path: '/api/search', body: true,\n run: ({ res, body }) => handleSearch(res, body),\n },\n {\n method: 'GET', path: '/api/blame', sameOrigin: true,\n run: ({ res, url }) => handleBlame(res, url.searchParams.get('scan'), url.searchParams.get('path')),\n },\n {\n method: 'POST', path: '/api/scan', body: true,\n run: ({ res, body, config }) => handleScan(res, body, config),\n },\n {\n method: 'DELETE', prefix: '/api/scan/',\n run: ({ res, rest }) => handleCleanup(res, decodeURIComponent(rest)),\n },\n {\n method: 'GET', path: '/api/file', sameOrigin: true,\n run: ({ res, url }) => handleFile(res, url.searchParams.get('scan'), url.searchParams.get('path')),\n },\n {\n // The proxy is the handler; there is no wrapper to write. It forwards the\n // browser's key to the endpoint the browser chose and never keeps either.\n method: 'POST', path: '/api/explain', body: true,\n run: ({ res, body }) => proxyChat(res, body),\n },\n {\n method: 'POST', path: '/api/doc', body: true,\n run: ({ res, body }) => handleDocs(res, body),\n },\n {\n // Reports which analyzers are installed — environment information a foreign\n // page has no business reading, so it is guarded like /api/file.\n method: 'GET', path: '/api/tools', sameOrigin: true,\n run: ({ res }) => handleToolsStatus(res),\n },\n {\n // Runs real analyzers over a scanned root. `body: true` applies the size\n // limit; the session check in the handler is the capability gate.\n method: 'POST', path: '/api/tools/run', body: true,\n run: ({ res, body }) => handleToolsRun(res, body),\n },\n {\n // Installs a missing analyzer from the GUI. Same-origin like every POST;\n // the tool name is validated against the registry and the install plans\n // are fixed data, so the request body never reaches a command line.\n method: 'POST', path: '/api/tools/install', body: true,\n run: ({ res, body }) => handleToolsInstall(res, body),\n },\n {\n // The MCP button's state, and the command an agent harness is configured with.\n // The config is not secret, but it is a path into this machine and a foreign\n // page has no business reading either one.\n method: 'GET', path: '/api/mcp', sameOrigin: true,\n run: ({ req, res, config }) => handleMcpStatus(req, res, config),\n },\n {\n method: 'GET', path: '/api/mcp/command', sameOrigin: true,\n run: ({ req, res, config }) => handleMcpCommand(req, res, config),\n },\n {\n // Starting and stopping a process is a side effect on the person's machine, so\n // these take a body like every other POST and are checked like every other POST.\n method: 'POST', path: '/api/mcp/start', body: true,\n run: ({ req, res, config }) => handleMcpStart(req, res, config),\n },\n {\n method: 'POST', path: '/api/mcp/stop', body: true,\n run: ({ req, res, config }) => handleMcpStop(req, res, config),\n },\n {\n method: 'GET', path: '/api/health',\n run: ({ res }) => sendJSON(res, 200, { ok: true }),\n },\n {\n method: 'GET', path: '/api/auth/status', sameOrigin: true,\n run: ({ req, res, settings }) => handleAuthStatus(req, res, settings),\n },\n {\n method: 'POST', path: '/api/auth/login', body: true, sameOrigin: true,\n run: ({ req, res, body, settings }) => handleLogin(req, res, body, settings),\n },\n {\n method: 'POST', path: '/api/auth/logout', body: true, sameOrigin: true,\n run: ({ req, res }) => handleLogout(req, res),\n },\n {\n // The settings drawer and the CLI read the same public shape: everything\n // about the configuration except the access key itself.\n method: 'GET', path: '/api/settings', sameOrigin: true,\n run: ({ req, res, config }) => handleGetSettings(req, res, config),\n },\n {\n // PATCH semantics over a small allow-listed key set; the validation that\n // rejects a bad port or a lawless mode lives in `server/config.js` and is\n // shared with the CLI wizard, so both surfaces enforce one schema.\n method: 'PUT', path: '/api/settings', body: true,\n run: ({ res, body, config }) => handleUpdateSettings(res, body, config),\n },\n {\n // The only way the key changes: generated on the server, shown once in\n // this response, never readable again. In self-hosted mode this endpoint\n // is itself behind the current key, so rotation requires possession.\n method: 'POST', path: '/api/settings/access-key', body: true,\n run: ({ req, res, config }) => handleRotateAccessKey(req, res, config),\n },\n];\n\n// The table itself, for the test that walks it. Exported read-only: the routes\n// are decided here, not assembled by whoever imports this.\nexport const routes = Object.freeze(ROUTES.map((r) => Object.freeze({ ...r })));\n\nexport function matchRoute(method, pathname) {\n for (const route of ROUTES) {\n if (route.method !== method) continue;\n if (route.path === pathname) return { route, rest: '' };\n if (route.prefix && pathname.startsWith(route.prefix)) {\n return { route, rest: pathname.slice(route.prefix.length) };\n }\n }\n return null;\n}\n\n// `config` carries the directories the handlers need — the project root to scan\n// for the demo, and the two roots static serving maps into. Passed in rather\n// than resolved here so a test can point the server somewhere else, and so this\n// module has nothing to say about where it was installed.\n//\n// Settings arrive the same way: `config.getSettings` is read on every request,\n// so a key rotated or a domain changed through the API takes effect on the next\n// request without a restart. A bare `createServer()` (the tests) has no getter\n// and sees the local-mode defaults — never somebody's home directory.\nasync function liveSettings(config) {\n if (typeof config?.getSettings !== 'function') return DEFAULT_SETTINGS;\n try {\n return await config.getSettings();\n } catch {\n return null; // unreadable config: fail closed, every /api call gets a 500\n }\n}\n\nexport function createRouter(config) {\n return async function handleRequest(req, res) {\n try {\n const url = new URL(req.url, 'http://127.0.0.1');\n\n const settings = await liveSettings(config);\n if (!settings) {\n return sendError(res, 500, 'The settings file could not be read — run `onboarder doctor` to find out why.');\n }\n\n const wrongHost = rebindingReason(req, allowedHosts(settings));\n if (wrongHost) {\n const scope = settings.mode === 'self-hosted'\n ? 'Onboarder only answers to its configured host and domain. '\n : 'Onboarder only answers to localhost. ';\n return sendError(res, 403, scope + wrongHost);\n }\n\n const found = matchRoute(req.method, url.pathname);\n const publicAuthRoute = ['/api/health', '/api/auth/status', '/api/auth/login', '/api/auth/logout'].includes(url.pathname);\n if (req.method !== 'GET' || found?.route.sameOrigin) {\n const foreign = crossOriginReason(req);\n if (foreign) {\n return sendError(res, 403, 'That request did not come from Onboarder’s own page. ' + foreign);\n }\n }\n\n // Bearer clients keep their existing API contract. A browser gets a signed,\n // HttpOnly session from the login form instead of storing the raw key.\n const bearerClient = accessKeysMatch(settings.accessKey, bearerToken(req));\n const browserAuthenticated = hasValidSession(req, settings);\n const authDenied = authReason(req, settings);\n const locallyExempt = !authDenied;\n const authenticated = bearerClient || browserAuthenticated || locallyExempt;\n const remoteSelfHosted = settings.mode === 'self-hosted' && Boolean(authDenied);\n\n if (remoteSelfHosted && !authenticated && !publicAuthRoute) {\n // API callers keep a machine-readable 401. A browser navigation gets the\n // themed sign-in document so the user never has to paste JSON into a tab.\n const accepts = String(req.headers?.accept || '');\n if (req.method === 'GET' && (url.pathname === '/' || accepts.includes('text/html'))) {\n res.statusCode = 200;\n return await serveStatic(res, '/login.html', config);\n }\n return sendError(res, 401, authDenied || 'Sign in with the Onboarder access key first.');\n }\n\n if (url.pathname === '/api/auth/logout') {\n // Always clear the browser cookie, even if it had already expired.\n return handleLogout(req, res);\n }\n\n if (found) {\n const body = found.route.body ? await readBody(req) : null;\n return await found.route.run({ req, res, url, body, rest: found.rest, config, settings });\n }\n\n if (req.method === 'GET') return await serveStatic(res, url.pathname, config);\n\n sendError(res, 404, 'Nothing lives at ' + url.pathname + '.');\n } catch (err) {\n // The last resort. Handlers turn their own expected failures into a status\n // that says something useful; anything reaching here is a bug or a broken\n // request body, and the message is more use to the person than \"500\".\n sendError(res, 500, err.message || 'Something went sideways.');\n }\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/server/router.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { Settings } from '../shared/contracts.js';\n\ninterface RouteContext { req: IncomingMessage; res: ServerResponse; url: URL; body: any; rest: string; config: any; settings: Settings }\ninterface Route { method: string; path?: string; prefix?: string; body?: boolean; sameOrigin?: boolean; run(context: RouteContext): unknown }\n// The route table and the gates in front of it.\n//\n// Everything arrives here: one function decides whether a request is allowed to\n// be answered at all, then which handler answers it. The routes are a list rather\n// than a ladder of `if` statements so that the whole surface of the server is\n// visible in one screen, and everything else is a static file.\n//\n// There are three concerns. Local mode relies on `Host`, `Origin`, and\n// `Sec-Fetch-Site` to stop another page and DNS rebinding from driving it.\n// Self-hosted mode adds real access-key authentication: local browser requests\n// stay open, remote browsers get a signed session, and API clients use Bearer.\n\nimport { handleDocs } from './apiDocs.js';\nimport { handleFile } from './apiFile.js';\nimport { handleCleanup, handleScan } from './apiScan.js';\nimport { sendError, sendJSON, readBody } from './http.js';\nimport { crossOriginReason, rebindingReason } from './httpGuards.js';\nimport { proxyChat } from './llmProxy.js';\nimport { hermesWeb } from './hermesChat.js';\nimport { serveStatic } from './static.js';\nimport { handleSearch } from './apiSearch.js';\nimport { handleBlame } from './apiGitBlame.js';\nimport { handleDiff, handleDiffRefs } from './apiDiff.js';\nimport { handleReview } from './apiReview.js';\nimport { handleToolsInstall, handleToolsRun, handleToolsStatus } from './apiTools.js';\nimport { handleMcpStart, handleMcpStatus, handleMcpStop, handleMcpCommand } from './apiMcp.js';\nimport { handleGetSettings, handleRotateAccessKey, handleUpdateSettings } from './apiSettings.js';\nimport { handleAuthStatus, handleLogin, handleLogout } from './apiAuth.js';\nimport { accessKeysMatch, allowedHosts, authReason, bearerToken, DEFAULT_SETTINGS } from './config.js';\nimport { hasValidSession } from './auth.js';\n\nconst ROUTES: Route[] = [\n { method: 'POST', path: '/api/review', body: true, run: ({ res, body }) => handleReview(res, body) },\n {\n method: 'GET', path: '/api/diff/refs', sameOrigin: true,\n run: ({ res, url }) => handleDiffRefs(res, url.searchParams.get('scan')),\n },\n {\n method: 'GET', path: '/api/diff', sameOrigin: true,\n run: ({ res, url }) => handleDiff(res, url.searchParams.get('scan'), url.searchParams),\n },\n {\n method: 'POST', path: '/api/search', body: true,\n run: ({ res, body }) => handleSearch(res, body),\n },\n {\n method: 'GET', path: '/api/blame', sameOrigin: true,\n run: ({ res, url }) => handleBlame(res, url.searchParams.get('scan'), url.searchParams.get('path')),\n },\n {\n method: 'POST', path: '/api/scan', body: true,\n run: ({ res, body, config }) => handleScan(res, body, config),\n },\n {\n method: 'DELETE', prefix: '/api/scan/',\n run: ({ res, rest }) => handleCleanup(res, decodeURIComponent(rest)),\n },\n {\n method: 'GET', path: '/api/file', sameOrigin: true,\n run: ({ res, url }) => handleFile(res, url.searchParams.get('scan'), url.searchParams.get('path')),\n },\n {\n // Hermes owns its server-side login. API mode forwards the browser's key\n // to its chosen endpoint. Both share the same streaming response contract.\n method: 'POST', path: '/api/explain', body: true,\n run: ({ res, body, config }) => body?.connection === 'hermes'\n ? (config.hermes || hermesWeb).chat(res, body) : proxyChat(res, body),\n },\n { method: 'GET', path: '/api/hermes', sameOrigin: true,\n run: ({ res, config }) => (config.hermes || hermesWeb).status(res) },\n {\n method: 'POST', path: '/api/doc', body: true,\n run: ({ res, body }) => handleDocs(res, body),\n },\n {\n // Reports which analyzers are installed — environment information a foreign\n // page has no business reading, so it is guarded like /api/file.\n method: 'GET', path: '/api/tools', sameOrigin: true,\n run: ({ res }) => handleToolsStatus(res),\n },\n {\n // Runs real analyzers over a scanned root. `body: true` applies the size\n // limit; the session check in the handler is the capability gate.\n method: 'POST', path: '/api/tools/run', body: true,\n run: ({ res, body }) => handleToolsRun(res, body),\n },\n {\n // Installs a missing analyzer from the GUI. Same-origin like every POST;\n // the tool name is validated against the registry and the install plans\n // are fixed data, so the request body never reaches a command line.\n method: 'POST', path: '/api/tools/install', body: true,\n run: ({ res, body }) => handleToolsInstall(res, body),\n },\n {\n // The MCP button's state, and the command an agent harness is configured with.\n // The config is not secret, but it is a path into this machine and a foreign\n // page has no business reading either one.\n method: 'GET', path: '/api/mcp', sameOrigin: true,\n run: ({ req, res, config }) => handleMcpStatus(req, res, config),\n },\n {\n method: 'GET', path: '/api/mcp/command', sameOrigin: true,\n run: ({ req, res, config }) => handleMcpCommand(req, res, config),\n },\n {\n // Starting and stopping a process is a side effect on the person's machine, so\n // these take a body like every other POST and are checked like every other POST.\n method: 'POST', path: '/api/mcp/start', body: true,\n run: ({ req, res, config }) => handleMcpStart(req, res, config),\n },\n {\n method: 'POST', path: '/api/mcp/stop', body: true,\n run: ({ req, res, config }) => handleMcpStop(req, res, config),\n },\n {\n method: 'GET', path: '/api/health',\n run: ({ res }) => sendJSON(res, 200, { ok: true }),\n },\n {\n method: 'GET', path: '/api/auth/status', sameOrigin: true,\n run: ({ req, res, settings }) => handleAuthStatus(req, res, settings),\n },\n {\n method: 'POST', path: '/api/auth/login', body: true, sameOrigin: true,\n run: ({ req, res, body, settings }) => handleLogin(req, res, body, settings),\n },\n {\n method: 'POST', path: '/api/auth/logout', body: true, sameOrigin: true,\n run: ({ req, res }) => handleLogout(req, res),\n },\n {\n // The settings drawer and the CLI read the same public shape: everything\n // about the configuration except the access key itself.\n method: 'GET', path: '/api/settings', sameOrigin: true,\n run: ({ req, res, config }) => handleGetSettings(req, res, config),\n },\n {\n // PATCH semantics over a small allow-listed key set; the validation that\n // rejects a bad port or a lawless mode lives in `server/config.js` and is\n // shared with the CLI wizard, so both surfaces enforce one schema.\n method: 'PUT', path: '/api/settings', body: true,\n run: ({ res, body, config }) => handleUpdateSettings(res, body, config),\n },\n {\n // The only way the key changes: generated on the server, shown once in\n // this response, never readable again. In self-hosted mode this endpoint\n // is itself behind the current key, so rotation requires possession.\n method: 'POST', path: '/api/settings/access-key', body: true,\n run: ({ req, res, config }) => handleRotateAccessKey(req, res, config),\n },\n];\n\n// The table itself, for the test that walks it. Exported read-only: the routes\n// are decided here, not assembled by whoever imports this.\nexport const routes = Object.freeze(ROUTES.map((r) => Object.freeze({ ...r })));\n\nexport function matchRoute(method, pathname) {\n for (const route of ROUTES) {\n if (route.method !== method) continue;\n if (route.path === pathname) return { route, rest: '' };\n if (route.prefix && pathname.startsWith(route.prefix)) {\n return { route, rest: pathname.slice(route.prefix.length) };\n }\n }\n return null;\n}\n\n// `config` carries the directories the handlers need — the project root to scan\n// for the demo, and the two roots static serving maps into. Passed in rather\n// than resolved here so a test can point the server somewhere else, and so this\n// module has nothing to say about where it was installed.\n//\n// Settings arrive the same way: `config.getSettings` is read on every request,\n// so a key rotated or a domain changed through the API takes effect on the next\n// request without a restart. A bare `createServer()` (the tests) has no getter\n// and sees the local-mode defaults — never somebody's home directory.\nasync function liveSettings(config) {\n if (typeof config?.getSettings !== 'function') return DEFAULT_SETTINGS;\n try {\n return await config.getSettings();\n } catch {\n return null; // unreadable config: fail closed, every /api call gets a 500\n }\n}\n\nexport function createRouter(config) {\n return async function handleRequest(req, res) {\n try {\n const url = new URL(req.url, 'http://127.0.0.1');\n\n const settings = await liveSettings(config);\n if (!settings) {\n return sendError(res, 500, 'The settings file could not be read — run `onboarder doctor` to find out why.');\n }\n\n const wrongHost = rebindingReason(req, allowedHosts(settings));\n if (wrongHost) {\n const scope = settings.mode === 'self-hosted'\n ? 'Onboarder only answers to its configured host and domain. '\n : 'Onboarder only answers to localhost. ';\n return sendError(res, 403, scope + wrongHost);\n }\n\n const found = matchRoute(req.method, url.pathname);\n const publicAuthRoute = ['/api/health', '/api/auth/status', '/api/auth/login', '/api/auth/logout'].includes(url.pathname);\n if (req.method !== 'GET' || found?.route.sameOrigin) {\n const foreign = crossOriginReason(req);\n if (foreign) {\n return sendError(res, 403, 'That request did not come from Onboarder’s own page. ' + foreign);\n }\n }\n\n // Bearer clients keep their existing API contract. A browser gets a signed,\n // HttpOnly session from the login form instead of storing the raw key.\n const bearerClient = accessKeysMatch(settings.accessKey, bearerToken(req));\n const browserAuthenticated = hasValidSession(req, settings);\n const authDenied = authReason(req, settings);\n const locallyExempt = !authDenied;\n const authenticated = bearerClient || browserAuthenticated || locallyExempt;\n const remoteSelfHosted = settings.mode === 'self-hosted' && Boolean(authDenied);\n\n if (remoteSelfHosted && !authenticated && !publicAuthRoute) {\n // API callers keep a machine-readable 401. A browser navigation gets the\n // themed sign-in document so the user never has to paste JSON into a tab.\n const accepts = String(req.headers?.accept || '');\n if (req.method === 'GET' && (url.pathname === '/' || accepts.includes('text/html'))) {\n res.statusCode = 200;\n return await serveStatic(res, '/login.html', config);\n }\n return sendError(res, 401, authDenied || 'Sign in with the Onboarder access key first.');\n }\n\n if (url.pathname === '/api/auth/logout') {\n // Always clear the browser cookie, even if it had already expired.\n return handleLogout(req, res);\n }\n\n if (found) {\n const body = found.route.body ? await readBody(req) : null;\n return await found.route.run({ req, res, url, body, rest: found.rest, config, settings });\n }\n\n if (req.method === 'GET') return await serveStatic(res, url.pathname, config);\n\n sendError(res, 404, 'Nothing lives at ' + url.pathname + '.');\n } catch (err) {\n // The last resort. Handlers turn their own expected failures into a status\n // that says something useful; anything reaching here is a bug or a broken\n // request body, and the message is more use to the person than \"500\".\n sendError(res, 500, err.message || 'Something went sideways.');\n }\n };\n}\n"]}
|
package/server/tools/scan.js
CHANGED
|
@@ -41,7 +41,7 @@ export function toolsStatus() {
|
|
|
41
41
|
function unavailableReason(def) {
|
|
42
42
|
return (def.install && def.install.length) ? def.install.join(' ') : 'Not found on PATH.';
|
|
43
43
|
}
|
|
44
|
-
async function runOneTool(def, root, toolOptions = {}) {
|
|
44
|
+
async function runOneTool(def, root, toolOptions = {}, signal) {
|
|
45
45
|
// A tool that reports through a file (gitleaks) gets a per-run temp path —
|
|
46
46
|
// never /dev/stdout, which does not exist on Windows, and never a fixed
|
|
47
47
|
// name, so two concurrent runs cannot read each other's report.
|
|
@@ -57,6 +57,7 @@ async function runOneTool(def, root, toolOptions = {}) {
|
|
|
57
57
|
const result = await runTool(resolved.argv, {
|
|
58
58
|
cwd: def.cwd ? root : undefined,
|
|
59
59
|
timeout: TOOL_TIMEOUT_MS,
|
|
60
|
+
signal,
|
|
60
61
|
});
|
|
61
62
|
// Read and remove the report file whatever happened above.
|
|
62
63
|
let reportText = null;
|
|
@@ -77,8 +78,12 @@ async function runOneTool(def, root, toolOptions = {}) {
|
|
|
77
78
|
if (!result.ok) {
|
|
78
79
|
return { ...emptyPass(def.id, def.label, result.error || 'The tool could not be run.'), available: true };
|
|
79
80
|
}
|
|
81
|
+
if (def.usesReportFile && reportText === null)
|
|
82
|
+
return { ...emptyPass(def.id, def.label, result.stderr || 'The analyzer did not create its report.'), available: true };
|
|
80
83
|
const stdout = (def.usesReportFile ? (reportText || '') : (result.stdout || '')).trim();
|
|
81
84
|
if (!stdout) {
|
|
85
|
+
if (result.status !== 0)
|
|
86
|
+
return { ...emptyPass(def.id, def.label, `Exited ${result.status}: ${result.stderr || 'No readable report.'}`), available: true };
|
|
82
87
|
// A clean pass: the tool ran and found nothing. That is a real answer, and
|
|
83
88
|
// worth saying — "Gitleaks: no secrets" is the outcome a self-hosting user
|
|
84
89
|
// runs the tool to hear.
|
|
@@ -106,12 +111,21 @@ function passResult(def, resolved, findings, started) {
|
|
|
106
111
|
// `options` is per-engine GUI settings, sanitized against the registry schema
|
|
107
112
|
// before any of it touches a command line.
|
|
108
113
|
export async function runExternalAnalysis(root, options = {}) {
|
|
114
|
+
options.signal?.throwIfAborted();
|
|
109
115
|
const kinds = options.kinds ? new Set(options.kinds) : null;
|
|
110
116
|
const tools = options.tools ? new Set(options.tools) : null;
|
|
111
117
|
const rawOptions = options.options && typeof options.options === 'object' ? options.options : {};
|
|
112
118
|
const defs = TOOL_DEFS.filter((d) => (!kinds || kinds.has(d.kind)) && (!tools || tools.has(d.id)));
|
|
113
119
|
const started = Date.now();
|
|
114
|
-
const passes = await Promise.all(defs.map((def) =>
|
|
120
|
+
const passes = await Promise.all(defs.map(async (def) => {
|
|
121
|
+
options.signal?.throwIfAborted();
|
|
122
|
+
options.onProgress?.({ phase: 'start', id: def.id, label: def.label });
|
|
123
|
+
const pass = await runOneTool(def, root, sanitizeOptions(def, rawOptions[def.id]), options.signal);
|
|
124
|
+
if (!options.signal?.aborted)
|
|
125
|
+
options.onProgress?.({ phase: 'complete', id: def.id, label: def.label, pass });
|
|
126
|
+
return pass;
|
|
127
|
+
}));
|
|
128
|
+
options.signal?.throwIfAborted();
|
|
115
129
|
const ran = passes.filter((p) => p.ok);
|
|
116
130
|
const findings = mergeFindings(...passes.map((p) => p.findings));
|
|
117
131
|
return {
|
package/server/tools/scan.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/server/tools/scan.ts"],"names":[],"mappings":"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","sourcesContent":["// The orchestrator: detect what is on the machine, run each available analyzer\n// against the scanned root, and hand back one normalized report.\n//\n// Two promises drive the shape. First, detection is *honest*: the report always\n// lists every tool and whether it ran, so \"no security issues\" and \"no security\n// tool installed\" are never the same sentence. Second, failure is *local*: a\n// tool that hangs, crashes or prints garbage fails its own pass and nothing\n// else — the scan the person asked for already succeeded, and this is a bonus\n// layer over it, exactly the way `collectHistory` treats git.\n\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { emptyPass, runTool, toolArgv, TOOL_TIMEOUT_MS } from '../tools.js';\nimport { defaultOptions, sanitizeOptions, TOOL_DEFS } from './registry.js';\nimport { mergeFindings } from './parse.js';\n\n// What is installed, for the UI's \"engines\" panel. Detection is cached by\n// `which`, so asking is cheap. The payload also carries each tool's option\n// schema and defaults, so the GUI can render a form for what the engine\n// actually offers without hard-coding a flag anywhere in the front end.\nexport function toolsStatus() {\n const out: Record<string, any> = {};\n for (const def of TOOL_DEFS) {\n const resolved = toolArgv(def, '.');\n out[def.id] = {\n id: def.id,\n label: def.label,\n kind: def.kind,\n purpose: def.purpose,\n available: !!resolved,\n how: resolved ? resolved.how : null,\n command: resolved ? resolved.display : null,\n reason: resolved ? null : unavailableReason(def),\n options: def.options || [],\n defaults: defaultOptions(def),\n installable: true,\n platform: process.platform,\n };\n }\n return out;\n}\n\nfunction unavailableReason(def) {\n return (def.install && def.install.length) ? def.install.join(' ') : 'Not found on PATH.';\n}\n\nasync function runOneTool(def, root, toolOptions: Record<string, any> = {}) {\n // A tool that reports through a file (gitleaks) gets a per-run temp path —\n // never /dev/stdout, which does not exist on Windows, and never a fixed\n // name, so two concurrent runs cannot read each other's report.\n const ctx: Record<string, any> = {};\n if (def.usesReportFile) {\n ctx.reportPath = path.join(os.tmpdir(), `onboarder-${def.id}-${Date.now()}-${Math.floor(Math.random() * 1e6)}.json`);\n }\n\n const resolved = toolArgv(def, root, toolOptions, ctx);\n if (!resolved) {\n return emptyPass(def.id, def.label, unavailableReason(def));\n }\n\n const started = Date.now();\n const result = await runTool(resolved.argv, {\n cwd: def.cwd ? root : undefined,\n timeout: TOOL_TIMEOUT_MS,\n });\n\n // Read and remove the report file whatever happened above.\n let reportText = null;\n if (ctx.reportPath) {\n try {\n reportText = fs.readFileSync(ctx.reportPath, 'utf8');\n } catch {\n reportText = null; // a crashed tool may never have written it\n }\n try { fs.unlinkSync(ctx.reportPath); } catch { /* already gone */ }\n }\n\n if (result.timedOut) return { ...emptyPass(def.id, def.label, result.error), available: true };\n if (!result.ok) {\n return { ...emptyPass(def.id, def.label, result.error || 'The tool could not be run.'), available: true };\n }\n\n const stdout = (def.usesReportFile ? (reportText || '') : (result.stdout || '')).trim();\n if (!stdout) {\n // A clean pass: the tool ran and found nothing. That is a real answer, and\n // worth saying — \"Gitleaks: no secrets\" is the outcome a self-hosting user\n // runs the tool to hear.\n return passResult(def, resolved, [], started);\n }\n\n try {\n const findings = def.parse(stdout, root);\n return passResult(def, resolved, findings, started);\n } catch (err) {\n return { ...emptyPass(def.id, def.label, 'Its output could not be read: ' + err.message), available: true };\n }\n}\n\nfunction passResult(def, resolved, findings, started) {\n return {\n id: def.id, label: def.label, kind: def.kind, ok: true, available: true,\n findings, source: 'external', tool: def.id, how: resolved.how, ms: Date.now() - started,\n };\n}\n\n// Tools run concurrently — a scan is I/O-bound and the analyzers are\n// independent — but a repo with zero analyzers still gets a well-formed report,\n// so the frontend renders the built-in floor and the \"what to install\" list.\n// `kinds` narrows by purpose, `tools` narrows to named engines; both exist so\n// the UI can offer \"run just the secrets scan\" as readily as \"run everything\".\n// `options` is per-engine GUI settings, sanitized against the registry schema\n// before any of it touches a command line.\nexport async function runExternalAnalysis(root, options: Record<string, any> = {}) {\n const kinds = options.kinds ? new Set(options.kinds) : null;\n const tools = options.tools ? new Set(options.tools) : null;\n const rawOptions = options.options && typeof options.options === 'object' ? options.options : {};\n const defs = TOOL_DEFS.filter((d) => (!kinds || kinds.has(d.kind)) && (!tools || tools.has(d.id)));\n\n const started = Date.now();\n const passes = await Promise.all(defs.map((def) => runOneTool(def, root, sanitizeOptions(def, rawOptions[def.id]))));\n\n const ran = passes.filter((p) => p.ok);\n const findings = mergeFindings(...passes.map((p) => p.findings));\n\n return {\n ms: Date.now() - started,\n passes,\n findings,\n ranCount: ran.length,\n totalTools: defs.length,\n unavailable: passes.filter((p) => !p.available).map((p) => ({ id: p.id, label: p.label, reason: ('reason' in p ? p.reason : undefined) })),\n source: ran.length ? 'external' : 'none',\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/server/tools/scan.ts"],"names":[],"mappings":"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","sourcesContent":["// The orchestrator: detect what is on the machine, run each available analyzer\n// against the scanned root, and hand back one normalized report.\n//\n// Two promises drive the shape. First, detection is *honest*: the report always\n// lists every tool and whether it ran, so \"no security issues\" and \"no security\n// tool installed\" are never the same sentence. Second, failure is *local*: a\n// tool that hangs, crashes or prints garbage fails its own pass and nothing\n// else — the scan the person asked for already succeeded, and this is a bonus\n// layer over it, exactly the way `collectHistory` treats git.\n\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { emptyPass, runTool, toolArgv, TOOL_TIMEOUT_MS } from '../tools.js';\nimport { defaultOptions, sanitizeOptions, TOOL_DEFS } from './registry.js';\nimport { mergeFindings } from './parse.js';\n\n// What is installed, for the UI's \"engines\" panel. Detection is cached by\n// `which`, so asking is cheap. The payload also carries each tool's option\n// schema and defaults, so the GUI can render a form for what the engine\n// actually offers without hard-coding a flag anywhere in the front end.\nexport function toolsStatus() {\n const out: Record<string, any> = {};\n for (const def of TOOL_DEFS) {\n const resolved = toolArgv(def, '.');\n out[def.id] = {\n id: def.id,\n label: def.label,\n kind: def.kind,\n purpose: def.purpose,\n available: !!resolved,\n how: resolved ? resolved.how : null,\n command: resolved ? resolved.display : null,\n reason: resolved ? null : unavailableReason(def),\n options: def.options || [],\n defaults: defaultOptions(def),\n installable: true,\n platform: process.platform,\n };\n }\n return out;\n}\n\nfunction unavailableReason(def) {\n return (def.install && def.install.length) ? def.install.join(' ') : 'Not found on PATH.';\n}\n\nasync function runOneTool(def, root, toolOptions: Record<string, any> = {}, signal?: AbortSignal) {\n // A tool that reports through a file (gitleaks) gets a per-run temp path —\n // never /dev/stdout, which does not exist on Windows, and never a fixed\n // name, so two concurrent runs cannot read each other's report.\n const ctx: Record<string, any> = {};\n if (def.usesReportFile) {\n ctx.reportPath = path.join(os.tmpdir(), `onboarder-${def.id}-${Date.now()}-${Math.floor(Math.random() * 1e6)}.json`);\n }\n\n const resolved = toolArgv(def, root, toolOptions, ctx);\n if (!resolved) {\n return emptyPass(def.id, def.label, unavailableReason(def));\n }\n\n const started = Date.now();\n const result = await runTool(resolved.argv, {\n cwd: def.cwd ? root : undefined,\n timeout: TOOL_TIMEOUT_MS,\n signal,\n });\n\n // Read and remove the report file whatever happened above.\n let reportText = null;\n if (ctx.reportPath) {\n try {\n reportText = fs.readFileSync(ctx.reportPath, 'utf8');\n } catch {\n reportText = null; // a crashed tool may never have written it\n }\n try { fs.unlinkSync(ctx.reportPath); } catch { /* already gone */ }\n }\n\n if (result.timedOut) return { ...emptyPass(def.id, def.label, result.error), available: true };\n if (!result.ok) {\n return { ...emptyPass(def.id, def.label, result.error || 'The tool could not be run.'), available: true };\n }\n if (def.usesReportFile && reportText === null) return { ...emptyPass(def.id, def.label, result.stderr || 'The analyzer did not create its report.'), available: true };\n\n const stdout = (def.usesReportFile ? (reportText || '') : (result.stdout || '')).trim();\n if (!stdout) {\n if (result.status !== 0) return { ...emptyPass(def.id, def.label, `Exited ${result.status}: ${result.stderr || 'No readable report.'}`), available: true };\n // A clean pass: the tool ran and found nothing. That is a real answer, and\n // worth saying — \"Gitleaks: no secrets\" is the outcome a self-hosting user\n // runs the tool to hear.\n return passResult(def, resolved, [], started);\n }\n\n try {\n const findings = def.parse(stdout, root);\n return passResult(def, resolved, findings, started);\n } catch (err) {\n return { ...emptyPass(def.id, def.label, 'Its output could not be read: ' + err.message), available: true };\n }\n}\n\nfunction passResult(def, resolved, findings, started) {\n return {\n id: def.id, label: def.label, kind: def.kind, ok: true, available: true,\n findings, source: 'external', tool: def.id, how: resolved.how, ms: Date.now() - started,\n };\n}\n\n// Tools run concurrently — a scan is I/O-bound and the analyzers are\n// independent — but a repo with zero analyzers still gets a well-formed report,\n// so the frontend renders the built-in floor and the \"what to install\" list.\n// `kinds` narrows by purpose, `tools` narrows to named engines; both exist so\n// the UI can offer \"run just the secrets scan\" as readily as \"run everything\".\n// `options` is per-engine GUI settings, sanitized against the registry schema\n// before any of it touches a command line.\nexport async function runExternalAnalysis(root, options: Record<string, any> = {}) {\n options.signal?.throwIfAborted();\n const kinds = options.kinds ? new Set(options.kinds) : null;\n const tools = options.tools ? new Set(options.tools) : null;\n const rawOptions = options.options && typeof options.options === 'object' ? options.options : {};\n const defs = TOOL_DEFS.filter((d) => (!kinds || kinds.has(d.kind)) && (!tools || tools.has(d.id)));\n\n const started = Date.now();\n const passes = await Promise.all(defs.map(async def => {\n options.signal?.throwIfAborted();\n options.onProgress?.({ phase: 'start', id: def.id, label: def.label });\n const pass = await runOneTool(def, root, sanitizeOptions(def, rawOptions[def.id]), options.signal);\n if (!options.signal?.aborted) options.onProgress?.({ phase: 'complete', id: def.id, label: def.label, pass });\n return pass;\n }));\n options.signal?.throwIfAborted();\n\n const ran = passes.filter((p) => p.ok);\n const findings = mergeFindings(...passes.map((p) => p.findings));\n\n return {\n ms: Date.now() - started,\n passes,\n findings,\n ranCount: ran.length,\n totalTools: defs.length,\n unavailable: passes.filter((p) => !p.available).map((p) => ({ id: p.id, label: p.label, reason: ('reason' in p ? p.reason : undefined) })),\n source: ran.length ? 'external' : 'none',\n };\n}\n"]}
|
package/server/tools.d.ts
CHANGED
package/server/tools.js
CHANGED
|
@@ -155,9 +155,13 @@ export function runTool(argv, options = {}) {
|
|
|
155
155
|
const timeout = options.timeout || TOOL_TIMEOUT_MS;
|
|
156
156
|
const { command, args } = spawnArgv(argv);
|
|
157
157
|
return new Promise((resolve) => {
|
|
158
|
+
if (options.signal?.aborted) {
|
|
159
|
+
resolve({ ok: false, stdout: '', cancelled: true, error: 'Analysis cancelled.' });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
158
162
|
let child;
|
|
159
163
|
try {
|
|
160
|
-
child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], env: options.env });
|
|
164
|
+
child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], env: options.env, detached: process.platform !== 'win32' });
|
|
161
165
|
}
|
|
162
166
|
catch (err) {
|
|
163
167
|
resolve({ ok: false, status: -1, stdout: '', stderr: String(err && err.message || err), timedOut: false });
|
|
@@ -166,27 +170,63 @@ export function runTool(argv, options = {}) {
|
|
|
166
170
|
let stdout = '';
|
|
167
171
|
let stderr = '';
|
|
168
172
|
let settled = false;
|
|
173
|
+
let stopped = null, bytes = 0;
|
|
174
|
+
const stop = (reason) => {
|
|
175
|
+
if (settled || stopped)
|
|
176
|
+
return;
|
|
177
|
+
stopped = reason;
|
|
178
|
+
if (process.platform === 'win32' && child.pid) {
|
|
179
|
+
const killer = spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
180
|
+
killer.on('error', () => child.kill());
|
|
181
|
+
killer.unref();
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
try {
|
|
185
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
child.kill('SIGKILL');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const abort = () => stop({ ok: false, cancelled: true, error: 'Analysis cancelled.' });
|
|
169
193
|
const finish = (extra) => {
|
|
170
194
|
if (settled)
|
|
171
195
|
return;
|
|
172
196
|
settled = true;
|
|
173
197
|
clearTimeout(timer);
|
|
198
|
+
options.signal?.removeEventListener('abort', abort);
|
|
174
199
|
resolve({ status: child.exitCode, stdout, stderr, ...extra });
|
|
175
200
|
};
|
|
176
201
|
const cap = (chunk, into) => {
|
|
177
202
|
const next = into + chunk;
|
|
178
203
|
return next.length > MAX_OUTPUT_BYTES ? next.slice(0, MAX_OUTPUT_BYTES) : next;
|
|
179
204
|
};
|
|
180
|
-
child.stdout.
|
|
181
|
-
child.stderr.
|
|
205
|
+
child.stdout.setEncoding('utf8');
|
|
206
|
+
child.stderr.setEncoding('utf8');
|
|
207
|
+
const collect = (chunk, stderrChunk = false) => {
|
|
208
|
+
bytes += Buffer.byteLength(chunk);
|
|
209
|
+
if (bytes > MAX_OUTPUT_BYTES) {
|
|
210
|
+
stop({ ok: false, error: 'Analyzer output exceeded the size limit.' });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (stderrChunk)
|
|
214
|
+
stderr = cap(chunk, stderr).slice(-4000);
|
|
215
|
+
else
|
|
216
|
+
stdout = cap(chunk, stdout);
|
|
217
|
+
};
|
|
218
|
+
child.stdout.on('data', c => collect(c));
|
|
219
|
+
child.stderr.on('data', c => collect(c, true));
|
|
182
220
|
const timer = setTimeout(() => {
|
|
183
|
-
|
|
184
|
-
finish({ ok: false, timedOut: true, error: `${argv[0]} took too long and was stopped.` });
|
|
221
|
+
stop({ ok: false, timedOut: true, error: `${argv[0]} took too long and was stopped.` });
|
|
185
222
|
}, timeout);
|
|
186
223
|
child.on('error', (err) => {
|
|
187
224
|
finish({ ok: false, timedOut: false, error: 'Could not run ' + argv[0] + ': ' + err.message });
|
|
188
225
|
});
|
|
189
|
-
child.on('close', () => finish({ ok: true, timedOut: false }));
|
|
226
|
+
child.on('close', () => finish(stopped || { ok: true, timedOut: false }));
|
|
227
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
228
|
+
if (options.signal?.aborted)
|
|
229
|
+
abort();
|
|
190
230
|
});
|
|
191
231
|
}
|
|
192
232
|
// The contract every analyzer hands back: findings normalized to the shape the
|
package/server/tools.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/server/tools.ts"],"names":[],"mappings":"AACA,6EAA6E;AAC7E,4CAA4C;AAC5C,EAAE;AACF,8EAA8E;AAC9E,6EAA6E;AAC7E,2EAA2E;AAC3E,qEAAqE;AACrE,6EAA6E;AAC7E,4EAA4E;AAC5E,6EAA6E;AAC7E,2EAA2E;AAC3E,qBAAqB;AACrB,EAAE;AACF,qDAAqD;AACrD,EAAE;AACF,2EAA2E;AAC3E,8EAA8E;AAC9E,+EAA+E;AAC/E,6EAA6E;AAC7E,oDAAoD;AACpD,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,iFAAiF;AACjF,+EAA+E;AAC/E,sBAAsB;AACtB,EAAE;AACF,gFAAgF;AAChF,+EAA+E;AAC/E,+EAA+E;AAC/E,2CAA2C;AAC3C,EAAE;AACF,6EAA6E;AAC7E,iFAAiF;AACjF,mBAAmB;AACnB,EAAE;AACF,6EAA6E;AAC7E,4EAA4E;AAC5E,8EAA8E;AAC9E,4DAA4D;AAE5D,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAE5D,+EAA+E;AAC/E,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,yEAAyE;AACzE,6EAA6E;AAC7E,gDAAgD;AAEhD,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;AAE/E,4EAA4E;AAC5E,8EAA8E;AAC9E,8EAA8E;AAC9E,8EAA8E;AAC9E,0EAA0E;AAC1E,iEAAiE;AACjE,MAAM,WAAW,GAAG,IAAI,GAAG,EAAE,CAAC;AAE9B,SAAS,KAAK,CAAC,GAAG;IAChB,IAAI,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACtD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;IAC9B,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC5B,OAAO,KAAK,CAAC;AACf,CAAC;AAED,6EAA6E;AAC7E,0EAA0E;AAC1E,MAAM,UAAU,mBAAmB;IACjC,WAAW,CAAC,KAAK,EAAE,CAAC;AACtB,CAAC;AAED,6EAA6E;AAC7E,wEAAwE;AACxE,MAAM,UAAU,aAAa;IAC3B,OAAO;QACL,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC;QACjB,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC;KAClB,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,0EAA0E;AAC1E,MAAM,UAAU,WAAW,CAAC,IAAI;IAC9B,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;IAChC,MAAM,GAAG,GAAwB,EAAE,CAAC;IACpC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAC7C,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG;YACZ,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,SAAS,EAAE,CAAC,CAAC,UAAU;YACvB,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI;YACvC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;YAC/C,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAG,EAAE,OAAO,CAAC;SAC5D,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,iBAAiB,CAAC,GAAG,EAAE,OAAO;IACrC,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7C,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;QACjC,OAAO,KAAK,CAAC,MAAM;YACjB,CAAC,CAAC,cAAc,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;YACjC,CAAC,CAAC,mDAAmD,CAAC;IAC1D,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC;AACvE,CAAC;AAED,6EAA6E;AAC7E,2EAA2E;AAC3E,qDAAqD;AACrD,SAAS,WAAW,CAAC,GAAG,EAAE,OAAO;IAC/B,KAAK,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;QAC/B,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC1B,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC7B,qEAAqE;YACrE,uEAAuE;YACvE,0CAA0C;YAC1C,IAAI,KAAK;gBAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;QACvE,CAAC;aAAM,IAAI,GAAG,CAAC,IAAI,KAAK,KAAK,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;YAC7C,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,GAAG,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QACtG,CAAC;aAAM,IAAI,GAAG,CAAC,IAAI,KAAK,KAAK,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;YAC7C,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,GAAG,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5F,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,gFAAgF;AAChF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,uEAAuE;AACvE,MAAM,UAAU,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,WAAW,GAAwB,EAAE,EAAE,GAAG,GAAwB,EAAE;IACtG,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE,aAAa,EAAE,CAAC,CAAC;IACrD,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAC7B,OAAO;QACL,IAAI,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,CAAC,CAAC;QACjE,GAAG,EAAE,UAAU,CAAC,GAAG;QACnB,OAAO,EAAE,UAAU,CAAC,OAAO;KAC5B,CAAC;AACJ,CAAC;AAED,+EAA+E;AAE/E,MAAM,CAAC,MAAM,eAAe,GAAG,OAAO,CAAC;AACvC,MAAM,gBAAgB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAE1C,+EAA+E;AAC/E,8EAA8E;AAC9E,8EAA8E;AAC9E,qEAAqE;AACrE,6EAA6E;AAC7E,2EAA2E;AAC3E,yEAAyE;AACzE,MAAM,UAAU,OAAO,CAAC,IAAI,EAAE,OAAO,GAAwB,EAAE;IAC7D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IACzC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,eAAe,CAAC;IACnD,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAE1C,OAAO,IAAI,OAAO,CAAa,CAAC,OAAO,EAAE,EAAE;QACzC,IAAI,KAAK,CAAC;QACV,IAAI,CAAC;YACH,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QAC7F,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;YAC3G,OAAO;QACT,CAAC;QAED,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,MAAM,GAAG,CAAC,KAAK,EAAE,EAAE;YACvB,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;QAChE,CAAC,CAAC;QAEF,MAAM,GAAG,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;YAC1B,MAAM,IAAI,GAAG,IAAI,GAAG,KAAK,CAAC;YAC1B,OAAO,IAAI,CAAC,MAAM,GAAG,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACjF,CAAC,CAAC;QACF,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACxE,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAErF,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACtB,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,iCAAiC,EAAE,CAAC,CAAC;QAC5F,CAAC,EAAE,OAAO,CAAC,CAAC;QAEZ,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACxB,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QACjG,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACjE,CAAC,CAAC,CAAC;AACL,CAAC;AAED,+EAA+E;AAC/E,4EAA4E;AAC5E,gFAAgF;AAChF,iFAAiF;AACjF,MAAM,UAAU,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM;IACzC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;AACxG,CAAC","sourcesContent":["export interface ToolResult { status?: number; ok: boolean; stdout: string; stderr?: string; error?: string; timedOut?: boolean; code?: number; ms?: number }\n// The deep-analysis engine: Onboarder becomes the frontend, and the sharpest\n// open-source analyzers become its backend.\n//\n// The idea, stated once: this tool's built-in scanner (`shared/analyzer/`) is\n// deliberately a *local, zero-dependency* pass — regex rules, import graphs,\n// metrics. It is honest about what it is. But the best answers to \"is this\n// safe?\" and \"is this dead?\" come from real engines: **Semgrep** and\n// **Gitleaks** for security, **Knip** / **Vulture** for dead code. Those are\n// command-line programs, which is exactly the right seam for a self-hosted,\n// zero-npm-dep project: we never import them, we *run* them, read their JSON\n// or SARIF, and normalize everything into one finding shape the UI already\n// knows how to draw.\n//\n// The rules that keep this self-host-first and safe:\n//\n// * **Optional, always.** No tool is required. Every analyzer the engine\n// detects is a bonus; every one it cannot find is a graceful absence, and\n// the built-in scanner is the floor that never goes away. The scan payload\n// reports `source: 'builtin'` or `'external'` per pass so the UI can say\n// which engine answered, instead of pretending.\n//\n// * **Detect, never install.** We look for tools on PATH (plus a few named\n// fallbacks — `uvx`, `npx`, `opengrep` — the ways these tools are actually\n// distributed) and report what is there. We do not download or `pip install`\n// anything behind a scan; the person self-hosting this decides what to put\n// on the machine.\n//\n// * **Run, never eval.** Every tool is spawned with an argument array — never\n// a shell — the same discipline `gitClone.js` uses, so a crafted path or a\n// malicious filename becomes an argument, not a command. No tool output is\n// ever executed; it is parsed as data.\n//\n// * **Bounded.** A tool gets a hard timeout and a capped buffer. A hung or\n// pathological analyzer fails that pass and nothing else; it cannot hold the\n// server open.\n//\n// Each tool is a descriptor (`TOOL_DEFS`): how to find it, how to invoke it,\n// and how to turn its output into findings. `scan.js` (this folder) parses;\n// this file owns process lifecycle. `shared/` stays isomorphic — none of this\n// lives there, because none of it can run in a browser tab.\n\nimport { spawn } from 'node:child_process';\nimport { findOnPath, spawnArgv } from './tools/platform.js';\n\n// ---- the registry ----------------------------------------------------------\n//\n// What the engine can drive. `id` is the key the rest of the app talks about;\n// `commands` is the preference order for locating the binary. `invocation` and\n// `parse` are filled in by the per-tool modules so this file stays about\n// lifecycle, not dialects. The whole list is data, which is what lets a test\n// walk it and assert every tool is well-formed.\n\nexport const TOOL_IDS = ['semgrep', 'gitleaks', 'knip', 'vulture', 'depcheck'];\n\n// Where a binary can come from. `direct` is a tool on PATH. `uvx` and `npx`\n// run a tool without a pre-installed binary, which is how a self-hosting user\n// who has neither semgrep nor a Python env still gets a run — the fallback is\n// declared, not hidden in a shell string. Detection goes through `findOnPath`\n// (PATH split + PATHEXT on Windows + the Onboarder bin dir), not the Unix\n// `which`, so the same code answers on macOS, Linux and Windows.\nconst WHICH_CACHE = new Map();\n\nfunction which(cmd) {\n if (WHICH_CACHE.has(cmd)) return WHICH_CACHE.get(cmd);\n const found = findOnPath(cmd);\n WHICH_CACHE.set(cmd, found);\n return found;\n}\n\n// An install from the GUI changes what is on disk; the cache must forget, or\n// the engine would keep reporting \"not installed\" until a server restart.\nexport function clearDetectionCache() {\n WHICH_CACHE.clear();\n}\n\n// Does the runtime have a way to run a tool that is not installed? uvx ships\n// with `uv`, npx ships with npm; both can fetch-and-run. Detected once.\nexport function detectRunners() {\n return {\n uvx: which('uvx'),\n npx: which('npx'),\n };\n}\n\n// What is actually on this machine, per tool. The UI reads this to grey out\n// what is unavailable and to say *why* — \"install semgrep\" beats silence.\nexport function detectTools(defs) {\n const runners = detectRunners();\n const out: Record<string, any> = {};\n for (const def of defs) {\n const resolution = resolveTool(def, runners);\n out[def.id] = {\n id: def.id,\n label: def.label,\n purpose: def.purpose,\n available: !!resolution,\n how: resolution ? resolution.how : null,\n command: resolution ? resolution.display : null,\n reason: resolution ? null : unavailableReason(def, runners),\n };\n }\n return out;\n}\n\nfunction unavailableReason(def, runners) {\n const parts = def.install ? def.install : [];\n if (!runners.uvx && !runners.npx) {\n return parts.length\n ? `Not found. ${parts.join(' ')}`\n : 'Not found on PATH, and no uvx/npx to run it with.';\n }\n return parts.length ? `Not found. ${parts.join(' ')}` : 'Not found.';\n}\n\n// Prefer a real binary on PATH; fall back to a runner that can fetch it. The\n// returned `{ prefix }` is prepended to the tool's own arguments — a plain\n// array, never a string, so nothing reaches a shell.\nfunction resolveTool(def, runners) {\n for (const cmd of def.commands) {\n if (cmd.kind === 'direct') {\n const found = which(cmd.bin);\n // The absolute path, not the bare name: on Windows the suffix is how\n // `spawnArgv` knows a shim needs cmd.exe, and `npm.cmd` on PATH is how\n // knip and depcheck usually arrive there.\n if (found) return { how: 'path', display: cmd.bin, prefix: [found] };\n } else if (cmd.kind === 'uvx' && runners.uvx) {\n return { how: 'uvx', display: `uvx ${cmd.pkg}`, prefix: [runners.uvx, '--from', cmd.pkg, cmd.bin] };\n } else if (cmd.kind === 'npx' && runners.npx) {\n return { how: 'npx', display: `npx ${cmd.pkg}`, prefix: [runners.npx, '--yes', cmd.pkg] };\n }\n }\n return null;\n}\n\n// The full command line for a tool against a root, or null if it cannot be run.\n// Detection and spawning both go through this, so \"available\" can never mean a\n// command we then fail to build. `toolOptions` are the GUI-tunable settings the\n// registry declares; `ctx` carries run-time necessities like a report path for\n// tools that cannot write JSON to stdout on every platform (gitleaks).\nexport function toolArgv(def, root, toolOptions: Record<string, any> = {}, ctx: Record<string, any> = {}) {\n const resolution = resolveTool(def, detectRunners());\n if (!resolution) return null;\n return {\n argv: [...resolution.prefix, ...def.argv(root, toolOptions, ctx)],\n how: resolution.how,\n display: resolution.display,\n };\n}\n\n// ---- running ---------------------------------------------------------------\n\nexport const TOOL_TIMEOUT_MS = 120_000;\nconst MAX_OUTPUT_BYTES = 16 * 1024 * 1024;\n\n// Spawn a tool with an argument array and collect stdout. Deliberately mirrors\n// `gitClone.run`: no shell, a timeout, a capped buffer, and a settled promise\n// that resolves with whatever was captured — a non-zero exit from an analyzer\n// is information (\"knip found dead code exits 1\"), not an exception.\n// `spawnArgv` handles the one platform wrinkle: a `.cmd` shim (npm-installed\n// CLIs on Windows) may only run through cmd.exe, with the arguments quoted\n// into a single command string by the helper — never user-authored text.\nexport function runTool(argv, options: Record<string, any> = {}) {\n const cwd = options.cwd || process.cwd();\n const timeout = options.timeout || TOOL_TIMEOUT_MS;\n const { command, args } = spawnArgv(argv);\n\n return new Promise<ToolResult>((resolve) => {\n let child;\n try {\n child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], env: options.env });\n } catch (err) {\n resolve({ ok: false, status: -1, stdout: '', stderr: String(err && err.message || err), timedOut: false });\n return;\n }\n\n let stdout = '';\n let stderr = '';\n let settled = false;\n const finish = (extra) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({ status: child.exitCode, stdout, stderr, ...extra });\n };\n\n const cap = (chunk, into) => {\n const next = into + chunk;\n return next.length > MAX_OUTPUT_BYTES ? next.slice(0, MAX_OUTPUT_BYTES) : next;\n };\n child.stdout.on('data', (c) => { stdout = cap(c.toString(), stdout); });\n child.stderr.on('data', (c) => { stderr = cap(c.toString(), stderr).slice(-4000); });\n\n const timer = setTimeout(() => {\n child.kill('SIGKILL');\n finish({ ok: false, timedOut: true, error: `${argv[0]} took too long and was stopped.` });\n }, timeout);\n\n child.on('error', (err) => {\n finish({ ok: false, timedOut: false, error: 'Could not run ' + argv[0] + ': ' + err.message });\n });\n child.on('close', () => finish({ ok: true, timedOut: false }));\n });\n}\n\n// The contract every analyzer hands back: findings normalized to the shape the\n// built-in scanner already uses (`rule`, `severity`, `category`, `message`,\n// `line`, `excerpt`), plus a `source` so the UI can attribute them. A tool that\n// produced nothing parseable is an `ok:false` pass with a reason, never a crash.\nexport function emptyPass(id, label, reason) {\n return { id, label, ok: false, available: false, findings: [], reason, source: 'external', tool: id };\n}\n\n"]}
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/server/tools.ts"],"names":[],"mappings":"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","sourcesContent":["export interface ToolResult { status?: number; ok: boolean; stdout: string; stderr?: string; error?: string; timedOut?: boolean; cancelled?: boolean; code?: number; ms?: number }\n// The deep-analysis engine: Onboarder becomes the frontend, and the sharpest\n// open-source analyzers become its backend.\n//\n// The idea, stated once: this tool's built-in scanner (`shared/analyzer/`) is\n// deliberately a *local, zero-dependency* pass — regex rules, import graphs,\n// metrics. It is honest about what it is. But the best answers to \"is this\n// safe?\" and \"is this dead?\" come from real engines: **Semgrep** and\n// **Gitleaks** for security, **Knip** / **Vulture** for dead code. Those are\n// command-line programs, which is exactly the right seam for a self-hosted,\n// zero-npm-dep project: we never import them, we *run* them, read their JSON\n// or SARIF, and normalize everything into one finding shape the UI already\n// knows how to draw.\n//\n// The rules that keep this self-host-first and safe:\n//\n// * **Optional, always.** No tool is required. Every analyzer the engine\n// detects is a bonus; every one it cannot find is a graceful absence, and\n// the built-in scanner is the floor that never goes away. The scan payload\n// reports `source: 'builtin'` or `'external'` per pass so the UI can say\n// which engine answered, instead of pretending.\n//\n// * **Detect, never install.** We look for tools on PATH (plus a few named\n// fallbacks — `uvx`, `npx`, `opengrep` — the ways these tools are actually\n// distributed) and report what is there. We do not download or `pip install`\n// anything behind a scan; the person self-hosting this decides what to put\n// on the machine.\n//\n// * **Run, never eval.** Every tool is spawned with an argument array — never\n// a shell — the same discipline `gitClone.js` uses, so a crafted path or a\n// malicious filename becomes an argument, not a command. No tool output is\n// ever executed; it is parsed as data.\n//\n// * **Bounded.** A tool gets a hard timeout and a capped buffer. A hung or\n// pathological analyzer fails that pass and nothing else; it cannot hold the\n// server open.\n//\n// Each tool is a descriptor (`TOOL_DEFS`): how to find it, how to invoke it,\n// and how to turn its output into findings. `scan.js` (this folder) parses;\n// this file owns process lifecycle. `shared/` stays isomorphic — none of this\n// lives there, because none of it can run in a browser tab.\n\nimport { spawn } from 'node:child_process';\nimport { findOnPath, spawnArgv } from './tools/platform.js';\n\n// ---- the registry ----------------------------------------------------------\n//\n// What the engine can drive. `id` is the key the rest of the app talks about;\n// `commands` is the preference order for locating the binary. `invocation` and\n// `parse` are filled in by the per-tool modules so this file stays about\n// lifecycle, not dialects. The whole list is data, which is what lets a test\n// walk it and assert every tool is well-formed.\n\nexport const TOOL_IDS = ['semgrep', 'gitleaks', 'knip', 'vulture', 'depcheck'];\n\n// Where a binary can come from. `direct` is a tool on PATH. `uvx` and `npx`\n// run a tool without a pre-installed binary, which is how a self-hosting user\n// who has neither semgrep nor a Python env still gets a run — the fallback is\n// declared, not hidden in a shell string. Detection goes through `findOnPath`\n// (PATH split + PATHEXT on Windows + the Onboarder bin dir), not the Unix\n// `which`, so the same code answers on macOS, Linux and Windows.\nconst WHICH_CACHE = new Map();\n\nfunction which(cmd) {\n if (WHICH_CACHE.has(cmd)) return WHICH_CACHE.get(cmd);\n const found = findOnPath(cmd);\n WHICH_CACHE.set(cmd, found);\n return found;\n}\n\n// An install from the GUI changes what is on disk; the cache must forget, or\n// the engine would keep reporting \"not installed\" until a server restart.\nexport function clearDetectionCache() {\n WHICH_CACHE.clear();\n}\n\n// Does the runtime have a way to run a tool that is not installed? uvx ships\n// with `uv`, npx ships with npm; both can fetch-and-run. Detected once.\nexport function detectRunners() {\n return {\n uvx: which('uvx'),\n npx: which('npx'),\n };\n}\n\n// What is actually on this machine, per tool. The UI reads this to grey out\n// what is unavailable and to say *why* — \"install semgrep\" beats silence.\nexport function detectTools(defs) {\n const runners = detectRunners();\n const out: Record<string, any> = {};\n for (const def of defs) {\n const resolution = resolveTool(def, runners);\n out[def.id] = {\n id: def.id,\n label: def.label,\n purpose: def.purpose,\n available: !!resolution,\n how: resolution ? resolution.how : null,\n command: resolution ? resolution.display : null,\n reason: resolution ? null : unavailableReason(def, runners),\n };\n }\n return out;\n}\n\nfunction unavailableReason(def, runners) {\n const parts = def.install ? def.install : [];\n if (!runners.uvx && !runners.npx) {\n return parts.length\n ? `Not found. ${parts.join(' ')}`\n : 'Not found on PATH, and no uvx/npx to run it with.';\n }\n return parts.length ? `Not found. ${parts.join(' ')}` : 'Not found.';\n}\n\n// Prefer a real binary on PATH; fall back to a runner that can fetch it. The\n// returned `{ prefix }` is prepended to the tool's own arguments — a plain\n// array, never a string, so nothing reaches a shell.\nfunction resolveTool(def, runners) {\n for (const cmd of def.commands) {\n if (cmd.kind === 'direct') {\n const found = which(cmd.bin);\n // The absolute path, not the bare name: on Windows the suffix is how\n // `spawnArgv` knows a shim needs cmd.exe, and `npm.cmd` on PATH is how\n // knip and depcheck usually arrive there.\n if (found) return { how: 'path', display: cmd.bin, prefix: [found] };\n } else if (cmd.kind === 'uvx' && runners.uvx) {\n return { how: 'uvx', display: `uvx ${cmd.pkg}`, prefix: [runners.uvx, '--from', cmd.pkg, cmd.bin] };\n } else if (cmd.kind === 'npx' && runners.npx) {\n return { how: 'npx', display: `npx ${cmd.pkg}`, prefix: [runners.npx, '--yes', cmd.pkg] };\n }\n }\n return null;\n}\n\n// The full command line for a tool against a root, or null if it cannot be run.\n// Detection and spawning both go through this, so \"available\" can never mean a\n// command we then fail to build. `toolOptions` are the GUI-tunable settings the\n// registry declares; `ctx` carries run-time necessities like a report path for\n// tools that cannot write JSON to stdout on every platform (gitleaks).\nexport function toolArgv(def, root, toolOptions: Record<string, any> = {}, ctx: Record<string, any> = {}) {\n const resolution = resolveTool(def, detectRunners());\n if (!resolution) return null;\n return {\n argv: [...resolution.prefix, ...def.argv(root, toolOptions, ctx)],\n how: resolution.how,\n display: resolution.display,\n };\n}\n\n// ---- running ---------------------------------------------------------------\n\nexport const TOOL_TIMEOUT_MS = 120_000;\nconst MAX_OUTPUT_BYTES = 16 * 1024 * 1024;\n\n// Spawn a tool with an argument array and collect stdout. Deliberately mirrors\n// `gitClone.run`: no shell, a timeout, a capped buffer, and a settled promise\n// that resolves with whatever was captured — a non-zero exit from an analyzer\n// is information (\"knip found dead code exits 1\"), not an exception.\n// `spawnArgv` handles the one platform wrinkle: a `.cmd` shim (npm-installed\n// CLIs on Windows) may only run through cmd.exe, with the arguments quoted\n// into a single command string by the helper — never user-authored text.\nexport function runTool(argv, options: Record<string, any> = {}) {\n const cwd = options.cwd || process.cwd();\n const timeout = options.timeout || TOOL_TIMEOUT_MS;\n const { command, args } = spawnArgv(argv);\n\n return new Promise<ToolResult>((resolve) => {\n if (options.signal?.aborted) { resolve({ ok: false, stdout: '', cancelled: true, error: 'Analysis cancelled.' }); return; }\n let child;\n try {\n child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], env: options.env, detached: process.platform !== 'win32' });\n } catch (err) {\n resolve({ ok: false, status: -1, stdout: '', stderr: String(err && err.message || err), timedOut: false });\n return;\n }\n\n let stdout = '';\n let stderr = '';\n let settled = false;\n let stopped: Partial<ToolResult> | null = null, bytes = 0;\n const stop = (reason: Partial<ToolResult>) => {\n if (settled || stopped) return;\n stopped = reason;\n if (process.platform === 'win32' && child.pid) {\n const killer = spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });\n killer.on('error', () => child.kill()); killer.unref();\n } else {\n try { process.kill(-child.pid, 'SIGKILL'); } catch { child.kill('SIGKILL'); }\n }\n };\n const abort = () => stop({ ok: false, cancelled: true, error: 'Analysis cancelled.' });\n const finish = (extra) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n options.signal?.removeEventListener('abort', abort);\n resolve({ status: child.exitCode, stdout, stderr, ...extra });\n };\n\n const cap = (chunk, into) => {\n const next = into + chunk;\n return next.length > MAX_OUTPUT_BYTES ? next.slice(0, MAX_OUTPUT_BYTES) : next;\n };\n child.stdout.setEncoding('utf8'); child.stderr.setEncoding('utf8');\n const collect = (chunk: string, stderrChunk = false) => {\n bytes += Buffer.byteLength(chunk);\n if (bytes > MAX_OUTPUT_BYTES) { stop({ ok: false, error: 'Analyzer output exceeded the size limit.' }); return; }\n if (stderrChunk) stderr = cap(chunk, stderr).slice(-4000); else stdout = cap(chunk, stdout);\n };\n child.stdout.on('data', c => collect(c));\n child.stderr.on('data', c => collect(c, true));\n\n const timer = setTimeout(() => {\n stop({ ok: false, timedOut: true, error: `${argv[0]} took too long and was stopped.` });\n }, timeout);\n\n child.on('error', (err) => {\n finish({ ok: false, timedOut: false, error: 'Could not run ' + argv[0] + ': ' + err.message });\n });\n child.on('close', () => finish(stopped || { ok: true, timedOut: false }));\n options.signal?.addEventListener('abort', abort, { once: true });\n if (options.signal?.aborted) abort();\n });\n}\n\n// The contract every analyzer hands back: findings normalized to the shape the\n// built-in scanner already uses (`rule`, `severity`, `category`, `message`,\n// `line`, `excerpt`), plus a `source` so the UI can attribute them. A tool that\n// produced nothing parseable is an `ok:false` pass with a reason, never a crash.\nexport function emptyPass(id, label, reason) {\n return { id, label, ok: false, available: false, findings: [], reason, source: 'external', tool: id };\n}\n"]}
|
|
@@ -40,21 +40,24 @@ export function matchNodeText(text, files) {
|
|
|
40
40
|
const t = String(text || '').trim();
|
|
41
41
|
if (!t)
|
|
42
42
|
return null;
|
|
43
|
-
const exact = files.find((f) => t === f.path || t.endsWith('/' + f.path)
|
|
43
|
+
const exact = files.find((f) => t === f.path || t.endsWith('/' + f.path));
|
|
44
44
|
if (exact)
|
|
45
45
|
return exact.path;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const dup = files.filter((x) => x.name === f.name);
|
|
53
|
-
if (dup.length > 1)
|
|
54
|
-
return null;
|
|
46
|
+
const containsLabel = (label) => {
|
|
47
|
+
let at = t.indexOf(label);
|
|
48
|
+
while (at !== -1) {
|
|
49
|
+
if (!/[\w./-]/.test(t[at - 1] || '') && !/[\w./-]/.test(t[at + label.length] || ''))
|
|
50
|
+
return true;
|
|
51
|
+
at = t.indexOf(label, at + 1);
|
|
55
52
|
}
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
53
|
+
return false;
|
|
54
|
+
};
|
|
55
|
+
// Models often annotate the full path with a role or dependency count.
|
|
56
|
+
// Prefer that path before considering potentially duplicated basenames.
|
|
57
|
+
const paths = files.filter(f => containsLabel(f.path));
|
|
58
|
+
if (paths.length)
|
|
59
|
+
return paths.length === 1 ? paths[0].path : null;
|
|
60
|
+
const names = files.filter(f => containsLabel(f.name));
|
|
61
|
+
return names.length === 1 ? names[0].path : null;
|
|
59
62
|
}
|
|
60
63
|
//# sourceMappingURL=aiMermaid.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aiMermaid.js","sourceRoot":"","sources":["../../src/shared/diagram/aiMermaid.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,4EAA4E;AAC5E,uDAAuD;AAEvD,yEAAyE;AACzE,2EAA2E;AAC3E,mCAAmC;AACnC,MAAM,UAAU,gBAAgB,CAAC,IAAI;IACnC,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAEnD,sDAAsD;IACtD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,IAAI,CAAC,CAAC;IAEzD,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC3D,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAEnD,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,0BAA0B,CAAC,CAAC;IAC1D,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAE9B,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChD,sEAAsE;IACtE,qEAAqE;IACrE,MAAM,YAAY,GAAG,kFAAkF,CAAC;IACxG,OAAO,KAAK,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5C,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,KAAK,CAAC,GAAG,EAAE,CAAC;YACZ,SAAS;QACX,CAAC;QACD,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,MAAM;QACnC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,iCAAiC;IAChD,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;IACrC,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;IACtF,IAAI,UAAU,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAEhC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED,4EAA4E;AAC5E,6EAA6E;AAC7E,MAAM,UAAU,aAAa,CAAC,IAAI,EAAE,KAAK;IACvC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACpC,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpB,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"aiMermaid.js","sourceRoot":"","sources":["../../src/shared/diagram/aiMermaid.ts"],"names":[],"mappings":"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","sourcesContent":["// Turns an LLM's raw answer into trustworthy Mermaid source.\n// Models wrap, apologize, and explain — this strips all of that back to the\n// diagram itself plus the one-line caption we ask for.\n\n// Pulls `{ caption, body }` out of `text`; returns null when there is no\n// flowchart to be found. `body` starts at the `flowchart`/`graph` line and\n// has trailing prose trimmed away.\nexport function extractAIDiagram(text) {\n if (!text || typeof text !== 'string') return null;\n\n // Fences gone first — ```mermaid and plain ``` alike.\n const unfenced = text.replace(/```(?:mermaid)?/gi, '\\n');\n\n const capMatch = unfenced.match(/^%%\\s*caption:\\s*(.+)$/m);\n const caption = capMatch ? capMatch[1].trim() : '';\n\n const start = unfenced.search(/^\\s*(flowchart|graph)\\b/m);\n if (start === -1) return null;\n\n const lines = unfenced.slice(start).split('\\n');\n // Trim trailing prose: walk back past anything that doesn't look like\n // Mermaid. Labels, edges, subgraph blocks and classDefs all survive.\n const MERMAID_HINT = /-->|---|\\[|\\]|\\(|\\)|\\bsubgraph\\b|\\bend\\b|classDef|^class\\b|%%|:::|^\\s*[\\w$]+\\s*:/;\n while (lines.length) {\n const tail = lines[lines.length - 1].trim();\n if (!tail) {\n lines.pop();\n continue;\n }\n if (MERMAID_HINT.test(tail)) break;\n lines.pop(); // a sentence, not a diagram line\n }\n\n const body = lines.join('\\n').trim();\n const meaningful = lines.filter((l) => l.trim() && !l.trim().startsWith('%%')).length;\n if (meaningful < 2) return null;\n\n return { caption, body };\n}\n\n// AI nodes carry no stable ids, so clicks are matched by their text: a full\n// path hit first, then a unique-basename hit. Imperfect and honest about it.\nexport function matchNodeText(text, files) {\n const t = String(text || '').trim();\n if (!t) return null;\n const exact = files.find((f) => t === f.path || t.endsWith('/' + f.path));\n if (exact) return exact.path;\n const containsLabel = (label: string) => {\n let at = t.indexOf(label);\n while (at !== -1) {\n if (!/[\\w./-]/.test(t[at - 1] || '') && !/[\\w./-]/.test(t[at + label.length] || '')) return true;\n at = t.indexOf(label, at + 1);\n }\n return false;\n };\n // Models often annotate the full path with a role or dependency count.\n // Prefer that path before considering potentially duplicated basenames.\n const paths = files.filter(f => containsLabel(f.path));\n if (paths.length) return paths.length === 1 ? paths[0].path : null;\n const names = files.filter(f => containsLabel(f.name));\n return names.length === 1 ? names[0].path : null;\n}\n"]}
|