chore: import upstream snapshot with attribution
CI / Test (ubuntu-latest, Node 18.x, bun) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, npm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, pnpm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, yarn) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 20.x, bun) (push) Failing after 17m13s
CI / Test (ubuntu-latest, Node 20.x, npm) (push) Failing after 18m42s
CI / Test (ubuntu-latest, Node 20.x, pnpm) (push) Failing after 15m0s
CI / Test (ubuntu-latest, Node 20.x, yarn) (push) Failing after 49m44s
CI / Test (ubuntu-latest, Node 22.x, bun) (push) Failing after 51m55s
CI / Test (ubuntu-latest, Node 22.x, pnpm) (push) Failing after 21m57s
CI / Test (ubuntu-latest, Node 22.x, npm) (push) Failing after 37m39s
CI / Test (ubuntu-latest, Node 22.x, yarn) (push) Failing after 34m7s
CI / Validate Components (push) Failing after 37m15s
CI / Python Tests (push) Failing after 10m1s
CI / Security Scan (push) Failing after 10m1s
CI / Lint (push) Failing after 17m12s
CI / Coverage (push) Failing after 20m19s
CI / Test (macos-latest, Node 18.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, yarn) (push) Has been cancelled
CI / Test (ubuntu-latest, Node 18.x, bun) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, npm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, pnpm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, yarn) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 20.x, bun) (push) Failing after 17m13s
CI / Test (ubuntu-latest, Node 20.x, npm) (push) Failing after 18m42s
CI / Test (ubuntu-latest, Node 20.x, pnpm) (push) Failing after 15m0s
CI / Test (ubuntu-latest, Node 20.x, yarn) (push) Failing after 49m44s
CI / Test (ubuntu-latest, Node 22.x, bun) (push) Failing after 51m55s
CI / Test (ubuntu-latest, Node 22.x, pnpm) (push) Failing after 21m57s
CI / Test (ubuntu-latest, Node 22.x, npm) (push) Failing after 37m39s
CI / Test (ubuntu-latest, Node 22.x, yarn) (push) Failing after 34m7s
CI / Validate Components (push) Failing after 37m15s
CI / Python Tests (push) Failing after 10m1s
CI / Security Scan (push) Failing after 10m1s
CI / Lint (push) Failing after 17m12s
CI / Coverage (push) Failing after 20m19s
CI / Test (macos-latest, Node 18.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, yarn) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,812 @@
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const { toCursorAgentRelativePath } = require('./cursor-agent-names');
|
||||
const { LEGACY_INSTALL_TARGETS, parseInstallArgs } = require('./install/request');
|
||||
const { SUPPORTED_INSTALL_TARGETS, listLegacyCompatibilityLanguages, resolveLegacyCompatibilitySelection, resolveInstallPlan } = require('./install-manifests');
|
||||
const { getInstallTargetAdapter } = require('./install-targets/registry');
|
||||
|
||||
const LANGUAGE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
||||
const CLAUDE_ECC_NAMESPACE = 'ecc';
|
||||
const EXCLUDED_GENERATED_SOURCE_SUFFIXES = ['/ecc-install-state.json', '/ecc/install-state.json'];
|
||||
|
||||
function getSourceRoot() {
|
||||
return path.join(__dirname, '../..');
|
||||
}
|
||||
|
||||
function getPackageVersion(sourceRoot) {
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(sourceRoot, 'package.json'), 'utf8'));
|
||||
return packageJson.version || null;
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getManifestVersion(sourceRoot) {
|
||||
try {
|
||||
const modulesManifest = JSON.parse(fs.readFileSync(path.join(sourceRoot, 'manifests', 'install-modules.json'), 'utf8'));
|
||||
return modulesManifest.version || 1;
|
||||
} catch (_error) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function getRepoCommit(sourceRoot) {
|
||||
try {
|
||||
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
||||
cwd: sourceRoot,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
timeout: 5000
|
||||
}).trim();
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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 <id,...> / --skills <id,...>).`
|
||||
);
|
||||
}
|
||||
throw new Error(`Unknown install target: ${target}. Expected one of ${SUPPORTED_INSTALL_TARGETS.join(', ')}`);
|
||||
}
|
||||
|
||||
const IGNORED_DIRECTORY_NAMES = new Set(['node_modules', '.git']);
|
||||
|
||||
function listFilesRecursive(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const files = [];
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const absolutePath = path.join(dirPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (IGNORED_DIRECTORY_NAMES.has(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
const childFiles = listFilesRecursive(absolutePath);
|
||||
for (const childFile of childFiles) {
|
||||
files.push(path.join(entry.name, childFile));
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
files.push(entry.name);
|
||||
}
|
||||
}
|
||||
|
||||
return files.sort();
|
||||
}
|
||||
|
||||
function isGeneratedRuntimeSourcePath(sourceRelativePath) {
|
||||
const normalizedPath = String(sourceRelativePath || '').replace(/\\/g, '/');
|
||||
return EXCLUDED_GENERATED_SOURCE_SUFFIXES.some(suffix => normalizedPath.endsWith(suffix));
|
||||
}
|
||||
|
||||
function createStatePreview(options) {
|
||||
const { createInstallState } = require('./install-state');
|
||||
return createInstallState(options);
|
||||
}
|
||||
|
||||
function applyInstallPlan(plan) {
|
||||
const { applyInstallPlan: applyPlan } = require('./install/apply');
|
||||
return applyPlan(plan);
|
||||
}
|
||||
|
||||
function buildCopyFileOperation({ moduleId, sourcePath, sourceRelativePath, destinationPath, strategy }) {
|
||||
return {
|
||||
kind: 'copy-file',
|
||||
moduleId,
|
||||
sourcePath,
|
||||
sourceRelativePath,
|
||||
destinationPath,
|
||||
strategy,
|
||||
ownership: 'managed',
|
||||
scaffoldOnly: false
|
||||
};
|
||||
}
|
||||
|
||||
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'
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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 readJsonObject(filePath, label) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse ${label} at ${filePath}: ${error.message}`);
|
||||
}
|
||||
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new Error(`Invalid ${label} at ${filePath}: expected a JSON object`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
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',
|
||||
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, 'skills')
|
||||
});
|
||||
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',
|
||||
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,
|
||||
target,
|
||||
profileId: null,
|
||||
moduleIds: selection.moduleIds,
|
||||
includeComponentIds,
|
||||
excludeComponentIds,
|
||||
legacyLanguages: selection.legacyLanguages,
|
||||
legacyMode: true,
|
||||
requestProfileId: null,
|
||||
requestModuleIds: [],
|
||||
requestIncludeComponentIds: includeComponentIds,
|
||||
requestExcludeComponentIds: excludeComponentIds,
|
||||
mode: 'legacy-compat'
|
||||
});
|
||||
}
|
||||
|
||||
function materializeScaffoldOperation(sourceRoot, operation) {
|
||||
if (operation.kind === 'merge-json') {
|
||||
return [
|
||||
{
|
||||
kind: 'merge-json',
|
||||
moduleId: operation.moduleId,
|
||||
sourceRelativePath: operation.sourceRelativePath,
|
||||
destinationPath: operation.destinationPath,
|
||||
strategy: operation.strategy || 'merge-json',
|
||||
ownership: operation.ownership || 'managed',
|
||||
scaffoldOnly: Object.hasOwn(operation, 'scaffoldOnly') ? operation.scaffoldOnly : false,
|
||||
mergePayload: readJsonObject(path.join(sourceRoot, operation.sourceRelativePath), operation.sourceRelativePath)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const sourcePath = path.join(sourceRoot, operation.sourceRelativePath);
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isGeneratedRuntimeSourcePath(operation.sourceRelativePath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stat = fs.statSync(sourcePath);
|
||||
if (stat.isFile()) {
|
||||
return [
|
||||
buildCopyFileOperation({
|
||||
moduleId: operation.moduleId,
|
||||
sourcePath,
|
||||
sourceRelativePath: operation.sourceRelativePath,
|
||||
destinationPath: operation.destinationPath,
|
||||
strategy: operation.strategy
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
const relativeFiles = listFilesRecursive(sourcePath).filter(relativeFile => {
|
||||
const sourceRelativePath = path.join(operation.sourceRelativePath, relativeFile);
|
||||
return !isGeneratedRuntimeSourcePath(sourceRelativePath);
|
||||
});
|
||||
return relativeFiles.map(relativeFile => {
|
||||
const sourceRelativePath = path.join(operation.sourceRelativePath, relativeFile);
|
||||
return buildCopyFileOperation({
|
||||
moduleId: operation.moduleId,
|
||||
sourcePath: path.join(sourcePath, relativeFile),
|
||||
sourceRelativePath,
|
||||
destinationPath: path.join(operation.destinationPath, relativeFile),
|
||||
strategy: operation.strategy
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function dedupeCopyFileOperations(operations) {
|
||||
// A `copy-file` operation fully overwrites its destination, so when several
|
||||
// of them target the same path (e.g. a generic `commands/<name>.md` shadowed
|
||||
// by an OpenCode `.opencode/commands/<name>.md` override) only the last one
|
||||
// actually determines the installed content. Recording the shadowed earlier
|
||||
// writes in install-state makes `doctor` report perpetual drift and drives
|
||||
// `repair` to clobber the override with the generic source (issue #2414).
|
||||
// Keep only the last `copy-file` per destination — matching the sequential
|
||||
// apply order in applyInstallPlan — and leave every other operation kind
|
||||
// (e.g. accumulating `merge-json` writes into a shared config) untouched and
|
||||
// in order.
|
||||
const lastCopyIndexByDestination = new Map();
|
||||
operations.forEach((operation, index) => {
|
||||
if (operation.kind === 'copy-file' && operation.destinationPath) {
|
||||
lastCopyIndexByDestination.set(operation.destinationPath, index);
|
||||
}
|
||||
});
|
||||
|
||||
return operations.filter((operation, index) => {
|
||||
if (operation.kind !== 'copy-file' || !operation.destinationPath) {
|
||||
return true;
|
||||
}
|
||||
return lastCopyIndexByDestination.get(operation.destinationPath) === index;
|
||||
});
|
||||
}
|
||||
|
||||
function createManifestInstallPlan(options = {}) {
|
||||
const sourceRoot = options.sourceRoot || getSourceRoot();
|
||||
const projectRoot = options.projectRoot || process.cwd();
|
||||
const target = options.target || 'claude';
|
||||
const legacyLanguages = Array.isArray(options.legacyLanguages) ? [...options.legacyLanguages] : [];
|
||||
const requestProfileId = Object.hasOwn(options, 'requestProfileId') ? options.requestProfileId : options.profileId || null;
|
||||
const requestModuleIds = Object.hasOwn(options, 'requestModuleIds') ? [...options.requestModuleIds] : Array.isArray(options.moduleIds) ? [...options.moduleIds] : [];
|
||||
const requestIncludeComponentIds = Object.hasOwn(options, 'requestIncludeComponentIds')
|
||||
? [...options.requestIncludeComponentIds]
|
||||
: Array.isArray(options.includeComponentIds)
|
||||
? [...options.includeComponentIds]
|
||||
: [];
|
||||
const requestExcludeComponentIds = Object.hasOwn(options, 'requestExcludeComponentIds')
|
||||
? [...options.requestExcludeComponentIds]
|
||||
: Array.isArray(options.excludeComponentIds)
|
||||
? [...options.excludeComponentIds]
|
||||
: [];
|
||||
const plan = resolveInstallPlan({
|
||||
repoRoot: sourceRoot,
|
||||
projectRoot,
|
||||
homeDir: options.homeDir,
|
||||
profileId: options.profileId || null,
|
||||
moduleIds: options.moduleIds || [],
|
||||
includeComponentIds: options.includeComponentIds || [],
|
||||
excludeComponentIds: options.excludeComponentIds || [],
|
||||
target,
|
||||
exemptValidationCodes: options.exemptValidationCodes || [],
|
||||
});
|
||||
const adapter = getInstallTargetAdapter(target);
|
||||
const operations = dedupeCopyFileOperations(
|
||||
plan.operations.flatMap(operation => materializeScaffoldOperation(sourceRoot, operation))
|
||||
);
|
||||
const source = {
|
||||
repoVersion: getPackageVersion(sourceRoot),
|
||||
repoCommit: getRepoCommit(sourceRoot),
|
||||
manifestVersion: getManifestVersion(sourceRoot)
|
||||
};
|
||||
const statePreview = createStatePreview({
|
||||
adapter,
|
||||
targetRoot: plan.targetRoot,
|
||||
installStatePath: plan.installStatePath,
|
||||
request: {
|
||||
profile: requestProfileId,
|
||||
modules: requestModuleIds,
|
||||
includeComponents: requestIncludeComponentIds,
|
||||
excludeComponents: requestExcludeComponentIds,
|
||||
legacyLanguages,
|
||||
legacyMode: Boolean(options.legacyMode)
|
||||
},
|
||||
resolution: {
|
||||
selectedModules: plan.selectedModuleIds,
|
||||
skippedModules: plan.skippedModuleIds
|
||||
},
|
||||
operations,
|
||||
source
|
||||
});
|
||||
|
||||
return {
|
||||
mode: options.mode || 'manifest',
|
||||
target,
|
||||
adapter: {
|
||||
id: adapter.id,
|
||||
target: adapter.target,
|
||||
kind: adapter.kind
|
||||
},
|
||||
targetRoot: plan.targetRoot,
|
||||
installRoot: plan.targetRoot,
|
||||
installStatePath: plan.installStatePath,
|
||||
warnings: Array.isArray(options.warnings) ? [...options.warnings] : [],
|
||||
languages: legacyLanguages,
|
||||
legacyLanguages,
|
||||
profileId: plan.profileId,
|
||||
requestedModuleIds: plan.requestedModuleIds,
|
||||
explicitModuleIds: plan.explicitModuleIds,
|
||||
includedComponentIds: plan.includedComponentIds,
|
||||
excludedComponentIds: plan.excludedComponentIds,
|
||||
selectedModuleIds: plan.selectedModuleIds,
|
||||
skippedModuleIds: plan.skippedModuleIds,
|
||||
excludedModuleIds: plan.excludedModuleIds,
|
||||
operations,
|
||||
statePreview
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SUPPORTED_INSTALL_TARGETS,
|
||||
LEGACY_INSTALL_TARGETS,
|
||||
applyInstallPlan,
|
||||
createLegacyCompatInstallPlan,
|
||||
createManifestInstallPlan,
|
||||
createLegacyInstallPlan,
|
||||
dedupeCopyFileOperations,
|
||||
getSourceRoot,
|
||||
listAvailableLanguages,
|
||||
parseInstallArgs
|
||||
};
|
||||
Reference in New Issue
Block a user