knowledge-graph-preview / cli /input-resolver.js
mr4's picture
Upload 136 files
fd8cdf5 verified
import * as fs from 'node:fs';
import * as path from 'node:path';
import { validateDashboard } from '../lib/dashboard-validator.js';
import { log } from './logger.js';
/** Directories to skip when recursively scanning for JSON files */
const SKIP_DIRS = new Set([
'node_modules', '.git', 'dist', 'build', '.next', '.cache',
'__pycache__', '.turbo', 'target', 'obj',
]);
function synthesizeMeta() {
return {
lastAnalyzedAt: new Date().toISOString(),
gitCommitHash: '',
version: '0.0.0',
analyzedFiles: 0,
};
}
function extractProjectName(data) {
if (data.project &&
typeof data.project === 'object' &&
!Array.isArray(data.project) &&
'name' in data.project &&
typeof data.project.name === 'string') {
return data.project.name;
}
return null;
}
function tryParseJsonFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = JSON.parse(content);
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
return { error: 'not a JSON object' };
}
return { data: parsed };
}
catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { error: message };
}
}
function handleSingleFile(absolutePath) {
const parseResult = tryParseJsonFile(absolutePath);
if ('error' in parseResult) {
return { success: false, error: `Failed to parse ${path.basename(absolutePath)}: ${parseResult.error}` };
}
const validation = validateDashboard(parseResult.data);
if (!validation.valid) {
return {
success: false,
error: `${path.basename(absolutePath)} is not a valid dashboard: ${validation.errors.join(', ')}`,
};
}
const projectName = extractProjectName(parseResult.data) || path.basename(absolutePath, '.json');
const dirName = path.basename(path.dirname(absolutePath));
const fileName = path.basename(absolutePath);
const manifest = [
{
dirName,
meta: synthesizeMeta(),
graphFiles: [{ fileName, projectName }],
},
];
const fileMapping = {
[`${dirName}/${fileName}`]: absolutePath,
};
return { success: true, manifest, fileMapping };
}
function handleProjectDirectory(dirPath, dirName) {
const metaPath = path.join(dirPath, 'meta.json');
const metaResult = tryParseJsonFile(metaPath);
let meta;
if ('error' in metaResult) {
log('warn', `Skipping ${dirName}/meta.json: ${metaResult.error}`);
return null;
}
meta = metaResult.data;
const graphFiles = [];
const fileMapping = {};
// Recursively collect all valid graph .json files from this directory and subdirectories
function scanDir(currentPath, relativePrefix) {
let entries;
try {
entries = fs.readdirSync(currentPath);
}
catch {
return;
}
for (const entry of entries) {
const fullPath = path.join(currentPath, entry);
let stat;
try {
stat = fs.statSync(fullPath);
}
catch {
continue;
}
if (stat.isDirectory()) {
// Skip common non-source directories
if (SKIP_DIRS.has(entry))
continue;
// Recurse into subdirectory
const childPrefix = relativePrefix ? `${relativePrefix}/${entry}` : entry;
scanDir(fullPath, childPrefix);
}
else if (stat.isFile() && entry.endsWith('.json') && entry !== 'meta.json') {
const parseResult = tryParseJsonFile(fullPath);
if ('error' in parseResult) {
continue; // Not valid JSON β€” silently skip
}
const validation = validateDashboard(parseResult.data);
if (!validation.valid) {
continue; // Not a dashboard graph file β€” silently skip
}
const projectName = extractProjectName(parseResult.data) || path.basename(entry, '.json');
// Use relative path from the project directory as the fileName for nested files
const relativeFileName = relativePrefix ? `${relativePrefix}/${entry}` : entry;
graphFiles.push({ fileName: relativeFileName, projectName });
fileMapping[`${dirName}/${relativeFileName}`] = fullPath;
}
}
}
scanDir(dirPath, '');
if (graphFiles.length === 0) {
log('warn', `Skipping ${dirName}: no valid graph files`);
return null;
}
return {
entry: { dirName, meta, graphFiles },
fileMapping,
};
}
/**
* Recursively collects all directories containing .json files,
* skipping common non-source directories.
* Returns a map of relative directory path β†’ absolute directory path.
*/
function collectJsonDirs(basePath, relativePath = '') {
const result = new Map();
const currentPath = relativePath ? path.join(basePath, relativePath) : basePath;
let entries;
try {
entries = fs.readdirSync(currentPath);
}
catch {
return result;
}
// Check if this directory has any .json files
let hasJsonFiles = false;
for (const entry of entries) {
if (entry.endsWith('.json')) {
const filePath = path.join(currentPath, entry);
try {
const stat = fs.statSync(filePath);
if (stat.isFile()) {
hasJsonFiles = true;
break;
}
}
catch {
// skip
}
}
}
if (hasJsonFiles) {
result.set(relativePath, currentPath);
}
// Recurse into subdirectories
for (const entry of entries) {
if (SKIP_DIRS.has(entry))
continue;
const entryPath = path.join(currentPath, entry);
try {
const stat = fs.statSync(entryPath);
if (stat.isDirectory()) {
const childRelative = relativePath ? `${relativePath}/${entry}` : entry;
const childResults = collectJsonDirs(basePath, childRelative);
for (const [rel, abs] of childResults) {
result.set(rel, abs);
}
}
}
catch {
// skip
}
}
return result;
}
function handleFlatDirectory(dirPath) {
const dirName = path.basename(dirPath);
const manifest = [];
const fileMapping = {};
// Recursively collect all directories with .json files
const jsonDirs = collectJsonDirs(dirPath);
for (const [relativeDirPath, absoluteDirPath] of jsonDirs) {
const entries = fs.readdirSync(absoluteDirPath);
const graphFiles = [];
for (const entry of entries) {
if (!entry.endsWith('.json'))
continue;
const filePath = path.join(absoluteDirPath, entry);
try {
const stat = fs.statSync(filePath);
if (!stat.isFile())
continue;
}
catch {
continue;
}
const parseResult = tryParseJsonFile(filePath);
if ('error' in parseResult) {
continue; // Not valid JSON β€” silently skip
}
const validation = validateDashboard(parseResult.data);
if (!validation.valid) {
continue; // Not a dashboard graph file β€” silently skip
}
const projectName = extractProjectName(parseResult.data) || path.basename(entry, '.json');
graphFiles.push({ fileName: entry, projectName });
// Build the file mapping key: for top-level files use dirName/fileName,
// for nested files use dirName/relativePath/fileName
const mappingKey = relativeDirPath
? `${dirName}/${relativeDirPath}/${entry}`
: `${dirName}/${entry}`;
fileMapping[mappingKey] = filePath;
}
if (graphFiles.length > 0) {
// Use relative path as the manifest entry dirName for nested dirs,
// or the base dirName for top-level files
const entryDirName = relativeDirPath
? `${dirName}/${relativeDirPath}`
: dirName;
manifest.push({
dirName: entryDirName,
meta: synthesizeMeta(),
graphFiles,
});
}
}
if (manifest.length === 0) {
return { success: false, error: `No valid dashboard files found in ${dirPath}` };
}
return { success: true, manifest, fileMapping };
}
/**
* Recursively finds all directories containing a meta.json file,
* returning their paths relative to the base directory.
* Skips common non-source directories.
*/
function findDirsWithMetaJson(basePath, currentPath) {
const results = [];
let entries;
try {
entries = fs.readdirSync(currentPath);
}
catch {
return results;
}
for (const entry of entries) {
if (SKIP_DIRS.has(entry))
continue;
const entryPath = path.join(currentPath, entry);
try {
const stat = fs.statSync(entryPath);
if (!stat.isDirectory())
continue;
if (fs.existsSync(path.join(entryPath, 'meta.json'))) {
// Use relative path from basePath
const relativePath = path.relative(basePath, entryPath);
results.push(relativePath);
}
// Continue recursing to find deeper directories with meta.json
const childResults = findDirsWithMetaJson(basePath, entryPath);
results.push(...childResults);
}
catch {
// Skip entries that can't be stat'd
}
}
return results;
}
function handleNestedDirectory(dirPath, subdirs) {
const manifest = [];
const fileMapping = {};
for (const subdir of subdirs) {
const subdirPath = path.join(dirPath, subdir);
// Use the relative path as dirName (handles nested paths like "a/b/c")
const dirName = subdir.replace(/\\/g, '/');
const result = handleProjectDirectory(subdirPath, dirName);
if (result) {
manifest.push(result.entry);
Object.assign(fileMapping, result.fileMapping);
}
}
if (manifest.length === 0) {
return { success: false, error: `No valid dashboard files found in ${dirPath}` };
}
return { success: true, manifest, fileMapping };
}
export function resolveInput(inputPath) {
const absolutePath = path.resolve(inputPath);
let stat;
try {
stat = fs.statSync(absolutePath);
}
catch {
return { success: false, error: `Path not found: ${inputPath}` };
}
// Case 1: Single file
if (stat.isFile()) {
return handleSingleFile(absolutePath);
}
// Case 2: Directory
if (stat.isDirectory()) {
// Check if directory itself has meta.json β†’ project directory
const hasMetaJson = fs.existsSync(path.join(absolutePath, 'meta.json'));
if (hasMetaJson) {
const dirName = path.basename(absolutePath);
const result = handleProjectDirectory(absolutePath, dirName);
if (!result) {
return { success: false, error: `No valid dashboard files found in ${absolutePath}` };
}
return { success: true, manifest: [result.entry], fileMapping: result.fileMapping };
}
// Recursively check for subdirectories containing meta.json β†’ nested directory
const subdirsWithMeta = findDirsWithMetaJson(absolutePath, absolutePath);
if (subdirsWithMeta.length > 0) {
return handleNestedDirectory(absolutePath, subdirsWithMeta);
}
// Flat directory: has .json files but no meta.json, no nested projects
return handleFlatDirectory(absolutePath);
}
return { success: false, error: `Path is not a file or directory: ${inputPath}` };
}