@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
@@ -58,11 +58,6 @@ const GLOBAL_CONFIG_FILE = path.join(os.homedir(), '.sylphx-flow', 'settings.jso
58
58
  const PROJECT_CONFIG_FILE = '.sylphx-flow/settings.json';
59
59
  const LOCAL_CONFIG_FILE = '.sylphx-flow/settings.local.json';
60
60
 
61
- /**
62
- * Deprecated config file (for migration)
63
- */
64
- const LEGACY_CONFIG_FILE = '.sylphx-flow/ai-config.json';
65
-
66
61
  /**
67
62
  * Get AI config file paths in priority order
68
63
  */
@@ -70,12 +65,10 @@ export const getAIConfigPaths = (cwd: string = process.cwd()): {
70
65
  global: string;
71
66
  project: string;
72
67
  local: string;
73
- legacy: string;
74
68
  } => ({
75
69
  global: GLOBAL_CONFIG_FILE,
76
70
  project: path.join(cwd, PROJECT_CONFIG_FILE),
77
71
  local: path.join(cwd, LOCAL_CONFIG_FILE),
78
- legacy: path.join(cwd, LEGACY_CONFIG_FILE),
79
72
  });
80
73
 
81
74
  /**
@@ -140,18 +133,12 @@ export const aiConfigExists = async (cwd: string = process.cwd()): Promise<boole
140
133
  return true;
141
134
  } catch {}
142
135
 
143
- try {
144
- await fs.access(paths.legacy);
145
- return true;
146
- } catch {}
147
-
148
136
  return false;
149
137
  };
150
138
 
151
139
  /**
152
140
  * Load AI configuration
153
141
  * Merges global, project, and local configs with priority: local > project > global
154
- * Automatically migrates legacy config on first load
155
142
  */
156
143
  export const loadAIConfig = async (cwd: string = process.cwd()): Promise<Result<AIConfig, Error>> => {
157
144
  return tryCatchAsync(
@@ -159,39 +146,19 @@ export const loadAIConfig = async (cwd: string = process.cwd()): Promise<Result<
159
146
  const paths = getAIConfigPaths(cwd);
160
147
 
161
148
  // Load all config files
162
- const [globalConfig, projectConfig, localConfig, legacyConfig] = await Promise.all([
149
+ const [globalConfig, projectConfig, localConfig] = await Promise.all([
163
150
  loadConfigFile(paths.global),
164
151
  loadConfigFile(paths.project),
165
152
  loadConfigFile(paths.local),
166
- loadConfigFile(paths.legacy),
167
153
  ]);
168
154
 
169
- // Auto-migrate legacy config if it exists and global doesn't
170
- if (legacyConfig && !globalConfig) {
171
- await migrateLegacyConfig(cwd);
172
- // Reload global config after migration
173
- const migratedGlobal = await loadConfigFile(paths.global);
174
- if (migratedGlobal) {
175
- // Start with empty config
176
- let merged: AIConfig = {};
177
-
178
- // Merge in priority order: global < project < local
179
- merged = mergeConfigs(merged, migratedGlobal);
180
- if (projectConfig) merged = mergeConfigs(merged, projectConfig);
181
- if (localConfig) merged = mergeConfigs(merged, localConfig);
182
-
183
- return merged;
184
- }
185
- }
186
-
187
155
  // Start with empty config
188
156
  let merged: AIConfig = {};
189
157
 
190
- // Merge in priority order: global < project < local < legacy (for backwards compat)
158
+ // Merge in priority order: global < project < local
191
159
  if (globalConfig) merged = mergeConfigs(merged, globalConfig);
192
160
  if (projectConfig) merged = mergeConfigs(merged, projectConfig);
193
161
  if (localConfig) merged = mergeConfigs(merged, localConfig);
194
- if (legacyConfig) merged = mergeConfigs(merged, legacyConfig);
195
162
 
196
163
  return merged;
197
164
  },
@@ -340,37 +307,3 @@ export const getConfiguredProviders = async (
340
307
  return providers;
341
308
  };
342
309
 
343
- /**
344
- * Migrate legacy ai-config.json to new settings system
345
- * Automatically called on first load if legacy config exists
346
- */
347
- export const migrateLegacyConfig = async (cwd: string = process.cwd()): Promise<Result<void, Error>> => {
348
- return tryCatchAsync(
349
- async () => {
350
- const paths = getAIConfigPaths(cwd);
351
-
352
- // Check if legacy config exists
353
- const legacyConfig = await loadConfigFile(paths.legacy);
354
- if (!legacyConfig) {
355
- return; // No legacy config to migrate
356
- }
357
-
358
- // Check if global config already exists
359
- const globalConfig = await loadConfigFile(paths.global);
360
- if (globalConfig) {
361
- // Global config exists, don't overwrite it
362
- console.log('Legacy config found but global config already exists. Skipping migration.');
363
- console.log(`You can manually delete ${paths.legacy} if migration is complete.`);
364
- return;
365
- }
366
-
367
- // Migrate to global config
368
- await fs.mkdir(path.dirname(paths.global), { recursive: true });
369
- await fs.writeFile(paths.global, JSON.stringify(legacyConfig, null, 2) + '\n', 'utf8');
370
-
371
- console.log(`✓ Migrated configuration from ${paths.legacy} to ${paths.global}`);
372
- console.log(` You can now safely delete the legacy file: ${paths.legacy}`);
373
- },
374
- (error: any) => new Error(`Failed to migrate legacy config: ${error.message}`)
375
- );
376
- };
@@ -113,14 +113,3 @@ export const isTargetImplemented = (id: string): boolean => {
113
113
  * Utility type for target IDs
114
114
  */
115
115
  export type TargetID = ReturnType<typeof getAllTargetIDs>[number];
116
-
117
- /**
118
- * Legacy aliases for backward compatibility
119
- * @deprecated Use getAllTargets() instead
120
- */
121
- export const ALL_TARGETS = getAllTargets;
122
-
123
- /**
124
- * @deprecated Use getImplementedTargets() instead
125
- */
126
- export const IMPLEMENTED_TARGETS = getImplementedTargets;
@@ -0,0 +1,495 @@
1
+ /**
2
+ * Attach Manager
3
+ * Handles merging Flow templates into user's project environment
4
+ * Strategy: Direct override with backup, restore on cleanup
5
+ */
6
+
7
+ import fs from 'node:fs/promises';
8
+ import path from 'node:path';
9
+ import { existsSync } from 'node:fs';
10
+ import { createHash } from 'node:crypto';
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
+ import { MCP_SERVER_REGISTRY } from '../config/servers.js';
16
+
17
+ export interface AttachResult {
18
+ agentsAdded: string[];
19
+ agentsOverridden: string[];
20
+ commandsAdded: string[];
21
+ commandsOverridden: string[];
22
+ rulesAppended: boolean;
23
+ mcpServersAdded: string[];
24
+ mcpServersOverridden: string[];
25
+ singleFilesMerged: string[];
26
+ hooksAdded: string[];
27
+ hooksOverridden: string[];
28
+ conflicts: ConflictInfo[];
29
+ }
30
+
31
+ export interface ConflictInfo {
32
+ type: 'agent' | 'command' | 'mcp' | 'hook';
33
+ name: string;
34
+ action: 'overridden' | 'merged';
35
+ message: string;
36
+ }
37
+
38
+ export interface FlowTemplates {
39
+ agents: Array<{ name: string; content: string }>;
40
+ commands: Array<{ name: string; content: string }>;
41
+ rules?: string;
42
+ mcpServers: Array<{ name: string; config: any }>;
43
+ hooks: Array<{ name: string; content: string }>;
44
+ singleFiles: Array<{ path: string; content: string }>;
45
+ }
46
+
47
+ export class AttachManager {
48
+ private projectManager: ProjectManager;
49
+ private configService: GlobalConfigService;
50
+
51
+ constructor(projectManager: ProjectManager) {
52
+ this.projectManager = projectManager;
53
+ this.configService = new GlobalConfigService();
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
+
68
+ /**
69
+ * Get target-specific directory names
70
+ */
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' };
78
+ }
79
+
80
+ /**
81
+ * Load global MCP servers from ~/.sylphx-flow/mcp-config.json
82
+ */
83
+ private async loadGlobalMCPServers(
84
+ target: 'claude-code' | 'opencode'
85
+ ): Promise<Array<{ name: string; config: any }>> {
86
+ try {
87
+ const enabledServers = await this.configService.getEnabledMCPServers();
88
+ const servers: Array<{ name: string; config: any }> = [];
89
+
90
+ for (const [serverKey, serverConfig] of Object.entries(enabledServers)) {
91
+ // Lookup server definition in registry
92
+ const serverDef = MCP_SERVER_REGISTRY[serverKey];
93
+
94
+ if (!serverDef) {
95
+ console.warn(`MCP server '${serverKey}' not found in registry, skipping`);
96
+ continue;
97
+ }
98
+
99
+ // Clone the server config from registry
100
+ let config: any = { ...serverDef.config };
101
+
102
+ // Merge environment variables from global config
103
+ if (serverConfig.env && Object.keys(serverConfig.env).length > 0) {
104
+ if (config.type === 'stdio' || config.type === 'local') {
105
+ config.env = { ...config.env, ...serverConfig.env };
106
+ }
107
+ }
108
+
109
+ servers.push({ name: serverDef.name, config });
110
+ }
111
+
112
+ return servers;
113
+ } catch (error) {
114
+ // If global config doesn't exist or fails to load, return empty array
115
+ return [];
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Attach Flow templates to project
121
+ * Strategy: Override with warning, backup handles restoration
122
+ */
123
+ async attach(
124
+ projectPath: string,
125
+ projectHash: string,
126
+ target: 'claude-code' | 'opencode',
127
+ templates: FlowTemplates,
128
+ manifest: BackupManifest
129
+ ): Promise<AttachResult> {
130
+ const targetDir = this.projectManager.getTargetConfigDir(projectPath, target);
131
+
132
+ const result: AttachResult = {
133
+ agentsAdded: [],
134
+ agentsOverridden: [],
135
+ commandsAdded: [],
136
+ commandsOverridden: [],
137
+ rulesAppended: false,
138
+ mcpServersAdded: [],
139
+ mcpServersOverridden: [],
140
+ singleFilesMerged: [],
141
+ hooksAdded: [],
142
+ hooksOverridden: [],
143
+ conflicts: [],
144
+ };
145
+
146
+ // Ensure target directory exists
147
+ await fs.mkdir(targetDir, { recursive: true });
148
+
149
+ // 1. Attach agents
150
+ await this.attachAgents(targetDir, target, templates.agents, result, manifest);
151
+
152
+ // 2. Attach commands
153
+ await this.attachCommands(targetDir, target, templates.commands, result, manifest);
154
+
155
+ // 3. Attach rules (if applicable)
156
+ if (templates.rules) {
157
+ await this.attachRules(targetDir, target, templates.rules, result, manifest);
158
+ }
159
+
160
+ // 4. Attach MCP servers (merge global + template servers)
161
+ const globalMCPServers = await this.loadGlobalMCPServers(target);
162
+ const allMCPServers = [...globalMCPServers, ...templates.mcpServers];
163
+
164
+ if (allMCPServers.length > 0) {
165
+ await this.attachMCPServers(
166
+ targetDir,
167
+ target,
168
+ allMCPServers,
169
+ result,
170
+ manifest
171
+ );
172
+ }
173
+
174
+ // 5. Attach hooks
175
+ if (templates.hooks.length > 0) {
176
+ await this.attachHooks(targetDir, templates.hooks, result, manifest);
177
+ }
178
+
179
+ // 6. Attach single files
180
+ if (templates.singleFiles.length > 0) {
181
+ await this.attachSingleFiles(projectPath, templates.singleFiles, result, manifest);
182
+ }
183
+
184
+ // Show conflict warnings
185
+ this.showConflictWarnings(result);
186
+
187
+ return result;
188
+ }
189
+
190
+ /**
191
+ * Attach agents (override strategy)
192
+ */
193
+ private async attachAgents(
194
+ targetDir: string,
195
+ target: 'claude-code' | 'opencode',
196
+ agents: Array<{ name: string; content: string }>,
197
+ result: AttachResult,
198
+ manifest: BackupManifest
199
+ ): 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);
226
+
227
+ // Track Flow agent
228
+ manifest.backup.agents.flow.push(agent.name);
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Attach commands (override strategy)
234
+ */
235
+ private async attachCommands(
236
+ targetDir: string,
237
+ target: 'claude-code' | 'opencode',
238
+ commands: Array<{ name: string; content: string }>,
239
+ result: AttachResult,
240
+ manifest: BackupManifest
241
+ ): 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);
268
+
269
+ // Track Flow command
270
+ manifest.backup.commands.flow.push(command.name);
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Attach rules (append strategy for AGENTS.md)
276
+ */
277
+ private async attachRules(
278
+ targetDir: string,
279
+ target: 'claude-code' | 'opencode',
280
+ rules: string,
281
+ result: AttachResult,
282
+ manifest: BackupManifest
283
+ ): 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
+ `;
310
+
311
+ await fs.writeFile(rulesPath, merged);
312
+
313
+ manifest.backup.rules = {
314
+ 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,
326
+ flowContentAdded: true,
327
+ };
328
+ }
329
+
330
+ result.rulesAppended = true;
331
+ }
332
+
333
+ /**
334
+ * Attach MCP servers (merge strategy)
335
+ */
336
+ private async attachMCPServers(
337
+ targetDir: string,
338
+ target: 'claude-code' | 'opencode',
339
+ mcpServers: Array<{ name: string; config: any }>,
340
+ result: AttachResult,
341
+ manifest: BackupManifest
342
+ ): 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');
349
+
350
+ let config: any = {};
351
+
352
+ if (existsSync(configPath)) {
353
+ config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
354
+ }
355
+
356
+ // Ensure mcp.servers exists
357
+ if (!config.mcp) config.mcp = {};
358
+ if (!config.mcp.servers) config.mcp.servers = {};
359
+
360
+ // Add Flow MCP servers
361
+ for (const server of mcpServers) {
362
+ if (config.mcp.servers[server.name]) {
363
+ // Conflict: user has same MCP server
364
+ result.mcpServersOverridden.push(server.name);
365
+ result.conflicts.push({
366
+ type: 'mcp',
367
+ name: server.name,
368
+ action: 'overridden',
369
+ message: `MCP server '${server.name}' overridden (will be restored on exit)`,
370
+ });
371
+ } else {
372
+ result.mcpServersAdded.push(server.name);
373
+ }
374
+
375
+ // Override with Flow config
376
+ config.mcp.servers[server.name] = server.config;
377
+ }
378
+
379
+ // Write updated config
380
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
381
+
382
+ // Track in manifest
383
+ manifest.backup.config = {
384
+ path: configPath,
385
+ hash: await this.calculateFileHash(configPath),
386
+ mcpServersCount: Object.keys(config.mcp.servers).length,
387
+ };
388
+ }
389
+
390
+ /**
391
+ * Attach hooks (override strategy)
392
+ */
393
+ private async attachHooks(
394
+ targetDir: string,
395
+ hooks: Array<{ name: string; content: string }>,
396
+ result: AttachResult,
397
+ manifest: BackupManifest
398
+ ): Promise<void> {
399
+ const hooksDir = path.join(targetDir, 'hooks');
400
+ await fs.mkdir(hooksDir, { recursive: true });
401
+
402
+ for (const hook of hooks) {
403
+ const hookPath = path.join(hooksDir, hook.name);
404
+ const existed = existsSync(hookPath);
405
+
406
+ if (existed) {
407
+ result.hooksOverridden.push(hook.name);
408
+ result.conflicts.push({
409
+ type: 'hook',
410
+ name: hook.name,
411
+ action: 'overridden',
412
+ message: `Hook '${hook.name}' overridden (will be restored on exit)`,
413
+ });
414
+ } else {
415
+ result.hooksAdded.push(hook.name);
416
+ }
417
+
418
+ await fs.writeFile(hookPath, hook.content);
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Attach single files (CLAUDE.md, .cursorrules, etc.)
424
+ */
425
+ private async attachSingleFiles(
426
+ projectPath: string,
427
+ singleFiles: Array<{ path: string; content: string }>,
428
+ result: AttachResult,
429
+ manifest: BackupManifest
430
+ ): Promise<void> {
431
+ for (const file of singleFiles) {
432
+ const filePath = path.join(projectPath, file.path);
433
+ const existed = existsSync(filePath);
434
+
435
+ if (existed) {
436
+ // User has file, append Flow content
437
+ const userContent = await fs.readFile(filePath, 'utf-8');
438
+
439
+ // Check if already appended
440
+ if (userContent.includes('<!-- Sylphx Flow Enhancement -->')) {
441
+ continue;
442
+ }
443
+
444
+ const merged = `${userContent}
445
+
446
+ ---
447
+
448
+ **Sylphx Flow Enhancement:**
449
+
450
+ ${file.content}
451
+ `;
452
+
453
+ await fs.writeFile(filePath, merged);
454
+
455
+ manifest.backup.singleFiles[file.path] = {
456
+ existed: true,
457
+ originalSize: userContent.length,
458
+ flowContentAdded: true,
459
+ };
460
+ } else {
461
+ // Create new file
462
+ await fs.writeFile(filePath, file.content);
463
+
464
+ manifest.backup.singleFiles[file.path] = {
465
+ existed: false,
466
+ originalSize: 0,
467
+ flowContentAdded: true,
468
+ };
469
+ }
470
+
471
+ result.singleFilesMerged.push(file.path);
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Show conflict warnings to user
477
+ */
478
+ private showConflictWarnings(result: AttachResult): void {
479
+ if (result.conflicts.length === 0) {
480
+ return;
481
+ }
482
+
483
+ console.log(chalk.yellow('\n⚠️ Conflicts detected:\n'));
484
+
485
+ for (const conflict of result.conflicts) {
486
+ console.log(
487
+ chalk.yellow(` • ${conflict.type}: ${conflict.name} - ${conflict.action}`)
488
+ );
489
+ }
490
+
491
+ console.log(
492
+ chalk.dim('\n Don\'t worry! All overridden content will be restored on exit.\n')
493
+ );
494
+ }
495
+ }