All files / lib/commands sync-yaml.js

67.03% Statements 61/91
69.23% Branches 36/52
85.71% Functions 6/7
65.9% Lines 58/88

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206                                1x                             10x 10x     10x                 10x   9x     81x               11x   11x   11x   11x   11x 10x 10x     11x   10x 9x       10x 10x       10x 25x 9x       10x 28x 25x     28x 28x         28x 28x 28x     27x 27x   27x 3x 3x   3x       24x 21x   24x 3x       27x 3x   24x     1x         10x 10x 1x   9x     10x 10x 10x 10x 10x 10x 10x     1x 1x       1x                                                                                                    
/**
 * Sync V2 Command
 *
 * Syncs rosetta.yaml to all IDE configuration files
 * This is the new YAML-first architecture implementation
 */
 
import chalk from 'chalk';
import path from 'path';
import { ensureDir, pathExists } from 'fs-extra';
import { writeFile } from 'fs/promises';
import { TARGETS } from '../constants.js';
import { ClaudeGenerator, CursorGenerator, WindsurfGenerator } from '../generators/index.js';
import { findRosettaYAML, parseYAMLFile } from '../parsers/yaml-parser.js';
import { TreeLogger } from '../utils.js';
 
const GENERATORS = {
  'claude': ClaudeGenerator,
  'cursor': CursorGenerator,
  'windsurf': WindsurfGenerator,
  // Future: 'codex': CodexGenerator,
  // Future: 'copilot': CopilotGenerator,
  // Future: 'kilo': KiloGenerator,
  // Future: 'continue': ContinueGenerator
};
 
/**
 * Get default YAML path (search from current directory)
 * @returns {Promise<string>} Path to rosetta.yaml
 */
async function getDefaultYAMLPath() {
  const yamlPath = await findRosettaYAML(process.cwd(), 5);
  Iif (!yamlPath) {
    throw new Error('No rosetta.yaml found in current or parent directories. Run "rosetta init" to create one.');
  }
  return yamlPath;
}
 
/**
 * Get target IDEs based on selection
 * @param {Array<string>|null} selectedIdes - Selected IDE labels or null for all
 * @returns {Array<Object>} Target configurations
 */
function getTargets(selectedIdes = null) {
  if (selectedIdes) {
    // Match by generator key (claude, cursor, etc.) provided via --ides
    return TARGETS.filter(t => t.generator && selectedIdes.includes(t.generator));
  }
  // Return all targets that have a generator defined
  return TARGETS.filter(t => t.generator && GENERATORS[t.generator]);
}
 
/**
 * Main sync function
 * @param {Object} options - Sync options
 */
export async function syncYAMLCommand(options = {}) {
  const { ides = null, from = null, dryRun = false, verbose = false } = options;
 
  const logger = dryRun ? null : new TreeLogger('Sync YAML');
 
  try {
    // 1. Find and parse rosetta.yaml
    const yamlPath = from || await getDefaultYAMLPath();
 
    if (logger) {
      logger.logStep(`Found rosetta.yaml at: ${yamlPath}`);
      logger.logStep('Parsing rosetta.yaml...');
    }
 
    const ast = await parseYAMLFile(yamlPath);
 
    if (logger) {
      logger.logStep(`Parsed successfully: ${ast.getProjectName()} (${ast.getProjectType()})`);
    }
 
    // 2. Get target IDEs
    const targets = getTargets(ides);
    Iif (targets.length === 0) {
      throw new Error('No supported IDE targets found. Supported IDEs: ' + Object.keys(GENERATORS).join(', '));
    }
 
    if (logger) {
      logger.logStep(`Target IDEs: ${targets.map(t => t.label).join(', ')}`);
      console.log('');
    }
 
    // 3. Generate IDE configurations
    for (const target of targets) {
      if (logger) {
        console.log(chalk.blue(`Generating ${target.label} → ${target.path}`));
      }
 
      const GeneratorClass = GENERATORS[target.generator];
      Iif (!GeneratorClass) {
        console.log(chalk.yellow(`  ⚠ No generator available for ${target.label} (${target.generator}), skipping...`));
        continue;
      }
 
      try {
        const generator = new GeneratorClass();
        const content = generator.generate(ast);
 
        // Write file
        const targetDir = path.dirname(target.path);
        await ensureDir(targetDir);
 
        if (await pathExists(target.path)) {
          Eif (!dryRun) {
            await writeFile(target.path, content, 'utf8');
          }
          Iif (verbose) {
            console.log(chalk.gray('  Updated existing file'));
          }
        } else {
          if (!dryRun) {
            await writeFile(target.path, content, 'utf8');
          }
          if (verbose) {
            console.log(chalk.gray('  Created new file'));
          }
        }
 
        if (dryRun) {
          console.log(chalk.yellow(`  [Dry Run] Would write to: ${target.path}`));
        } else {
          console.log(chalk.green(`  ✓ Wrote ${target.path}`));
        }
      } catch (error) {
        console.log(chalk.red(`  ✗ Failed to generate ${target.label}: ${error.message}`));
      }
    }
 
    // 4. Summary
    console.log('');
    if (dryRun) {
      console.log(chalk.yellow('Dry run complete. No files were modified.'));
    } else {
      console.log(chalk.green('✓ rosetta.yaml sync complete!'));
    }
 
    console.log('');
    console.log(`Source: ${yamlPath}`);
    console.log(`Targets: ${targets.length} IDE configuration(s)`);
    console.log('');
    console.log('Next steps:');
    console.log('  - rosetta translate <file> --to yaml   # Convert existing configs');
    console.log('  - rosetta validate-config            # Validate your rosetta.yaml');
 
  } catch (error) {
    console.log(chalk.red(`Sync failed: ${error.message}`));
    Iif (verbose) {
      console.error(error);
    }
    // Throw error to allow callers (including tests) to handle
    throw new Error(`Sync failed: ${error.message}`);
  }
}
 
/**
 * Validate command
 * Validates a rosetta.yaml file
 */
export async function validateConfigCommand(options = {}) {
  const { file = null } = options;
 
  try {
    const yamlPath = file || await getDefaultYAMLPath();
    console.log(chalk.blue(`Validating: ${yamlPath}`));
 
    const { parseYAMLContent, validateYAMLFile } = await import('../parsers/yaml-parser.js');
 
    // Try parsing and validation together
    try {
      const ast = await parseYAMLFile(yamlPath);
      console.log(chalk.green('✓ rosetta.yaml is valid'));
      console.log(`  Project: ${ast.getProjectName()}`);
      console.log(`  Type: ${ast.getProjectType()}`);
      console.log(`  Language: ${ast.getLanguage()}`);
      console.log(`  Conventions: ${ast.getConventions().length}`);
      console.log(`  Agents: ${ast.getAgents().length}`);
      console.log(`  Notes: ${ast.getNotes().length}`);
      process.exit(0);
    } catch (error) {
      const validation = await validateYAMLFile(yamlPath);
      if (!validation.valid) {
        console.log(chalk.red('Validation failed:'));
        for (const err of validation.errors) {
          console.log(chalk.red(`  - ${err.path}: ${err.message}`));
        }
      } else {
        console.log(chalk.red(`Parse error: ${error.message}`));
      }
      process.exit(1);
    }
  } catch (error) {
    console.log(chalk.red(`Error: ${error.message}`));
    process.exit(1);
  }
}
 
export default {
  syncYAMLCommand,
  validateConfigCommand
};