codebase-onboarder 1.0.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.
@@ -1 +1 @@
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; 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"]}