@sylphx/flow 1.8.2 → 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.
Files changed (37) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/UPGRADE.md +151 -0
  3. package/package.json +11 -6
  4. package/src/commands/flow/execute-v2.ts +372 -0
  5. package/src/commands/flow/execute.ts +1 -18
  6. package/src/commands/flow/types.ts +3 -2
  7. package/src/commands/flow-command.ts +32 -69
  8. package/src/commands/flow-orchestrator.ts +18 -55
  9. package/src/commands/run-command.ts +12 -6
  10. package/src/commands/settings-command.ts +536 -0
  11. package/src/config/ai-config.ts +2 -69
  12. package/src/config/targets.ts +0 -11
  13. package/src/core/attach-manager.ts +495 -0
  14. package/src/core/backup-manager.ts +308 -0
  15. package/src/core/cleanup-handler.ts +166 -0
  16. package/src/core/flow-executor.ts +323 -0
  17. package/src/core/git-stash-manager.ts +133 -0
  18. package/src/core/installers/file-installer.ts +0 -57
  19. package/src/core/installers/mcp-installer.ts +0 -33
  20. package/src/core/project-manager.ts +274 -0
  21. package/src/core/secrets-manager.ts +229 -0
  22. package/src/core/session-manager.ts +268 -0
  23. package/src/core/template-loader.ts +189 -0
  24. package/src/core/upgrade-manager.ts +79 -47
  25. package/src/index.ts +15 -29
  26. package/src/services/auto-upgrade.ts +248 -0
  27. package/src/services/first-run-setup.ts +220 -0
  28. package/src/services/global-config.ts +337 -0
  29. package/src/services/target-installer.ts +254 -0
  30. package/src/targets/claude-code.ts +5 -7
  31. package/src/targets/opencode.ts +6 -26
  32. package/src/utils/__tests__/package-manager-detector.test.ts +163 -0
  33. package/src/utils/agent-enhancer.ts +40 -22
  34. package/src/utils/errors.ts +9 -0
  35. package/src/utils/package-manager-detector.ts +139 -0
  36. package/src/utils/prompt-helpers.ts +48 -0
  37. package/src/utils/target-selection.ts +169 -0
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Global Configuration Service
3
+ * Manages all Flow settings in ~/.sylphx-flow/
4
+ */
5
+
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import { existsSync } from 'node:fs';
10
+
11
+ export interface GlobalSettings {
12
+ version: string;
13
+ defaultTarget?: 'claude-code' | 'opencode' | 'cursor' | 'ask-every-time';
14
+ defaultAgent?: string; // Default agent to use (e.g., 'coder', 'writer', 'reviewer', 'orchestrator')
15
+ firstRun: boolean;
16
+ lastUpdated: string;
17
+ }
18
+
19
+ export interface AgentConfig {
20
+ enabled: boolean;
21
+ }
22
+
23
+ export interface RuleConfig {
24
+ enabled: boolean;
25
+ }
26
+
27
+ export interface OutputStyleConfig {
28
+ enabled: boolean;
29
+ }
30
+
31
+ export interface FlowConfig {
32
+ version: string;
33
+ agents: Record<string, AgentConfig>; // e.g., { coder: { enabled: true }, writer: { enabled: false } }
34
+ rules: Record<string, RuleConfig>; // e.g., { core: { enabled: true }, workspace: { enabled: true } }
35
+ outputStyles: Record<string, OutputStyleConfig>; // e.g., { silent: { enabled: true } }
36
+ }
37
+
38
+ export interface ProviderConfig {
39
+ claudeCode: {
40
+ defaultProvider: 'default' | 'kimi' | 'zai' | 'ask-every-time';
41
+ providers: {
42
+ kimi?: {
43
+ apiKey?: string;
44
+ enabled: boolean;
45
+ };
46
+ zai?: {
47
+ apiKey?: string;
48
+ enabled: boolean;
49
+ };
50
+ };
51
+ };
52
+ }
53
+
54
+ export interface MCPServerConfig {
55
+ enabled: boolean;
56
+ command?: string;
57
+ args?: string[];
58
+ env?: Record<string, string>;
59
+ }
60
+
61
+ export interface MCPConfig {
62
+ version: string;
63
+ servers: Record<string, MCPServerConfig>;
64
+ }
65
+
66
+ export class GlobalConfigService {
67
+ private flowHomeDir: string;
68
+
69
+ constructor() {
70
+ this.flowHomeDir = path.join(os.homedir(), '.sylphx-flow');
71
+ }
72
+
73
+ /**
74
+ * Get Flow home directory
75
+ */
76
+ getFlowHomeDir(): string {
77
+ return this.flowHomeDir;
78
+ }
79
+
80
+ /**
81
+ * Initialize Flow home directory structure
82
+ */
83
+ async initialize(): Promise<void> {
84
+ const dirs = [
85
+ this.flowHomeDir,
86
+ path.join(this.flowHomeDir, 'sessions'),
87
+ path.join(this.flowHomeDir, 'backups'),
88
+ path.join(this.flowHomeDir, 'secrets'),
89
+ ];
90
+
91
+ for (const dir of dirs) {
92
+ await fs.mkdir(dir, { recursive: true });
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get global settings file path
98
+ */
99
+ private getSettingsPath(): string {
100
+ return path.join(this.flowHomeDir, 'settings.json');
101
+ }
102
+
103
+ /**
104
+ * Get provider config file path
105
+ */
106
+ private getProviderConfigPath(): string {
107
+ return path.join(this.flowHomeDir, 'provider-config.json');
108
+ }
109
+
110
+ /**
111
+ * Get MCP config file path
112
+ */
113
+ private getMCPConfigPath(): string {
114
+ return path.join(this.flowHomeDir, 'mcp-config.json');
115
+ }
116
+
117
+ /**
118
+ * Get Flow config file path
119
+ */
120
+ private getFlowConfigPath(): string {
121
+ return path.join(this.flowHomeDir, 'flow-config.json');
122
+ }
123
+
124
+ /**
125
+ * Check if this is first run
126
+ */
127
+ async isFirstRun(): Promise<boolean> {
128
+ const settingsPath = this.getSettingsPath();
129
+ if (!existsSync(settingsPath)) {
130
+ return true;
131
+ }
132
+
133
+ try {
134
+ const settings = await this.loadSettings();
135
+ return settings.firstRun !== false;
136
+ } catch {
137
+ return true;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Load global settings
143
+ */
144
+ async loadSettings(): Promise<GlobalSettings> {
145
+ const settingsPath = this.getSettingsPath();
146
+
147
+ if (!existsSync(settingsPath)) {
148
+ return {
149
+ version: '1.0.0',
150
+ firstRun: true,
151
+ lastUpdated: new Date().toISOString(),
152
+ };
153
+ }
154
+
155
+ const data = await fs.readFile(settingsPath, 'utf-8');
156
+ return JSON.parse(data);
157
+ }
158
+
159
+ /**
160
+ * Save global settings
161
+ */
162
+ async saveSettings(settings: GlobalSettings): Promise<void> {
163
+ await this.initialize();
164
+ const settingsPath = this.getSettingsPath();
165
+ settings.lastUpdated = new Date().toISOString();
166
+ await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
167
+ }
168
+
169
+ /**
170
+ * Load provider config
171
+ */
172
+ async loadProviderConfig(): Promise<ProviderConfig> {
173
+ const configPath = this.getProviderConfigPath();
174
+
175
+ if (!existsSync(configPath)) {
176
+ return {
177
+ claudeCode: {
178
+ defaultProvider: 'ask-every-time',
179
+ providers: {
180
+ kimi: { enabled: false },
181
+ zai: { enabled: false },
182
+ },
183
+ },
184
+ };
185
+ }
186
+
187
+ const data = await fs.readFile(configPath, 'utf-8');
188
+ return JSON.parse(data);
189
+ }
190
+
191
+ /**
192
+ * Save provider config
193
+ */
194
+ async saveProviderConfig(config: ProviderConfig): Promise<void> {
195
+ await this.initialize();
196
+ const configPath = this.getProviderConfigPath();
197
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
198
+ }
199
+
200
+ /**
201
+ * Load MCP config
202
+ */
203
+ async loadMCPConfig(): Promise<MCPConfig> {
204
+ const configPath = this.getMCPConfigPath();
205
+
206
+ if (!existsSync(configPath)) {
207
+ return {
208
+ version: '1.0.0',
209
+ servers: {},
210
+ };
211
+ }
212
+
213
+ const data = await fs.readFile(configPath, 'utf-8');
214
+ return JSON.parse(data);
215
+ }
216
+
217
+ /**
218
+ * Save MCP config
219
+ */
220
+ async saveMCPConfig(config: MCPConfig): Promise<void> {
221
+ await this.initialize();
222
+ const configPath = this.getMCPConfigPath();
223
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
224
+ }
225
+
226
+ /**
227
+ * Get enabled MCP servers
228
+ */
229
+ async getEnabledMCPServers(): Promise<Record<string, MCPServerConfig>> {
230
+ const config = await this.loadMCPConfig();
231
+ const enabled: Record<string, MCPServerConfig> = {};
232
+
233
+ for (const [name, serverConfig] of Object.entries(config.servers)) {
234
+ if (serverConfig.enabled) {
235
+ enabled[name] = serverConfig;
236
+ }
237
+ }
238
+
239
+ return enabled;
240
+ }
241
+
242
+ /**
243
+ * Load Flow config (agents, rules, output styles)
244
+ */
245
+ async loadFlowConfig(): Promise<FlowConfig> {
246
+ const configPath = this.getFlowConfigPath();
247
+
248
+ if (!existsSync(configPath)) {
249
+ // Default: all agents, all rules, all output styles enabled
250
+ return {
251
+ version: '1.0.0',
252
+ agents: {
253
+ coder: { enabled: true },
254
+ writer: { enabled: true },
255
+ reviewer: { enabled: true },
256
+ orchestrator: { enabled: true },
257
+ },
258
+ rules: {
259
+ core: { enabled: true },
260
+ 'code-standards': { enabled: true },
261
+ workspace: { enabled: true },
262
+ },
263
+ outputStyles: {
264
+ silent: { enabled: true },
265
+ },
266
+ };
267
+ }
268
+
269
+ const data = await fs.readFile(configPath, 'utf-8');
270
+ return JSON.parse(data);
271
+ }
272
+
273
+ /**
274
+ * Save Flow config
275
+ */
276
+ async saveFlowConfig(config: FlowConfig): Promise<void> {
277
+ await this.initialize();
278
+ const configPath = this.getFlowConfigPath();
279
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
280
+ }
281
+
282
+ /**
283
+ * Get enabled agents
284
+ */
285
+ async getEnabledAgents(): Promise<string[]> {
286
+ const config = await this.loadFlowConfig();
287
+ return Object.entries(config.agents)
288
+ .filter(([_, agentConfig]) => agentConfig.enabled)
289
+ .map(([name]) => name);
290
+ }
291
+
292
+ /**
293
+ * Get enabled rules
294
+ */
295
+ async getEnabledRules(): Promise<string[]> {
296
+ const config = await this.loadFlowConfig();
297
+ return Object.entries(config.rules)
298
+ .filter(([_, ruleConfig]) => ruleConfig.enabled)
299
+ .map(([name]) => name);
300
+ }
301
+
302
+ /**
303
+ * Get enabled output styles
304
+ */
305
+ async getEnabledOutputStyles(): Promise<string[]> {
306
+ const config = await this.loadFlowConfig();
307
+ return Object.entries(config.outputStyles)
308
+ .filter(([_, styleConfig]) => styleConfig.enabled)
309
+ .map(([name]) => name);
310
+ }
311
+
312
+ /**
313
+ * Update default target
314
+ */
315
+ async setDefaultTarget(target: 'claude-code' | 'opencode'): Promise<void> {
316
+ const settings = await this.loadSettings();
317
+ settings.defaultTarget = target;
318
+ await this.saveSettings(settings);
319
+ }
320
+
321
+ /**
322
+ * Get default target
323
+ */
324
+ async getDefaultTarget(): Promise<'claude-code' | 'opencode' | undefined> {
325
+ const settings = await this.loadSettings();
326
+ return settings.defaultTarget;
327
+ }
328
+
329
+ /**
330
+ * Mark first run as complete
331
+ */
332
+ async markFirstRunComplete(): Promise<void> {
333
+ const settings = await this.loadSettings();
334
+ settings.firstRun = false;
335
+ await this.saveSettings(settings);
336
+ }
337
+ }
@@ -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) => {
@@ -3,8 +3,8 @@ import path from 'node:path';
3
3
  import chalk from 'chalk';
4
4
  import { getRulesPath, ruleFileExists } from '../config/rules.js';
5
5
  import { MCP_SERVER_REGISTRY } from '../config/servers.js';
6
- import { FileInstaller } from '../core/installers/file-installer.js';
7
- import { MCPInstaller } from '../core/installers/mcp-installer.js';
6
+ import { installToDirectory, installFile } 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 { getAgentsDir, getOutputStylesDir, getSlashCommandsDir } from '../utils/config/paths.js';
@@ -232,19 +232,9 @@ export const opencodeTarget: Target = {
232
232
  * Install agents to .opencode/agent/ directory
233
233
  */
234
234
  async setupAgents(cwd: string, options: CommonOptions): Promise<SetupResult> {
235
- // Clean up old 'commands' directory if it exists (migration from old structure)
236
- // This ensures OpenCode won't crash with ConfigDirectoryTypoError
237
- const oldCommandsDir = path.join(cwd, '.opencode/commands');
238
- try {
239
- await fs.rm(oldCommandsDir, { recursive: true, force: true });
240
- } catch {
241
- // Ignore if doesn't exist
242
- }
243
-
244
- const installer = new FileInstaller();
245
235
  const agentsDir = path.join(cwd, this.config.agentDir);
246
236
 
247
- const results = await installer.installToDirectory(
237
+ const results = await installToDirectory(
248
238
  getAgentsDir(),
249
239
  agentsDir,
250
240
  async (content, sourcePath) => {
@@ -336,11 +326,10 @@ export const opencodeTarget: Target = {
336
326
  throw new Error('Core rules file not found');
337
327
  }
338
328
 
339
- const installer = new FileInstaller();
340
329
  const rulesDestPath = path.join(cwd, this.config.rulesFile);
341
330
  const rulePath = getRulesPath('core');
342
331
 
343
- await installer.installFile(
332
+ await installFile(
344
333
  rulePath,
345
334
  rulesDestPath,
346
335
  async (content) => {
@@ -371,7 +360,7 @@ export const opencodeTarget: Target = {
371
360
  }
372
361
 
373
362
  // Install MCP servers
374
- const installer = new MCPInstaller(this);
363
+ const installer = createMCPInstaller(this);
375
364
  const result = await installer.setupMCP({ ...options, quiet: true });
376
365
 
377
366
  return { count: result.selectedServers.length };
@@ -386,18 +375,9 @@ export const opencodeTarget: Target = {
386
375
  return { count: 0 };
387
376
  }
388
377
 
389
- // Clean up old 'commands' directory if it exists (migration from old structure)
390
- const oldCommandsDir = path.join(cwd, '.opencode/commands');
391
- try {
392
- await fs.rm(oldCommandsDir, { recursive: true, force: true });
393
- } catch {
394
- // Ignore if doesn't exist
395
- }
396
-
397
- const installer = new FileInstaller();
398
378
  const slashCommandsDir = path.join(cwd, this.config.slashCommandsDir);
399
379
 
400
- const results = await installer.installToDirectory(
380
+ const results = await installToDirectory(
401
381
  getSlashCommandsDir(),
402
382
  slashCommandsDir,
403
383
  async (content) => {