UNPKG

voyageai-cli

Version:

CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search

241 lines (215 loc) 8.09 kB
'use strict'; const fs = require('fs'); const path = require('path'); const { loadProject } = require('../lib/project'); const { renderTemplate, buildContext, listTemplates } = require('../lib/codegen'); const { PROJECT_STRUCTURE } = require('../lib/scaffold-structure'); const ui = require('../lib/ui'); /** * Create a directory if it doesn't exist. */ function ensureDir(dirPath) { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } /** * Write a file, creating parent directories as needed. */ function writeFile(filePath, content) { ensureDir(path.dirname(filePath)); fs.writeFileSync(filePath, content, 'utf8'); } /** * Check if a directory exists and is not empty. */ function directoryExists(dirPath) { if (!fs.existsSync(dirPath)) return false; const files = fs.readdirSync(dirPath); return files.length > 0; } /** * Register the scaffold command. * @param {import('commander').Command} program */ function registerScaffold(program) { program .command('scaffold <name>') .description('Create a complete starter project') .option('-t, --target <target>', 'Target framework: vanilla, nextjs, python', 'vanilla') .option('-m, --model <model>', 'Override embedding model') .option('--db <database>', 'Override database name') .option('--collection <name>', 'Override collection name') .option('--field <name>', 'Override embedding field name') .option('--index <name>', 'Override vector index name') .option('-d, --dimensions <n>', 'Override dimensions', parseInt) .option('--no-rerank', 'Omit reranking from generated code') .option('--rerank-model <model>', 'Rerank model to use') .option('--force', 'Overwrite existing directory') .option('--json', 'Output file manifest as JSON (no file creation)') .option('--dry-run', 'Show what would be created without writing') .option('-q, --quiet', 'Suppress non-essential output') .action(async (name, opts) => { const telemetry = require('../lib/telemetry'); try { telemetry.send('cli_scaffold', { template: opts.target }); const target = opts.target; const projectDir = path.resolve(process.cwd(), name); // Validate target if (!PROJECT_STRUCTURE[target]) { console.error(ui.error(`Invalid target: ${target}`)); console.error(ui.dim(` Valid targets: ${Object.keys(PROJECT_STRUCTURE).join(', ')}`)); process.exit(1); } const structure = PROJECT_STRUCTURE[target]; // Check if directory exists if (directoryExists(projectDir) && !opts.force && !opts.dryRun && !opts.json) { console.error(ui.error(`Directory already exists: ${name}`)); console.error(ui.dim(' Use --force to overwrite.')); process.exit(1); } // Load project config let project = {}; try { project = loadProject(); } catch (e) { // No .vai.json, use defaults if (!opts.quiet && !opts.json) { console.error(ui.warn('No .vai.json found, using defaults')); } } // Build context with overrides const context = buildContext(project, { model: opts.model, db: opts.db, collection: opts.collection, field: opts.field, index: opts.index, dimensions: opts.dimensions, rerank: opts.rerank, rerankModel: opts.rerankModel, projectName: name, }); // Build file manifest const manifest = []; // Render template files for (const file of structure.files) { const content = renderTemplate(target, file.template, context); manifest.push({ path: file.output, fullPath: path.join(projectDir, file.output), source: `${target}/${file.template}`, size: content.length, content, }); } // Add extra static files if (structure.extraFiles) { for (const file of structure.extraFiles) { const content = typeof file.content === 'function' ? file.content(context) : file.content; manifest.push({ path: file.output, fullPath: path.join(projectDir, file.output), source: 'static', size: content.length, content, }); } } // Add binary files (copied as-is from templates dir) if (structure.binaryFiles) { for (const file of structure.binaryFiles) { const srcPath = path.join(__dirname, '..', 'lib', 'templates', target, file.source); const destPath = path.join(projectDir, file.output); const binarySize = fs.existsSync(srcPath) ? fs.statSync(srcPath).size : 0; manifest.push({ path: file.output, fullPath: destPath, source: `${target}/${file.source}`, size: binarySize, binary: true, binarySrc: srcPath, }); } } // JSON output mode if (opts.json) { const output = { name, target, directory: projectDir, files: manifest.map(f => ({ path: f.path, size: f.size, source: f.source, })), config: context, }; console.log(JSON.stringify(output, null, 2)); return; } // Dry run mode if (opts.dryRun) { console.log(''); console.log(ui.bold(`Would create: ${name}/ (${structure.description})`)); console.log(''); for (const file of manifest) { console.log(` ${ui.cyan('+')} ${file.path} ${ui.dim(`(${file.size} bytes)`)}`); } console.log(''); console.log(ui.dim(`Total: ${manifest.length} files`)); return; } // Create project directory if (!opts.quiet) { console.log(''); console.log(ui.bold(`Creating ${name}/ (${structure.description})`)); console.log(''); } ensureDir(projectDir); // Write all files for (const file of manifest) { if (file.binary) { ensureDir(path.dirname(file.fullPath)); fs.copyFileSync(file.binarySrc, file.fullPath); } else { writeFile(file.fullPath, file.content); } if (!opts.quiet) { console.log(` ${ui.cyan('✓')} ${file.path}`); } } // Success message with next steps if (!opts.quiet) { console.log(''); console.log(ui.success(`Created ${manifest.length} files in ${name}/`)); console.log(''); // Use clack's note for next steps const steps = [ `cd ${name}`, `cp .env.example .env`, `# Edit .env with your API keys`, structure.postInstall, structure.startCommand, ]; require('@clack/prompts').note(steps.join('\n'), 'Next steps'); console.log(''); console.log(ui.dim('Configuration:')); console.log(ui.dim(` Model: ${context.model}`)); console.log(ui.dim(` Database: ${context.db}.${context.collection}`)); console.log(ui.dim(` Dimensions: ${context.dimensions}`)); if (context.rerank) { console.log(ui.dim(` Reranking: ${context.rerankModel}`)); } console.log(''); } } catch (err) { console.error(ui.error(err.message)); if (process.env.DEBUG) console.error(err.stack); process.exit(1); } }); } module.exports = { registerScaffold, PROJECT_STRUCTURE };