orchestrix
Version:
Orchestrix - Universal AI Agent Framework for Coordinated AI-Driven Development
1,367 lines (1,158 loc) • 65.7 kB
JavaScript
const path = require("node:path");
const fileManager = require("./file-manager");
const configLoader = require("./config-loader");
const ideSetup = require("./ide-setup");
const { extractYamlFromAgent, loadAgentYaml, findAgentPath } = require("../../lib/yaml-utils");
const YamlCompiler = require("../../lib/yaml-compiler");
// Dynamic imports for ES modules
let chalk, ora, inquirer;
// Initialize ES modules
async function initializeModules() {
if (!chalk) {
chalk = (await import("chalk")).default;
ora = (await import("ora")).default;
inquirer = (await import("inquirer")).default;
}
}
class Installer {
async getCoreVersion() {
const yaml = require("js-yaml");
const fs = require("fs-extra");
const coreConfigPath = path.join(__dirname, "../../../orchestrix-core/core-config.yaml");
try {
const coreConfigContent = await fs.readFile(coreConfigPath, "utf8");
const coreConfig = yaml.load(coreConfigContent);
return coreConfig.version || "unknown";
} catch (error) {
console.warn("Could not read version from core-config.yaml, using 'unknown'");
return "unknown";
}
}
async compileAgentConfigurations(spinner) {
try {
const compiler = new YamlCompiler({ verbose: false });
const sourceDir = path.join(__dirname, "../../../orchestrix-core/agents");
const outputDir = sourceDir; // Compile in place
// Check if there are any .src.yaml files to compile
const fs = require("fs-extra");
const files = await fs.readdir(sourceDir);
const srcFiles = files.filter(f => f.endsWith('.src.yaml'));
if (srcFiles.length === 0) {
// No source files to compile, skip this step
return;
}
const compiled = await compiler.compileAllAgents(sourceDir, outputDir);
if (compiled > 0) {
spinner.text = `✓ 编译了 ${compiled} 个代理配置文件`;
}
} catch (error) {
console.warn(chalk.yellow(`警告: 编译代理配置时出现问题: ${error.message}`));
console.warn(chalk.yellow('将继续使用现有的 .yaml 文件'));
// Don't fail installation if compilation fails - existing .yaml files might be sufficient
}
}
async install(config) {
// Initialize ES modules
await initializeModules();
// const spinner = ora("Analyzing installation directory...").start();
const spinner = {
text: '',
start: () => spinner,
stop: () => {},
succeed: (msg) => msg && console.log(chalk.green(`✓ ${msg}`)),
fail: (msg) => msg && console.log(chalk.red(`✗ ${msg}`)),
warn: (msg) => msg && console.log(chalk.yellow(`⚠ ${msg}`))
};
try {
// Store the original CWD where npx was executed
const originalCwd = process.env.INIT_CWD || process.env.PWD || process.cwd();
// Resolve installation directory relative to where the user ran the command
let installDir = path.isAbsolute(config.directory)
? config.directory
: path.resolve(originalCwd, config.directory);
if (path.basename(installDir) === '.orchestrix-core') {
// If user points directly to .orchestrix-core, treat its parent as the project root
installDir = path.dirname(installDir);
}
// Log resolved path for clarity
if (!path.isAbsolute(config.directory)) {
spinner.text = `Resolving "${config.directory}" to: ${installDir}`;
}
// Check if directory exists and handle non-existent directories
if (!(await fileManager.pathExists(installDir))) {
spinner.stop();
console.log(chalk.yellow(`\n目录 ${chalk.bold(installDir)} 不存在。`));
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: '您希望做什么?',
choices: [
{
name: '创建目录并继续',
value: 'create'
},
{
name: '选择一个不同的目录',
value: 'change'
},
{
name: '取消安装',
value: 'cancel'
}
]
}
]);
if (action === 'cancel') {
console.log(chalk.red('安装已取消。'));
process.exit(0);
} else if (action === 'change') {
const { newDirectory } = await inquirer.prompt([
{
type: 'input',
name: 'newDirectory',
message: '请输入新的目录路径:',
validate: (input) => {
if (!input.trim()) {
return '请输入有效的目录路径';
}
return true;
}
}
]);
// Preserve the original CWD for the recursive call
config.directory = newDirectory;
return await this.install(config); // Recursive call with new directory
} else if (action === 'create') {
try {
await fileManager.ensureDirectory(installDir);
console.log(chalk.green(`✓ 已创建目录: ${installDir}`));
} catch (error) {
console.error(chalk.red(`创建目录失败: ${error.message}`));
console.error(chalk.yellow('您可能需要检查权限或使用不同的路径。'));
process.exit(1);
}
}
spinner.start("正在分析安装目录...");
}
// If this is an update request from early detection, handle it directly
if (config.installType === 'update') {
const state = await this.detectInstallationState(installDir);
if (state.type === 'existing') {
return await this.performUpdate(config, installDir, state.manifest, spinner);
} else {
spinner.fail('未找到要更新的现有安装');
throw new Error('未找到现有安装');
}
}
// Detect current state
const state = await this.detectInstallationState(installDir);
// Handle different states
switch (state.type) {
case "clean":
return await this.performFreshInstall(config, installDir, spinner);
case "existing":
return await this.handleExistingInstallation(
config,
installDir,
state,
spinner
);
case "unknown_existing":
return await this.handleUnknownInstallation(
config,
installDir,
state,
spinner
);
}
} catch (error) {
spinner.fail("Installation failed");
throw error;
}
}
async detectInstallationState(installDir) {
// Ensure modules are initialized
await initializeModules();
const state = {
type: "clean",
hasManifest: false,
hasorchestrixCore: false,
hasOtherFiles: false,
manifest: null,
expansionPacks: {},
};
// Check if directory exists
if (!(await fileManager.pathExists(installDir))) {
return state; // clean install
}
// Check for existing installation (has .orchestrix-core with manifest)
const orchestrixCorePath = path.join(installDir, ".orchestrix-core");
const manifestPath = path.join(orchestrixCorePath, "install-manifest.yaml");
if (await fileManager.pathExists(manifestPath)) {
state.type = "existing";
state.hasManifest = true;
state.hasorchestrixCore = true;
state.manifest = await fileManager.readManifest(installDir);
return state;
}
// Check for .orchestrix-core without manifest (broken installation or manual copy)
if (await fileManager.pathExists(orchestrixCorePath)) {
state.type = "unknown_existing";
state.hasorchestrixCore = true;
return state;
}
// Check if directory has other files
const glob = require("glob");
const files = glob.sync("**/*", {
cwd: installDir,
nodir: true,
ignore: ["**/.git/**", "**/node_modules/**"],
});
if (files.length > 0) {
// Directory has other files, but no orchestrix installation.
// Treat as clean install but record that it isn't empty.
state.hasOtherFiles = true;
}
// Check for expansion packs (folders starting with .)
const expansionPacks = await this.detectExpansionPacks(installDir);
state.expansionPacks = expansionPacks;
return state; // clean install
}
async performFreshInstall(config, installDir, spinner, options = {}) {
// Ensure modules are initialized
await initializeModules();
spinner.text = "正在安装 Orchestrix...";
// Compile .src.yaml files before installation
if (config.installType === "full" || config.installType === "single-agent" || config.installType === "team") {
spinner.text = "正在编译代理配置文件...";
await this.compileAgentConfigurations(spinner);
}
let files = [];
if (config.installType === "full") {
// Full installation - copy entire .orchestrix-core folder as a subdirectory
spinner.text = "正在复制完整的 .orchestrix-core 文件夹...";
const sourceDir = configLoader.getOrchestrixCorePath();
const orchestrixCoreDestDir = path.join(installDir, ".orchestrix-core");
await fileManager.copyDirectoryWithRootReplacement(sourceDir, orchestrixCoreDestDir, ".orchestrix-core");
// Copy common/ items to .orchestrix-core
spinner.text = "正在复制通用工具...";
await this.copyCommonItems(installDir, ".orchestrix-core", spinner);
// Get list of all files for manifest
const glob = require("glob");
files = glob
.sync("**/*", {
cwd: orchestrixCoreDestDir,
nodir: true,
ignore: ["**/.git/**", "**/node_modules/**"],
})
.map((file) => path.join(".orchestrix-core", file));
} else if (config.installType === "single-agent") {
// Single agent installation
spinner.text = `正在安装 ${config.agent} 代理...`;
// Copy agent file with {root} replacement
const agentPath = configLoader.getAgentPath(config.agent);
const destAgentPath = path.join(
installDir,
".orchestrix-core",
"agents",
`${config.agent}.md`
);
await fileManager.copyFileWithRootReplacement(agentPath, destAgentPath, ".orchestrix-core");
files.push(`.orchestrix-core/agents/${config.agent}.md`);
// Copy dependencies
const dependencies = await configLoader.getAgentDependencies(
config.agent
);
const sourceBase = configLoader.getOrchestrixCorePath();
for (const dep of dependencies) {
spinner.text = `Copying dependency: ${dep}`;
if (dep.includes("*")) {
// Handle glob patterns with {root} replacement
const copiedFiles = await fileManager.copyGlobPattern(
dep.replace(".orchestrix-core/", ""),
sourceBase,
path.join(installDir, ".orchestrix-core"),
".orchestrix-core"
);
files.push(...copiedFiles.map(f => `.orchestrix-core/${f}`));
} else {
// Handle single files with {root} replacement if needed
const sourcePath = path.join(
sourceBase,
dep.replace(".orchestrix-core/", "")
);
const destPath = path.join(
installDir,
dep
);
const needsRootReplacement = dep.endsWith('.md') || dep.endsWith('.yaml') || dep.endsWith('.yml');
let success = false;
if (needsRootReplacement) {
success = await fileManager.copyFileWithRootReplacement(sourcePath, destPath, ".orchestrix-core");
} else {
success = await fileManager.copyFile(sourcePath, destPath);
}
if (success) {
files.push(dep);
}
}
}
// Copy common/ items to .orchestrix-core
spinner.text = "Copying common utilities...";
const commonFiles = await this.copyCommonItems(installDir, ".orchestrix-core", spinner);
files.push(...commonFiles);
} else if (config.installType === "team") {
// Team installation
spinner.text = `正在安装 ${config.team} 团队...`;
// Get team dependencies
const teamDependencies = await configLoader.getTeamDependencies(config.team);
const sourceBase = configLoader.getOrchestrixCorePath();
// Install all team dependencies
for (const dep of teamDependencies) {
spinner.text = `Copying team dependency: ${dep}`;
if (dep.includes("*")) {
// Handle glob patterns with {root} replacement
const copiedFiles = await fileManager.copyGlobPattern(
dep.replace(".orchestrix-core/", ""),
sourceBase,
path.join(installDir, ".orchestrix-core"),
".orchestrix-core"
);
files.push(...copiedFiles.map(f => `.orchestrix-core/${f}`));
} else {
// Handle single files with {root} replacement if needed
const sourcePath = path.join(sourceBase, dep.replace(".orchestrix-core/", ""));
const destPath = path.join(installDir, dep);
const needsRootReplacement = dep.endsWith('.md') || dep.endsWith('.yaml') || dep.endsWith('.yml');
let success = false;
if (needsRootReplacement) {
success = await fileManager.copyFileWithRootReplacement(sourcePath, destPath, ".orchestrix-core");
} else {
success = await fileManager.copyFile(sourcePath, destPath);
}
if (success) {
files.push(dep);
}
}
}
// Copy common/ items to .orchestrix-core
spinner.text = "Copying common utilities...";
const commonFiles = await this.copyCommonItems(installDir, ".orchestrix-core", spinner);
files.push(...commonFiles);
} else if (config.installType === "expansion-only") {
// Expansion-only installation - DO NOT create .orchestrix-core
// Only install expansion packs
spinner.text = "正在安装扩展包...";
}
// Install expansion packs if requested
const expansionFiles = await this.installExpansionPacks(installDir, config.expansionPacks, spinner, config);
files.push(...expansionFiles);
// Install web bundles if requested
if (config.includeWebBundles && config.webBundlesDirectory) {
spinner.text = "正在安装 web bundles...";
// Resolve web bundles directory using the same logic as the main installation directory
const originalCwd = process.env.INIT_CWD || process.env.PWD || process.cwd();
let resolvedWebBundlesDir = path.isAbsolute(config.webBundlesDirectory)
? config.webBundlesDirectory
: path.resolve(originalCwd, config.webBundlesDirectory);
await this.installWebBundles(resolvedWebBundlesDir, config, spinner);
}
// Set up IDE integration if requested
const ides = config.ides || (config.ide ? [config.ide] : []);
if (ides.length > 0) {
for (const ide of ides) {
spinner.text = `Setting up ${ide} integration...`;
const preConfiguredSettings = ide === 'github-copilot' ? config.githubCopilotConfig : null;
await ideSetup.setup(ide, installDir, config.agent, spinner, preConfiguredSettings);
}
}
// Modify core-config.yaml if sharding preferences were provided
if (config.installType !== "expansion-only" && (config.prdSharded !== undefined || config.architectureSharded !== undefined)) {
spinner.text = "Configuring document sharding settings...";
await fileManager.modifyCoreConfig(installDir, config);
}
// Create manifest (skip for expansion-only installations)
if (config.installType !== "expansion-only") {
spinner.text = "Creating installation manifest...";
await fileManager.createManifest(installDir, config, files);
}
spinner.succeed("安装完成!");
this.showSuccessMessage(config, installDir, options);
}
async handleExistingInstallation(config, installDir, state, spinner) {
// Ensure modules are initialized
await initializeModules();
spinner.stop();
const currentVersion = state.manifest.version;
const newVersion = await this.getCoreVersion();
const versionCompare = this.compareVersions(currentVersion, newVersion);
console.log(chalk.yellow("\n🔍 发现现有的 orchestrix 安装"));
console.log(` 目录: ${installDir}`);
console.log(` 当前版本: ${currentVersion}`);
console.log(` 可用版本: ${newVersion}`);
console.log(
` 安装日期: ${new Date(
state.manifest.installed_at
).toLocaleDateString()}`
);
// Check file integrity
spinner.start("正在检查安装完整性...");
const integrity = await fileManager.checkFileIntegrity(installDir, state.manifest);
spinner.stop();
const hasMissingFiles = integrity.missing.length > 0;
const hasModifiedFiles = integrity.modified.length > 0;
const hasIntegrityIssues = hasMissingFiles || hasModifiedFiles;
if (hasIntegrityIssues) {
console.log(chalk.red("\n⚠️ 检测到安装问题:"));
if (hasMissingFiles) {
console.log(chalk.red(` 丢失的文件: ${integrity.missing.length}`));
if (integrity.missing.length <= 5) {
integrity.missing.forEach(file => console.log(chalk.dim(` - ${file}`)));
}
}
if (hasModifiedFiles) {
console.log(chalk.yellow(` 修改的文件: ${integrity.modified.length}`));
if (integrity.modified.length <= 5) {
integrity.modified.forEach(file => console.log(chalk.dim(` - ${file}`)));
}
}
}
// Show existing expansion packs
if (Object.keys(state.expansionPacks).length > 0) {
console.log(chalk.cyan("\n📦 已安装的扩展包:"));
for (const [packId, packInfo] of Object.entries(state.expansionPacks)) {
if (packInfo.hasManifest && packInfo.manifest) {
console.log(` - ${packId} (v${packInfo.manifest.version || '未知'})`);
} else {
console.log(` - ${packId} (无清单文件)`);
}
}
}
let choices = [];
if (versionCompare < 0) {
console.log(chalk.cyan("\n⬆️ orchestrix 核心有可用升级"));
choices.push({ name: `升级 orchestrix 核心 (v${currentVersion} → v${newVersion})`, value: "upgrade" });
} else if (versionCompare === 0) {
if (hasIntegrityIssues) {
// Offer repair option when files are missing or modified
choices.push({
name: "修复安装 (恢复丢失/修改的文件)",
value: "repair"
});
}
console.log(chalk.yellow("\n⚠️ 已安装相同版本"));
choices.push({ name: `强制重新安装 orchestrix 核心 (v${currentVersion} - 重新安装)`, value: "reinstall" });
} else {
console.log(chalk.yellow("\n⬇️ 已安装的版本比可用版本新"));
choices.push({ name: `降级 orchestrix 核心 (v${currentVersion} → v${newVersion})`, value: "reinstall" });
}
choices.push(
{ name: "仅添加/更新扩展包", value: "expansions" },
{ name: "取消", value: "cancel" }
);
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: "您希望做什么?",
choices: choices,
},
]);
switch (action) {
case "upgrade":
return await this.performUpdate(config, installDir, state.manifest, spinner);
case "repair":
// For repair, restore missing/modified files while backing up modified ones
return await this.performRepair(config, installDir, state.manifest, integrity, spinner);
case "reinstall":
// For reinstall, don't check for modifications - just overwrite
return await this.performReinstall(config, installDir, spinner);
case "expansions":
// Ask which expansion packs to install
const availableExpansionPacks = await this.getAvailableExpansionPacks();
if (availableExpansionPacks.length === 0) {
console.log(chalk.yellow("没有可用的扩展包。"));
return;
}
const { selectedPacks } = await inquirer.prompt([
{
type: 'checkbox',
name: 'selectedPacks',
message: '选择要安装/更新的扩展包:',
choices: availableExpansionPacks.map(pack => ({
name: `${pack.name} v${pack.version} - ${pack.description}`,
value: pack.id,
checked: state.expansionPacks[pack.id] !== undefined
}))
}
]);
if (selectedPacks.length === 0) {
console.log(chalk.yellow("未选择任何扩展包。"));
return;
}
spinner.start("正在安装扩展包...");
const expansionFiles = await this.installExpansionPacks(installDir, selectedPacks, spinner, { ides: config.ides || [] });
spinner.succeed("扩展包安装成功!");
console.log(chalk.green("\n✓ 安装完成!"));
console.log(chalk.green(`✓ 已安装/更新的扩展包:`));
for (const packId of selectedPacks) {
console.log(chalk.green(` - ${packId} → .${packId}/`));
}
return;
case "cancel":
console.log("安装已取消。");
return;
}
}
async handleUnknownInstallation(config, installDir, state, spinner) {
// Ensure modules are initialized
await initializeModules();
spinner.stop();
console.log(chalk.yellow("\n⚠️ 目录包含现有文件"));
console.log(` 目录: ${installDir}`);
if (state.hasorchestrixCore) {
console.log(" 发现: .orchestrix-core 目录 (但没有清单文件)");
}
if (state.hasOtherFiles) {
console.log(" 发现: 目录中有其他文件");
}
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: "您希望做什么?",
choices: [
{ name: "仍然安装 (可能会覆盖文件)", value: "force" },
{ name: "选择不同的目录", value: "different" },
{ name: "取消", value: "cancel" },
],
},
]);
switch (action) {
case "force":
return await this.performFreshInstall(config, installDir, spinner);
case "different": {
const { newDir } = await inquirer.prompt([
{
type: "input",
name: "newDir",
message: "输入新的安装目录:",
default: path.join(path.dirname(installDir), "orchestrix-project"),
},
]);
config.directory = newDir;
return await this.install(config);
}
case "cancel":
console.log("安装已取消。");
return;
}
}
async performUpdate(newConfig, installDir, manifest, spinner) {
spinner.start("Checking for updates...");
try {
// Get current and new versions
const currentVersion = manifest.version;
const newVersion = await this.getCoreVersion();
const versionCompare = this.compareVersions(currentVersion, newVersion);
// Only check for modified files if it's an actual version upgrade
let modifiedFiles = [];
if (versionCompare !== 0) {
spinner.text = "Checking for modified files...";
modifiedFiles = await fileManager.checkModifiedFiles(
installDir,
manifest
);
}
if (modifiedFiles.length > 0) {
spinner.warn("Found modified files");
console.log(chalk.yellow("\n以下文件已被修改:"));
for (const file of modifiedFiles) {
console.log(` - ${file}`);
}
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: "您希望如何继续?",
choices: [
{ name: "备份并覆盖修改过的文件", value: "backup" },
{ name: "跳过修改过的文件", value: "skip" },
{ name: "取消更新", value: "cancel" },
],
},
]);
if (action === "cancel") {
console.log("更新已取消。");
return;
}
if (action === "backup") {
spinner.start("Backing up modified files...");
for (const file of modifiedFiles) {
const filePath = path.join(installDir, file);
const backupPath = await fileManager.backupFile(filePath);
console.log(
chalk.dim(` Backed up: ${file} → ${path.basename(backupPath)}`)
);
}
}
}
// Perform update by re-running installation
spinner.text = versionCompare === 0 ? "Reinstalling files..." : "Updating files...";
const config = {
installType: manifest.install_type,
agent: manifest.agent,
directory: installDir,
ides: newConfig?.ides || manifest.ides_setup || [],
};
await this.performFreshInstall(config, installDir, spinner, { isUpdate: true });
// Clean up .yml files that now have .yaml counterparts
spinner.text = "正在清理旧的 .yml 文件...";
await this.cleanupLegacyYmlFiles(installDir, spinner);
} catch (error) {
spinner.fail("Update failed");
throw error;
}
}
async performRepair(config, installDir, manifest, integrity, spinner) {
spinner.start("Preparing to repair installation...");
try {
// Back up modified files
if (integrity.modified.length > 0) {
spinner.text = "Backing up modified files...";
for (const file of integrity.modified) {
const filePath = path.join(installDir, file);
if (await fileManager.pathExists(filePath)) {
const backupPath = await fileManager.backupFile(filePath);
console.log(chalk.dim(` Backed up: ${file} → ${path.basename(backupPath)}`));
}
}
}
// Restore missing and modified files
spinner.text = "Restoring files...";
const sourceBase = configLoader.getOrchestrixCorePath();
const filesToRestore = [...integrity.missing, ...integrity.modified];
for (const file of filesToRestore) {
// Skip the manifest file itself
if (file.endsWith('install-manifest.yaml')) continue;
const relativePath = file.replace('.orchestrix-core/', '');
const destPath = path.join(installDir, file);
// Check if this is a common/ file that needs special processing
const commonBase = path.dirname(path.dirname(path.dirname(path.dirname(__filename))));
const commonSourcePath = path.join(commonBase, 'common', relativePath);
if (await fileManager.pathExists(commonSourcePath)) {
// This is a common/ file - needs template processing
const fs = require('fs').promises;
const content = await fs.readFile(commonSourcePath, 'utf8');
const updatedContent = content.replace(/\{root\}/g, '.orchestrix-core');
await fileManager.ensureDirectory(path.dirname(destPath));
await fs.writeFile(destPath, updatedContent, 'utf8');
spinner.text = `Restored: ${file}`;
} else {
// Regular file from orchestrix-core
const sourcePath = path.join(sourceBase, relativePath);
if (await fileManager.pathExists(sourcePath)) {
await fileManager.copyFile(sourcePath, destPath);
spinner.text = `Restored: ${file}`;
// If this is a .yaml file, check for and remove corresponding .yml file
if (file.endsWith('.yaml')) {
const ymlFile = file.replace(/\.yaml$/, '.yml');
const ymlPath = path.join(installDir, ymlFile);
if (await fileManager.pathExists(ymlPath)) {
const fs = require('fs').promises;
await fs.unlink(ymlPath);
console.log(chalk.dim(` Removed legacy: ${ymlFile} (replaced by ${file})`));
}
}
} else {
console.warn(chalk.yellow(` Warning: Source file not found: ${file}`));
}
}
}
// Clean up .yml files that now have .yaml counterparts
spinner.text = "正在清理旧的 .yml 文件...";
await this.cleanupLegacyYmlFiles(installDir, spinner);
spinner.succeed("Repair completed successfully!");
// Show summary
console.log(chalk.green("\n✓ 安装已修复!"));
if (integrity.missing.length > 0) {
console.log(chalk.green(` Restored ${integrity.missing.length} missing files`));
}
if (integrity.modified.length > 0) {
console.log(chalk.green(` Restored ${integrity.modified.length} modified files (backups created)`));
}
// Warning for Cursor custom modes if agents were repaired
const ides = manifest.ides_setup || [];
if (ides.includes('cursor')) {
console.log(chalk.yellow.bold("\n⚠️ 重要提示:需要更新 Cursor 自定义模式"));
console.log(chalk.yellow("由于代理文件已修复,您需要根据 Cursor 文档在 Cursor 自定义代理 GUI 中更新任何已配置的自定义代理模式。"));
}
} catch (error) {
spinner.fail("Repair failed");
throw error;
}
}
async performReinstall(config, installDir, spinner) {
spinner.start("Preparing to reinstall Orchestrix...");
// Remove existing .orchestrix-core
const orchestrixCorePath = path.join(installDir, ".orchestrix-core");
if (await fileManager.pathExists(orchestrixCorePath)) {
spinner.text = "Removing existing installation...";
await fileManager.removeDirectory(orchestrixCorePath);
}
spinner.text = "Installing fresh copy...";
const result = await this.performFreshInstall(config, installDir, spinner, { isUpdate: true });
// Clean up .yml files that now have .yaml counterparts
spinner.text = "正在清理旧的 .yml 文件...";
await this.cleanupLegacyYmlFiles(installDir, spinner);
return result;
}
showSuccessMessage(config, installDir, options = {}) {
console.log(chalk.green("\n✓ Orchestrix 安装成功!\n"));
const ides = config.ides || (config.ide ? [config.ide] : []);
if (ides.length === 0) {
console.log(chalk.yellow("未设置 IDE 配置。"));
console.log(
"您可以手动配置您的 IDE,使用以下目录中的代理文件:",
installDir
);
}
// Information about installation components
console.log(chalk.bold("\n🎯 安装摘要:"));
if (config.installType !== "expansion-only") {
console.log(chalk.green("✓ 核心框架和代理已安装"));
}
if (config.expansionPacks && config.expansionPacks.length > 0) {
console.log(chalk.green(`✓ 扩展包: ${config.expansionPacks.join(', ')}`));
}
if (config.includeWebBundles && config.webBundlesDirectory) {
console.log(chalk.green(`✓ Web bundles 已安装`));
}
if (ides.length > 0) {
const ideNames = ides.map(ide => {
const ideConfig = configLoader.getIdeConfiguration(ide);
return ideConfig?.name || ide;
}).join(", ");
console.log(chalk.green(`✓ IDE 集成: ${ideNames}`));
}
// Simplified web bundles info
if (!config.includeWebBundles) {
console.log(chalk.dim("\n💡 提示: 可运行安装程序添加 Web bundles (适用于 ChatGPT, Claude, Gemini)"));
}
if (config.installType === "single-agent") {
console.log(chalk.dim("\n💡 提示: 运行 'npx orchestrix install --full' 安装完整版本"));
}
// Cursor custom mode warning removed per user request
}
// Legacy method for backward compatibility
async update() {
// Initialize ES modules
await initializeModules();
console.log(chalk.yellow(' "update" 命令已弃用。'));
console.log(
'请改用 "install" - 它将检测并提供更新现有安装的选项。'
);
const installDir = await this.findInstallation();
if (installDir) {
const config = {
installType: "full",
directory: path.dirname(installDir),
ide: null,
};
return await this.install(config);
}
console.log(chalk.red("未找到 orchestrix 安装。"));
}
async listAgents() {
// Initialize ES modules
await initializeModules();
const agents = await configLoader.getAvailableAgents();
console.log(chalk.bold("\n可用的 orchestrix 代理:\n"));
for (const agent of agents) {
console.log(chalk.cyan(` ${agent.id.padEnd(20)}`), agent.description);
}
console.log(
chalk.dim("\n使用以下命令安装: npx orchestrix install --agent=<id>\n")
);
}
async listExpansionPacks() {
// Initialize ES modules
await initializeModules();
const expansionPacks = await this.getAvailableExpansionPacks();
console.log(chalk.bold("\n可用的 orchestrix 扩展包:\n"));
if (expansionPacks.length === 0) {
console.log(chalk.yellow("未找到扩展包。"));
return;
}
for (const pack of expansionPacks) {
console.log(chalk.cyan(` ${pack.id.padEnd(20)}`),
`${pack.name} v${pack.version}`);
console.log(chalk.dim(` ${' '.repeat(22)}${pack.description}`));
if (pack.author && pack.author !== '未知') {
console.log(chalk.dim(` ${' '.repeat(22)}作者 ${pack.author}`));
}
console.log();
}
console.log(
chalk.dim("使用以下命令安装: npx orchestrix install --full --expansion-packs <id>\n")
);
}
async showStatus() {
// Initialize ES modules
await initializeModules();
const orchestrixCoreDir = await this.findInstallation();
if (!orchestrixCoreDir) {
console.log(
chalk.yellow("在当前目录树中未找到 orchestrix 安装")
);
return;
}
// Convert .orchestrix-core path to project root path
const installDir = path.dirname(orchestrixCoreDir);
const manifest = await fileManager.readManifest(installDir);
if (!manifest) {
console.log(chalk.red("无效的安装 - 未找到清单文件"));
return;
}
console.log(chalk.bold("\norchestrix 安装状态:\n"));
console.log(` 目录: ${installDir}`);
console.log(` 版本: ${manifest.version}`);
console.log(
` 安装日期: ${new Date(
manifest.installed_at
).toLocaleDateString()}`
);
console.log(` 类型: ${manifest.install_type}`);
if (manifest.agent) {
console.log(` 代理: ${manifest.agent}`);
}
if (manifest.ides_setup && manifest.ides_setup.length > 0) {
console.log(` IDE 设置: ${manifest.ides_setup.join(', ')}`);
}
console.log(` 总文件数: ${manifest.files.length}`);
// Check for modifications
const modifiedFiles = await fileManager.checkModifiedFiles(
installDir,
manifest
);
if (modifiedFiles.length > 0) {
console.log(chalk.yellow(` 修改的文件: ${modifiedFiles.length}`));
}
console.log("");
}
async getAvailableAgents() {
return configLoader.getAvailableAgents();
}
async getAvailableExpansionPacks() {
return configLoader.getAvailableExpansionPacks();
}
async getAvailableTeams() {
return configLoader.getAvailableTeams();
}
async installExpansionPacks(installDir, selectedPacks, spinner, config = {}) {
if (!selectedPacks || selectedPacks.length === 0) {
return [];
}
const installedFiles = [];
const glob = require('glob');
for (const packId of selectedPacks) {
spinner.text = `正在安装扩展包: ${packId}...`;
try {
const expansionPacks = await this.getAvailableExpansionPacks();
const pack = expansionPacks.find(p => p.id === packId);
if (!pack) {
console.warn(`未找到扩展包 ${packId},正在跳过...`);
continue;
}
// Check if expansion pack already exists
let expansionDotFolder = path.join(installDir, `.${packId}`);
const existingManifestPath = path.join(expansionDotFolder, 'install-manifest.yaml');
if (await fileManager.pathExists(existingManifestPath)) {
spinner.stop();
const existingManifest = await fileManager.readExpansionPackManifest(installDir, packId);
console.log(chalk.yellow(`\n🔍 发现现有的 ${pack.name} 安装`));
console.log(` 当前版本: ${existingManifest.version || '未知'}`);
console.log(` 新版本: ${pack.version}`);
// Check integrity of existing expansion pack
const packIntegrity = await fileManager.checkFileIntegrity(installDir, existingManifest);
const hasPackIntegrityIssues = packIntegrity.missing.length > 0 || packIntegrity.modified.length > 0;
if (hasPackIntegrityIssues) {
console.log(chalk.red(" ⚠️ 检测到安装问题:"));
if (packIntegrity.missing.length > 0) {
console.log(chalk.red(` 丢失的文件: ${packIntegrity.missing.length}`));
}
if (packIntegrity.modified.length > 0) {
console.log(chalk.yellow(` 修改的文件: ${packIntegrity.modified.length}`));
}
}
const versionCompare = this.compareVersions(existingManifest.version || '0.0.0', pack.version);
if (versionCompare === 0) {
console.log(chalk.yellow(' ⚠️ 已安装相同版本'));
const choices = [];
if (hasPackIntegrityIssues) {
choices.push({ name: '修复 (恢复丢失/修改的文件)', value: 'repair' });
}
choices.push(
{ name: '强制重新安装 (覆盖)', value: 'overwrite' },
{ name: '跳过此扩展包', value: 'skip' },
{ name: '取消安装', value: 'cancel' }
);
const { action } = await inquirer.prompt([{
type: 'list',
name: 'action',
message: `${pack.name} v${pack.version} 已安装。您希望做什么?`,
choices: choices
}]);
if (action === 'skip') {
spinner.start();
continue;
} else if (action === 'cancel') {
console.log(chalk.red('安装已取消。'));
process.exit(0);
} else if (action === 'repair') {
// Repair the expansion pack
await this.repairExpansionPack(installDir, packId, pack, packIntegrity, spinner);
continue;
}
} else if (versionCompare < 0) {
console.log(chalk.cyan(' ⬆️ 有可用升级'));
const { proceed } = await inquirer.prompt([{
type: 'confirm',
name: 'proceed',
message: `将 ${pack.name} 从 v${existingManifest.version} 升级到 v${pack.version}?`,
default: true
}]);
if (!proceed) {
spinner.start();
continue;
}
} else {
console.log(chalk.yellow(' ⬇️ 已安装的版本比可用版本新'));
const { action } = await inquirer.prompt([{
type: 'list',
name: 'action',
message: '您希望做什么?',
choices: [
{ name: '保留当前版本', value: 'skip' },
{ name: '降级到可用版本', value: 'downgrade' },
{ name: '取消安装', value: 'cancel' }
]
}]);
if (action === 'skip') {
spinner.start();
continue;
} else if (action === 'cancel') {
console.log(chalk.red('安装已取消。'));
process.exit(0);
}
}
// If we get here, we're proceeding with installation
spinner.start(`正在移除旧的 ${pack.name} 安装...`);
await fileManager.removeDirectory(expansionDotFolder);
}
const expansionPackDir = pack.packPath;
// Ensure dedicated dot folder exists for this expansion pack
expansionDotFolder = path.join(installDir, `.${packId}`);
await fileManager.ensureDirectory(expansionDotFolder);
// Define the folders to copy from expansion packs
const foldersToSync = [
'agents',
'agent-teams',
'templates',
'tasks',
'checklists',
'workflows',
'data',
'utils',
'schemas'
];
// Copy each folder if it exists
for (const folder of foldersToSync) {
const sourceFolder = path.join(expansionPackDir, folder);
// Check if folder exists in expansion pack
if (await fileManager.pathExists(sourceFolder)) {
// Get all files in this folder
const files = glob.sync('**/*', {
cwd: sourceFolder,
nodir: true
});
// Copy each file to the expansion pack's dot folder with {root} replacement
for (const file of files) {
const sourcePath = path.join(sourceFolder, file);
const destPath = path.join(expansionDotFolder, folder, file);
const needsRootReplacement = file.endsWith('.md') || file.endsWith('.yaml') || file.endsWith('.yml');
let success = false;
if (needsRootReplacement) {
success = await fileManager.copyFileWithRootReplacement(sourcePath, destPath, `.${packId}`);
} else {
success = await fileManager.copyFile(sourcePath, destPath);
}
if (success) {
installedFiles.push(path.join(`.${packId}`, folder, file));
}
}
}
}
// Copy config.yaml with {root} replacement
const configPath = path.join(expansionPackDir, 'config.yaml');
if (await fileManager.pathExists(configPath)) {
const configDestPath = path.join(expansionDotFolder, 'config.yaml');
if (await fileManager.copyFileWithRootReplacement(configPath, configDestPath, `.${packId}`)) {
installedFiles.push(path.join(`.${packId}`, 'config.yaml'));
}
}
// Copy README if it exists with {root} replacement
const readmePath = path.join(expansionPackDir, 'README.md');
if (await fileManager.pathExists(readmePath)) {
const readmeDestPath = path.join(expansionDotFolder, 'README.md');
if (await fileManager.copyFileWithRootReplacement(readmePath, readmeDestPath, `.${packId}`)) {
installedFiles.push(path.join(`.${packId}`, 'README.md'));
}
}
// Copy common/ items to expansion pack folder
spinner.text = `Copying common utilities to ${packId}...`;
await this.copyCommonItems(installDir, `.${packId}`, spinner);
// Check and resolve core dependencies
await this.resolveExpansionPackCoreDependencies(installDir, expansionDotFolder, packId, spinner);
// Check and resolve core agents referenced by teams
await this.resolveExpansionPackCoreAgents(installDir, expansionDotFolder, packId, spinner);
// Create manifest for this expansion pack
spinner.text = `Creating manifest for ${packId}...`;
const expansionConfig = {
installType: 'expansion-pack',
expansionPackId: packId,
expansionPackName: pack.name,
expansionPackVersion: pack.version,
ides: config.ides || [] // Use ides_setup instead of ide_setup
};
// Get all files installed in this expansion pack
const expansionPackFiles = glob.sync('**/*', {
cwd: expansionDotFolder,
nodir: true
}).map(f => path.join(`.${packId}`, f));
await fileManager.createExpansionPackManifest(installDir, packId, expansionConfig, expansionPackFiles);
console.log(chalk.green(`✓ 已安装扩展包: ${pack.name} 到 ${`.${packId}`}`));
} catch (error) {
console.error(chalk.red(`安装扩展包 ${packId} 失败: ${error.message}`));
console.error(chalk.red(`堆栈跟踪: ${error.stack}`));
}
}
return installedFiles;
}
async resolveExpansionPackCoreDependencies(installDir, expansionDotFolder, packId, spinner) {
const glob = require('glob');
const yaml = require('js-yaml');
const fs = require('fs').promises;
// Find all agent files in the expansion pack
const agentFiles = glob.sync('agents/*.md', {
cwd: expansionDotFolder
});
for (const agentFile of agentFiles) {
const agentPath = path.join(expansionDotFolder, agentFile);
const agentContent = await fs.readFile(agentPath, 'utf8');
// Extract YAML frontmatter to check dependencies
const yamlContent = extractYamlFromAgent(agentContent);
if (yamlContent) {
try {
const agentConfig = yaml.load(yamlContent);
const dependencies = agentConfig.dependencies || {};
// Check for core dependencies (those that don't exist in the expansion pack)
for (const depType of ['tasks', 'templates', 'checklists', 'workflows', 'utils', 'data']) {
const deps = dependencies[depType] || [];
for (const dep of deps) {
const depFileName = dep.endsWith('.md') ? dep : `${dep}.md`;
const expansionDepPath = path.join(expansionDotFolder, depType, depFileName);
// Check if dependency exists in expansion pack
if (!(await fileManager.pathExists(expansionDepPath))) {
// Try to find it in core
const coreDepPath = path.join(configLoader.getOrchestrixCorePath(), depType, depFileName);
if (await fileManager.pathExists(coreDepPath)) {
spinner.text = `Copying core dependency ${dep} for ${packId}...`;
// Copy from core to expansion pack dot folder with {root} replacement
const destPath = path.join(expansionDotFolder, depType, depFileName);
await fileManager.copyFileWithRootReplacement(coreDepPath, destPath, `.${packId}`);
console.log(chalk.dim(` Added core dependency: ${depType}/${depFileName}`));
} else {
console.warn(chalk.yellow(` Warning: Dependency ${depType}/${dep} not found in core or expansion pack`));
}
}
}
}
} catch (error) {
console.warn(chalk.yellow(` Warning: Could not parse agent dependencies: ${error.message}`));
}
}
}
}
async resolveExpansionPackCoreAgents(installDir, expansionDotFolder, packId, spinner) {
const glob = require('glob');
const yaml = require('js-yaml');
const fs = require('fs').promises;
// Find all team files in the expansion pack
const teamFiles = glob.sync('agent-teams/*.yaml', {
cwd: expansionDotFolder
});
// Also get existing agents in the expansion pack
const existingAgents = new Set();
const agentFiles = glob.sync('agents/*.md', {
cwd: expansionDotFolder
});
for (const agentFile of agentFiles) {
const agentName = path.basename(agentFile, '.md');
existingAgents.add(agentName);
}
// Process each team file
for (const teamFile of teamFiles) {
const teamPath = path.join(expansionDotFolder, teamFile);
const teamContent = await fs.readFile(teamPath, 'utf8');
try {
const teamConfig = yaml.load(teamContent);
const agents = teamConfig.agents || [];
// Add orchestrix-orchestrator if not present (required for all teams)
if (!agents.includes('orchestrix-orchestrator')) {
agents.unshift('orchestrix-orchestrator');
}
// Check each agent in the team
for (const agentId of agents) {
if (!existingAgents.has(agentId)) {
// Agent not in expansion pack, try to get from core
const coreAgentPath = await findAgentPath(agentId, configLoader.getOrchestrixCorePath());
if (coreAgentPath && await fileManager.pathExists(coreAgentPath)) {
spinner.text = `Copying core agent ${agentId} for ${packId}...`;
// Copy agent file with {root} replacement
const fileExt = path.extname(coreAgentPath);
const destPath = path.join(expansionDotFolder, 'agents', `${agentId}${fileExt}`);
await fileManager.copyFileWithRootReplacement(coreAgentPath, destPath, `.${packId}`);
existingAgents.add(agentId);
console.log(chalk.dim(` Added core agent: ${agentId}`));
// Now resolve this agent's dependencies too
let agentConfig;
if (coreAgentPath.endsWith('.yaml')) {
agentConfig = await loadAgentYaml(coreAgentPath);
} else {
const agentContent = await fs.readFile(coreAgentPath, 'utf8');
const yamlContent = extractYamlFromAgent(agentContent, true);
if (yamlContent) {
agentConfig = yaml.load(yamlContent);
}
}
if (agentConfig) {
try {
const dependencies = agentConfig.dependencies || {};
// Copy all dependencies for this agent
for (const depType of ['tasks', 'templates', 'checklists', 'workflows', 'utils', 'data']) {
const deps = dependencies[depType] || [];
for (const dep of deps) {
const depFileName = dep.endsWith('.md') || dep.endsWith('.yaml') ? dep : `${dep}.md`;
const expansionDepPath = path.join(expansionDotFolder, depType, depFileName);
// Check if dependency exists in expansion pack
if (!(await fileManager.pathExists(expansionDepPath))) {
// Try to find it in core
const coreDepPath = path.join(configLoader.getOrchestrixCorePath(), depType, depFileName);
if (await fileManager.pathExists(coreDepPath)) {
const destDepPath = path.join(expansionDotFolder, depType, depFileName);
await fileManager.copyFileWithRootReplacement(coreDepPath, destDepPath, `.${packId}`);
console.log(chalk.dim(` Added agent dependency: ${depType}/${depFileName}`));
} else {
// Try common f