mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-04-28 06:19:46 +00:00
Introduce full AI orchestration ecosystem: - MCP Server with 16 tools, scoped auth, and audit logging - A2A v0.3 server with JSON-RPC 2.0, SSE streaming, and task manager - Auto-Combo engine with 6-factor scoring and self-healing - VS Code extension with smart dispatch and budget tracking - Harden CI pipeline: add static checks, remove continue-on-error - Add translator schema validation tests - Update .gitignore and CHANGELOG for release checklist
177 lines
4.7 KiB
JavaScript
177 lines
4.7 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const cwd = process.cwd();
|
|
const defaultRoots = ["src/shared/components", "src/lib/db", "open-sse/translator"];
|
|
const roots = process.argv.slice(2).length > 0 ? process.argv.slice(2) : defaultRoots;
|
|
const sourceExtensions = [".ts", ".tsx", ".js", ".mjs", ".jsx", ".mts", ".cts"];
|
|
|
|
function toPosix(filePath) {
|
|
return filePath.split(path.sep).join("/");
|
|
}
|
|
|
|
function listSourceFiles(rootDir) {
|
|
const absRoot = path.resolve(cwd, rootDir);
|
|
if (!fs.existsSync(absRoot)) {
|
|
return [];
|
|
}
|
|
|
|
const stack = [absRoot];
|
|
const files = [];
|
|
|
|
while (stack.length > 0) {
|
|
const current = stack.pop();
|
|
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(current, entry.name);
|
|
if (entry.isDirectory()) {
|
|
stack.push(fullPath);
|
|
continue;
|
|
}
|
|
|
|
if (sourceExtensions.includes(path.extname(entry.name))) {
|
|
files.push(path.resolve(fullPath));
|
|
}
|
|
}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
function resolveRelativeImport(fromFile, specifier) {
|
|
const base = path.resolve(path.dirname(fromFile), specifier);
|
|
const ext = path.extname(base);
|
|
|
|
if (ext && fs.existsSync(base) && fs.statSync(base).isFile()) {
|
|
return path.resolve(base);
|
|
}
|
|
|
|
for (const extension of sourceExtensions) {
|
|
const candidate = `${base}${extension}`;
|
|
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
return path.resolve(candidate);
|
|
}
|
|
}
|
|
|
|
for (const extension of sourceExtensions) {
|
|
const candidate = path.join(base, `index${extension}`);
|
|
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
return path.resolve(candidate);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function extractImportSpecifiers(fileContents) {
|
|
const specs = [];
|
|
const regex = /\b(?:import|export)\s+(?:[^"'`]*?\sfrom\s*)?["'`]([^"'`]+)["'`]/g;
|
|
let match = regex.exec(fileContents);
|
|
while (match) {
|
|
specs.push(match[1]);
|
|
match = regex.exec(fileContents);
|
|
}
|
|
return specs;
|
|
}
|
|
|
|
function buildGraph(files) {
|
|
const fileSet = new Set(files);
|
|
const graph = new Map();
|
|
|
|
for (const filePath of files) {
|
|
const code = fs.readFileSync(filePath, "utf8");
|
|
const dependencies = new Set();
|
|
const importSpecifiers = extractImportSpecifiers(code);
|
|
|
|
for (const specifier of importSpecifiers) {
|
|
if (!specifier.startsWith(".")) continue;
|
|
const resolved = resolveRelativeImport(filePath, specifier);
|
|
if (!resolved) continue;
|
|
if (!fileSet.has(resolved)) continue;
|
|
dependencies.add(resolved);
|
|
}
|
|
|
|
graph.set(filePath, dependencies);
|
|
}
|
|
|
|
return graph;
|
|
}
|
|
|
|
function stronglyConnectedComponents(graph) {
|
|
const indexMap = new Map();
|
|
const lowLinkMap = new Map();
|
|
const onStack = new Set();
|
|
const stack = [];
|
|
const components = [];
|
|
let indexCounter = 0;
|
|
|
|
function strongConnect(node) {
|
|
indexMap.set(node, indexCounter);
|
|
lowLinkMap.set(node, indexCounter);
|
|
indexCounter += 1;
|
|
stack.push(node);
|
|
onStack.add(node);
|
|
|
|
for (const neighbor of graph.get(node) || []) {
|
|
if (!indexMap.has(neighbor)) {
|
|
strongConnect(neighbor);
|
|
lowLinkMap.set(node, Math.min(lowLinkMap.get(node), lowLinkMap.get(neighbor)));
|
|
} else if (onStack.has(neighbor)) {
|
|
lowLinkMap.set(node, Math.min(lowLinkMap.get(node), indexMap.get(neighbor)));
|
|
}
|
|
}
|
|
|
|
if (lowLinkMap.get(node) === indexMap.get(node)) {
|
|
const component = [];
|
|
while (stack.length > 0) {
|
|
const candidate = stack.pop();
|
|
onStack.delete(candidate);
|
|
component.push(candidate);
|
|
if (candidate === node) break;
|
|
}
|
|
components.push(component);
|
|
}
|
|
}
|
|
|
|
for (const node of graph.keys()) {
|
|
if (!indexMap.has(node)) {
|
|
strongConnect(node);
|
|
}
|
|
}
|
|
|
|
return components;
|
|
}
|
|
|
|
function isSelfCycle(component, graph) {
|
|
if (component.length !== 1) return false;
|
|
const [file] = component;
|
|
return (graph.get(file) || new Set()).has(file);
|
|
}
|
|
|
|
const files = roots.flatMap((root) => listSourceFiles(root));
|
|
const graph = buildGraph(files);
|
|
const components = stronglyConnectedComponents(graph);
|
|
const cycles = components.filter(
|
|
(component) => component.length > 1 || isSelfCycle(component, graph)
|
|
);
|
|
|
|
if (cycles.length === 0) {
|
|
console.log(
|
|
`[cycles] OK - no cycles detected across ${graph.size} files in: ${roots.join(", ")}`
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
console.error(`[cycles] FAIL - detected ${cycles.length} strongly connected component(s):`);
|
|
for (const component of cycles) {
|
|
const sorted = [...component].sort((a, b) => a.localeCompare(b));
|
|
console.error(`\n- SCC (${sorted.length} files)`);
|
|
for (const filePath of sorted) {
|
|
console.error(` - ${toPosix(path.relative(cwd, filePath))}`);
|
|
}
|
|
}
|
|
|
|
process.exit(1);
|