@sylphx/flow 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@
7
7
  import fs from 'node:fs/promises';
8
8
  import path from 'node:path';
9
9
  import { existsSync } from 'node:fs';
10
+ import { createHash } from 'node:crypto';
10
11
  import chalk from 'chalk';
11
12
  import { ProjectManager } from './project-manager.js';
12
13
  import type { BackupManifest } from './backup-manager.js';
@@ -52,6 +53,18 @@ export class AttachManager {
52
53
  this.configService = new GlobalConfigService();
53
54
  }
54
55
 
56
+ /**
57
+ * Calculate SHA256 hash of file content
58
+ */
59
+ private async calculateFileHash(filePath: string): Promise<string> {
60
+ try {
61
+ const content = await fs.readFile(filePath, 'utf-8');
62
+ return createHash('sha256').update(content).digest('hex');
63
+ } catch {
64
+ return '';
65
+ }
66
+ }
67
+
55
68
  /**
56
69
  * Get target-specific directory names
57
70
  */
@@ -369,7 +382,7 @@ ${rules}
369
382
  // Track in manifest
370
383
  manifest.backup.config = {
371
384
  path: configPath,
372
- hash: '', // TODO: calculate hash
385
+ hash: await this.calculateFileHash(configPath),
373
386
  mcpServersCount: Object.keys(config.mcp.servers).length,
374
387
  };
375
388
  }
@@ -257,60 +257,3 @@ export async function installFile(
257
257
  }
258
258
  }
259
259
 
260
- /**
261
- * File installer interface for backward compatibility
262
- */
263
- export interface FileInstaller {
264
- installToDirectory(
265
- sourceDir: string,
266
- targetDir: string,
267
- transform: FileTransformFn,
268
- options?: InstallOptions
269
- ): Promise<ProcessResult[]>;
270
- appendToFile(
271
- sourceDir: string,
272
- targetFile: string,
273
- transform: FileTransformFn,
274
- options?: InstallOptions
275
- ): Promise<void>;
276
- installFile(
277
- sourceFile: string,
278
- targetFile: string,
279
- transform: FileTransformFn,
280
- options?: InstallOptions
281
- ): Promise<void>;
282
- }
283
-
284
- /**
285
- * Composable file installer
286
- * Handles copying files from source to destination with optional transformation
287
- * @deprecated Use standalone functions (installToDirectory, appendToFile, installFile) for new code
288
- */
289
- export class FileInstaller {
290
- async installToDirectory(
291
- sourceDir: string,
292
- targetDir: string,
293
- transform: FileTransformFn,
294
- options: InstallOptions = {}
295
- ): Promise<ProcessResult[]> {
296
- return installToDirectory(sourceDir, targetDir, transform, options);
297
- }
298
-
299
- async appendToFile(
300
- sourceDir: string,
301
- targetFile: string,
302
- transform: FileTransformFn,
303
- options: InstallOptions = {}
304
- ): Promise<void> {
305
- return appendToFile(sourceDir, targetFile, transform, options);
306
- }
307
-
308
- async installFile(
309
- sourceFile: string,
310
- targetFile: string,
311
- transform: FileTransformFn,
312
- options: InstallOptions = {}
313
- ): Promise<void> {
314
- return installFile(sourceFile, targetFile, transform, options);
315
- }
316
- }
@@ -178,36 +178,3 @@ export function createMCPInstaller(target: Target): MCPInstaller {
178
178
  };
179
179
  }
180
180
 
181
- /**
182
- * @deprecated Use createMCPInstaller() for new code
183
- */
184
- export class MCPInstaller {
185
- private installer: ReturnType<typeof createMCPInstaller>;
186
-
187
- constructor(target: Target) {
188
- this.installer = createMCPInstaller(target);
189
- }
190
-
191
- async selectServers(options: { quiet?: boolean } = {}): Promise<MCPServerID[]> {
192
- return this.installer.selectServers(options);
193
- }
194
-
195
- async configureServers(
196
- selectedServers: MCPServerID[],
197
- options: { quiet?: boolean } = {}
198
- ): Promise<Record<MCPServerID, Record<string, string>>> {
199
- return this.installer.configureServers(selectedServers, options);
200
- }
201
-
202
- async installServers(
203
- selectedServers: MCPServerID[],
204
- serverConfigsMap: Record<MCPServerID, Record<string, string>>,
205
- options: { quiet?: boolean } = {}
206
- ): Promise<void> {
207
- return this.installer.installServers(selectedServers, serverConfigsMap, options);
208
- }
209
-
210
- async setupMCP(options: { quiet?: boolean; dryRun?: boolean } = {}): Promise<MCPInstallResult> {
211
- return this.installer.setupMCP(options);
212
- }
213
- }
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Sylphx Flow - Legacy CLI
4
- * Project initialization and development flow management
3
+ * Sylphx Flow CLI
4
+ * AI-powered development flow management
5
5
  */
6
6
 
7
7
  import { readFileSync } from 'node:fs';
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Auto-Upgrade Service
3
+ * Automatically checks and upgrades Flow and target CLI before each execution
4
+ */
5
+
6
+ import { exec } from 'node:child_process';
7
+ import { promisify } from 'node:util';
8
+ import fs from 'node:fs/promises';
9
+ import path from 'node:path';
10
+ import chalk from 'chalk';
11
+ import ora from 'ora';
12
+ import { detectPackageManager, getUpgradeCommand } from '../utils/package-manager-detector.js';
13
+ import { TargetInstaller } from './target-installer.js';
14
+
15
+ const execAsync = promisify(exec);
16
+
17
+ export interface UpgradeStatus {
18
+ flowNeedsUpgrade: boolean;
19
+ targetNeedsUpgrade: boolean;
20
+ flowVersion: { current: string; latest: string } | null;
21
+ targetVersion: { current: string; latest: string } | null;
22
+ }
23
+
24
+ export interface AutoUpgradeOptions {
25
+ verbose?: boolean;
26
+ skipFlow?: boolean;
27
+ skipTarget?: boolean;
28
+ }
29
+
30
+ export class AutoUpgrade {
31
+ private projectPath: string;
32
+ private options: AutoUpgradeOptions;
33
+ private targetInstaller: TargetInstaller;
34
+
35
+ constructor(projectPath: string = process.cwd(), options: AutoUpgradeOptions = {}) {
36
+ this.projectPath = projectPath;
37
+ this.options = options;
38
+ this.targetInstaller = new TargetInstaller(projectPath);
39
+ }
40
+
41
+ /**
42
+ * Check for available upgrades for Flow and target CLI
43
+ * @param targetId - Optional target CLI ID to check for upgrades
44
+ * @returns Upgrade status indicating what needs upgrading
45
+ */
46
+ async checkForUpgrades(targetId?: string): Promise<UpgradeStatus> {
47
+ const [flowVersion, targetVersion] = await Promise.all([
48
+ this.options.skipFlow ? null : this.checkFlowVersion(),
49
+ this.options.skipTarget || !targetId ? null : this.checkTargetVersion(targetId),
50
+ ]);
51
+
52
+ return {
53
+ flowNeedsUpgrade: !!flowVersion,
54
+ targetNeedsUpgrade: !!targetVersion,
55
+ flowVersion,
56
+ targetVersion,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Check Flow version
62
+ */
63
+ private async checkFlowVersion(): Promise<{ current: string; latest: string } | null> {
64
+ try {
65
+ // Get current version from package.json
66
+ const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
67
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
68
+ const currentVersion = packageJson.version;
69
+
70
+ // Get latest version from npm
71
+ const { stdout } = await execAsync('npm view @sylphx/flow version');
72
+ const latestVersion = stdout.trim();
73
+
74
+ if (currentVersion !== latestVersion) {
75
+ return { current: currentVersion, latest: latestVersion };
76
+ }
77
+
78
+ return null;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Check target CLI version
86
+ */
87
+ private async checkTargetVersion(
88
+ targetId: string
89
+ ): Promise<{ current: string; latest: string } | null> {
90
+ const installation = this.targetInstaller.getInstallationInfo(targetId);
91
+ if (!installation) {
92
+ return null;
93
+ }
94
+
95
+ try {
96
+ // Get current version
97
+ const { stdout: currentOutput } = await execAsync(installation.checkCommand);
98
+ const currentMatch = currentOutput.match(/v?(\d+\.\d+\.\d+)/);
99
+ if (!currentMatch) {
100
+ return null;
101
+ }
102
+ const currentVersion = currentMatch[1];
103
+
104
+ // Get latest version from npm
105
+ const { stdout: latestOutput } = await execAsync(`npm view ${installation.package} version`);
106
+ const latestVersion = latestOutput.trim();
107
+
108
+ if (currentVersion !== latestVersion) {
109
+ return { current: currentVersion, latest: latestVersion };
110
+ }
111
+
112
+ return null;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Upgrade Flow to latest version using detected package manager
120
+ * @returns True if upgrade successful, false otherwise
121
+ */
122
+ async upgradeFlow(): Promise<boolean> {
123
+ const packageManager = detectPackageManager(this.projectPath);
124
+ const spinner = ora('Upgrading Flow...').start();
125
+
126
+ try {
127
+ const upgradeCmd = getUpgradeCommand('@sylphx/flow', packageManager);
128
+ await execAsync(upgradeCmd);
129
+
130
+ spinner.succeed(chalk.green('✓ Flow upgraded to latest version'));
131
+ return true;
132
+ } catch (error) {
133
+ spinner.fail(chalk.red('✗ Flow upgrade failed'));
134
+
135
+ if (this.options.verbose) {
136
+ console.error(error);
137
+ }
138
+
139
+ return false;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Upgrade target CLI to latest version
145
+ * Tries built-in upgrade command first, falls back to package manager
146
+ * @param targetId - Target CLI ID to upgrade
147
+ * @returns True if upgrade successful, false otherwise
148
+ */
149
+ async upgradeTarget(targetId: string): Promise<boolean> {
150
+ const installation = this.targetInstaller.getInstallationInfo(targetId);
151
+ if (!installation) {
152
+ return false;
153
+ }
154
+
155
+ const packageManager = detectPackageManager(this.projectPath);
156
+ const spinner = ora(`Upgrading ${installation.name}...`).start();
157
+
158
+ try {
159
+ // For Claude Code, use built-in update command if available
160
+ if (targetId === 'claude-code') {
161
+ try {
162
+ await execAsync('claude update');
163
+ spinner.succeed(chalk.green(`✓ ${installation.name} upgraded`));
164
+ return true;
165
+ } catch {
166
+ // Fall back to npm upgrade
167
+ }
168
+ }
169
+
170
+ // For OpenCode, use built-in upgrade command if available
171
+ if (targetId === 'opencode') {
172
+ try {
173
+ await execAsync('opencode upgrade');
174
+ spinner.succeed(chalk.green(`✓ ${installation.name} upgraded`));
175
+ return true;
176
+ } catch {
177
+ // Fall back to npm upgrade
178
+ }
179
+ }
180
+
181
+ // Fall back to npm/bun/pnpm/yarn upgrade
182
+ const upgradeCmd = getUpgradeCommand(installation.package, packageManager);
183
+ await execAsync(upgradeCmd);
184
+
185
+ spinner.succeed(chalk.green(`✓ ${installation.name} upgraded`));
186
+ return true;
187
+ } catch (error) {
188
+ spinner.fail(chalk.red(`✗ ${installation.name} upgrade failed`));
189
+
190
+ if (this.options.verbose) {
191
+ console.error(error);
192
+ }
193
+
194
+ return false;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Run auto-upgrade check and upgrade if needed
200
+ * Shows upgrade status and performs upgrades automatically
201
+ * @param targetId - Optional target CLI ID to check and upgrade
202
+ */
203
+ async runAutoUpgrade(targetId?: string): Promise<void> {
204
+ console.log(chalk.cyan('🔄 Checking for updates...\n'));
205
+
206
+ const status = await this.checkForUpgrades(targetId);
207
+
208
+ // Show upgrade status
209
+ if (status.flowNeedsUpgrade && status.flowVersion) {
210
+ console.log(
211
+ chalk.yellow(
212
+ `📦 Flow update available: ${status.flowVersion.current} → ${status.flowVersion.latest}`
213
+ )
214
+ );
215
+ } else if (!this.options.skipFlow) {
216
+ console.log(chalk.green('✓ Flow is up to date'));
217
+ }
218
+
219
+ if (status.targetNeedsUpgrade && status.targetVersion && targetId) {
220
+ const installation = this.targetInstaller.getInstallationInfo(targetId);
221
+ console.log(
222
+ chalk.yellow(
223
+ `📦 ${installation?.name} update available: ${status.targetVersion.current} → ${status.targetVersion.latest}`
224
+ )
225
+ );
226
+ } else if (!this.options.skipTarget && targetId) {
227
+ const installation = this.targetInstaller.getInstallationInfo(targetId);
228
+ console.log(chalk.green(`✓ ${installation?.name} is up to date`));
229
+ }
230
+
231
+ // Perform upgrades if needed
232
+ if (status.flowNeedsUpgrade || status.targetNeedsUpgrade) {
233
+ console.log(chalk.cyan('\n📦 Installing updates...\n'));
234
+
235
+ if (status.flowNeedsUpgrade) {
236
+ await this.upgradeFlow();
237
+ }
238
+
239
+ if (status.targetNeedsUpgrade && targetId) {
240
+ await this.upgradeTarget(targetId);
241
+ }
242
+
243
+ console.log(chalk.green('\n✓ All tools upgraded\n'));
244
+ } else {
245
+ console.log();
246
+ }
247
+ }
248
+ }
@@ -10,7 +10,7 @@ import { existsSync } from 'node:fs';
10
10
 
11
11
  export interface GlobalSettings {
12
12
  version: string;
13
- defaultTarget?: 'claude-code' | 'opencode';
13
+ defaultTarget?: 'claude-code' | 'opencode' | 'cursor' | 'ask-every-time';
14
14
  defaultAgent?: string; // Default agent to use (e.g., 'coder', 'writer', 'reviewer', 'orchestrator')
15
15
  firstRun: boolean;
16
16
  lastUpdated: string;
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Target Installation Service
3
+ * Auto-detects and installs AI CLI tools (Claude Code, OpenCode, Cursor)
4
+ */
5
+
6
+ import { exec } from 'node:child_process';
7
+ import { promisify } from 'node:util';
8
+ import chalk from 'chalk';
9
+ import ora from 'ora';
10
+ import inquirer from 'inquirer';
11
+ import { detectPackageManager, type PackageManager } from '../utils/package-manager-detector.js';
12
+ import { UserCancelledError } from '../utils/errors.js';
13
+
14
+ const execAsync = promisify(exec);
15
+
16
+ export interface TargetInstallation {
17
+ id: 'claude-code' | 'opencode' | 'cursor';
18
+ name: string;
19
+ package: string;
20
+ checkCommand: string;
21
+ installCommand: (pm: PackageManager) => string;
22
+ }
23
+
24
+ /**
25
+ * Supported target installations
26
+ */
27
+ const TARGET_INSTALLATIONS: TargetInstallation[] = [
28
+ {
29
+ id: 'claude-code',
30
+ name: 'Claude Code',
31
+ package: '@anthropic-ai/claude-code',
32
+ checkCommand: 'claude --version',
33
+ installCommand: (pm: PackageManager) => {
34
+ switch (pm) {
35
+ case 'npm':
36
+ return 'npm install -g @anthropic-ai/claude-code';
37
+ case 'bun':
38
+ return 'bun install -g @anthropic-ai/claude-code';
39
+ case 'pnpm':
40
+ return 'pnpm install -g @anthropic-ai/claude-code';
41
+ case 'yarn':
42
+ return 'yarn global add @anthropic-ai/claude-code';
43
+ }
44
+ },
45
+ },
46
+ {
47
+ id: 'opencode',
48
+ name: 'OpenCode',
49
+ package: 'opencode-ai',
50
+ checkCommand: 'opencode --version',
51
+ installCommand: (pm: PackageManager) => {
52
+ switch (pm) {
53
+ case 'npm':
54
+ return 'npm install -g opencode-ai@latest';
55
+ case 'bun':
56
+ return 'bun install -g opencode-ai@latest';
57
+ case 'pnpm':
58
+ return 'pnpm install -g opencode-ai@latest';
59
+ case 'yarn':
60
+ return 'yarn global add opencode-ai@latest';
61
+ }
62
+ },
63
+ },
64
+ {
65
+ id: 'cursor',
66
+ name: 'Cursor',
67
+ package: 'cursor',
68
+ checkCommand: 'cursor --version',
69
+ installCommand: () => {
70
+ // Cursor is typically installed via installer, not npm
71
+ return 'Visit https://cursor.sh to download and install';
72
+ },
73
+ },
74
+ ];
75
+
76
+ export class TargetInstaller {
77
+ private packageManager: PackageManager;
78
+
79
+ constructor(projectPath: string = process.cwd()) {
80
+ this.packageManager = detectPackageManager(projectPath);
81
+ }
82
+
83
+ /**
84
+ * Check if a target CLI is installed
85
+ * @param targetId - Target ID to check
86
+ * @returns True if installed, false otherwise
87
+ */
88
+ async isInstalled(targetId: string): Promise<boolean> {
89
+ const installation = TARGET_INSTALLATIONS.find((t) => t.id === targetId);
90
+ if (!installation) {
91
+ return false;
92
+ }
93
+
94
+ try {
95
+ await execAsync(installation.checkCommand);
96
+ return true;
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Detect which target CLIs are currently installed
104
+ * @returns Array of installed target IDs
105
+ */
106
+ async detectInstalledTargets(): Promise<string[]> {
107
+ const installed: string[] = [];
108
+
109
+ for (const installation of TARGET_INSTALLATIONS) {
110
+ if (await this.isInstalled(installation.id)) {
111
+ installed.push(installation.id);
112
+ }
113
+ }
114
+
115
+ return installed;
116
+ }
117
+
118
+ /**
119
+ * Prompt user to select a target to install
120
+ * @returns Selected target ID
121
+ * @throws {UserCancelledError} If user cancels the prompt
122
+ */
123
+ async promptForTargetSelection(): Promise<string> {
124
+ try {
125
+ const { targetId } = await inquirer.prompt([
126
+ {
127
+ type: 'list',
128
+ name: 'targetId',
129
+ message: 'No AI CLI detected. Which would you like to use?',
130
+ choices: TARGET_INSTALLATIONS.map((t) => ({
131
+ name: t.name,
132
+ value: t.id,
133
+ })),
134
+ default: 'claude-code',
135
+ },
136
+ ]);
137
+
138
+ return targetId;
139
+ } catch (error: any) {
140
+ // Handle user cancellation (Ctrl+C)
141
+ if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
142
+ throw new UserCancelledError('Target selection cancelled');
143
+ }
144
+ throw error;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Install a target CLI using detected package manager
150
+ * @param targetId - Target ID to install
151
+ * @param autoConfirm - Skip confirmation prompt if true
152
+ * @returns True if installation successful, false otherwise
153
+ * @throws {UserCancelledError} If user cancels installation
154
+ */
155
+ async install(targetId: string, autoConfirm: boolean = false): Promise<boolean> {
156
+ const installation = TARGET_INSTALLATIONS.find((t) => t.id === targetId);
157
+ if (!installation) {
158
+ console.log(chalk.red(`✗ Unknown target: ${targetId}`));
159
+ return false;
160
+ }
161
+
162
+ // Special handling for Cursor (not npm-installable)
163
+ if (targetId === 'cursor') {
164
+ console.log(chalk.yellow('\n⚠️ Cursor requires manual installation'));
165
+ console.log(chalk.cyan(' Visit https://cursor.sh to download and install\n'));
166
+ return false;
167
+ }
168
+
169
+ // Confirm installation unless auto-confirm is enabled
170
+ if (!autoConfirm) {
171
+ try {
172
+ const { confirmInstall } = await inquirer.prompt([
173
+ {
174
+ type: 'confirm',
175
+ name: 'confirmInstall',
176
+ message: `Install ${installation.name}?`,
177
+ default: true,
178
+ },
179
+ ]);
180
+
181
+ if (!confirmInstall) {
182
+ console.log(chalk.yellow('\n⚠️ Installation cancelled\n'));
183
+ return false;
184
+ }
185
+ } catch (error: any) {
186
+ // Handle user cancellation (Ctrl+C)
187
+ if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
188
+ throw new UserCancelledError('Installation cancelled');
189
+ }
190
+ throw error;
191
+ }
192
+ }
193
+
194
+ const spinner = ora(`Installing ${installation.name}...`).start();
195
+
196
+ try {
197
+ const installCmd = installation.installCommand(this.packageManager);
198
+ await execAsync(installCmd);
199
+
200
+ spinner.succeed(chalk.green(`✓ ${installation.name} installed successfully`));
201
+ return true;
202
+ } catch (error) {
203
+ spinner.fail(chalk.red(`✗ Failed to install ${installation.name}`));
204
+
205
+ const installCmd = installation.installCommand(this.packageManager);
206
+ console.log(chalk.yellow('\n⚠️ Auto-install failed. Please run manually:'));
207
+ console.log(chalk.cyan(` ${installCmd}\n`));
208
+
209
+ return false;
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Auto-detect installed targets or prompt to install one
215
+ * @returns Target ID if found or installed, null if installation failed
216
+ * @throws {UserCancelledError} If user cancels selection or installation
217
+ */
218
+ async autoDetectAndInstall(): Promise<string | null> {
219
+ console.log(chalk.cyan('🔍 Detecting installed AI CLIs...\n'));
220
+
221
+ const installedTargets = await this.detectInstalledTargets();
222
+
223
+ // If we found installed targets, return the first one (priority order)
224
+ if (installedTargets.length > 0) {
225
+ const targetId = installedTargets[0];
226
+ const installation = TARGET_INSTALLATIONS.find((t) => t.id === targetId);
227
+ console.log(chalk.green(`✓ Found ${installation?.name}\n`));
228
+ return targetId;
229
+ }
230
+
231
+ // No targets found - prompt user to select one
232
+ console.log(chalk.yellow('⚠️ No AI CLI detected\n'));
233
+ const selectedTargetId = await this.promptForTargetSelection();
234
+
235
+ // Try to install the selected target
236
+ console.log();
237
+ const installed = await this.install(selectedTargetId, false);
238
+
239
+ if (!installed) {
240
+ return null;
241
+ }
242
+
243
+ return selectedTargetId;
244
+ }
245
+
246
+ /**
247
+ * Get installation metadata for a target
248
+ * @param targetId - Target ID to get info for
249
+ * @returns Installation info or undefined if target not found
250
+ */
251
+ getInstallationInfo(targetId: string): TargetInstallation | undefined {
252
+ return TARGET_INSTALLATIONS.find((t) => t.id === targetId);
253
+ }
254
+ }
@@ -3,8 +3,8 @@ import fs from 'node:fs';
3
3
  import fsPromises from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import chalk from 'chalk';
6
- import { FileInstaller } from '../core/installers/file-installer.js';
7
- import { MCPInstaller } from '../core/installers/mcp-installer.js';
6
+ import { installToDirectory } from '../core/installers/file-installer.js';
7
+ import { createMCPInstaller } from '../core/installers/mcp-installer.js';
8
8
  import type { AgentMetadata } from '../types/target-config.types.js';
9
9
  import type { CommonOptions, MCPServerConfigUnion, SetupResult, Target } from '../types.js';
10
10
  import { CLIError } from '../utils/error-handler.js';
@@ -432,10 +432,9 @@ Please begin your response with a comprehensive summary of all the instructions
432
432
  */
433
433
  async setupAgents(cwd: string, options: CommonOptions): Promise<SetupResult> {
434
434
  const { enhanceAgentContent } = await import('../utils/agent-enhancer.js');
435
- const installer = new FileInstaller();
436
435
  const agentsDir = path.join(cwd, this.config.agentDir);
437
436
 
438
- const results = await installer.installToDirectory(
437
+ const results = await installToDirectory(
439
438
  getAgentsDir(),
440
439
  agentsDir,
441
440
  async (content, sourcePath) => {
@@ -491,7 +490,7 @@ Please begin your response with a comprehensive summary of all the instructions
491
490
  * Select, configure, install, and approve MCP servers
492
491
  */
493
492
  async setupMCP(cwd: string, options: CommonOptions): Promise<SetupResult> {
494
- const installer = new MCPInstaller(this);
493
+ const installer = createMCPInstaller(this);
495
494
  const result = await installer.setupMCP({ ...options, quiet: true });
496
495
 
497
496
  // Approve servers in Claude Code settings
@@ -513,10 +512,9 @@ Please begin your response with a comprehensive summary of all the instructions
513
512
  return { count: 0 };
514
513
  }
515
514
 
516
- const installer = new FileInstaller();
517
515
  const slashCommandsDir = path.join(cwd, this.config.slashCommandsDir);
518
516
 
519
- const results = await installer.installToDirectory(
517
+ const results = await installToDirectory(
520
518
  getSlashCommandsDir(),
521
519
  slashCommandsDir,
522
520
  async (content) => {