quantum-cli-core
Version:
Quantum CLI Core - Multi-LLM Collaboration System
141 lines • 6.76 kB
JavaScript
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'fs/promises';
import * as path from 'path';
// Simple console logger for import processing
const logger = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
debug: (...args) => console.debug('[DEBUG] [ImportProcessor]', ...args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
warn: (...args) => console.warn('[WARN] [ImportProcessor]', ...args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: (...args) => console.error('[ERROR] [ImportProcessor]', ...args),
};
/**
* Processes import statements in QUANTUM.md content
* Supports @path/to/file.md syntax for importing content from other files
*
* @param content - The content to process for imports
* @param basePath - The directory path where the current file is located
* @param debugMode - Whether to enable debug logging
* @param importState - State tracking for circular import prevention
* @returns Processed content with imports resolved
*/
export async function processImports(content, basePath, debugMode = false, importState = {
processedFiles: new Set(),
maxDepth: 10,
currentDepth: 0,
}) {
if (importState.currentDepth >= importState.maxDepth) {
if (debugMode) {
logger.warn(`Maximum import depth (${importState.maxDepth}) reached. Stopping import processing.`);
}
return content;
}
// Regex to match @path/to/file imports (supports any file extension)
// Supports both @path/to/file.md and @./path/to/file.md syntax
const importRegex = /@([./]?[^\s\n]+\.[^\s\n]+)/g;
let processedContent = content;
let match;
// Process all imports in the content
while ((match = importRegex.exec(content)) !== null) {
const importPath = match[1];
// Validate import path to prevent path traversal attacks
if (!validateImportPath(importPath, basePath, [basePath])) {
processedContent = processedContent.replace(match[0], `<!-- Import failed: ${importPath} - Path traversal attempt -->`);
continue;
}
// Check if the import is for a non-md file and warn
if (!importPath.endsWith('.md')) {
logger.warn(`Import processor only supports .md files. Attempting to import non-md file: ${importPath}. This will fail.`);
// Replace the import with a warning comment
processedContent = processedContent.replace(match[0], `<!-- Import failed: ${importPath} - Only .md files are supported -->`);
continue;
}
const fullPath = path.resolve(basePath, importPath);
if (debugMode) {
logger.debug(`Processing import: ${importPath} -> ${fullPath}`);
}
// Check for circular imports - if we're already processing this file
if (importState.currentFile === fullPath) {
if (debugMode) {
logger.warn(`Circular import detected: ${importPath}`);
}
// Replace the import with a warning comment
processedContent = processedContent.replace(match[0], `<!-- Circular import detected: ${importPath} -->`);
continue;
}
// Check if we've already processed this file in this import chain
if (importState.processedFiles.has(fullPath)) {
if (debugMode) {
logger.warn(`File already processed in this chain: ${importPath}`);
}
// Replace the import with a warning comment
processedContent = processedContent.replace(match[0], `<!-- File already processed: ${importPath} -->`);
continue;
}
// Check for potential circular imports by looking at the import chain
if (importState.currentFile) {
const currentFileDir = path.dirname(importState.currentFile);
const potentialCircularPath = path.resolve(currentFileDir, importPath);
if (potentialCircularPath === importState.currentFile) {
if (debugMode) {
logger.warn(`Circular import detected: ${importPath}`);
}
// Replace the import with a warning comment
processedContent = processedContent.replace(match[0], `<!-- Circular import detected: ${importPath} -->`);
continue;
}
}
try {
// Check if the file exists
await fs.access(fullPath);
// Read the imported file content
const importedContent = await fs.readFile(fullPath, 'utf-8');
if (debugMode) {
logger.debug(`Successfully read imported file: ${fullPath}`);
}
// Recursively process imports in the imported content
const processedImportedContent = await processImports(importedContent, path.dirname(fullPath), debugMode, {
...importState,
processedFiles: new Set([...importState.processedFiles, fullPath]),
currentDepth: importState.currentDepth + 1,
currentFile: fullPath, // Set the current file being processed
});
// Replace the import statement with the processed content
processedContent = processedContent.replace(match[0], `<!-- Imported from: ${importPath} -->\n${processedImportedContent}\n<!-- End of import from: ${importPath} -->`);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (debugMode) {
logger.error(`Failed to import ${importPath}: ${errorMessage}`);
}
// Replace the import with an error comment
processedContent = processedContent.replace(match[0], `<!-- Import failed: ${importPath} - ${errorMessage} -->`);
}
}
return processedContent;
}
/**
* Validates import paths to ensure they are safe and within allowed directories
*
* @param importPath - The import path to validate
* @param basePath - The base directory for resolving relative paths
* @param allowedDirectories - Array of allowed directory paths
* @returns Whether the import path is valid
*/
export function validateImportPath(importPath, basePath, allowedDirectories) {
// Reject URLs
if (/^(file|https?):\/\//.test(importPath)) {
return false;
}
const resolvedPath = path.resolve(basePath, importPath);
return allowedDirectories.some((allowedDir) => {
const normalizedAllowedDir = path.resolve(allowedDir);
return resolvedPath.startsWith(normalizedAllowedDir);
});
}
//# sourceMappingURL=memoryImportProcessor.js.map