@sylphx/flow 2.1.4 → 2.1.5

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.
@@ -12,6 +12,7 @@ import chalk from 'chalk';
12
12
  import { MCP_SERVER_REGISTRY } from '../config/servers.js';
13
13
  import { GlobalConfigService } from '../services/global-config.js';
14
14
  import type { Target } from '../types/target.types.js';
15
+ import { attachItemsToDir, attachRulesFile } from './attach/index.js';
15
16
  import type { BackupManifest } from './backup-manager.js';
16
17
  import type { ProjectManager } from './project-manager.js';
17
18
  import { targetManager } from './target-manager.js';
@@ -188,6 +189,7 @@ export class AttachManager {
188
189
 
189
190
  /**
190
191
  * Attach agents (override strategy)
192
+ * Uses shared attachItemsToDir function
191
193
  */
192
194
  private async attachAgents(
193
195
  projectPath: string,
@@ -196,40 +198,24 @@ export class AttachManager {
196
198
  result: AttachResult,
197
199
  manifest: BackupManifest
198
200
  ): Promise<void> {
199
- // Use full path from target config
200
201
  const agentsDir = path.join(projectPath, target.config.agentDir);
201
- await fs.mkdir(agentsDir, { recursive: true });
202
-
203
- for (const agent of agents) {
204
- const agentPath = path.join(agentsDir, agent.name);
205
- const existed = existsSync(agentPath);
206
-
207
- if (existed) {
208
- // Conflict: user has same agent
209
- result.agentsOverridden.push(agent.name);
210
- result.conflicts.push({
211
- type: 'agent',
212
- name: agent.name,
213
- action: 'overridden',
214
- message: `Agent '${agent.name}' overridden (will be restored on exit)`,
215
- });
216
-
217
- // Track in manifest
218
- manifest.backup.agents.user.push(agent.name);
219
- } else {
220
- result.agentsAdded.push(agent.name);
221
- }
222
-
223
- // Write Flow agent (override)
224
- await fs.writeFile(agentPath, agent.content);
225
-
226
- // Track Flow agent
227
- manifest.backup.agents.flow.push(agent.name);
228
- }
202
+ const { stats, manifest: itemManifest } = await attachItemsToDir(agents, agentsDir, 'agent');
203
+
204
+ // Update result
205
+ result.agentsAdded.push(...stats.added);
206
+ result.agentsOverridden.push(...stats.overridden);
207
+ result.conflicts.push(
208
+ ...stats.conflicts.map((c) => ({ ...c, type: 'agent' as const }))
209
+ );
210
+
211
+ // Update manifest
212
+ manifest.backup.agents.user.push(...itemManifest.user);
213
+ manifest.backup.agents.flow.push(...itemManifest.flow);
229
214
  }
230
215
 
231
216
  /**
232
217
  * Attach commands (override strategy)
218
+ * Uses shared attachItemsToDir function
233
219
  */
234
220
  private async attachCommands(
235
221
  projectPath: string,
@@ -238,40 +224,24 @@ export class AttachManager {
238
224
  result: AttachResult,
239
225
  manifest: BackupManifest
240
226
  ): Promise<void> {
241
- // Use full path from target config
242
227
  const commandsDir = path.join(projectPath, target.config.slashCommandsDir);
243
- await fs.mkdir(commandsDir, { recursive: true });
244
-
245
- for (const command of commands) {
246
- const commandPath = path.join(commandsDir, command.name);
247
- const existed = existsSync(commandPath);
248
-
249
- if (existed) {
250
- // Conflict: user has same command
251
- result.commandsOverridden.push(command.name);
252
- result.conflicts.push({
253
- type: 'command',
254
- name: command.name,
255
- action: 'overridden',
256
- message: `Command '${command.name}' overridden (will be restored on exit)`,
257
- });
258
-
259
- // Track in manifest
260
- manifest.backup.commands.user.push(command.name);
261
- } else {
262
- result.commandsAdded.push(command.name);
263
- }
264
-
265
- // Write Flow command (override)
266
- await fs.writeFile(commandPath, command.content);
267
-
268
- // Track Flow command
269
- manifest.backup.commands.flow.push(command.name);
270
- }
228
+ const { stats, manifest: itemManifest } = await attachItemsToDir(commands, commandsDir, 'command');
229
+
230
+ // Update result
231
+ result.commandsAdded.push(...stats.added);
232
+ result.commandsOverridden.push(...stats.overridden);
233
+ result.conflicts.push(
234
+ ...stats.conflicts.map((c) => ({ ...c, type: 'command' as const }))
235
+ );
236
+
237
+ // Update manifest
238
+ manifest.backup.commands.user.push(...itemManifest.user);
239
+ manifest.backup.commands.flow.push(...itemManifest.flow);
271
240
  }
272
241
 
273
242
  /**
274
243
  * Attach rules (append strategy for AGENTS.md)
244
+ * Uses shared attachRulesFile function
275
245
  */
276
246
  private async attachRules(
277
247
  projectPath: string,
@@ -280,52 +250,20 @@ export class AttachManager {
280
250
  result: AttachResult,
281
251
  manifest: BackupManifest
282
252
  ): Promise<void> {
283
- // Use full paths from target config:
284
- // - rulesFile defined (e.g., OpenCode): projectPath/rulesFile
285
- // - rulesFile undefined (e.g., Claude Code): projectPath/agentDir/AGENTS.md
286
253
  const rulesPath = target.config.rulesFile
287
254
  ? path.join(projectPath, target.config.rulesFile)
288
255
  : path.join(projectPath, target.config.agentDir, 'AGENTS.md');
289
256
 
290
- if (existsSync(rulesPath)) {
291
- // User has AGENTS.md, append Flow rules
292
- const userRules = await fs.readFile(rulesPath, 'utf-8');
293
-
294
- // Check if already appended (avoid duplicates)
295
- if (userRules.includes('<!-- Sylphx Flow Rules -->')) {
296
- // Already appended, skip
297
- return;
298
- }
299
-
300
- const merged = `${userRules}
301
-
302
- <!-- ========== Sylphx Flow Rules (Auto-injected) ========== -->
303
-
304
- ${rules}
305
-
306
- <!-- ========== End of Sylphx Flow Rules ========== -->
307
- `;
308
-
309
- await fs.writeFile(rulesPath, merged);
257
+ const { originalSize, flowContentAdded } = await attachRulesFile(rulesPath, rules);
310
258
 
259
+ if (flowContentAdded) {
311
260
  manifest.backup.rules = {
312
261
  path: rulesPath,
313
- originalSize: userRules.length,
314
- flowContentAdded: true,
315
- };
316
- } else {
317
- // User doesn't have AGENTS.md, create new
318
- await fs.mkdir(path.dirname(rulesPath), { recursive: true });
319
- await fs.writeFile(rulesPath, rules);
320
-
321
- manifest.backup.rules = {
322
- path: rulesPath,
323
- originalSize: 0,
262
+ originalSize,
324
263
  flowContentAdded: true,
325
264
  };
265
+ result.rulesAppended = true;
326
266
  }
327
-
328
- result.rulesAppended = true;
329
267
  }
330
268
 
331
269
  /**
@@ -10,7 +10,7 @@ import path from 'node:path';
10
10
 
11
11
  export interface GlobalSettings {
12
12
  version: string;
13
- defaultTarget?: 'claude-code' | 'opencode' | 'cursor' | 'ask-every-time';
13
+ defaultTarget?: 'claude-code' | 'opencode' | '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;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Target Installation Service
3
- * Auto-detects and installs AI CLI tools (Claude Code, OpenCode, Cursor)
3
+ * Auto-detects and installs AI CLI tools (Claude Code, OpenCode)
4
4
  */
5
5
 
6
6
  import { exec } from 'node:child_process';
@@ -14,7 +14,7 @@ import { detectPackageManager, type PackageManager } from '../utils/package-mana
14
14
  const execAsync = promisify(exec);
15
15
 
16
16
  export interface TargetInstallation {
17
- id: 'claude-code' | 'opencode' | 'cursor';
17
+ id: 'claude-code' | 'opencode';
18
18
  name: string;
19
19
  package: string;
20
20
  checkCommand: string;
@@ -61,16 +61,6 @@ const TARGET_INSTALLATIONS: TargetInstallation[] = [
61
61
  }
62
62
  },
63
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
64
  ];
75
65
 
76
66
  export class TargetInstaller {
@@ -160,13 +150,6 @@ export class TargetInstaller {
160
150
  return false;
161
151
  }
162
152
 
163
- // Special handling for Cursor (not npm-installable)
164
- if (targetId === 'cursor') {
165
- console.log(chalk.yellow('\n⚠️ Cursor requires manual installation'));
166
- console.log(chalk.cyan(' Visit https://cursor.sh to download and install\n'));
167
- return false;
168
- }
169
-
170
153
  // Confirm installation unless auto-confirm is enabled
171
154
  if (!autoConfirm) {
172
155
  try {
@@ -3,14 +3,19 @@ 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 { installToDirectory } from '../core/installers/file-installer.js';
7
6
  import { createMCPInstaller } from '../core/installers/mcp-installer.js';
8
7
  import type { AgentMetadata } from '../types/target-config.types.js';
9
8
  import type { CommonOptions, MCPServerConfigUnion, SetupResult, Target } from '../types.js';
10
- import { getAgentsDir, getSlashCommandsDir } from '../utils/config/paths.js';
9
+ import { getAgentsDir } from '../utils/config/paths.js';
11
10
  import { fileUtils, generateHelpText, pathUtils, yamlUtils } from '../utils/config/target-utils.js';
12
11
  import { CLIError } from '../utils/error-handler.js';
13
12
  import { sanitize } from '../utils/security/security.js';
13
+ import {
14
+ transformMCPConfig as transformMCP,
15
+ detectTargetConfig,
16
+ stripFrontMatter,
17
+ setupSlashCommandsTo,
18
+ } from './shared/index.js';
14
19
 
15
20
  /**
16
21
  * Claude Code target - composition approach with all original functionality
@@ -68,50 +73,10 @@ export const claudeCodeTarget: Target = {
68
73
 
69
74
  /**
70
75
  * Transform MCP server configuration for Claude Code
71
- * Convert from various formats to Claude Code's optimal format
76
+ * Uses shared pure function for bidirectional conversion
72
77
  */
73
78
  transformMCPConfig(config: MCPServerConfigUnion, _serverId?: string): Record<string, unknown> {
74
- // Handle legacy OpenCode 'local' type
75
- if (config.type === 'local') {
76
- // Convert OpenCode 'local' array command to Claude Code format
77
- const [command, ...args] = config.command;
78
- return {
79
- type: 'stdio',
80
- command,
81
- ...(args && args.length > 0 && { args }),
82
- ...(config.environment && { env: config.environment }),
83
- };
84
- }
85
-
86
- // Handle new stdio format (already optimized for Claude Code)
87
- if (config.type === 'stdio') {
88
- return {
89
- type: 'stdio',
90
- command: config.command,
91
- ...(config.args && config.args.length > 0 && { args: config.args }),
92
- ...(config.env && { env: config.env }),
93
- };
94
- }
95
-
96
- // Handle legacy OpenCode 'remote' type
97
- if (config.type === 'remote') {
98
- return {
99
- type: 'http',
100
- url: config.url,
101
- ...(config.headers && { headers: config.headers }),
102
- };
103
- }
104
-
105
- // Handle new http format (already optimized for Claude Code)
106
- if (config.type === 'http') {
107
- return {
108
- type: 'http',
109
- url: config.url,
110
- ...(config.headers && { headers: config.headers }),
111
- };
112
- }
113
-
114
- return config;
79
+ return transformMCP(config, 'claude-code');
115
80
  },
116
81
 
117
82
  getConfigPath: (cwd: string) =>
@@ -321,12 +286,7 @@ Please begin your response with a comprehensive summary of all the instructions
321
286
  * Detect if this target is being used in the current environment
322
287
  */
323
288
  detectFromEnvironment(): boolean {
324
- try {
325
- const cwd = process.cwd();
326
- return fs.existsSync(path.join(cwd, '.mcp.json'));
327
- } catch {
328
- return false;
329
- }
289
+ return detectTargetConfig(process.cwd(), '.mcp.json');
330
290
  },
331
291
 
332
292
  /**
@@ -377,9 +337,7 @@ Please begin your response with a comprehensive summary of all the instructions
377
337
  * Transform rules content for Claude Code
378
338
  * Claude Code doesn't need front matter in rules files (CLAUDE.md)
379
339
  */
380
- async transformRulesContent(content: string): Promise<string> {
381
- return yamlUtils.stripFrontMatter(content);
382
- },
340
+ transformRulesContent: stripFrontMatter,
383
341
 
384
342
  /**
385
343
  * Setup hooks for Claude Code
@@ -524,23 +482,7 @@ Please begin your response with a comprehensive summary of all the instructions
524
482
  if (!this.config.slashCommandsDir) {
525
483
  return { count: 0 };
526
484
  }
527
-
528
- const slashCommandsDir = path.join(cwd, this.config.slashCommandsDir);
529
-
530
- const results = await installToDirectory(
531
- getSlashCommandsDir(),
532
- slashCommandsDir,
533
- async (content) => {
534
- // Slash commands are plain markdown with front matter - no transformation needed
535
- return content;
536
- },
537
- {
538
- ...options,
539
- showProgress: false, // UI handled by init-command
540
- }
541
- );
542
-
543
- return { count: results.length };
485
+ return setupSlashCommandsTo(path.join(cwd, this.config.slashCommandsDir), undefined, options);
544
486
  },
545
487
  };
546
488
 
@@ -11,6 +11,12 @@ import { getAgentsDir, getOutputStylesDir, getSlashCommandsDir } from '../utils/
11
11
  import { fileUtils, generateHelpText, yamlUtils } from '../utils/config/target-utils.js';
12
12
  import { CLIError } from '../utils/error-handler.js';
13
13
  import { secretUtils } from '../utils/security/secret-utils.js';
14
+ import {
15
+ transformMCPConfig as transformMCP,
16
+ detectTargetConfig,
17
+ stripFrontMatter,
18
+ setupSlashCommandsTo,
19
+ } from './shared/index.js';
14
20
 
15
21
  /**
16
22
  * OpenCode target - composition approach with all original functionality
@@ -80,44 +86,10 @@ export const opencodeTarget: Target = {
80
86
 
81
87
  /**
82
88
  * Transform MCP server configuration for OpenCode
83
- * Convert from Claude Code's optimal format to OpenCode's format
89
+ * Uses shared pure function for bidirectional conversion
84
90
  */
85
91
  transformMCPConfig(config: MCPServerConfigUnion, _serverId?: string): Record<string, unknown> {
86
- // Handle new Claude Code stdio format
87
- if (config.type === 'stdio') {
88
- // Convert Claude Code format to OpenCode format
89
- const openCodeConfig: Record<string, unknown> = {
90
- type: 'local',
91
- command: [config.command],
92
- };
93
-
94
- if (config.args && config.args.length > 0) {
95
- openCodeConfig.command.push(...config.args);
96
- }
97
-
98
- if (config.env) {
99
- openCodeConfig.environment = config.env;
100
- }
101
-
102
- return openCodeConfig;
103
- }
104
-
105
- // Handle new Claude Code http format
106
- if (config.type === 'http') {
107
- // Claude Code http format is compatible with OpenCode remote format
108
- return {
109
- type: 'remote',
110
- url: config.url,
111
- ...(config.headers && { headers: config.headers }),
112
- };
113
- }
114
-
115
- // Handle legacy OpenCode formats (pass through)
116
- if (config.type === 'local' || config.type === 'remote') {
117
- return config;
118
- }
119
-
120
- return config;
92
+ return transformMCP(config, 'opencode');
121
93
  },
122
94
 
123
95
  getConfigPath: (cwd: string) =>
@@ -217,21 +189,14 @@ export const opencodeTarget: Target = {
217
189
  * Detect if this target is being used in the current environment
218
190
  */
219
191
  detectFromEnvironment(): boolean {
220
- try {
221
- const cwd = process.cwd();
222
- return fs.existsSync(path.join(cwd, 'opencode.jsonc'));
223
- } catch {
224
- return false;
225
- }
192
+ return detectTargetConfig(process.cwd(), 'opencode.jsonc');
226
193
  },
227
194
 
228
195
  /**
229
196
  * Transform rules content for OpenCode
230
197
  * OpenCode doesn't need front matter in rules files (AGENTS.md)
231
198
  */
232
- async transformRulesContent(content: string): Promise<string> {
233
- return yamlUtils.stripFrontMatter(content);
234
- },
199
+ transformRulesContent: stripFrontMatter,
235
200
 
236
201
  /**
237
202
  * Setup agents for OpenCode
@@ -380,23 +345,7 @@ export const opencodeTarget: Target = {
380
345
  if (!this.config.slashCommandsDir) {
381
346
  return { count: 0 };
382
347
  }
383
-
384
- const slashCommandsDir = path.join(cwd, this.config.slashCommandsDir);
385
-
386
- const results = await installToDirectory(
387
- getSlashCommandsDir(),
388
- slashCommandsDir,
389
- async (content) => {
390
- // Slash commands are plain markdown with front matter - no transformation needed
391
- return content;
392
- },
393
- {
394
- ...options,
395
- showProgress: false, // UI handled by init-command
396
- }
397
- );
398
-
399
- return { count: results.length };
348
+ return setupSlashCommandsTo(path.join(cwd, this.config.slashCommandsDir), undefined, options);
400
349
  },
401
350
 
402
351
  /**
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Shared target utilities
3
+ * Pure functions for common target operations
4
+ */
5
+
6
+ export * from './mcp-transforms.js';
7
+ export * from './target-operations.js';
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Pure functions for MCP configuration transformations
3
+ * Bidirectional conversion between Claude Code and OpenCode formats
4
+ */
5
+
6
+ import type { MCPServerConfigUnion } from '../../types.js';
7
+
8
+ // ============================================================================
9
+ // Types
10
+ // ============================================================================
11
+
12
+ export type MCPFormat = 'claude-code' | 'opencode';
13
+
14
+ export interface StdioConfig {
15
+ type: 'stdio';
16
+ command: string;
17
+ args?: string[];
18
+ env?: Record<string, string>;
19
+ }
20
+
21
+ export interface HttpConfig {
22
+ type: 'http';
23
+ url: string;
24
+ headers?: Record<string, string>;
25
+ }
26
+
27
+ export interface LocalConfig {
28
+ type: 'local';
29
+ command: string[];
30
+ environment?: Record<string, string>;
31
+ }
32
+
33
+ export interface RemoteConfig {
34
+ type: 'remote';
35
+ url: string;
36
+ headers?: Record<string, string>;
37
+ }
38
+
39
+ // ============================================================================
40
+ // Pure Transform Functions
41
+ // ============================================================================
42
+
43
+ /**
44
+ * Convert stdio format to local format (Claude Code → OpenCode)
45
+ */
46
+ export const stdioToLocal = (config: StdioConfig): LocalConfig => ({
47
+ type: 'local',
48
+ command: config.args ? [config.command, ...config.args] : [config.command],
49
+ ...(config.env && { environment: config.env }),
50
+ });
51
+
52
+ /**
53
+ * Convert local format to stdio format (OpenCode → Claude Code)
54
+ */
55
+ export const localToStdio = (config: LocalConfig): StdioConfig => {
56
+ const [command, ...args] = config.command;
57
+ return {
58
+ type: 'stdio',
59
+ command,
60
+ ...(args.length > 0 && { args }),
61
+ ...(config.environment && { env: config.environment }),
62
+ };
63
+ };
64
+
65
+ /**
66
+ * Convert http format to remote format (Claude Code → OpenCode)
67
+ */
68
+ export const httpToRemote = (config: HttpConfig): RemoteConfig => ({
69
+ type: 'remote',
70
+ url: config.url,
71
+ ...(config.headers && { headers: config.headers }),
72
+ });
73
+
74
+ /**
75
+ * Convert remote format to http format (OpenCode → Claude Code)
76
+ */
77
+ export const remoteToHttp = (config: RemoteConfig): HttpConfig => ({
78
+ type: 'http',
79
+ url: config.url,
80
+ ...(config.headers && { headers: config.headers }),
81
+ });
82
+
83
+ /**
84
+ * Normalize stdio config (ensure consistent structure)
85
+ */
86
+ export const normalizeStdio = (config: StdioConfig): StdioConfig => ({
87
+ type: 'stdio',
88
+ command: config.command,
89
+ ...(config.args && config.args.length > 0 && { args: config.args }),
90
+ ...(config.env && { env: config.env }),
91
+ });
92
+
93
+ /**
94
+ * Normalize http config (ensure consistent structure)
95
+ */
96
+ export const normalizeHttp = (config: HttpConfig): HttpConfig => ({
97
+ type: 'http',
98
+ url: config.url,
99
+ ...(config.headers && { headers: config.headers }),
100
+ });
101
+
102
+ // ============================================================================
103
+ // Main Transform Function
104
+ // ============================================================================
105
+
106
+ /**
107
+ * Transform MCP config to target format
108
+ * Pure function - no side effects
109
+ */
110
+ export const transformMCPConfig = (
111
+ config: MCPServerConfigUnion,
112
+ targetFormat: MCPFormat
113
+ ): Record<string, unknown> => {
114
+ // Claude Code format (stdio/http)
115
+ if (targetFormat === 'claude-code') {
116
+ if (config.type === 'local') return localToStdio(config as LocalConfig);
117
+ if (config.type === 'remote') return remoteToHttp(config as RemoteConfig);
118
+ if (config.type === 'stdio') return normalizeStdio(config as StdioConfig);
119
+ if (config.type === 'http') return normalizeHttp(config as HttpConfig);
120
+ return config;
121
+ }
122
+
123
+ // OpenCode format (local/remote)
124
+ if (targetFormat === 'opencode') {
125
+ if (config.type === 'stdio') return stdioToLocal(config as StdioConfig);
126
+ if (config.type === 'http') return httpToRemote(config as HttpConfig);
127
+ if (config.type === 'local' || config.type === 'remote') return config;
128
+ return config;
129
+ }
130
+
131
+ return config;
132
+ };