Nicolas Cantu 088eab84b7 Platform docs, services, ia_dev submodule, smart_ide project config
- Add ia_dev submodule (projects/smart_ide on forge 4nk)
- Document APIs, orchestrator, gateway, local-office, rollout
- Add systemd/scripts layout; relocate setup scripts
- Remove obsolete nginx/enso-only docs from this repo scope
2026-04-03 16:07:58 +02:00

186 lines
4.0 KiB
TypeScript

import { spawn } from "node:child_process";
const MAX_PATTERN_LEN = 8192;
export interface RgMatchRow {
path: string;
lineNumber: number;
line: string;
}
export interface RgResult {
matches: RgMatchRow[];
truncated: boolean;
exitCode: number;
stderr: string;
}
const isRecord = (v: unknown): v is Record<string, unknown> =>
typeof v === "object" && v !== null && !Array.isArray(v);
const readPathText = (v: unknown): string => {
if (!isRecord(v)) {
return "";
}
const t = v.text;
return typeof t === "string" ? t : "";
};
const readLinesText = (v: unknown): string => {
if (!isRecord(v)) {
return "";
}
const t = v.lines;
if (!isRecord(t)) {
return "";
}
const text = t.text;
return typeof text === "string" ? text : "";
};
export const runRipgrepJson = (
pattern: string,
searchPath: string,
maxMatches: number,
timeoutMs: number,
): Promise<RgResult> => {
if (pattern.length === 0) {
return Promise.resolve({
matches: [],
truncated: false,
exitCode: 2,
stderr: "Empty pattern",
});
}
if (pattern.length > MAX_PATTERN_LEN) {
return Promise.resolve({
matches: [],
truncated: false,
exitCode: 2,
stderr: "Pattern too long",
});
}
return new Promise((resolve) => {
const matches: RgMatchRow[] = [];
let truncated = false;
let stderr = "";
let settled = false;
let timer: ReturnType<typeof setTimeout> | undefined;
const finish = (r: RgResult): void => {
if (settled) {
return;
}
settled = true;
if (timer !== undefined) {
clearTimeout(timer);
}
resolve(r);
};
const child = spawn(
"rg",
[
"--json",
"--line-number",
"--regexp",
pattern,
"--",
searchPath,
],
{
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
},
);
timer = setTimeout(() => {
child.kill("SIGKILL");
finish({
matches,
truncated: true,
exitCode: 124,
stderr: `${stderr}\nTimed out`.trim(),
});
}, timeoutMs);
const flushErr = (): void => {
child.stderr?.on("data", (chunk: Buffer) => {
stderr += chunk.toString("utf8");
});
};
flushErr();
let buf = "";
child.stdout?.setEncoding("utf8");
child.stdout?.on("data", (chunk: string) => {
buf += chunk;
let idx: number;
while ((idx = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, idx);
buf = buf.slice(idx + 1);
if (line.length === 0) {
continue;
}
let obj: unknown;
try {
obj = JSON.parse(line) as unknown;
} catch {
continue;
}
if (!isRecord(obj) || obj.type !== "match") {
continue;
}
const data = obj.data;
if (!isRecord(data)) {
continue;
}
const pathText = readPathText(data.path);
const lineNum = data.line_number;
const lineText = readLinesText(data).replace(/\n$/, "");
if (typeof lineNum !== "number" || pathText.length === 0) {
continue;
}
matches.push({
path: pathText,
lineNumber: lineNum,
line: lineText,
});
if (matches.length >= maxMatches) {
truncated = true;
child.kill("SIGTERM");
break;
}
}
});
child.on("error", (err: NodeJS.ErrnoException) => {
if (err.code === "ENOENT") {
finish({
matches: [],
truncated: false,
exitCode: 127,
stderr: "ripgrep (rg) not found in PATH",
});
return;
}
finish({
matches,
truncated,
exitCode: 1,
stderr: `${stderr}\n${err.message}`.trim(),
});
});
child.on("close", (code) => {
finish({
matches,
truncated,
exitCode: code ?? 0,
stderr: stderr.trim(),
});
});
});
};