its-compiler-js
Version:
JavaScript/TypeScript implementation of the Instruction Template Specification (ITS) compiler
287 lines • 11.8 kB
JavaScript
/**
* Command-line interface for ITS Compiler
*/
import { promises as fs } from 'fs';
import { Command } from 'commander';
import chalk from 'chalk';
import chokidar from 'chokidar';
import { ITSCompiler } from './compiler.js';
import { DEFAULT_SECURITY_CONFIG } from './security.js';
import { ITSError, ITSValidationError, ITSCompilationError, ITSSecurityError } from './types.js';
const program = new Command();
async function loadVariables(variablesPath) {
try {
const content = await fs.readFile(variablesPath, 'utf-8');
const variables = JSON.parse(content);
if (typeof variables !== 'object' || variables === null) {
throw new Error('Variables file must contain a JSON object');
}
return variables;
}
catch (error) {
if (error instanceof SyntaxError) {
throw new Error(`Invalid JSON in variables file: ${error.message}`);
}
throw new Error(`Failed to load variables file: ${error}`);
}
}
function createSecurityConfig(options) {
const config = {
...DEFAULT_SECURITY_CONFIG,
};
// Override specific settings based on CLI options
if (options.allowHttp) {
config.allowHttp = true;
}
if (options.timeout) {
config.requestTimeout = parseInt(options.timeout, 10) * 1000;
}
if (options.strict) {
// Stricter limits in strict mode
config.maxTemplateSize = 512 * 1024; // 512KB
config.maxContentElements = 500;
config.maxNestingDepth = 8;
}
if (options.maxTemplateSize) {
const sizeInKB = parseInt(options.maxTemplateSize, 10);
config.maxTemplateSize = sizeInKB * 1024;
}
if (options.maxContentElements) {
config.maxContentElements = parseInt(options.maxContentElements, 10);
}
return config;
}
async function compileTemplate(templatePath, options) {
try {
// Load variables if provided
let variables = {};
if (options.variables) {
try {
variables = await loadVariables(options.variables);
if (options.verbose) {
console.log(chalk.blue(`Loaded ${Object.keys(variables).length} variables from ${options.variables}`));
}
}
catch (error) {
console.error(chalk.red(`Error loading variables: ${error}`));
return false;
}
}
// Create security configuration
const securityConfig = createSecurityConfig(options);
// Create compiler
const compiler = new ITSCompiler(securityConfig);
if (options.verbose) {
console.log(chalk.blue('Security Configuration:'));
console.log(` HTTP allowed: ${securityConfig.allowHttp}`);
console.log(` Block localhost: ${securityConfig.blockLocalhost}`);
console.log(` Max template size: ${Math.round(securityConfig.maxTemplateSize / 1024)}KB`);
console.log(` Max content elements: ${securityConfig.maxContentElements}`);
console.log(` Request timeout: ${securityConfig.requestTimeout}ms`);
}
const startTime = Date.now();
if (options.validateOnly) {
// Validation only
const template = JSON.parse(await fs.readFile(templatePath, 'utf-8'));
const result = await compiler.validate(template);
if (result.isValid) {
console.log(chalk.green('✓ Template is valid'));
if (result.warnings.length > 0 && options.verbose) {
result.warnings.forEach((warning) => {
console.log(chalk.yellow(`⚠ Warning: ${warning}`));
});
}
return true;
}
else {
console.log(chalk.red('✗ Template validation failed'));
result.errors.forEach((error) => {
console.log(chalk.red(`Error: ${error}`));
});
result.securityIssues.forEach((issue) => {
console.log(chalk.red(`Security: ${issue}`));
});
return false;
}
}
else {
// Full compilation
const result = await compiler.compileFile(templatePath, variables);
const compilationTime = Date.now() - startTime;
if (process.stdout.isTTY) {
console.log(chalk.green(`✓ Template compiled successfully (${compilationTime}ms)`));
}
// Show warnings and overrides if verbose
if (options.verbose) {
if (result.overrides.length > 0) {
console.log(chalk.yellow('Type Overrides:'));
result.overrides.forEach((override) => {
console.log(` ${override.typeName}: ${override.overrideSource} -> ${override.overriddenSource}`);
});
}
if (result.warnings.length > 0) {
console.log(chalk.yellow('Warnings:'));
result.warnings.forEach((warning) => {
console.log(` ${warning}`);
});
}
}
// Output result
if (options.output) {
try {
await fs.writeFile(options.output, result.prompt, 'utf-8');
console.log(chalk.blue(`Output written to: ${options.output}`));
}
catch (error) {
console.error(chalk.red(`Failed to write output file: ${error}`));
return false;
}
}
else {
// Only show delimiters when outputting to a terminal (TTY)
// When redirected or piped, process.stdout.isTTY will be false
if (process.stdout.isTTY) {
console.log('\n' + '='.repeat(80));
console.log(result.prompt);
console.log('='.repeat(80));
}
else {
// Direct output without delimiters for redirection/piping
console.log(result.prompt);
}
}
return true;
}
}
catch (error) {
if (error instanceof ITSSecurityError) {
console.error(chalk.red(`Security Error: ${error.message}`));
if (options.verbose && error.threatType) {
console.error(chalk.red(`Threat Type: ${error.threatType}`));
}
}
else if (error instanceof ITSValidationError) {
console.error(chalk.red(`Validation Error: ${error.message}`));
if (error.path) {
console.error(chalk.red(`Path: ${error.path}`));
}
error.validationErrors.forEach((err) => {
console.error(chalk.red(` • ${err}`));
});
error.securityIssues.forEach((issue) => {
console.error(chalk.red(` • Security: ${issue}`));
});
}
else if (error instanceof ITSCompilationError) {
console.error(chalk.red(`Compilation Error: ${error.message}`));
if (error.elementId) {
console.error(chalk.red(`Element ID: ${error.elementId}`));
}
}
else if (error instanceof ITSError) {
console.error(chalk.red(`ITS Error: ${error.message}`));
if (options.verbose && error.details) {
console.error(chalk.red(`Details: ${JSON.stringify(error.details, null, 2)}`));
}
}
else {
console.error(chalk.red(`Unexpected error: ${error}`));
if (options.verbose && error instanceof Error) {
console.error(chalk.red(error.stack || ''));
}
}
return false;
}
}
async function watchMode(templatePath, options) {
console.log(chalk.blue(`\nWatching ${templatePath} for changes... (Press Ctrl+C to stop)`));
const watcher = chokidar.watch(templatePath);
watcher.on('change', async () => {
console.log(chalk.yellow(`\nFile changed: ${templatePath}`));
try {
const success = await compileTemplate(templatePath, options);
if (success) {
console.log(chalk.green('✓ Watch compilation successful'));
}
else {
console.log(chalk.blue('⏳ Waiting for fixes... (Ctrl+C to stop)'));
}
}
catch (error) {
console.error(chalk.red(`Watch compilation failed: ${error}`));
console.log(chalk.blue('⏳ Continuing to watch for changes...'));
}
});
watcher.on('error', error => {
console.error(chalk.red(`Watch error: ${error}`));
});
// Keep the process running
process.on('SIGINT', () => {
console.log(chalk.yellow('\n🛑 Stopping watch mode...'));
watcher.close();
process.exit(0);
});
}
// CLI setup
program
.name('its-compile-js')
.description('ITS Compiler - Convert ITS templates to AI prompts')
.version('1.0.0')
.argument('<template-file>', 'Path to the ITS template JSON file')
.option('-o, --output <file>', 'Output file (default: stdout)')
.option('-v, --variables <file>', 'JSON file with variable values')
.option('-w, --watch', 'Watch template file for changes')
.option('--validate-only', 'Validate template without compiling')
.option('--verbose', 'Show detailed output')
.option('--strict', 'Enable strict validation mode (smaller limits)')
.option('--allow-http', 'Allow HTTP URLs (not recommended for production)')
.option('--timeout <seconds>', 'Network timeout in seconds', '10')
.option('--max-template-size <kb>', 'Maximum template size in KB')
.option('--max-content-elements <number>', 'Maximum number of content elements')
.action(async (templateFile, options) => {
try {
// Check if template file exists
await fs.access(templateFile);
}
catch {
console.error(chalk.red(`Template file not found: ${templateFile}`));
process.exit(1);
}
if (options.watch && options.validateOnly) {
console.error(chalk.red('Cannot use --watch with --validate-only'));
process.exit(1);
}
// Validate numeric options
if (options.timeout && (isNaN(parseInt(options.timeout)) || parseInt(options.timeout) <= 0)) {
console.error(chalk.red('Timeout must be a positive number'));
process.exit(1);
}
if (options.maxTemplateSize &&
(isNaN(parseInt(options.maxTemplateSize)) || parseInt(options.maxTemplateSize) <= 0)) {
console.error(chalk.red('Max template size must be a positive number'));
process.exit(1);
}
if (options.maxContentElements &&
(isNaN(parseInt(options.maxContentElements)) || parseInt(options.maxContentElements) <= 0)) {
console.error(chalk.red('Max content elements must be a positive number'));
process.exit(1);
}
// Initial compilation
const success = await compileTemplate(templateFile, options);
if (!success && !options.watch) {
process.exit(1);
}
// Watch mode
if (options.watch && success) {
await watchMode(templateFile, options);
}
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.error(chalk.red('Unhandled Rejection at:'), promise, chalk.red('reason:'), reason);
process.exit(1);
});
// Parse arguments
program.parse();
//# sourceMappingURL=cli.js.map