All files / lib ide-selection.js

24.7% Statements 21/85
23.61% Branches 17/72
35% Functions 7/20
22.5% Lines 18/80

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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285                                                                                                                                                                                                14x   5x 9x     4x       4x 4x                                                                                                                                             4x 2x     10x   2x 1x   9x       1x             1x                                                                                             3x 1x           2x 2x                                                              
import inquirer from 'inquirer';
import chalk from 'chalk';
import path from 'path';
import os from 'os';
import fs from 'fs-extra';
import { detectIdes } from './context.js';
import { TARGETS } from './constants.js';
 
/**
 * Prompts user to select their primary IDE.
 * Auto-detects configured IDEs as defaults and allows manual override.
 *
 * @param {Object} options - Selection options
 * @param {string[]} options.preferredIdes - Preferred IDE labels (auto-detected)
 * @param {boolean} options.allowMultiIde - Allow multi-IDE selection
 * @param {boolean} options.silent - Skip prompt and return first detected IDE
 * @returns {Promise<string|null>} - Selected IDE label or null for multi-IDE
 */
export async function selectPrimaryIde(options = {}) {
  const {
    preferredIdes = [],
    allowMultiIde = true,
    silent = false
  } = options;
 
  // Detect configured IDEs in the project
  const detectedIdes = await detectIdes();
  const detectedLabels = detectedIdes.map(ide => ide.name);
 
  // Use preferred IDEs if provided, otherwise use detected
  const defaultIdes = preferredIdes.length > 0 ? preferredIdes : detectedLabels;
 
  // Silent mode: return first detected IDE or null
  if (silent) {
    return defaultIdees.length > 0 ? defaultIdees[0] : null;
  }
 
  // Build choices for the prompt
  const choices = [];
 
  // Detected IDEs section
  if (detectedLabels.length > 0) {
    choices.push(new inquirer.Separator('--- Detected IDEs ---'));
    detectedLabels.forEach((label, index) => {
      const isDefault = index === 0;
      choices.push({
        name: `${label}${isDefault ? ' (detected)' : ''}`,
        value: label
      });
    });
  }
 
  // All available IDEs section
  const availableLabels = TARGETS.map(t => t.label);
  const undetectedLabels = availableLabels.filter(
    label => !detectedLabels.includes(label)
  );
 
  if (undetectedLabels.length > 0) {
    choices.push(new inquirer.Separator('--- All Available IDEs ---'));
    undetectedLabels.forEach(label => {
      choices.push({
        name: label,
        value: label
      });
    });
  }
 
  // Multi-IDE option
  if (allowMultiIde) {
    choices.push(new inquirer.Separator());
    choices.push({
      name: 'Multi-IDE support (installs to .rosetta/skills)',
      value: null
    });
  }
 
  const { ide } = await inquirer.prompt([{
    type: 'list',
    name: 'ide',
    message: 'Select your primary IDE for skill installation:',
    choices,
    default: detectedLabels[0] || null
  }]);
 
  return ide;
}
 
/**
 * Gets the skills directory path for a specific IDE.
 *
 * @param {string} ideLabel - The IDE label (e.g., "Claude Code", "Cursor")
 * @param {boolean} isGlobal - Whether to use global path (~/.ide/skills) or project local (./ide/skills)
 * @returns {string} - Absolute path to the IDE's skills directory
 */
export function getIdeSkillsDir(ideLabel, isGlobal = false) {
  const target = TARGETS.find(t => t.label === ideLabel);
 
  if (!target) {
    throw new Error(`Unknown IDE: "${ideLabel}". Valid options: ${TARGETS.map(t => t.label).join(', ')}`);
  }
 
  Iif (!target.skillsDir) {
    throw new Error(`IDE "${ideLabel}" does not have a skills directory configured`);
  }
 
  const basePath = isGlobal ? os.homedir() : process.cwd();
  return path.join(basePath, target.skillsDir);
}
 
/**
 * Determines if multi-IDE support is needed based on user configuration.
 * Prompts user if they want to install to multiple IDEs.
 *
 * @param {Object} options - Options
 * @param {string} options.primaryIde - The selected primary IDE
 * @param {boolean} options.silent - Skip prompt
 * @returns {Promise<string[]>} - Array of IDE labels or ['multi-ide']
 */
export async function determineIdeScope(options = {}) {
  const { primaryIde, silent = false } = options;
 
  if (silent) {
    return primaryIde ? [primaryIde] : ['multi-ide'];
  }
 
  // If primary IDE is already null (multi-ide), return as-is
  if (!primaryIde) {
    return ['multi-ide'];
  }
 
  const { multiIde } = await inquirer.prompt([{
    type: 'confirm',
    name: 'multiIde',
    message: 'Install to multiple IDEs?',
    default: false
  }]);
 
  if (multiIde) {
    // Prompt to select additional IDEs
    const additionalIdes = await selectAdditionalIdes(primaryIde);
    return [primaryIde, ...additionalIdes];
  }
 
  return [primaryIde];
}
 
/**
 * Prompts user to select additional IDEs for multi-IDE installation.
 *
 * @param {string} primaryIde - The primary IDE to exclude from selection
 * @returns {Promise<string[]>} - Array of additional IDE labels
 */
async function selectAdditionalIdes(primaryIde) {
  const availableIdes = TARGETS
    .map(t => t.label)
    .filter(label => label !== primaryIde);
 
  const { additional } = await inquirer.prompt([{
    type: 'checkbox',
    name: 'additional',
    message: 'Select additional IDEs:',
    choices: availableIdes,
    validate: (answer) => {
      return true; // Empty is valid (user selected primary only)
    }
  }]);
 
  return additional;
}
 
/**
 * Validates that an IDE label is valid and has a skills directory.
 *
 * @param {string} ideLabel - The IDE label to validate
 * @returns {Object} - { valid: boolean, error: string|null }
 */
export function validateIdeLabel(ideLabel) {
  if (ideLabel === null || ideLabel === 'multi-ide') {
    return { valid: true };
  }
 
  const target = TARGETS.find(t => t.label === ideLabel);
 
  if (!target) {
    return {
      valid: false,
      error: `Unknown IDE: "${ideLabel}". Valid options: ${TARGETS.map(t => t.label).join(', ')}`
    };
  }
 
  Iif (!target.skillsDir) {
    return {
      valid: false,
      error: `IDE "${ideLabel}" does not have a skills directory configured`
    };
  }
 
  return { valid: true };
}
 
/**
 * Ensures the skills directory exists for the given IDE.
 * Creates the directory structure if it doesn't exist.
 *
 * @param {string} ideLabel - The IDE label
 * @param {boolean} isGlobal - Whether to use global path
 * @param {Object} options - Options
 * @param {boolean} options.dryRun - Skip actual directory creation
 * @returns {Promise<string>} - The path to the skills directory
 */
export async function ensureIdeSkillsDir(ideLabel, isGlobal = false, options = {}) {
  const { dryRun = false } = options;
 
  // Handle multi-IDE case
  if (ideLabel === null || ideLabel === 'multi-ide') {
    const basePath = isGlobal
      ? path.join(os.homedir(), '.rosetta', 'skills')
      : path.join(process.cwd(), '.rosetta', 'skills');
 
    if (!dryRun) {
      await fs.ensureDir(basePath);
    }
 
    return basePath;
  }
 
  const skillsDir = getIdeSkillsDir(ideLabel, isGlobal);
 
  if (!dryRun) {
    await fs.ensureDir(skillsDir);
  }
 
  return skillsDir;
}
 
/**
 * Gets the manifest file path for a given IDE and scope.
 *
 * @param {string|null} ideLabel - The IDE label or null for multi-ide
 * @param {boolean} isGlobal - Whether to use global path
 * @returns {string} - Path to the manifest file
 */
export function getIdeManifestPath(ideLabel, isGlobal = false) {
  // Multi-IDE uses .rosetta/skills/manifest.json
  if (ideLabel === null || ideLabel === 'multi-ide') {
    return isGlobal
      ? path.join(os.homedir(), '.rosetta', 'skills', 'manifest.json')
      : path.join(process.cwd(), '.rosetta', 'skills', 'manifest.json');
  }
 
  // IDE-specific manifests
  const skillsDir = getIdeSkillsDir(ideLabel, isGlobal);
  return path.join(skillsDir, 'manifest.json');
}
 
/**
 * Gets all manifest paths across installed IDEs.
 * Useful for aggregating skills from multiple IDEs.
 *
 * @param {boolean} isGlobal - Whether to use global paths
 * @returns {Promise<string[]>} - Array of manifest file paths that exist
 */
export async function getAllManifestPaths(isGlobal = false) {
  const manifestPaths = [];
 
  // Check multi-IDE manifest
  const multiIdeManifest = getIdeManifestPath(null, isGlobal);
  if (await fs.pathExists(multiIdeManifest)) {
    manifestPaths.push(multiIdeManifest);
  }
 
  // Check IDE-specific manifests
  for (const target of TARGETS) {
    if (target.skillsDir) {
      const ideManifest = getIdeManifestPath(target.label, isGlobal);
      if (await fs.pathExists(ideManifest)) {
        manifestPaths.push(ideManifest);
      }
    }
  }
 
  return manifestPaths;
}