meld
Version:
Meld: A template language for LLM prompts
1,316 lines (1,143 loc) • 48.6 kB
text/typescript
import 'reflect-metadata';
import '@core/di-config.js';
// CLI initialization
import { main as apiMain } from '@api/index.js';
import { version } from '@core/version.js';
import { cliLogger as logger } from '@core/utils/logger.js';
import { loggingConfig } from '@core/config/logging.js';
import { IFileSystem } from '@services/fs/FileSystemService/IFileSystem.js';
import { createInterface } from 'readline';
import { initCommand } from './commands/init.js';
import { ProcessOptions } from '../core/types/index.js';
import { MeldError, ErrorSeverity } from '@core/errors/MeldError.js';
import fs from 'fs/promises';
import path from 'path';
import { watch } from 'fs/promises';
import { NodeFileSystem } from '@services/fs/FileSystemService/NodeFileSystem.js';
import { debugResolutionCommand } from './commands/debug-resolution.js';
import { debugContextCommand } from './commands/debug-context.js';
import { debugTransformCommand } from './commands/debug-transform.js';
import chalk from 'chalk';
import { existsSync } from 'fs';
// CLI Options interface
export interface CLIOptions {
input: string;
output?: string;
format?: 'markdown' | 'md' | 'xml';
stdout?: boolean;
verbose?: boolean;
debug?: boolean;
strict?: boolean;
homePath?: string;
watch?: boolean;
version?: boolean;
help?: boolean;
custom?: boolean; // Flag for custom filesystem in tests
debugResolution?: boolean;
variableName?: string;
outputFormat?: 'json' | 'text' | 'mermaid';
debugContext?: boolean;
visualizationType?: 'hierarchy' | 'variable-propagation' | 'combined' | 'timeline';
rootStateId?: string;
includeVars?: boolean;
includeTimestamps?: boolean;
includeFilePaths?: boolean;
debugTransform?: boolean;
directiveType?: string;
includeContent?: boolean;
debugSourceMaps?: boolean; // Flag to display source mapping information
detailedSourceMaps?: boolean; // Flag to display detailed source mapping information
// No transform options - transformation is always enabled
}
/**
* Normalize format string to supported output format
*/
function normalizeFormat(format?: string): 'markdown' | 'xml' {
if (!format) return 'markdown';
switch (format.toLowerCase()) {
case 'md':
case 'markdown':
return 'markdown';
case 'xml':
return 'xml'; // Return 'xml' for XML format
default:
return 'markdown';
}
}
/**
* Get file extension for the given format
*/
function getOutputExtension(format: 'markdown' | 'xml'): string {
switch (format) {
case 'markdown':
return '.md';
case 'xml':
return '.xml';
default:
return '.md';
}
}
/**
* Parse command line arguments
*/
function parseArgs(args: string[]): CLIOptions {
const options: CLIOptions = {
input: '',
format: 'markdown', // Default to markdown format
strict: false // Default to permissive mode
};
// Check for debug-resolution command
if (args.length > 0 && args[0] === 'debug-resolution') {
options.debugResolution = true;
// Remove the command from args
args = args.slice(1);
}
// Check for debug-transform command
if (args.length > 0 && args[0] === 'debug-transform') {
options.debugTransform = true;
// Remove the command from args
args = args.slice(1);
}
// Check for debug-context command
if (args.length > 0 && args[0] === 'debug-context') {
options.debugContext = true;
// Remove the command from args
args = args.slice(1);
}
// Add context debug options
if (args.includes('--debug-context')) {
options.debugContext = true;
// Remove the flag so it doesn't get treated as a file path
args = args.filter(arg => arg !== '--debug-context');
}
// Handle visualization type
const vizTypeIndex = args.findIndex(arg => arg === '--viz-type');
if (vizTypeIndex !== -1 && vizTypeIndex < args.length - 1) {
const vizType = args[vizTypeIndex + 1];
if (['hierarchy', 'variable-propagation', 'combined', 'timeline'].includes(vizType)) {
options.visualizationType = vizType as 'hierarchy' | 'variable-propagation' | 'combined' | 'timeline';
} else {
console.error(`Invalid visualization type: ${vizType}. Using default.`);
}
// Remove from args to avoid treating as file path
args.splice(vizTypeIndex, 2);
}
// Handle root state ID
const rootStateIdIndex = args.findIndex(arg => arg === '--root-state-id');
if (rootStateIdIndex !== -1 && rootStateIdIndex < args.length - 1) {
options.rootStateId = args[rootStateIdIndex + 1];
// Remove from args
args.splice(rootStateIdIndex, 2);
}
// Include vars option
if (args.includes('--no-vars')) {
options.includeVars = false;
args = args.filter(arg => arg !== '--no-vars');
}
// Include timestamps option
if (args.includes('--no-timestamps')) {
options.includeTimestamps = false;
args = args.filter(arg => arg !== '--no-timestamps');
}
// Include file paths option
if (args.includes('--no-file-paths')) {
options.includeFilePaths = false;
args = args.filter(arg => arg !== '--no-file-paths');
}
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case '--version':
case '-V':
options.version = true;
break;
case '--output':
case '-o':
options.output = args[++i];
break;
case '--format':
case '-f':
options.format = normalizeFormat(args[++i]);
break;
case '--stdout':
options.stdout = true;
break;
case '--verbose':
case '-v':
options.verbose = true;
break;
case '--debug':
case '-d':
options.debug = true;
break;
case '--debug-source-maps':
options.debugSourceMaps = true;
break;
case '--detailed-source-maps':
options.detailedSourceMaps = true;
break;
case '--strict':
options.strict = true;
break;
case '--permissive':
options.strict = false;
break;
case '--home-path':
options.homePath = args[++i];
break;
case '--watch':
case '-w':
options.watch = true;
break;
case '--help':
case '-h':
options.help = true;
break;
// Add new debug-resolution options
case '--var':
case '--variable':
case '--variable-name':
options.variableName = args[++i];
break;
case '--output-format':
options.outputFormat = args[++i] as 'json' | 'text' | 'mermaid';
break;
// Add directive type option for debug-transform
case '--directive':
options.directiveType = args[++i];
break;
// Add include-content option for debug-transform
case '--include-content':
options.includeContent = true;
break;
// Transformation is always enabled by default
// No transform flags needed
default:
if (!arg.startsWith('-') && !options.input) {
options.input = arg;
} else {
throw new Error(`Unknown option: ${arg}`);
}
}
}
// Version and help can be used without an input file
if (!options.input && !options.version && !options.help) {
throw new Error('No input file specified');
}
return options;
}
/**
* Display help information
*/
function displayHelp(command?: string) {
if (command === 'debug-resolution') {
console.log(`
Usage: meld debug-resolution [options] <input-file>
Debug variable resolution in a Meld file.
Options:
--var, --variable <n> Filter to a specific variable
--output-format <format> Output format (json, text) [default: text]
-w, --watch Watch for changes and reprocess
-v, --verbose Enable verbose output
--home-path <path> Custom home path for ~/ substitution
-h, --help Display this help message
`);
return;
}
if (command === 'debug-transform') {
console.log(`
Usage: meld debug-transform [options] <input-file>
Debug node transformations through the pipeline.
Options:
--directive <type> Focus on a specific directive type
--output-format <format> Output format (text, json, mermaid) [default: text]
--output <path> Output file path
--include-content Include node content in output
-v, --verbose Enable verbose output
-h, --help Display this help message
`);
return;
}
console.log(`
Usage: meld [command] [options] <input-file>
Commands:
init Create a new Meld project
debug-resolution Debug variable resolution in a Meld file
debug-transform Debug node transformations through the pipeline
Options:
-f, --format <format> Output format: md, markdown, xml, llm [default: llm]
-o, --output <path> Output file path
--stdout Print to stdout instead of file
--strict Enable strict mode (fail on all errors)
--permissive Enable permissive mode (ignore recoverable errors) [default]
--home-path <path> Custom home path for ~/ substitution
-v, --verbose Enable verbose output (some additional info)
-d, --debug Enable debug output (full verbose logging)
-w, --watch Watch for changes and reprocess
-h, --help Display this help message
-V, --version Display version information
`);
if (!command || command === 'debug-context') {
console.log('\nContext Debugging Options:');
console.log(' --debug-context Debug context boundaries and variable propagation');
console.log(' --viz-type <type> Type of visualization (hierarchy, variable-propagation, combined, timeline)');
console.log(' --root-state-id <id> Root state ID to start visualization from');
console.log(' --variable-name <n> Variable name to track (required for variable-propagation and timeline)');
console.log(' --output-format <format> Output format (mermaid, dot, json)');
console.log(' --no-vars Exclude variables from context visualization');
console.log(' --no-timestamps Exclude timestamps from visualization');
console.log(' --no-file-paths Exclude file paths from visualization');
}
}
/**
* Prompt for file overwrite confirmation
*/
async function confirmOverwrite(filePath: string): Promise<{ outputPath: string; shouldOverwrite: boolean }> {
// In test mode, always return true to allow overwriting
if (process.env.NODE_ENV === 'test') {
return { outputPath: filePath, shouldOverwrite: true };
}
// Get the current CLI options from the outer scope
const cliOptions = getCurrentCLIOptions();
// For .md files, auto-redirect to .o.md unless explicitly set with -o
if (filePath.endsWith(".md") && !cliOptions.output) {
console.log("Auto-redirecting .md file to prevent overwrite");
const baseName = filePath.slice(0, -3); // Remove .md extension
const newOutputPath = `${baseName}.o.md`;
if (!(await fs.access(newOutputPath).then(() => true).catch(() => false))) {
return { outputPath: newOutputPath, shouldOverwrite: true };
}
}
// Check if we can use raw mode (might not be available in all environments)
const canUseRawMode = process.stdin.isTTY && typeof process.stdin.setRawMode === 'function';
// If raw mode isn't available, fall back to readline
if (!canUseRawMode) {
const rl = createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
rl.question(`File ${filePath} already exists. Overwrite? [Y/n] `, (answer) => {
rl.close();
// If user doesn't want to overwrite, find an incremental filename
if (answer.toLowerCase() === 'n') {
const newPath = findAvailableIncrementalFilename(filePath);
console.log(`Using alternative filename: ${newPath}`);
resolve({ outputPath: newPath, shouldOverwrite: true });
} else {
resolve({ outputPath: filePath, shouldOverwrite: true });
}
});
});
}
// Use raw mode to detect a single keypress
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdout.write(`File ${filePath} already exists. Overwrite? [Y/n] `);
return new Promise((resolve) => {
const onKeypress = (key: string) => {
// Ctrl-C
if (key === '\u0003') {
process.stdout.write('\n');
process.exit(0);
}
// Convert to lowercase for comparison
const keyLower = key.toLowerCase();
// Only process y, n, or enter (which is '\r' in raw mode)
if (keyLower === 'y' || keyLower === 'n' || key === '\r') {
// Echo the key (since raw mode doesn't show keystrokes)
process.stdout.write(key === '\r' ? 'y\n' : `${key}\n`);
// Restore the terminal to cooked mode
process.stdin.setRawMode(false);
process.stdin.pause();
process.stdin.removeListener('data', onKeypress);
// If user doesn't want to overwrite or pressed Enter (default to Y), find an incremental filename
if (keyLower === 'n') {
const newPath = findAvailableIncrementalFilename(filePath);
console.log(`Using alternative filename: ${newPath}`);
resolve({ outputPath: newPath, shouldOverwrite: true });
} else {
resolve({ outputPath: filePath, shouldOverwrite: true });
}
}
};
// Listen for keypresses
process.stdin.on('data', onKeypress);
});
}
// Store the current CLI options for access by other functions
let currentCLIOptions: CLIOptions | null = null;
function getCurrentCLIOptions(): CLIOptions {
if (!currentCLIOptions) {
throw new Error('CLI options not initialized');
}
return currentCLIOptions;
}
function setCurrentCLIOptions(options: CLIOptions): void {
currentCLIOptions = options;
}
/**
* Finds an available filename by appending an incremental number
* If file.md exists, tries file-1.md, file-2.md, etc.
*/
function findAvailableIncrementalFilename(filePath: string): string {
// Extract the base name and extension
const lastDotIndex = filePath.lastIndexOf('.');
const baseName = lastDotIndex !== -1 ? filePath.slice(0, lastDotIndex) : filePath;
const extension = lastDotIndex !== -1 ? filePath.slice(lastDotIndex) : '';
// Try incremental filenames until we find one that doesn't exist
let counter = 1;
let newPath = `${baseName}-${counter}${extension}`;
while (existsSync(newPath)) {
counter++;
newPath = `${baseName}-${counter}${extension}`;
}
return newPath;
}
/**
* Convert CLI options to API options
*/
function cliToApiOptions(cliOptions: CLIOptions): ProcessOptions {
// Always use transformation mode
const options: ProcessOptions = {
format: normalizeFormat(cliOptions.format),
debug: cliOptions.debug,
// Always transform by default
transformation: true,
fs: cliOptions.custom ? undefined : new NodeFileSystem() // Allow custom filesystem in test mode
};
// Add strict property to options for backward compatibility with tests
if (cliOptions.strict !== undefined) {
(options as any).strict = cliOptions.strict;
}
return options;
}
/**
* Watch for file changes and reprocess
*/
async function watchFiles(options: CLIOptions): Promise<void> {
logger.info('Starting watch mode', { input: options.input });
const inputPath = options.input;
const watchDir = path.dirname(inputPath);
try {
console.log(`Watching for changes in ${watchDir}...`);
const watcher = watch(watchDir, { recursive: true });
for await (const event of watcher) {
// Only process .meld files or the specific input file
if (event.filename?.endsWith('.meld') || event.filename === path.basename(inputPath)) {
console.log(`Change detected in ${event.filename}, reprocessing...`);
await processFile(options);
}
}
} catch (error) {
logger.error('Watch mode failed', {
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
/**
* Process a file with specific API options
*/
async function processFileWithOptions(cliOptions: CLIOptions, apiOptions: ProcessOptions): Promise<void> {
try {
// Show source map debug info before processing if requested
if (cliOptions.debugSourceMaps || cliOptions.detailedSourceMaps) {
console.log(chalk.cyan('Source map debugging enabled for file:', cliOptions.input));
}
// Process the file through the API with provided options
const result = await apiMain(cliOptions.input, apiOptions);
// Show source map debug info after processing if requested
if (cliOptions.debugSourceMaps) {
try {
const { getSourceMapDebugInfo } = require('@core/utils/sourceMapUtils.js');
console.log(chalk.cyan('\nSource map debug information:'));
console.log(getSourceMapDebugInfo());
} catch (e) {
console.error('Failed to get source map debug info:', e);
}
}
// Show detailed source map debug info if requested
if (cliOptions.detailedSourceMaps) {
try {
const { getDetailedSourceMapDebugInfo } = require('@core/utils/sourceMapUtils.js');
console.log(chalk.cyan('\nDetailed source map debug information:'));
console.log(getDetailedSourceMapDebugInfo());
} catch (e) {
console.error('Failed to get detailed source map debug info:', e);
}
}
// Handle output based on CLI options
if (cliOptions.stdout) {
console.log(result);
if (!cliOptions.debug) {
console.log('✅ Successfully processed Meld file');
} else {
logger.info('Successfully wrote output to stdout');
}
} else {
// Handle output path
let outputPath = cliOptions.output;
if (!outputPath) {
// If no output path specified, use input path with .o.{format} extension pattern
const inputPath = cliOptions.input;
const inputExt = path.extname(inputPath);
const outputExt = getOutputExtension(normalizeFormat(cliOptions.format));
// Extract the base filename without extension
const basePath = inputPath.substring(0, inputPath.length - inputExt.length);
// Always append .o.{format} for default behavior
outputPath = `${basePath}.o${outputExt}`;
} else if (!outputPath.includes('.')) {
// If output path has no extension, add default extension
outputPath += getOutputExtension(normalizeFormat(cliOptions.format));
}
// In test mode with custom filesystem, we might need special handling
if (cliOptions.custom && apiOptions.fs) {
// Use the filesystem from API options if available
const fs = apiOptions.fs;
if (typeof fs.writeFile === 'function') {
// Check if file exists first
const fileExists = await fs.exists(outputPath);
if (fileExists) {
const { outputPath: confirmedPath, shouldOverwrite } = await confirmOverwrite(outputPath);
if (!shouldOverwrite) {
logger.info('Operation cancelled by user');
return;
}
// Update the output path with the confirmed path
outputPath = confirmedPath;
}
await fs.writeFile(outputPath, result);
logger.info('Successfully wrote output file using custom filesystem', { path: outputPath });
return;
}
}
// Standard file system operations
const fileExists = await fs.access(outputPath).then(() => true).catch(() => false);
if (fileExists) {
const { outputPath: confirmedPath, shouldOverwrite } = await confirmOverwrite(outputPath);
if (!shouldOverwrite) {
logger.info('Operation cancelled by user');
return;
}
// Update the output path with the confirmed path
outputPath = confirmedPath;
}
await fs.writeFile(outputPath, result);
// Show a clean success message in normal mode
if (!cliOptions.debug) {
console.log(`✅ Successfully processed Meld file and wrote output to ${outputPath}`);
} else {
logger.info('Successfully wrote output file', { path: outputPath });
}
}
} catch (error) {
// Show source map debug info on error if requested
if (cliOptions.debugSourceMaps) {
try {
const { getSourceMapDebugInfo } = require('@core/utils/sourceMapUtils.js');
console.log(chalk.cyan('\nSource map debug information (on error):'));
console.log(getSourceMapDebugInfo());
} catch (e) {
console.error('Failed to get source map debug info:', e);
}
}
// Show detailed source map debug info on error if requested
if (cliOptions.detailedSourceMaps) {
try {
const { getDetailedSourceMapDebugInfo } = require('@core/utils/sourceMapUtils.js');
console.log(chalk.cyan('\nDetailed source map debug information (on error):'));
console.log(getDetailedSourceMapDebugInfo());
} catch (e) {
console.error('Failed to get detailed source map debug info:', e);
}
}
// Convert to MeldError if needed
const meldError = error instanceof MeldError
? error
: new MeldError(error instanceof Error ? error.message : String(error), {
severity: ErrorSeverity.Fatal,
code: 'PROCESSING_ERROR'
});
// Log the error for detailed debugging
logger.error('Error processing file', {
error: meldError.message,
code: meldError.code,
severity: meldError.severity
});
// Format error message appropriately for tests vs. normal mode
if (process.env.NODE_ENV === 'test') {
console.error(`Error: ${meldError.message}`);
} else if (!cliOptions.debug) {
// For regular users, we want to show the source location if available
if (meldError.filePath && meldError.context?.sourceLocation) {
const sourceLocation = meldError.context.sourceLocation;
console.error(`Error in ${sourceLocation.filePath}:${sourceLocation.line}: ${meldError.message}`);
} else if (meldError.filePath) {
console.error(`Error in ${meldError.filePath}: ${meldError.message}`);
} else {
console.error(`Error: ${meldError.message}`);
}
}
// Rethrow for the main function to handle
throw meldError;
}
}
/**
* Process a single file
*/
async function processFile(options: CLIOptions): Promise<void> {
// Convert CLI options to API options
const apiOptions = cliToApiOptions(options);
if (options.debugContext) {
await debugContextCommand({
filePath: options.input,
variableName: options.variableName,
visualizationType: options.visualizationType || 'hierarchy',
rootStateId: options.rootStateId,
outputFormat: options.outputFormat as 'mermaid' | 'dot' | 'json' || 'mermaid',
outputFile: options.output,
includeVars: options.includeVars,
includeTimestamps: options.includeTimestamps,
includeFilePaths: options.includeFilePaths
});
return;
}
// Use the common processing function
await processFileWithOptions(options, apiOptions);
}
// Track if an error has been logged to prevent duplicate messages
let errorLogged = false;
// Keep track of error messages we've seen
const seenErrors = new Set<string>();
// Flag to bypass the error deduplication for formatted errors
let bypassDeduplication = false;
// Check if error deduplication should be completely disabled
const disableDeduplication = !!(global as any).MELD_DISABLE_ERROR_DEDUPLICATION;
// Store the original console.error
const originalConsoleError = console.error;
// Replace console.error with our custom implementation
console.error = function(...args: any[]) {
// If deduplication is completely disabled via global flag, call original directly
if (disableDeduplication) {
originalConsoleError.apply(console, args);
return;
}
// Enhanced error displays from our service should bypass deduplication
if (bypassDeduplication) {
// Call the original console.error directly
originalConsoleError.apply(console, args);
return;
}
// Convert the arguments to a string for comparison
const errorMsg = args.join(' ');
// If we've seen this error before, don't print it
if (seenErrors.has(errorMsg)) {
return;
}
// Add this error to the set of seen errors
seenErrors.add(errorMsg);
// Call the original console.error
originalConsoleError.apply(console, args);
};
/**
* Main CLI entry point
*/
export async function main(fsAdapter?: any): Promise<void> {
process.title = 'meld';
// Reset errorLogged flag for each invocation of main
errorLogged = false;
// Clear the set of seen errors
seenErrors.clear();
// Explicitly disable debug mode by default
process.env.DEBUG = '';
// Parse command-line arguments
const args = process.argv.slice(2);
// Define options outside try block so it's accessible in catch block
let options: CLIOptions = {
input: '',
};
try {
// Parse command-line arguments
options = parseArgs(args);
// Store the current CLI options for access by other functions
setCurrentCLIOptions(options);
// Handle version flag
if (options.version) {
console.log(`meld version ${version}`);
return;
}
// Handle help flag
if (options.help) {
displayHelp(args[0]);
return;
}
// Handle init command
if (options.input === 'init') {
await initCommand();
return;
}
// Handle debug-resolution command
if (options.debugResolution) {
try {
await debugResolutionCommand({
filePath: options.input,
variableName: options.variableName,
outputFormat: options.outputFormat as 'json' | 'text',
watchMode: options.watch
});
} catch (error) {
logger.error('Error running debug-resolution command', { error });
throw error;
}
return;
}
// Handle debug-context command
if (options.debugContext) {
await debugContextCommand({
filePath: options.input,
variableName: options.variableName,
visualizationType: options.visualizationType || 'hierarchy',
rootStateId: options.rootStateId,
outputFormat: options.outputFormat as 'mermaid' | 'dot' | 'json',
outputFile: options.output,
includeVars: options.includeVars !== false,
includeTimestamps: options.includeTimestamps !== false,
includeFilePaths: options.includeFilePaths !== false
});
return;
}
// Handle debug-transform command
if (options.debugTransform) {
await debugTransformCommand({
filePath: options.input,
directiveType: options.directiveType,
outputFormat: options.outputFormat as 'text' | 'json' | 'mermaid',
outputFile: options.output,
includeContent: options.includeContent
});
return;
}
// Configure logging based on options
if (options.debug) {
// Set environment variable for child processes and imported modules
process.env.DEBUG = 'true';
logger.level = 'trace';
// Set log level for all service loggers
Object.values(loggingConfig.services).forEach(serviceConfig => {
(serviceConfig as any).level = 'debug';
});
} else if (options.verbose) {
// Show info level messages for verbose, but no debug logs
logger.level = 'info';
process.env.DEBUG = ''; // Explicitly disable DEBUG
} else {
// Only show errors by default (no debug logs)
logger.level = 'error';
process.env.DEBUG = ''; // Explicitly disable DEBUG
// Set all service loggers to only show errors
Object.values(loggingConfig.services).forEach(serviceConfig => {
(serviceConfig as any).level = 'error';
});
}
// Handle testing with custom filesystem
let customApiOptions: ProcessOptions | undefined;
if (fsAdapter) {
// Mark for special handling in cliToApiOptions
options.custom = true;
// Create custom API options with the test filesystem
customApiOptions = cliToApiOptions(options);
customApiOptions.fs = fsAdapter;
}
// Watch mode handling
if (options.watch) {
await watchFiles(options);
return;
}
// Process the file with custom filesystem if provided
if (customApiOptions) {
await processFileWithOptions(options, customApiOptions);
} else {
await processFile(options);
}
} catch (error) {
// Only log if not already logged
if (!errorLogged) {
logger.error('CLI execution failed', {
error: error instanceof Error ? error.message : String(error)
});
// Create a simple error display function instead of using the service
const displayErrorWithSourceContext = async (error: MeldError) => {
if (!error.filePath) {
return chalk.red.bold(`Error: ${error.message}`);
}
// Debug the actual file path being searched
if (options.debug) {
console.error(`DEBUG: Searching for source file: ${error.filePath}`);
}
try {
// Get file path and location
let filePath = error.filePath;
let line = 1;
let column = 1;
// Extract location from error
if ((error as any).location?.start) {
line = (error as any).location.start.line;
column = (error as any).location.start.column;
} else if (error.context?.sourceLocation) {
line = error.context.sourceLocation.line;
column = error.context.sourceLocation.column;
}
// Fix for hard-coded 'examples/error-test.meld' paths in error objects
if (filePath === 'examples/error-test.meld' && options.input) {
if (options.debug) {
console.error(`DEBUG: Replacing hardcoded path with input file: ${options.input}`);
}
filePath = options.input;
}
// Verify file exists
let fileExists = false;
try {
await fs.access(filePath);
fileExists = true;
} catch (err) {
if (options.debug) {
console.error(`DEBUG: Could not access file: ${filePath}`);
}
}
if (!fileExists) {
return chalk.red.bold(`Error in ${filePath}: ${error.message}`);
}
// Read the source file
const sourceCode = await fs.readFile(filePath, 'utf8');
const lines = sourceCode.split('\n');
// Get the error line
const errorLine = lines[line - 1] || '';
// Highlight the error character
const beforeError = errorLine.substring(0, column - 1);
const errorChar = errorLine.substring(column - 1, column) || ' ';
const afterError = errorLine.substring(column);
const highlightedLine = chalk.white(beforeError) +
chalk.bgRed.white(errorChar) +
chalk.white(afterError);
// Create pointer line
const pointerLine = ' '.repeat(column - 1 + 6) + chalk.red('^');
// Get context lines (up to 2 lines before and after)
const contextLines = [];
for (let i = Math.max(1, line - 2); i < line; i++) {
contextLines.push(
chalk.dim(`${i.toString().padStart(4)} | `) +
chalk.dim(lines[i - 1] || '')
);
}
// Add error line
contextLines.push(
chalk.bold(`${line.toString().padStart(4)} | `) +
highlightedLine
);
contextLines.push(pointerLine);
// Add lines after
for (let i = line + 1; i <= Math.min(lines.length, line + 2); i++) {
contextLines.push(
chalk.dim(`${i.toString().padStart(4)} | `) +
chalk.dim(lines[i - 1] || '')
);
}
// Compose the final message
const errorType = error.constructor.name.replace('Meld', '').replace('Error', ' Error');
return [
chalk.red.bold(`${errorType}: `) + error.message,
chalk.dim(` at ${chalk.cyan(filePath)}:${chalk.yellow(line.toString())}:${chalk.yellow(column.toString())}`),
'',
...contextLines
].join('\n');
} catch (err) {
console.log("Failed to format error with source context:", err);
return chalk.red(`Error in ${error.filePath}: ${error.message}`);
}
};
// Display error to user in a clean format
if (process.env.NODE_ENV === 'test') {
// Show errors with the "Error:" prefix for test expectations
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
} else {
try {
// Add debug logging to see what kind of error we're dealing with
if (options.debug) {
// Log the error type for debugging
const errorType = error instanceof MeldError
? `MeldError (${error.constructor.name})`
: (error instanceof Error ? error.constructor.name : typeof error);
console.error('DEBUG: Error type:', errorType);
// For MeldError types, log additional properties
if (error instanceof MeldError) {
console.error('DEBUG: Error properties:');
console.error(' - message:', error.message);
console.error(' - code:', error.code);
console.error(' - severity:', error.severity);
console.error(' - filePath:', error.filePath);
// Log specialized properties based on error type
if ('directiveKind' in error) {
console.error(' - directiveKind:', (error as any).directiveKind);
}
if ('location' in error) {
console.error(' - location:', JSON.stringify((error as any).location, null, 2));
}
if ('details' in error) {
console.error(' - details:', JSON.stringify((error as any).details, null, 2));
}
// Log context for debugging
console.error(' - context:', JSON.stringify(error.context, null, 2));
if (error.stack) {
console.error(' - stack trace available');
}
} else if (error instanceof Error) {
console.error('DEBUG: Standard Error properties:');
console.error(' - message:', error.message);
console.error(' - name:', error.name);
if (error.stack) {
console.error(' - stack trace available');
}
// Add debugging for direct meld-ast errors
if ('line' in error && 'column' in error) {
console.error('DEBUG: Raw meld-ast error properties:');
console.error(' - line:', (error as any).line);
console.error(' - column:', (error as any).column);
if ('sourceFile' in error) {
console.error(' - sourceFile:', (error as any).sourceFile);
}
}
}
}
// Handle both MeldError and raw errors that might come from meld-ast
try {
// Always use input file path if available to handle hardcoded paths
if (error instanceof MeldError && options.input && error.filePath === 'examples/error-test.meld') {
// Create a clone of the error with the correct file path
const fixedPathError = new MeldError(error.message, {
code: error.code,
severity: error.severity,
filePath: options.input,
context: error.context ? { ...error.context } : {},
cause: error.cause as Error | undefined
});
// Copy special properties if they exist (like location)
for (const prop of ['location', 'details', 'directiveKind', 'originalError']) {
if (prop in error) {
(fixedPathError as any)[prop] = (error as any)[prop];
}
}
// Use this error instead
error = fixedPathError;
}
// Bypass deduplication for our enhanced display
bypassDeduplication = true;
// Clear previous errors from the seen set that might conflict
seenErrors.clear(); // Clear all seen errors to be safe
// Convert to a consistent format for deduplication
const errorKey = error instanceof Error ? `Error: ${error.message}` : `Error: ${String(error)}`;
// In a real implementation, we would import and use the ErrorDisplayService directly
// Here we need to dynamically load it to avoid circular dependencies
try {
// Dynamic import of the ErrorDisplayService and file system
const { ErrorDisplayService } = await import('@services/display/ErrorDisplayService/ErrorDisplayService.js');
const { FileSystemService } = await import('@services/fs/FileSystemService/FileSystemService.js');
const { NodeFileSystem } = await import('@services/fs/FileSystemService/NodeFileSystem.js');
const { PathOperationsService } = await import('@services/fs/FileSystemService/PathOperationsService.js');
// Create the required services
const nodeFs = fsAdapter || new NodeFileSystem();
const pathOps = new PathOperationsService();
const fsService = new FileSystemService(pathOps, nodeFs);
// Create a new instance of the ErrorDisplayService with the file system
const errorDisplayService = new ErrorDisplayService(fsService);
// Debug logging for file path issues
if (options.debug) {
console.error("DEBUG: Input file path:", options.input);
// Cast error to MeldError for type safety
const meldError = error as MeldError;
console.error("DEBUG: Error filePath property:", meldError.filePath);
if (meldError.context?.sourceLocation) {
console.error("DEBUG: Error sourceLocation.filePath:", meldError.context.sourceLocation.filePath);
}
if (meldError.context?.errorFilePath) {
console.error("DEBUG: Error context.errorFilePath:", meldError.context.errorFilePath);
}
if ((meldError as any).location?.filePath) {
console.error("DEBUG: Error location.filePath:", (meldError as any).location.filePath);
}
// Check if file exists at the input path
try {
fsService.exists(options.input).then(exists => {
console.error("DEBUG: Input file exists:", exists);
});
} catch (err) {
console.error("DEBUG: Error checking if file exists:", err);
}
}
// Fix file path issues - if filePath is missing, create a new error with the correct path
// Cast error to MeldError for type safety
let meldError = error as MeldError;
if (!meldError.filePath && options.input) {
// Start with a clean context object
const newContext: Record<string, any> = {};
// Copy the original context if available
if (meldError.context) {
Object.assign(newContext, meldError.context);
}
// Set sourceLocation with the proper file path
if (meldError.context?.sourceLocation) {
newContext.sourceLocation = {
...meldError.context.sourceLocation,
filePath: options.input
};
}
// Create a new error with the correct file path
const updatedError = new MeldError(meldError.message, {
filePath: options.input,
code: meldError.code,
severity: meldError.severity,
cause: meldError instanceof Error ? meldError : undefined,
context: newContext
});
// For any other properties, try to copy them
for (const prop of ['location', 'details', 'directiveKind', 'originalError']) {
if (prop in meldError) {
(updatedError as any)[prop] = (meldError as any)[prop];
}
}
// Use this updated error instead
meldError = updatedError;
error = updatedError;
}
// Use the enhanced error display service which now handles nested errors correctly
const enhancedErrorDisplay = await errorDisplayService.enhanceErrorDisplay(error);
// Check if we've seen this error before
if (!seenErrors.has(errorKey)) {
// This is a new error, add it to our set
seenErrors.add(errorKey);
// The service will now display just the filepath and source context
console.error(enhancedErrorDisplay);
}
} catch (importError) {
// If dynamic import fails, fall back to our simple display function
if (options.debug) {
console.error('DEBUG: Failed to load ErrorDisplayService:', importError);
}
// Use our custom error display function as fallback
const enhancedErrorDisplay = await displayErrorWithSourceContext(error instanceof MeldError ? error : new MeldError(
error instanceof Error ? error.message : String(error),
{
filePath: options.input,
cause: error instanceof Error ? error : undefined,
context: {
// Copy line/column from meld-ast errors if available
sourceLocation: (typeof error === 'object' && error !== null && 'line' in error && 'column' in error) ? {
filePath: (typeof error === 'object' && error !== null && 'sourceFile' in error && typeof (error as any).sourceFile === 'string')
? (error as any).sourceFile
: options.input,
line: (error as any).line,
column: (error as any).column
} : undefined
}
}
));
// Check if we've seen this error before
if (!seenErrors.has(errorKey)) {
// This is a new error, add it to our set
seenErrors.add(errorKey);
// Display the enhanced error with a blank line for separation
console.log('\n');
console.error(enhancedErrorDisplay);
}
}
// Reset the bypass flag
bypassDeduplication = false;
} catch (displayError) {
// If the enhanced display fails, fall back to basic formatting
if (error instanceof MeldError) {
const errorMsg = `\nError in ${error.filePath || 'unknown'}: ${error.message}`;
// Check if we've seen this error before
if (!seenErrors.has(errorMsg.trim())) {
seenErrors.add(errorMsg.trim());
console.error(errorMsg);
}
} else if (error instanceof Error) {
// Check for meld-ast error properties
if ('line' in error && 'column' in error) {
const filePath = ('sourceFile' in error) ? (error as any).sourceFile : options.input;
const errorMsg = `\nError in ${filePath}:${(error as any).line}:${(error as any).column}: ${error.message}`;
// Check if we've seen this error before
if (!seenErrors.has(errorMsg.trim())) {
seenErrors.add(errorMsg.trim());
console.error(errorMsg);
}
} else {
const errorMsg = `\nError: ${error.message}`;
// Check if we've seen this error before
if (!seenErrors.has(errorMsg.trim())) {
seenErrors.add(errorMsg.trim());
console.error(errorMsg);
}
}
} else {
const errorMsg = `\nError: ${String(error)}`;
// Check if we've seen this error before
if (!seenErrors.has(errorMsg.trim())) {
seenErrors.add(errorMsg.trim());
console.error(errorMsg);
}
}
if (options.debug) {
console.error(`\nDebug: Display error: ${displayError instanceof Error ? displayError.message : String(displayError)}`);
}
}
} catch (displayError) {
// Fallback if enhanced display fails
logger.error('Error display failed', {
error: displayError instanceof Error ? displayError.message : String(displayError)
});
// Add more debugging for display errors
if (options.debug) {
console.error('DEBUG: Error display failure:', displayError);
}
// Display a basic error message as fallback
if (error instanceof MeldError) {
if (error.filePath) {
console.error(`Error in ${error.filePath}: ${error.message}`);
} else {
console.error(`Error: ${error.message}`);
}
} else if (error instanceof Error && 'line' in error && 'column' in error) {
// Handle raw meld-ast errors
const filePath = ('sourceFile' in error) ? (error as any).sourceFile : options.input;
console.error(`Error in ${filePath}:${(error as any).line}:${(error as any).column}: ${error.message}`);
} else {
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
// Mark as logged to prevent duplicate logging
errorLogged = true;
// Add a property to the error object to indicate it's been logged
// This helps bin/meld.ts avoid duplicate logging
if (error && typeof error === 'object') {
(error as any).__logged = true;
}
}
// Exit with error code for non-test environments
if (process.env.NODE_ENV !== 'test') {
process.exit(1);
} else {
// For tests, rethrow to be caught by the test runner
throw error;
}
}
}
// Only call main if this file is being run directly (not imported)
if (require.main === module) {
main().catch(err => {
// Don't log the error again since it's already logged in the main function
process.exit(1);
});
}