@sylphx/flow 2.1.3 → 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.
Files changed (73) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +44 -0
  3. package/package.json +79 -73
  4. package/src/commands/flow/execute-v2.ts +37 -29
  5. package/src/commands/flow/prompt.ts +5 -3
  6. package/src/commands/flow/types.ts +0 -2
  7. package/src/commands/flow-command.ts +20 -13
  8. package/src/commands/hook-command.ts +1 -3
  9. package/src/commands/settings/checkbox-config.ts +128 -0
  10. package/src/commands/settings/index.ts +6 -0
  11. package/src/commands/settings-command.ts +84 -156
  12. package/src/config/ai-config.ts +60 -41
  13. package/src/core/agent-loader.ts +11 -6
  14. package/src/core/attach/file-attacher.ts +172 -0
  15. package/src/core/attach/index.ts +5 -0
  16. package/src/core/attach-manager.ts +117 -171
  17. package/src/core/backup-manager.ts +35 -29
  18. package/src/core/cleanup-handler.ts +11 -8
  19. package/src/core/error-handling.ts +23 -30
  20. package/src/core/flow-executor.ts +58 -76
  21. package/src/core/formatting/bytes.ts +2 -4
  22. package/src/core/functional/async.ts +5 -4
  23. package/src/core/functional/error-handler.ts +2 -2
  24. package/src/core/git-stash-manager.ts +21 -10
  25. package/src/core/installers/file-installer.ts +0 -1
  26. package/src/core/installers/mcp-installer.ts +0 -1
  27. package/src/core/project-manager.ts +24 -18
  28. package/src/core/secrets-manager.ts +54 -73
  29. package/src/core/session-manager.ts +20 -22
  30. package/src/core/state-detector.ts +139 -80
  31. package/src/core/template-loader.ts +13 -31
  32. package/src/core/upgrade-manager.ts +122 -69
  33. package/src/index.ts +8 -5
  34. package/src/services/auto-upgrade.ts +1 -1
  35. package/src/services/config-service.ts +41 -29
  36. package/src/services/global-config.ts +3 -3
  37. package/src/services/target-installer.ts +11 -26
  38. package/src/targets/claude-code.ts +35 -81
  39. package/src/targets/opencode.ts +28 -68
  40. package/src/targets/shared/index.ts +7 -0
  41. package/src/targets/shared/mcp-transforms.ts +132 -0
  42. package/src/targets/shared/target-operations.ts +135 -0
  43. package/src/types/cli.types.ts +2 -2
  44. package/src/types/provider.types.ts +1 -7
  45. package/src/types/session.types.ts +11 -11
  46. package/src/types/target.types.ts +3 -1
  47. package/src/types/todo.types.ts +1 -1
  48. package/src/types.ts +1 -1
  49. package/src/utils/__tests__/package-manager-detector.test.ts +6 -6
  50. package/src/utils/agent-enhancer.ts +4 -4
  51. package/src/utils/config/paths.ts +3 -1
  52. package/src/utils/config/target-utils.ts +2 -2
  53. package/src/utils/display/banner.ts +2 -2
  54. package/src/utils/display/notifications.ts +58 -45
  55. package/src/utils/display/status.ts +29 -12
  56. package/src/utils/files/file-operations.ts +1 -1
  57. package/src/utils/files/sync-utils.ts +38 -41
  58. package/src/utils/index.ts +19 -27
  59. package/src/utils/package-manager-detector.ts +15 -5
  60. package/src/utils/security/security.ts +8 -4
  61. package/src/utils/target-selection.ts +6 -8
  62. package/src/utils/version.ts +4 -2
  63. package/src/commands/flow-orchestrator.ts +0 -328
  64. package/src/commands/init-command.ts +0 -92
  65. package/src/commands/init-core.ts +0 -331
  66. package/src/core/agent-manager.ts +0 -174
  67. package/src/core/loop-controller.ts +0 -200
  68. package/src/core/rule-loader.ts +0 -147
  69. package/src/core/rule-manager.ts +0 -240
  70. package/src/services/claude-config-service.ts +0 -252
  71. package/src/services/first-run-setup.ts +0 -220
  72. package/src/services/smart-config-service.ts +0 -269
  73. package/src/types/api.types.ts +0 -9
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Pure functions for file attachment operations
3
+ * Generic utilities for attaching files with conflict tracking
4
+ */
5
+
6
+ import { existsSync } from 'node:fs';
7
+ import fs from 'node:fs/promises';
8
+ import path from 'node:path';
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ export interface AttachItem {
15
+ name: string;
16
+ content: string;
17
+ }
18
+
19
+ export interface ConflictInfo {
20
+ type: string;
21
+ name: string;
22
+ action: 'overridden' | 'added' | 'skipped';
23
+ message: string;
24
+ }
25
+
26
+ export interface AttachStats {
27
+ added: string[];
28
+ overridden: string[];
29
+ conflicts: ConflictInfo[];
30
+ }
31
+
32
+ export interface ManifestTracker {
33
+ user: string[];
34
+ flow: string[];
35
+ }
36
+
37
+ // ============================================================================
38
+ // Pure Functions
39
+ // ============================================================================
40
+
41
+ /**
42
+ * Create conflict info object
43
+ */
44
+ export const createConflict = (
45
+ type: string,
46
+ name: string,
47
+ action: 'overridden' | 'added' | 'skipped' = 'overridden'
48
+ ): ConflictInfo => ({
49
+ type,
50
+ name,
51
+ action,
52
+ message: `${type.charAt(0).toUpperCase() + type.slice(1)} '${name}' ${action} (will be restored on exit)`,
53
+ });
54
+
55
+ /**
56
+ * Check if file exists at path
57
+ */
58
+ export const fileExists = (filePath: string): boolean => existsSync(filePath);
59
+
60
+ /**
61
+ * Ensure directory exists
62
+ */
63
+ export const ensureDir = (dirPath: string): Promise<void> =>
64
+ fs.mkdir(dirPath, { recursive: true }).then(() => {});
65
+
66
+ /**
67
+ * Write file content
68
+ */
69
+ export const writeFile = (filePath: string, content: string): Promise<void> =>
70
+ fs.writeFile(filePath, content);
71
+
72
+ /**
73
+ * Read file content
74
+ */
75
+ export const readFile = (filePath: string): Promise<string> =>
76
+ fs.readFile(filePath, 'utf-8');
77
+
78
+ // ============================================================================
79
+ // Generic Attach Function
80
+ // ============================================================================
81
+
82
+ /**
83
+ * Attach multiple items to a directory with conflict tracking
84
+ * Pure function that returns stats and manifest updates
85
+ */
86
+ export const attachItemsToDir = async (
87
+ items: AttachItem[],
88
+ targetDir: string,
89
+ itemType: string
90
+ ): Promise<{ stats: AttachStats; manifest: ManifestTracker }> => {
91
+ await ensureDir(targetDir);
92
+
93
+ const stats: AttachStats = {
94
+ added: [],
95
+ overridden: [],
96
+ conflicts: [],
97
+ };
98
+
99
+ const manifest: ManifestTracker = {
100
+ user: [],
101
+ flow: [],
102
+ };
103
+
104
+ for (const item of items) {
105
+ const itemPath = path.join(targetDir, item.name);
106
+ const existed = fileExists(itemPath);
107
+
108
+ if (existed) {
109
+ stats.overridden.push(item.name);
110
+ stats.conflicts.push(createConflict(itemType, item.name, 'overridden'));
111
+ manifest.user.push(item.name);
112
+ } else {
113
+ stats.added.push(item.name);
114
+ }
115
+
116
+ await writeFile(itemPath, item.content);
117
+ manifest.flow.push(item.name);
118
+ }
119
+
120
+ return { stats, manifest };
121
+ };
122
+
123
+ // ============================================================================
124
+ // Rules Attachment (Append Strategy)
125
+ // ============================================================================
126
+
127
+ const FLOW_RULES_START = '<!-- ========== Sylphx Flow Rules (Auto-injected) ========== -->';
128
+ const FLOW_RULES_END = '<!-- ========== End of Sylphx Flow Rules ========== -->';
129
+ const FLOW_RULES_MARKER = '<!-- Sylphx Flow Rules -->';
130
+
131
+ /**
132
+ * Check if content already has Flow rules appended
133
+ */
134
+ export const hasFlowRules = (content: string): boolean =>
135
+ content.includes(FLOW_RULES_MARKER);
136
+
137
+ /**
138
+ * Wrap rules content with markers
139
+ */
140
+ export const wrapRulesContent = (rules: string): string =>
141
+ `\n\n${FLOW_RULES_START}\n\n${rules}\n\n${FLOW_RULES_END}\n`;
142
+
143
+ /**
144
+ * Append rules to existing content
145
+ */
146
+ export const appendRules = (existingContent: string, rules: string): string =>
147
+ existingContent + wrapRulesContent(rules);
148
+
149
+ /**
150
+ * Attach rules file with append strategy
151
+ */
152
+ export const attachRulesFile = async (
153
+ rulesPath: string,
154
+ rules: string
155
+ ): Promise<{ originalSize: number; flowContentAdded: boolean }> => {
156
+ if (fileExists(rulesPath)) {
157
+ const existingContent = await readFile(rulesPath);
158
+
159
+ // Skip if already appended
160
+ if (hasFlowRules(existingContent)) {
161
+ return { originalSize: existingContent.length, flowContentAdded: false };
162
+ }
163
+
164
+ await writeFile(rulesPath, appendRules(existingContent, rules));
165
+ return { originalSize: existingContent.length, flowContentAdded: true };
166
+ }
167
+
168
+ // Create new file
169
+ await ensureDir(path.dirname(rulesPath));
170
+ await writeFile(rulesPath, rules);
171
+ return { originalSize: 0, flowContentAdded: true };
172
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Attach module - Pure functions for file attachment
3
+ */
4
+
5
+ export * from './file-attacher.js';
@@ -4,15 +4,18 @@
4
4
  * Strategy: Direct override with backup, restore on cleanup
5
5
  */
6
6
 
7
+ import { createHash } from 'node:crypto';
8
+ import { existsSync } from 'node:fs';
7
9
  import fs from 'node:fs/promises';
8
10
  import path from 'node:path';
9
- import { existsSync } from 'node:fs';
10
- import { createHash } from 'node:crypto';
11
11
  import chalk from 'chalk';
12
- import { ProjectManager } from './project-manager.js';
13
- import type { BackupManifest } from './backup-manager.js';
14
- import { GlobalConfigService } from '../services/global-config.js';
15
12
  import { MCP_SERVER_REGISTRY } from '../config/servers.js';
13
+ import { GlobalConfigService } from '../services/global-config.js';
14
+ import type { Target } from '../types/target.types.js';
15
+ import { attachItemsToDir, attachRulesFile } from './attach/index.js';
16
+ import type { BackupManifest } from './backup-manager.js';
17
+ import type { ProjectManager } from './project-manager.js';
18
+ import { targetManager } from './target-manager.js';
16
19
 
17
20
  export interface AttachResult {
18
21
  agentsAdded: string[];
@@ -39,13 +42,12 @@ export interface FlowTemplates {
39
42
  agents: Array<{ name: string; content: string }>;
40
43
  commands: Array<{ name: string; content: string }>;
41
44
  rules?: string;
42
- mcpServers: Array<{ name: string; config: any }>;
45
+ mcpServers: Array<{ name: string; config: Record<string, unknown> }>;
43
46
  hooks: Array<{ name: string; content: string }>;
44
47
  singleFiles: Array<{ path: string; content: string }>;
45
48
  }
46
49
 
47
50
  export class AttachManager {
48
- private projectManager: ProjectManager;
49
51
  private configService: GlobalConfigService;
50
52
 
51
53
  constructor(projectManager: ProjectManager) {
@@ -66,26 +68,25 @@ export class AttachManager {
66
68
  }
67
69
 
68
70
  /**
69
- * Get target-specific directory names
71
+ * Resolve target from ID string to Target object
70
72
  */
71
- private getTargetDirs(target: 'claude-code' | 'opencode'): {
72
- agents: string;
73
- commands: string;
74
- } {
75
- return target === 'claude-code'
76
- ? { agents: 'agents', commands: 'commands' }
77
- : { agents: 'agent', commands: 'command' };
73
+ private resolveTarget(targetId: string): Target {
74
+ const targetOption = targetManager.getTarget(targetId);
75
+ if (targetOption._tag === 'None') {
76
+ throw new Error(`Unknown target: ${targetId}`);
77
+ }
78
+ return targetOption.value;
78
79
  }
79
80
 
80
81
  /**
81
82
  * Load global MCP servers from ~/.sylphx-flow/mcp-config.json
82
83
  */
83
84
  private async loadGlobalMCPServers(
84
- target: 'claude-code' | 'opencode'
85
- ): Promise<Array<{ name: string; config: any }>> {
85
+ _target: Target
86
+ ): Promise<Array<{ name: string; config: Record<string, unknown> }>> {
86
87
  try {
87
88
  const enabledServers = await this.configService.getEnabledMCPServers();
88
- const servers: Array<{ name: string; config: any }> = [];
89
+ const servers: Array<{ name: string; config: Record<string, unknown> }> = [];
89
90
 
90
91
  for (const [serverKey, serverConfig] of Object.entries(enabledServers)) {
91
92
  // Lookup server definition in registry
@@ -97,7 +98,7 @@ export class AttachManager {
97
98
  }
98
99
 
99
100
  // Clone the server config from registry
100
- let config: any = { ...serverDef.config };
101
+ const config: Record<string, unknown> = { ...serverDef.config };
101
102
 
102
103
  // Merge environment variables from global config
103
104
  if (serverConfig.env && Object.keys(serverConfig.env).length > 0) {
@@ -110,7 +111,7 @@ export class AttachManager {
110
111
  }
111
112
 
112
113
  return servers;
113
- } catch (error) {
114
+ } catch (_error) {
114
115
  // If global config doesn't exist or fails to load, return empty array
115
116
  return [];
116
117
  }
@@ -119,15 +120,21 @@ export class AttachManager {
119
120
  /**
120
121
  * Attach Flow templates to project
121
122
  * Strategy: Override with warning, backup handles restoration
123
+ * @param projectPath - Project root path
124
+ * @param _projectHash - Project hash (unused but kept for API compatibility)
125
+ * @param targetOrId - Target object or target ID string
126
+ * @param templates - Flow templates to attach
127
+ * @param manifest - Backup manifest to track changes
122
128
  */
123
129
  async attach(
124
130
  projectPath: string,
125
- projectHash: string,
126
- target: 'claude-code' | 'opencode',
131
+ _projectHash: string,
132
+ targetOrId: Target | string,
127
133
  templates: FlowTemplates,
128
134
  manifest: BackupManifest
129
135
  ): Promise<AttachResult> {
130
- const targetDir = this.projectManager.getTargetConfigDir(projectPath, target);
136
+ // Resolve target from ID if needed
137
+ const target = typeof targetOrId === 'string' ? this.resolveTarget(targetOrId) : targetOrId;
131
138
 
132
139
  const result: AttachResult = {
133
140
  agentsAdded: [],
@@ -143,18 +150,17 @@ export class AttachManager {
143
150
  conflicts: [],
144
151
  };
145
152
 
146
- // Ensure target directory exists
147
- await fs.mkdir(targetDir, { recursive: true });
153
+ // All paths are relative to projectPath, using target.config.* directly
148
154
 
149
155
  // 1. Attach agents
150
- await this.attachAgents(targetDir, target, templates.agents, result, manifest);
156
+ await this.attachAgents(projectPath, target, templates.agents, result, manifest);
151
157
 
152
158
  // 2. Attach commands
153
- await this.attachCommands(targetDir, target, templates.commands, result, manifest);
159
+ await this.attachCommands(projectPath, target, templates.commands, result, manifest);
154
160
 
155
161
  // 3. Attach rules (if applicable)
156
162
  if (templates.rules) {
157
- await this.attachRules(targetDir, target, templates.rules, result, manifest);
163
+ await this.attachRules(projectPath, target, templates.rules, result, manifest);
158
164
  }
159
165
 
160
166
  // 4. Attach MCP servers (merge global + template servers)
@@ -162,18 +168,12 @@ export class AttachManager {
162
168
  const allMCPServers = [...globalMCPServers, ...templates.mcpServers];
163
169
 
164
170
  if (allMCPServers.length > 0) {
165
- await this.attachMCPServers(
166
- targetDir,
167
- target,
168
- allMCPServers,
169
- result,
170
- manifest
171
- );
171
+ await this.attachMCPServers(projectPath, target, allMCPServers, result, manifest);
172
172
  }
173
173
 
174
174
  // 5. Attach hooks
175
175
  if (templates.hooks.length > 0) {
176
- await this.attachHooks(targetDir, templates.hooks, result, manifest);
176
+ await this.attachHooks(projectPath, target, templates.hooks, result, manifest);
177
177
  }
178
178
 
179
179
  // 6. Attach single files
@@ -189,177 +189,122 @@ export class AttachManager {
189
189
 
190
190
  /**
191
191
  * Attach agents (override strategy)
192
+ * Uses shared attachItemsToDir function
192
193
  */
193
194
  private async attachAgents(
194
- targetDir: string,
195
- target: 'claude-code' | 'opencode',
195
+ projectPath: string,
196
+ target: Target,
196
197
  agents: Array<{ name: string; content: string }>,
197
198
  result: AttachResult,
198
199
  manifest: BackupManifest
199
200
  ): Promise<void> {
200
- const dirs = this.getTargetDirs(target);
201
- const agentsDir = path.join(targetDir, dirs.agents);
202
- await fs.mkdir(agentsDir, { recursive: true });
203
-
204
- for (const agent of agents) {
205
- const agentPath = path.join(agentsDir, agent.name);
206
- const existed = existsSync(agentPath);
207
-
208
- if (existed) {
209
- // Conflict: user has same agent
210
- result.agentsOverridden.push(agent.name);
211
- result.conflicts.push({
212
- type: 'agent',
213
- name: agent.name,
214
- action: 'overridden',
215
- message: `Agent '${agent.name}' overridden (will be restored on exit)`,
216
- });
217
-
218
- // Track in manifest
219
- manifest.backup.agents.user.push(agent.name);
220
- } else {
221
- result.agentsAdded.push(agent.name);
222
- }
223
-
224
- // Write Flow agent (override)
225
- await fs.writeFile(agentPath, agent.content);
201
+ const agentsDir = path.join(projectPath, target.config.agentDir);
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
+ );
226
210
 
227
- // Track Flow agent
228
- manifest.backup.agents.flow.push(agent.name);
229
- }
211
+ // Update manifest
212
+ manifest.backup.agents.user.push(...itemManifest.user);
213
+ manifest.backup.agents.flow.push(...itemManifest.flow);
230
214
  }
231
215
 
232
216
  /**
233
217
  * Attach commands (override strategy)
218
+ * Uses shared attachItemsToDir function
234
219
  */
235
220
  private async attachCommands(
236
- targetDir: string,
237
- target: 'claude-code' | 'opencode',
221
+ projectPath: string,
222
+ target: Target,
238
223
  commands: Array<{ name: string; content: string }>,
239
224
  result: AttachResult,
240
225
  manifest: BackupManifest
241
226
  ): Promise<void> {
242
- const dirs = this.getTargetDirs(target);
243
- const commandsDir = path.join(targetDir, dirs.commands);
244
- await fs.mkdir(commandsDir, { recursive: true });
245
-
246
- for (const command of commands) {
247
- const commandPath = path.join(commandsDir, command.name);
248
- const existed = existsSync(commandPath);
249
-
250
- if (existed) {
251
- // Conflict: user has same command
252
- result.commandsOverridden.push(command.name);
253
- result.conflicts.push({
254
- type: 'command',
255
- name: command.name,
256
- action: 'overridden',
257
- message: `Command '${command.name}' overridden (will be restored on exit)`,
258
- });
259
-
260
- // Track in manifest
261
- manifest.backup.commands.user.push(command.name);
262
- } else {
263
- result.commandsAdded.push(command.name);
264
- }
265
-
266
- // Write Flow command (override)
267
- await fs.writeFile(commandPath, command.content);
227
+ const commandsDir = path.join(projectPath, target.config.slashCommandsDir);
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
+ );
268
236
 
269
- // Track Flow command
270
- manifest.backup.commands.flow.push(command.name);
271
- }
237
+ // Update manifest
238
+ manifest.backup.commands.user.push(...itemManifest.user);
239
+ manifest.backup.commands.flow.push(...itemManifest.flow);
272
240
  }
273
241
 
274
242
  /**
275
243
  * Attach rules (append strategy for AGENTS.md)
244
+ * Uses shared attachRulesFile function
276
245
  */
277
246
  private async attachRules(
278
- targetDir: string,
279
- target: 'claude-code' | 'opencode',
247
+ projectPath: string,
248
+ target: Target,
280
249
  rules: string,
281
250
  result: AttachResult,
282
251
  manifest: BackupManifest
283
252
  ): Promise<void> {
284
- // Claude Code: .claude/agents/AGENTS.md
285
- // OpenCode: .opencode/AGENTS.md
286
- const dirs = this.getTargetDirs(target);
287
- const rulesPath =
288
- target === 'claude-code'
289
- ? path.join(targetDir, dirs.agents, 'AGENTS.md')
290
- : path.join(targetDir, 'AGENTS.md');
291
-
292
- if (existsSync(rulesPath)) {
293
- // User has AGENTS.md, append Flow rules
294
- const userRules = await fs.readFile(rulesPath, 'utf-8');
295
-
296
- // Check if already appended (avoid duplicates)
297
- if (userRules.includes('<!-- Sylphx Flow Rules -->')) {
298
- // Already appended, skip
299
- return;
300
- }
301
-
302
- const merged = `${userRules}
303
-
304
- <!-- ========== Sylphx Flow Rules (Auto-injected) ========== -->
305
-
306
- ${rules}
307
-
308
- <!-- ========== End of Sylphx Flow Rules ========== -->
309
- `;
253
+ const rulesPath = target.config.rulesFile
254
+ ? path.join(projectPath, target.config.rulesFile)
255
+ : path.join(projectPath, target.config.agentDir, 'AGENTS.md');
310
256
 
311
- await fs.writeFile(rulesPath, merged);
257
+ const { originalSize, flowContentAdded } = await attachRulesFile(rulesPath, rules);
312
258
 
259
+ if (flowContentAdded) {
313
260
  manifest.backup.rules = {
314
261
  path: rulesPath,
315
- originalSize: userRules.length,
316
- flowContentAdded: true,
317
- };
318
- } else {
319
- // User doesn't have AGENTS.md, create new
320
- await fs.mkdir(path.dirname(rulesPath), { recursive: true });
321
- await fs.writeFile(rulesPath, rules);
322
-
323
- manifest.backup.rules = {
324
- path: rulesPath,
325
- originalSize: 0,
262
+ originalSize,
326
263
  flowContentAdded: true,
327
264
  };
265
+ result.rulesAppended = true;
328
266
  }
329
-
330
- result.rulesAppended = true;
331
267
  }
332
268
 
333
269
  /**
334
270
  * Attach MCP servers (merge strategy)
271
+ * Uses target.config.configFile and target.config.mcpConfigPath
272
+ * Note: configFile is relative to project root, not targetDir
335
273
  */
336
274
  private async attachMCPServers(
337
- targetDir: string,
338
- target: 'claude-code' | 'opencode',
339
- mcpServers: Array<{ name: string; config: any }>,
275
+ projectPath: string,
276
+ target: Target,
277
+ mcpServers: Array<{ name: string; config: Record<string, unknown> }>,
340
278
  result: AttachResult,
341
279
  manifest: BackupManifest
342
280
  ): Promise<void> {
343
- // Claude Code: .claude/settings.json (mcp.servers)
344
- // OpenCode: .opencode/.mcp.json
345
- const configPath =
346
- target === 'claude-code'
347
- ? path.join(targetDir, 'settings.json')
348
- : path.join(targetDir, '.mcp.json');
281
+ // Use target config for file path and MCP config structure
282
+ // Claude Code: .mcp.json at project root with mcpServers key
283
+ // OpenCode: opencode.jsonc at project root with mcp key
284
+ const configPath = path.join(projectPath, target.config.configFile);
285
+ const mcpPath = target.config.mcpConfigPath;
349
286
 
350
- let config: any = {};
287
+ let config: Record<string, unknown> = {};
351
288
 
352
289
  if (existsSync(configPath)) {
353
290
  config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
354
291
  }
355
292
 
356
- // Ensure mcp.servers exists
357
- if (!config.mcp) config.mcp = {};
358
- if (!config.mcp.servers) config.mcp.servers = {};
293
+ // Get or create the MCP servers object at the correct path
294
+ // Claude Code: config.mcpServers = {}
295
+ // OpenCode: config.mcp = {}
296
+ let mcpContainer = config[mcpPath] as Record<string, unknown> | undefined;
297
+ if (!mcpContainer) {
298
+ mcpContainer = {};
299
+ config[mcpPath] = mcpContainer;
300
+ }
359
301
 
360
302
  // Add Flow MCP servers
361
303
  for (const server of mcpServers) {
362
- if (config.mcp.servers[server.name]) {
304
+ // Transform the server config for this target
305
+ const transformedConfig = target.transformMCPConfig(server.config as any, server.name);
306
+
307
+ if (mcpContainer[server.name]) {
363
308
  // Conflict: user has same MCP server
364
309
  result.mcpServersOverridden.push(server.name);
365
310
  result.conflicts.push({
@@ -372,8 +317,8 @@ ${rules}
372
317
  result.mcpServersAdded.push(server.name);
373
318
  }
374
319
 
375
- // Override with Flow config
376
- config.mcp.servers[server.name] = server.config;
320
+ // Override with Flow config (transformed for target)
321
+ mcpContainer[server.name] = transformedConfig;
377
322
  }
378
323
 
379
324
  // Write updated config
@@ -383,7 +328,7 @@ ${rules}
383
328
  manifest.backup.config = {
384
329
  path: configPath,
385
330
  hash: await this.calculateFileHash(configPath),
386
- mcpServersCount: Object.keys(config.mcp.servers).length,
331
+ mcpServersCount: Object.keys(mcpContainer).length,
387
332
  };
388
333
  }
389
334
 
@@ -391,12 +336,14 @@ ${rules}
391
336
  * Attach hooks (override strategy)
392
337
  */
393
338
  private async attachHooks(
394
- targetDir: string,
339
+ projectPath: string,
340
+ target: Target,
395
341
  hooks: Array<{ name: string; content: string }>,
396
342
  result: AttachResult,
397
- manifest: BackupManifest
343
+ _manifest: BackupManifest
398
344
  ): Promise<void> {
399
- const hooksDir = path.join(targetDir, 'hooks');
345
+ // Hooks are in configDir/hooks
346
+ const hooksDir = path.join(projectPath, target.config.configDir, 'hooks');
400
347
  await fs.mkdir(hooksDir, { recursive: true });
401
348
 
402
349
  for (const hook of hooks) {
@@ -430,13 +377,16 @@ ${rules}
430
377
  result: AttachResult,
431
378
  manifest: BackupManifest
432
379
  ): Promise<void> {
433
- // Get target from manifest to determine correct directory
434
- const target = manifest.target;
435
- const targetDir = this.projectManager.getTargetConfigDir(projectPath, target);
380
+ // Get target from manifest to determine config directory
381
+ const targetOption = targetManager.getTarget(manifest.target);
382
+ if (targetOption._tag === 'None') {
383
+ return; // Unknown target, skip
384
+ }
385
+ const target = targetOption.value;
436
386
 
437
387
  for (const file of singleFiles) {
438
- // Write to target config directory, not project root
439
- const filePath = path.join(targetDir, file.path);
388
+ // Write to target config directory (e.g., .claude/ or .opencode/)
389
+ const filePath = path.join(projectPath, target.config.configDir, file.path);
440
390
  const existed = existsSync(filePath);
441
391
 
442
392
  if (existed) {
@@ -474,13 +424,9 @@ ${rules}
474
424
  console.log(chalk.yellow('\n⚠️ Conflicts detected:\n'));
475
425
 
476
426
  for (const conflict of result.conflicts) {
477
- console.log(
478
- chalk.yellow(` • ${conflict.type}: ${conflict.name} - ${conflict.action}`)
479
- );
427
+ console.log(chalk.yellow(` • ${conflict.type}: ${conflict.name} - ${conflict.action}`));
480
428
  }
481
429
 
482
- console.log(
483
- chalk.dim('\n Don\'t worry! All overridden content will be restored on exit.\n')
484
- );
430
+ console.log(chalk.dim("\n Don't worry! All overridden content will be restored on exit.\n"));
485
431
  }
486
432
  }