const fs = require('fs'); const os = require('os'); const path = require('path'); const { toCursorAgentRelativePath } = require('./cursor-agent-names'); const { LEGACY_INSTALL_TARGETS, parseInstallArgs } = require('./install/request'); const { buildCopyFileOperation, createManifestInstallPlan, createStatePreview, dedupeCopyFileOperations, getManifestVersion, getPackageVersion, getRepoCommit, getSourceRoot, listFilesRecursive, readJsonObject, } = require('./install/plan'); const { SUPPORTED_INSTALL_TARGETS, listLegacyCompatibilityLanguages, resolveLegacyCompatibilitySelection } = require('./install-manifests'); const { getInstallTargetAdapter } = require('./install-targets/registry'); const { resolveInvocationEnvironment } = require('./invocation-environment'); const LANGUAGE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/; const CLAUDE_ECC_NAMESPACE = 'ecc'; function readDirectoryNames(dirPath) { if (!fs.existsSync(dirPath)) { return []; } return fs .readdirSync(dirPath, { withFileTypes: true }) .filter(entry => entry.isDirectory()) .map(entry => entry.name) .sort(); } function listAvailableLanguages(sourceRoot = getSourceRoot()) { return [...new Set([...listLegacyCompatibilityLanguages(), ...readDirectoryNames(path.join(sourceRoot, 'rules')).filter(name => name !== 'common')])].sort(); } function validateLegacyTarget(target) { if (LEGACY_INSTALL_TARGETS.includes(target)) { return; } // A target can be fully supported yet not installable via the bare-language // positional syntax (which is legacy-only). Guide the user to the right mode // instead of implying the target is unknown (#2282). if (SUPPORTED_INSTALL_TARGETS.includes(target)) { throw new Error( `Target '${target}' is supported, but the bare-language install syntax only accepts ${LEGACY_INSTALL_TARGETS.join(', ')}. ` + `Install '${target}' with a component selection instead, e.g. \`install.sh --target ${target} --profile full\` ` + `(or --modules / --skills ).` ); } throw new Error(`Unknown install target: ${target}. Expected one of ${SUPPORTED_INSTALL_TARGETS.join(', ')}`); } function applyInstallPlan(plan, dependencies = {}) { const { applyInstallPlan: applyPlan } = require('./install/apply'); return applyPlan(plan, dependencies); } function previewInstallPlan(plan) { const { previewInstallPlan: previewPlan } = require('./install/apply'); return previewPlan(plan); } function addRecursiveCopyOperations(operations, options) { const sourceDir = path.join(options.sourceRoot, options.sourceRelativeDir); if (!fs.existsSync(sourceDir)) { return 0; } const relativeFiles = listFilesRecursive(sourceDir); for (const relativeFile of relativeFiles) { const sourceRelativePath = path.join(options.sourceRelativeDir, relativeFile); const sourcePath = path.join(options.sourceRoot, sourceRelativePath); const destinationRelativePath = typeof options.destinationRelativePathTransform === 'function' ? options.destinationRelativePathTransform(relativeFile, sourceRelativePath) : relativeFile; if (!destinationRelativePath) { continue; } const destinationPath = path.join(options.destinationDir, destinationRelativePath); operations.push( buildCopyFileOperation({ moduleId: options.moduleId, sourcePath, sourceRelativePath, destinationPath, strategy: options.strategy || 'preserve-relative-path', contentTransform: options.contentTransform, }) ); } return relativeFiles.length; } function addFileCopyOperation(operations, options) { const sourcePath = path.join(options.sourceRoot, options.sourceRelativePath); if (!fs.existsSync(sourcePath)) { return false; } operations.push( buildCopyFileOperation({ moduleId: options.moduleId, sourcePath, sourceRelativePath: options.sourceRelativePath, destinationPath: options.destinationPath, strategy: options.strategy || 'preserve-relative-path' }) ); return true; } function addCursorAgentDataScaffoldOperations(operations, options) { const scaffoldRoot = path.join(options.sourceRoot, 'scaffolds', 'cursor'); if (!fs.existsSync(scaffoldRoot)) { return; } addFileCopyOperation(operations, { moduleId: options.moduleId, sourceRoot: options.sourceRoot, sourceRelativePath: path.join('scaffolds', 'cursor', 'ecc-agent-data.json'), destinationPath: path.join(options.targetRoot, 'ecc-agent-data.json'), strategy: 'preserve-relative-path' }); addFileCopyOperation(operations, { moduleId: options.moduleId, sourceRoot: options.sourceRoot, sourceRelativePath: path.join('scaffolds', 'cursor', 'rules', 'ecc-agent-data-home.mdc'), destinationPath: path.join(options.targetRoot, 'rules', 'ecc-agent-data-home.mdc'), strategy: 'preserve-relative-path' }); addJsonMergeOperation(operations, { moduleId: options.moduleId, sourceRoot: options.sourceRoot, sourceRelativePath: path.join('scaffolds', 'cursor', 'hooks.json'), destinationPath: path.join(options.targetRoot, 'hooks.json') }); const cursorSessionHookDeps = [path.join('scripts', 'hooks', 'cursor-session-env.js'), path.join('scripts', 'lib', 'agent-data-home.js'), path.join('scripts', 'lib', 'utils.js')]; for (const sourceRelativePath of cursorSessionHookDeps) { addFileCopyOperation(operations, { moduleId: options.moduleId, sourceRoot: options.sourceRoot, sourceRelativePath, destinationPath: path.join(options.targetRoot, sourceRelativePath), strategy: 'preserve-relative-path' }); } } function addJsonMergeOperation(operations, options) { const sourcePath = path.join(options.sourceRoot, options.sourceRelativePath); if (!fs.existsSync(sourcePath)) { return false; } operations.push({ kind: 'merge-json', moduleId: options.moduleId, sourceRelativePath: options.sourceRelativePath, destinationPath: options.destinationPath, strategy: 'merge-json', ownership: 'managed', scaffoldOnly: false, mergePayload: readJsonObject(sourcePath, options.sourceRelativePath) }); return true; } function addMatchingRuleOperations(operations, options) { const sourceDir = path.join(options.sourceRoot, options.sourceRelativeDir); if (!fs.existsSync(sourceDir)) { return 0; } const files = fs .readdirSync(sourceDir, { withFileTypes: true }) .filter(entry => entry.isFile() && options.matcher(entry.name)) .map(entry => entry.name) .sort(); for (const fileName of files) { const sourceRelativePath = path.join(options.sourceRelativeDir, fileName); const sourcePath = path.join(options.sourceRoot, sourceRelativePath); const destinationPath = path.join(options.destinationDir, options.rename ? options.rename(fileName) : fileName); operations.push( buildCopyFileOperation({ moduleId: options.moduleId, sourcePath, sourceRelativePath, destinationPath, strategy: options.strategy || 'flatten-copy' }) ); } return files.length; } function isDirectoryNonEmpty(dirPath) { return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory() && fs.readdirSync(dirPath).length > 0; } function planClaudeStyleLegacyInstall(context, { adapterId, adapterRootInput, rulesDir: rulesDirOverride }) { const adapter = getInstallTargetAdapter(adapterId); const targetRoot = adapter.resolveRoot(adapterRootInput); const rulesDir = rulesDirOverride || path.join(targetRoot, 'rules', CLAUDE_ECC_NAMESPACE); const installStatePath = adapter.getInstallStatePath(adapterRootInput); const operations = []; const warnings = []; if (isDirectoryNonEmpty(rulesDir)) { warnings.push(`Destination ${rulesDir}/ already exists and files may be overwritten`); } addRecursiveCopyOperations(operations, { moduleId: 'legacy-claude-rules', sourceRoot: context.sourceRoot, sourceRelativeDir: path.join('rules', 'common'), destinationDir: path.join(rulesDir, 'common') }); for (const language of context.languages) { if (!LANGUAGE_NAME_PATTERN.test(language)) { warnings.push(`Invalid language name '${language}'. Only alphanumeric, dash, and underscore are allowed`); continue; } const sourceDir = path.join(context.sourceRoot, 'rules', language); if (!fs.existsSync(sourceDir)) { warnings.push(`rules/${language}/ does not exist, skipping`); continue; } addRecursiveCopyOperations(operations, { moduleId: 'legacy-claude-rules', sourceRoot: context.sourceRoot, sourceRelativeDir: path.join('rules', language), destinationDir: path.join(rulesDir, language) }); } return { mode: 'legacy', sourceRoot: context.sourceRoot, adapter, target: adapterId, targetRoot, installRoot: rulesDir, installStatePath, operations, warnings, selectedModules: ['legacy-claude-rules'] }; } function planClaudeLegacyInstall(context) { return planClaudeStyleLegacyInstall(context, { adapterId: 'claude', adapterRootInput: { homeDir: context.homeDir }, rulesDir: context.claudeRulesDir || null }); } function planClaudeProjectLegacyInstall(context) { return planClaudeStyleLegacyInstall(context, { adapterId: 'claude-project', adapterRootInput: { repoRoot: context.projectRoot }, rulesDir: null }); } function planCursorLegacyInstall(context) { const adapter = getInstallTargetAdapter('cursor'); const targetRoot = adapter.resolveRoot({ repoRoot: context.projectRoot }); const installStatePath = adapter.getInstallStatePath({ repoRoot: context.projectRoot }); const operations = []; const warnings = []; addMatchingRuleOperations(operations, { moduleId: 'legacy-cursor-install', sourceRoot: context.sourceRoot, sourceRelativeDir: path.join('.cursor', 'rules'), destinationDir: path.join(targetRoot, 'rules'), matcher: fileName => /^common-.*\.md$/.test(fileName) }); for (const language of context.languages) { if (!LANGUAGE_NAME_PATTERN.test(language)) { warnings.push(`Invalid language name '${language}'. Only alphanumeric, dash, and underscore are allowed`); continue; } const matches = addMatchingRuleOperations(operations, { moduleId: 'legacy-cursor-install', sourceRoot: context.sourceRoot, sourceRelativeDir: path.join('.cursor', 'rules'), destinationDir: path.join(targetRoot, 'rules'), matcher: fileName => fileName.startsWith(`${language}-`) && fileName.endsWith('.md') }); if (matches === 0) { warnings.push(`No Cursor rules for '${language}' found, skipping`); } } addRecursiveCopyOperations(operations, { moduleId: 'legacy-cursor-install', sourceRoot: context.sourceRoot, sourceRelativeDir: path.join('.cursor', 'agents'), destinationDir: path.join(targetRoot, 'agents'), destinationRelativePathTransform: toCursorAgentRelativePath }); addRecursiveCopyOperations(operations, { moduleId: 'legacy-cursor-install', sourceRoot: context.sourceRoot, sourceRelativeDir: path.join('.cursor', 'skills'), destinationDir: path.join(targetRoot, 'skills') }); addRecursiveCopyOperations(operations, { moduleId: 'legacy-cursor-install', sourceRoot: context.sourceRoot, sourceRelativeDir: path.join('.cursor', 'commands'), destinationDir: path.join(targetRoot, 'commands') }); addRecursiveCopyOperations(operations, { moduleId: 'legacy-cursor-install', sourceRoot: context.sourceRoot, sourceRelativeDir: path.join('.cursor', 'hooks'), destinationDir: path.join(targetRoot, 'hooks') }); addFileCopyOperation(operations, { moduleId: 'legacy-cursor-install', sourceRoot: context.sourceRoot, sourceRelativePath: path.join('.cursor', 'hooks.json'), destinationPath: path.join(targetRoot, 'hooks.json') }); addJsonMergeOperation(operations, { moduleId: 'legacy-cursor-install', sourceRoot: context.sourceRoot, sourceRelativePath: '.mcp.json', destinationPath: path.join(targetRoot, 'mcp.json') }); addCursorAgentDataScaffoldOperations(operations, { moduleId: 'legacy-cursor-install', sourceRoot: context.sourceRoot, targetRoot }); return { mode: 'legacy', adapter, target: 'cursor', targetRoot, installRoot: targetRoot, installStatePath, operations, warnings, selectedModules: ['legacy-cursor-install'] }; } function planAntigravityLegacyInstall(context) { const adapter = getInstallTargetAdapter('antigravity'); const targetRoot = adapter.resolveRoot({ repoRoot: context.projectRoot }); const installStatePath = adapter.getInstallStatePath({ repoRoot: context.projectRoot }); const operations = []; const warnings = []; if (isDirectoryNonEmpty(path.join(targetRoot, 'rules'))) { warnings.push(`Destination ${path.join(targetRoot, 'rules')}/ already exists and files may be overwritten`); } addMatchingRuleOperations(operations, { moduleId: 'legacy-antigravity-install', sourceRoot: context.sourceRoot, sourceRelativeDir: path.join('rules', 'common'), destinationDir: path.join(targetRoot, 'rules'), matcher: fileName => fileName.endsWith('.md'), rename: fileName => `common-${fileName}` }); for (const language of context.languages) { if (!LANGUAGE_NAME_PATTERN.test(language)) { warnings.push(`Invalid language name '${language}'. Only alphanumeric, dash, and underscore are allowed`); continue; } const sourceDir = path.join(context.sourceRoot, 'rules', language); if (!fs.existsSync(sourceDir)) { warnings.push(`rules/${language}/ does not exist, skipping`); continue; } addMatchingRuleOperations(operations, { moduleId: 'legacy-antigravity-install', sourceRoot: context.sourceRoot, sourceRelativeDir: path.join('rules', language), destinationDir: path.join(targetRoot, 'rules'), matcher: fileName => fileName.endsWith('.md'), rename: fileName => `${language}-${fileName}` }); } addRecursiveCopyOperations(operations, { moduleId: 'legacy-antigravity-install', sourceRoot: context.sourceRoot, sourceRelativeDir: 'commands', destinationDir: path.join(targetRoot, 'workflows') }); addRecursiveCopyOperations(operations, { moduleId: 'legacy-antigravity-install', sourceRoot: context.sourceRoot, sourceRelativeDir: 'agents', destinationDir: path.join(targetRoot, 'agents'), contentTransform: 'antigravity-agent-frontmatter' }); addRecursiveCopyOperations(operations, { moduleId: 'legacy-antigravity-install', sourceRoot: context.sourceRoot, sourceRelativeDir: 'skills', destinationDir: path.join(targetRoot, 'skills') }); return { mode: 'legacy', adapter, target: 'antigravity', targetRoot, installRoot: targetRoot, installStatePath, operations, warnings, selectedModules: ['legacy-antigravity-install'] }; } function createLegacyInstallPlan(options = {}) { const sourceRoot = options.sourceRoot || getSourceRoot(); const projectRoot = options.projectRoot || process.cwd(); const homeDir = options.homeDir || process.env.HOME || os.homedir(); const target = options.target || 'claude'; validateLegacyTarget(target); const context = { sourceRoot, projectRoot, homeDir, languages: Array.isArray(options.languages) ? options.languages : [], claudeRulesDir: options.claudeRulesDir || process.env.CLAUDE_RULES_DIR || null }; let plan; if (target === 'claude') { plan = planClaudeLegacyInstall(context); } else if (target === 'claude-project') { plan = planClaudeProjectLegacyInstall(context); } else if (target === 'cursor') { plan = planCursorLegacyInstall(context); } else { plan = planAntigravityLegacyInstall(context); } const source = { repoVersion: getPackageVersion(sourceRoot), repoCommit: getRepoCommit(sourceRoot), manifestVersion: getManifestVersion(sourceRoot) }; const statePreview = createStatePreview({ adapter: plan.adapter, targetRoot: plan.targetRoot, installStatePath: plan.installStatePath, request: { profile: null, modules: [], legacyLanguages: context.languages, legacyMode: true }, resolution: { selectedModules: plan.selectedModules, skippedModules: [] }, operations: plan.operations, source }); return { mode: 'legacy', sourceRoot, target: plan.target, adapter: { id: plan.adapter.id, target: plan.adapter.target, kind: plan.adapter.kind }, targetRoot: plan.targetRoot, installRoot: plan.installRoot, installStatePath: plan.installStatePath, warnings: plan.warnings, languages: context.languages, operations: plan.operations, statePreview }; } function createLegacyCompatInstallPlan(options = {}) { const sourceRoot = options.sourceRoot || getSourceRoot(); const projectRoot = options.projectRoot || process.cwd(); const target = options.target || 'claude'; const includeComponentIds = Array.isArray(options.includeComponentIds) ? [...options.includeComponentIds] : []; const excludeComponentIds = Array.isArray(options.excludeComponentIds) ? [...options.excludeComponentIds] : []; validateLegacyTarget(target); const selection = resolveLegacyCompatibilitySelection({ repoRoot: sourceRoot, target, legacyLanguages: options.legacyLanguages || [] }); return createManifestInstallPlan({ sourceRoot, projectRoot, homeDir: options.homeDir, env: resolveInvocationEnvironment(options), target, profileId: null, moduleIds: selection.moduleIds, includeComponentIds, excludeComponentIds, legacyLanguages: selection.legacyLanguages, ruleLanguages: selection.ruleLanguages, legacyMode: true, exemptValidationCodes: options.exemptValidationCodes || [], requestProfileId: null, requestModuleIds: [], requestIncludeComponentIds: includeComponentIds, requestExcludeComponentIds: excludeComponentIds, mode: 'legacy-compat' }); } module.exports = { SUPPORTED_INSTALL_TARGETS, LEGACY_INSTALL_TARGETS, applyInstallPlan, previewInstallPlan, createLegacyCompatInstallPlan, createManifestInstallPlan, createLegacyInstallPlan, dedupeCopyFileOperations, getSourceRoot, listAvailableLanguages, parseInstallArgs };