UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

260 lines 8.08 kB
/** * Command generation and deployment */ import { mkdir, writeFile, readFile, access } from 'fs/promises'; import { join, dirname } from 'path'; import { constants } from 'fs'; import { generateCommandContent } from './templates.js'; import { getCommandsDirectory, getFileExtension } from '../platform-paths.js'; /** * Validate command name (kebab-case) */ function validateCommandName(name) { if (!name || name.trim() === '') { return 'Command name cannot be empty'; } if (!/^[a-z][a-z0-9-]*$/.test(name)) { return 'Command name must be kebab-case (lowercase letters, numbers, hyphens only, must start with letter)'; } if (name.startsWith('-') || name.endsWith('-')) { return 'Command name cannot start or end with a hyphen'; } if (name.includes('--')) { return 'Command name cannot contain consecutive hyphens'; } return null; } /** * Validate command options */ export function validateCommand(command) { const issues = []; // Validate name const nameError = validateCommandName(command.name); if (nameError) { issues.push({ type: 'error', field: 'name', message: nameError, suggestion: 'Use lowercase letters, numbers, and hyphens only (e.g., my-command)', }); } // Validate content if (!command.content || command.content.trim() === '') { issues.push({ type: 'error', field: 'content', message: 'Command content cannot be empty', }); } // Validate platform const validPlatforms = ['claude', 'codex', 'copilot', 'cursor', 'factory', 'generic', 'windsurf']; if (!validPlatforms.includes(command.platform)) { issues.push({ type: 'error', field: 'platform', message: `Invalid platform: ${command.platform}`, suggestion: `Must be one of: ${validPlatforms.join(', ')}`, }); } // Warn if content doesn't include standard options if (!command.content.includes('--interactive') && !command.content.includes('--guidance')) { issues.push({ type: 'warning', field: 'content', message: 'Command should support --interactive and --guidance options', suggestion: 'Add these standard options to command arguments', }); } return { valid: issues.filter(i => i.type === 'error').length === 0, issues, command, }; } /** * Transform command content for Cursor platform (JSON format) */ function transformForCursor(name, description, options) { const spec = { name, description, arguments: (options.args || []).map(arg => ({ name: arg.name, description: arg.description, required: arg.required, type: 'string', default: arg.default, })), options: [ ...(options.options || []).map(opt => ({ name: opt.name, description: opt.description, type: opt.type, default: opt.default, })), { name: 'interactive', description: 'Enable interactive mode with guided questions', type: 'boolean', default: false, }, { name: 'guidance', description: 'Provide strategic guidance for execution', type: 'string', }, ], execution: { steps: [ 'Validate inputs', 'Process request', 'Generate output', 'Report status', ], }, }; return JSON.stringify(spec, null, 2); } /** * Generate a command from options */ export async function generateCommand(options) { // Validate command name const nameError = validateCommandName(options.name); if (nameError) { throw new Error(nameError); } // Default template const template = options.template || 'utility'; // Determine target directory const targetDir = options.targetDir || getCommandsDirectory(options.platform, options.projectPath); // Get file extension const ext = getFileExtension(options.platform); // Build file path const filePath = join(targetDir, `${options.name}${ext}`); // Generate content based on platform let content; if (options.platform === 'cursor') { // Cursor uses JSON format content = transformForCursor(options.name, options.description, options); } else { // Other platforms use markdown content = generateCommandContent(template, options.name, options.description, options.args, options.options, options.guidance); } return { name: options.name, path: filePath, content, platform: options.platform, template, }; } /** * Check if file exists */ async function fileExists(path) { try { await access(path, constants.F_OK); return true; } catch { return false; } } /** * Backup existing file */ async function backupFile(path) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); const backupPath = `${path}.backup-${timestamp}`; const content = await readFile(path, 'utf-8'); await writeFile(backupPath, content, 'utf-8'); return backupPath; } /** * Deploy a generated command to the filesystem */ export async function deployCommand(command, options = {}) { try { // Validate command first const validation = validateCommand(command); if (!validation.valid) { const errors = validation.issues.filter(i => i.type === 'error').map(i => i.message); return { command, success: false, error: `Validation failed: ${errors.join('; ')}`, }; } // Check if file exists const exists = await fileExists(command.path); // Handle existing file if (exists && !options.force) { return { command, success: false, error: `File already exists: ${command.path}. Use force option to overwrite.`, }; } // Backup if requested and file exists let backed_up = false; if (exists && options.backup) { await backupFile(command.path); backed_up = true; } // Ensure directory exists const dir = dirname(command.path); await mkdir(dir, { recursive: true }); // Write command file await writeFile(command.path, command.content, 'utf-8'); return { command, success: true, backed_up, }; } catch (error) { return { command, success: false, error: error instanceof Error ? error.message : String(error), }; } } /** * Generate and deploy a command in one step */ export async function generateAndDeploy(options, deployOptions = {}) { const command = await generateCommand(options); // If dry run, don't deploy if (options.dryRun) { return { command, success: true, error: undefined, }; } return deployCommand(command, deployOptions); } /** * List existing commands in a project */ export async function listCommands(platform, projectPath) { const commandsDir = getCommandsDirectory(platform, projectPath); const ext = getFileExtension(platform); try { const { readdir } = await import('fs/promises'); const files = await readdir(commandsDir); return files .filter(f => f.endsWith(ext)) .map(f => f.replace(ext, '')) .sort(); } catch { // Directory doesn't exist or is empty return []; } } //# sourceMappingURL=generator.js.map