@sylphx/flow 1.3.1 → 1.4.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @sylphx/flow
2
2
 
3
+ ## 1.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix rules scanning showing all project markdown files:
8
+ - Skip rules scanning for Claude Code (rules embedded in agent files)
9
+ - Only scan when target has explicit rulesFile config
10
+ - Prevent scanning entire project directory
11
+
12
+ ## 1.4.0
13
+
14
+ ### Minor Changes
15
+
16
+ - Complete sync redesign with intelligent file categorization:
17
+ - Categorize all files: agents, commands, rules, MCP servers
18
+ - Separate Flow templates (auto-sync) from unknown files (user decides)
19
+ - New flow: preview → select unknowns → summary → confirm → execute
20
+ - Preserve user custom files by default (no accidental deletion)
21
+ - Multi-select UI for unknown files
22
+ - Clear visibility: what syncs, what's removed, what's preserved
23
+ - Remove all Chinese text (English only)
24
+
3
25
  ## 1.3.1
4
26
 
5
27
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/flow",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "description": "AI-powered development workflow automation with autonomous loop mode and smart configuration",
5
5
  "type": "module",
6
6
  "bin": {
@@ -380,7 +380,7 @@ async function executeSetupPhase(prompt: string | undefined, options: FlowOption
380
380
 
381
381
  // Handle sync mode - delete template files first
382
382
  if (options.sync && !options.dryRun) {
383
- const { buildSyncManifest, showSyncPreview, confirmSync, executeSyncDelete, checkMCPServers, selectServersToRemove, removeMCPServers } = await import('../utils/sync-utils.js');
383
+ const { buildSyncManifest, showSyncPreview, selectUnknownFilesToRemove, showFinalSummary, confirmSync, executeSyncDelete, removeMCPServers } = await import('../utils/sync-utils.js');
384
384
 
385
385
  // Need target to build manifest
386
386
  const targetId = await selectAndValidateTarget(initOptions);
@@ -394,31 +394,43 @@ async function executeSetupPhase(prompt: string | undefined, options: FlowOption
394
394
  const target = targetOption.value;
395
395
  const manifest = await buildSyncManifest(process.cwd(), target);
396
396
 
397
- // Check MCP servers before showing preview
398
- const nonRegistryServers = await checkMCPServers(process.cwd());
399
-
397
+ // Show preview
400
398
  console.log(chalk.cyan.bold('━━━ 🔄 Synchronizing Files\n'));
401
- showSyncPreview(manifest, process.cwd(), nonRegistryServers);
399
+ showSyncPreview(manifest, process.cwd());
400
+
401
+ // Select unknown files to remove
402
+ const selectedUnknowns = await selectUnknownFilesToRemove(manifest);
403
+
404
+ // Show final summary
405
+ showFinalSummary(manifest, selectedUnknowns);
402
406
 
407
+ // Confirm
403
408
  const confirmed = await confirmSync();
404
409
  if (!confirmed) {
405
410
  console.log(chalk.yellow('\n✗ Sync cancelled\n'));
406
411
  process.exit(0);
407
412
  }
408
413
 
409
- // Delete templates
410
- const deletedCount = await executeSyncDelete(manifest);
411
- console.log(chalk.green(`\n✓ Deleted ${deletedCount} template files\n`));
414
+ // Execute deletion
415
+ const { templates, unknowns } = await executeSyncDelete(manifest, selectedUnknowns);
412
416
 
413
- // Handle MCP servers if any found
414
- if (nonRegistryServers.length > 0) {
415
- const serversToRemove = await selectServersToRemove(nonRegistryServers);
417
+ // Remove MCP servers
418
+ const mcpServersToRemove = selectedUnknowns.filter(s => !s.includes('/'));
419
+ let mcpRemoved = 0;
420
+ if (mcpServersToRemove.length > 0) {
421
+ mcpRemoved = await removeMCPServers(process.cwd(), mcpServersToRemove);
422
+ }
416
423
 
417
- if (serversToRemove.length > 0) {
418
- const removedCount = await removeMCPServers(process.cwd(), serversToRemove);
419
- console.log(chalk.green(`✓ Removed ${removedCount} MCP server(s)\n`));
420
- }
424
+ // Summary
425
+ console.log(chalk.green(`\n✓ Synced ${templates} templates`));
426
+ if (unknowns > 0 || mcpRemoved > 0) {
427
+ console.log(chalk.green(`✓ Removed ${unknowns + mcpRemoved} files`));
428
+ }
429
+ const preserved = manifest.agents.unknown.length + manifest.slashCommands.unknown.length + manifest.rules.unknown.length + manifest.mcpServers.notInRegistry.length - selectedUnknowns.length;
430
+ if (preserved > 0) {
431
+ console.log(chalk.green(`✓ Preserved ${preserved} custom files`));
421
432
  }
433
+ console.log('');
422
434
  } else if (!options.sync) {
423
435
  const targetId = await selectAndValidateTarget(initOptions);
424
436
  selectedTarget = targetId;
@@ -709,7 +721,7 @@ async function executeFlowOnce(prompt: string | undefined, options: FlowOptions)
709
721
 
710
722
  // Handle sync mode - delete template files first
711
723
  if (options.sync && !options.dryRun) {
712
- const { buildSyncManifest, showSyncPreview, confirmSync, executeSyncDelete, checkMCPServers, selectServersToRemove, removeMCPServers } = await import('../utils/sync-utils.js');
724
+ const { buildSyncManifest, showSyncPreview, selectUnknownFilesToRemove, showFinalSummary, confirmSync, executeSyncDelete, removeMCPServers } = await import('../utils/sync-utils.js');
713
725
 
714
726
  // Need target to build manifest
715
727
  const targetId = await selectAndValidateTarget(initOptions);
@@ -723,31 +735,43 @@ async function executeFlowOnce(prompt: string | undefined, options: FlowOptions)
723
735
  const target = targetOption.value;
724
736
  const manifest = await buildSyncManifest(process.cwd(), target);
725
737
 
726
- // Check MCP servers before showing preview
727
- const nonRegistryServers = await checkMCPServers(process.cwd());
728
-
738
+ // Show preview
729
739
  console.log(chalk.cyan.bold('━━━ 🔄 Synchronizing Files\n'));
730
- showSyncPreview(manifest, process.cwd(), nonRegistryServers);
740
+ showSyncPreview(manifest, process.cwd());
741
+
742
+ // Select unknown files to remove
743
+ const selectedUnknowns = await selectUnknownFilesToRemove(manifest);
744
+
745
+ // Show final summary
746
+ showFinalSummary(manifest, selectedUnknowns);
731
747
 
748
+ // Confirm
732
749
  const confirmed = await confirmSync();
733
750
  if (!confirmed) {
734
751
  console.log(chalk.yellow('\n✗ Sync cancelled\n'));
735
752
  process.exit(0);
736
753
  }
737
754
 
738
- // Delete templates
739
- const deletedCount = await executeSyncDelete(manifest);
740
- console.log(chalk.green(`\n✓ Deleted ${deletedCount} template files\n`));
755
+ // Execute deletion
756
+ const { templates, unknowns } = await executeSyncDelete(manifest, selectedUnknowns);
741
757
 
742
- // Handle MCP servers if any found
743
- if (nonRegistryServers.length > 0) {
744
- const serversToRemove = await selectServersToRemove(nonRegistryServers);
758
+ // Remove MCP servers
759
+ const mcpServersToRemove = selectedUnknowns.filter(s => !s.includes('/'));
760
+ let mcpRemoved = 0;
761
+ if (mcpServersToRemove.length > 0) {
762
+ mcpRemoved = await removeMCPServers(process.cwd(), mcpServersToRemove);
763
+ }
745
764
 
746
- if (serversToRemove.length > 0) {
747
- const removedCount = await removeMCPServers(process.cwd(), serversToRemove);
748
- console.log(chalk.green(`✓ Removed ${removedCount} MCP server(s)\n`));
749
- }
765
+ // Summary
766
+ console.log(chalk.green(`\n✓ Synced ${templates} templates`));
767
+ if (unknowns > 0 || mcpRemoved > 0) {
768
+ console.log(chalk.green(`✓ Removed ${unknowns + mcpRemoved} files`));
769
+ }
770
+ const preserved = manifest.agents.unknown.length + manifest.slashCommands.unknown.length + manifest.rules.unknown.length + manifest.mcpServers.notInRegistry.length - selectedUnknowns.length;
771
+ if (preserved > 0) {
772
+ console.log(chalk.green(`✓ Preserved ${preserved} custom files`));
750
773
  }
774
+ console.log('');
751
775
  } else {
752
776
  // Select and validate target (will use existing in repair mode, or prompt if needed)
753
777
  const targetId = await selectAndValidateTarget(initOptions);
@@ -5,23 +5,62 @@ import type { Target } from '../types.js';
5
5
  import { MCP_SERVER_REGISTRY } from '../config/servers.js';
6
6
 
7
7
  /**
8
- * Files to delete during sync for each target
8
+ * Flow template filenames (source of truth)
9
+ */
10
+ const FLOW_AGENTS = ['coder.md', 'orchestrator.md', 'reviewer.md', 'writer.md'];
11
+ const FLOW_SLASH_COMMANDS = ['commit.md', 'context.md', 'explain.md', 'review.md', 'test.md'];
12
+ const FLOW_RULES = ['code-standards.md', 'core.md'];
13
+
14
+ /**
15
+ * Categorized files for sync
16
+ */
17
+ interface CategorizedFiles {
18
+ inFlow: string[]; // Files that exist in Flow templates
19
+ unknown: string[]; // Files not in Flow templates (custom or removed)
20
+ }
21
+
22
+ /**
23
+ * Sync manifest with categorization
9
24
  */
10
25
  interface SyncManifest {
11
- agents: string[];
12
- slashCommands: string[];
13
- rules: string[];
26
+ agents: CategorizedFiles;
27
+ slashCommands: CategorizedFiles;
28
+ rules: CategorizedFiles;
29
+ mcpServers: {
30
+ inRegistry: string[];
31
+ notInRegistry: string[];
32
+ };
14
33
  preserve: string[];
15
34
  }
16
35
 
17
36
  /**
18
- * Build sync manifest - list files to delete and preserve
37
+ * Categorize files into Flow templates vs unknown
38
+ */
39
+ function categorizeFiles(files: string[], flowTemplates: string[]): CategorizedFiles {
40
+ const inFlow: string[] = [];
41
+ const unknown: string[] = [];
42
+
43
+ for (const file of files) {
44
+ const basename = path.basename(file);
45
+ if (flowTemplates.includes(basename)) {
46
+ inFlow.push(file);
47
+ } else {
48
+ unknown.push(file);
49
+ }
50
+ }
51
+
52
+ return { inFlow, unknown };
53
+ }
54
+
55
+ /**
56
+ * Build sync manifest - categorize all files
19
57
  */
20
58
  export async function buildSyncManifest(cwd: string, target: Target): Promise<SyncManifest> {
21
59
  const manifest: SyncManifest = {
22
- agents: [],
23
- slashCommands: [],
24
- rules: [],
60
+ agents: { inFlow: [], unknown: [] },
61
+ slashCommands: { inFlow: [], unknown: [] },
62
+ rules: { inFlow: [], unknown: [] },
63
+ mcpServers: { inRegistry: [], notInRegistry: [] },
25
64
  preserve: [],
26
65
  };
27
66
 
@@ -30,9 +69,11 @@ export async function buildSyncManifest(cwd: string, target: Target): Promise<Sy
30
69
  const agentsDir = path.join(cwd, target.config.agentDir);
31
70
  if (fs.existsSync(agentsDir)) {
32
71
  const files = fs.readdirSync(agentsDir, { withFileTypes: true });
33
- manifest.agents = files
72
+ const agentFiles = files
34
73
  .filter((f) => f.isFile() && f.name.endsWith(target.config.agentExtension || '.md'))
35
74
  .map((f) => path.join(agentsDir, f.name));
75
+
76
+ manifest.agents = categorizeFiles(agentFiles, FLOW_AGENTS);
36
77
  }
37
78
  }
38
79
 
@@ -41,17 +82,58 @@ export async function buildSyncManifest(cwd: string, target: Target): Promise<Sy
41
82
  const commandsDir = path.join(cwd, target.config.slashCommandsDir);
42
83
  if (fs.existsSync(commandsDir)) {
43
84
  const files = fs.readdirSync(commandsDir, { withFileTypes: true });
44
- manifest.slashCommands = files
85
+ const commandFiles = files
45
86
  .filter((f) => f.isFile() && f.name.endsWith('.md'))
46
87
  .map((f) => path.join(commandsDir, f.name));
88
+
89
+ manifest.slashCommands = categorizeFiles(commandFiles, FLOW_SLASH_COMMANDS);
47
90
  }
48
91
  }
49
92
 
50
- // Rules files
93
+ // Rules files - only for targets with separate rules directory
94
+ // Claude Code has rules in agent files, so skip
51
95
  if (target.config.rulesFile) {
52
96
  const rulesPath = path.join(cwd, target.config.rulesFile);
97
+
98
+ // Check if it's a directory or file
53
99
  if (fs.existsSync(rulesPath)) {
54
- manifest.rules.push(rulesPath);
100
+ const stat = fs.statSync(rulesPath);
101
+
102
+ if (stat.isDirectory()) {
103
+ // Scan directory for rule files
104
+ const files = fs.readdirSync(rulesPath, { withFileTypes: true });
105
+ const ruleFiles = files
106
+ .filter((f) => f.isFile() && f.name.endsWith('.md'))
107
+ .map((f) => path.join(rulesPath, f.name));
108
+
109
+ manifest.rules = categorizeFiles(ruleFiles, FLOW_RULES);
110
+ } else {
111
+ // Single rules file - check if it matches Flow templates
112
+ manifest.rules = categorizeFiles([rulesPath], FLOW_RULES);
113
+ }
114
+ }
115
+ }
116
+
117
+ // MCP servers
118
+ const mcpPath = path.join(cwd, '.mcp.json');
119
+ if (fs.existsSync(mcpPath)) {
120
+ try {
121
+ const content = await fs.promises.readFile(mcpPath, 'utf-8');
122
+ const mcpConfig = JSON.parse(content);
123
+
124
+ if (mcpConfig.mcpServers) {
125
+ const installedServers = Object.keys(mcpConfig.mcpServers);
126
+ const registryServers = Object.keys(MCP_SERVER_REGISTRY);
127
+
128
+ manifest.mcpServers.inRegistry = installedServers.filter(id =>
129
+ registryServers.includes(id)
130
+ );
131
+ manifest.mcpServers.notInRegistry = installedServers.filter(id =>
132
+ !registryServers.includes(id)
133
+ );
134
+ }
135
+ } catch (error) {
136
+ console.warn(chalk.yellow('⚠ Failed to read .mcp.json'));
55
137
  }
56
138
  }
57
139
 
@@ -68,156 +150,283 @@ export async function buildSyncManifest(cwd: string, target: Target): Promise<Sy
68
150
  }
69
151
 
70
152
  /**
71
- * Show sync preview - what will be deleted
153
+ * Show sync preview with categorization
72
154
  */
73
- export function showSyncPreview(
74
- manifest: SyncManifest,
75
- cwd: string,
76
- nonRegistryServers: string[]
77
- ): void {
78
- console.log(chalk.cyan.bold('📋 Sync Preview\n'));
155
+ export function showSyncPreview(manifest: SyncManifest, cwd: string): void {
156
+ console.log(chalk.cyan.bold('━━━ 🔄 Sync Preview\n'));
79
157
 
80
- // Template files section
81
- const allFiles = [...manifest.agents, ...manifest.slashCommands, ...manifest.rules];
158
+ // Will sync section
159
+ const hasFlowFiles =
160
+ manifest.agents.inFlow.length > 0 ||
161
+ manifest.slashCommands.inFlow.length > 0 ||
162
+ manifest.rules.inFlow.length > 0 ||
163
+ manifest.mcpServers.inRegistry.length > 0;
82
164
 
83
- if (allFiles.length > 0) {
84
- console.log(chalk.yellow('🔄 Templates (delete + reinstall):\n'));
165
+ if (hasFlowFiles) {
166
+ console.log(chalk.green('Will sync (delete + reinstall):\n'));
85
167
 
86
- if (manifest.agents.length > 0) {
168
+ if (manifest.agents.inFlow.length > 0) {
87
169
  console.log(chalk.dim(' Agents:'));
88
- manifest.agents.forEach((file) => {
89
- const relative = path.relative(cwd, file);
90
- console.log(chalk.dim(` - ${relative}`));
170
+ manifest.agents.inFlow.forEach((file) => {
171
+ console.log(chalk.dim(` ✓ ${path.basename(file)}`));
91
172
  });
92
173
  console.log('');
93
174
  }
94
175
 
95
- if (manifest.slashCommands.length > 0) {
96
- console.log(chalk.dim(' Slash Commands:'));
97
- manifest.slashCommands.forEach((file) => {
98
- const relative = path.relative(cwd, file);
99
- console.log(chalk.dim(` - ${relative}`));
176
+ if (manifest.slashCommands.inFlow.length > 0) {
177
+ console.log(chalk.dim(' Commands:'));
178
+ manifest.slashCommands.inFlow.forEach((file) => {
179
+ console.log(chalk.dim(` ✓ ${path.basename(file)}`));
100
180
  });
101
181
  console.log('');
102
182
  }
103
183
 
104
- if (manifest.rules.length > 0) {
184
+ if (manifest.rules.inFlow.length > 0) {
105
185
  console.log(chalk.dim(' Rules:'));
106
- manifest.rules.forEach((file) => {
107
- const relative = path.relative(cwd, file);
108
- console.log(chalk.dim(` - ${relative}`));
186
+ manifest.rules.inFlow.forEach((file) => {
187
+ console.log(chalk.dim(` ✓ ${path.basename(file)}`));
188
+ });
189
+ console.log('');
190
+ }
191
+
192
+ if (manifest.mcpServers.inRegistry.length > 0) {
193
+ console.log(chalk.dim(' MCP Servers:'));
194
+ manifest.mcpServers.inRegistry.forEach((server) => {
195
+ console.log(chalk.dim(` ✓ ${server}`));
109
196
  });
110
197
  console.log('');
111
198
  }
112
- } else {
113
- console.log(chalk.yellow('🔄 Templates: None found\n'));
114
199
  }
115
200
 
116
- // MCP servers section
117
- if (nonRegistryServers.length > 0) {
118
- console.log(chalk.yellow('🔍 MCP Servers (not in registry):\n'));
119
- nonRegistryServers.forEach((server) => {
120
- console.log(chalk.dim(` - ${server}`));
121
- });
122
- console.log(chalk.dim('\n Possible reasons:'));
123
- console.log(chalk.dim(' 1. Removed from Flow registry'));
124
- console.log(chalk.dim(' 2. Custom installation\n'));
201
+ // Unknown files section
202
+ const hasUnknownFiles =
203
+ manifest.agents.unknown.length > 0 ||
204
+ manifest.slashCommands.unknown.length > 0 ||
205
+ manifest.rules.unknown.length > 0 ||
206
+ manifest.mcpServers.notInRegistry.length > 0;
207
+
208
+ if (hasUnknownFiles) {
209
+ console.log(chalk.yellow('Unknown files (not in Flow templates):\n'));
210
+
211
+ if (manifest.agents.unknown.length > 0) {
212
+ console.log(chalk.dim(' Agents:'));
213
+ manifest.agents.unknown.forEach((file) => {
214
+ console.log(chalk.dim(` ? ${path.basename(file)}`));
215
+ });
216
+ console.log('');
217
+ }
218
+
219
+ if (manifest.slashCommands.unknown.length > 0) {
220
+ console.log(chalk.dim(' Commands:'));
221
+ manifest.slashCommands.unknown.forEach((file) => {
222
+ console.log(chalk.dim(` ? ${path.basename(file)}`));
223
+ });
224
+ console.log('');
225
+ }
226
+
227
+ if (manifest.rules.unknown.length > 0) {
228
+ console.log(chalk.dim(' Rules:'));
229
+ manifest.rules.unknown.forEach((file) => {
230
+ console.log(chalk.dim(` ? ${path.basename(file)}`));
231
+ });
232
+ console.log('');
233
+ }
234
+
235
+ if (manifest.mcpServers.notInRegistry.length > 0) {
236
+ console.log(chalk.dim(' MCP Servers:'));
237
+ manifest.mcpServers.notInRegistry.forEach((server) => {
238
+ console.log(chalk.dim(` ? ${server}`));
239
+ });
240
+ console.log('');
241
+ }
125
242
  } else {
126
- console.log(chalk.green('✓ MCP Servers: All in registry\n'));
243
+ console.log(chalk.green('✓ No unknown files\n'));
127
244
  }
128
245
 
129
- // Preserved files section
130
- console.log(chalk.green('Preserved:\n'));
246
+ // Preserved section
247
+ console.log(chalk.green('Preserved:\n'));
131
248
  manifest.preserve.forEach((file) => {
132
249
  const relative = path.relative(cwd, file);
133
250
  if (fs.existsSync(file)) {
134
- console.log(chalk.dim(` - ${relative}`));
251
+ console.log(chalk.dim(` ${relative}`));
135
252
  }
136
253
  });
137
254
  console.log('');
138
255
  }
139
256
 
140
257
  /**
141
- * Execute sync - delete template files
258
+ * Select unknown files to remove
142
259
  */
143
- export async function executeSyncDelete(manifest: SyncManifest): Promise<number> {
144
- const allFiles = [...manifest.agents, ...manifest.slashCommands, ...manifest.rules];
145
- let deletedCount = 0;
260
+ export async function selectUnknownFilesToRemove(manifest: SyncManifest): Promise<string[]> {
261
+ const unknownFiles: Array<{ name: string; value: string; type: string }> = [];
262
+
263
+ // Collect all unknown files
264
+ manifest.agents.unknown.forEach((file) => {
265
+ unknownFiles.push({
266
+ name: `Agents: ${path.basename(file)}`,
267
+ value: file,
268
+ type: 'agent',
269
+ });
270
+ });
146
271
 
147
- for (const file of allFiles) {
148
- try {
149
- await fs.promises.unlink(file);
150
- deletedCount++;
151
- } catch (error) {
152
- // Ignore if file doesn't exist
153
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
154
- console.warn(chalk.yellow(`⚠ Failed to delete: ${file}`));
155
- }
156
- }
157
- }
272
+ manifest.slashCommands.unknown.forEach((file) => {
273
+ unknownFiles.push({
274
+ name: `Commands: ${path.basename(file)}`,
275
+ value: file,
276
+ type: 'command',
277
+ });
278
+ });
158
279
 
159
- return deletedCount;
160
- }
280
+ manifest.rules.unknown.forEach((file) => {
281
+ unknownFiles.push({
282
+ name: `Rules: ${path.basename(file)}`,
283
+ value: file,
284
+ type: 'rule',
285
+ });
286
+ });
287
+
288
+ manifest.mcpServers.notInRegistry.forEach((server) => {
289
+ unknownFiles.push({
290
+ name: `MCP: ${server}`,
291
+ value: server,
292
+ type: 'mcp',
293
+ });
294
+ });
295
+
296
+ if (unknownFiles.length === 0) {
297
+ return [];
298
+ }
161
299
 
162
- /**
163
- * Confirm sync with user
164
- */
165
- export async function confirmSync(): Promise<boolean> {
166
300
  const { default: inquirer } = await import('inquirer');
167
- const { confirm } = await inquirer.prompt([
301
+ const { selected } = await inquirer.prompt([
168
302
  {
169
- type: 'confirm',
170
- name: 'confirm',
171
- message: 'Proceed with sync? This will delete the files listed above.',
172
- default: false,
303
+ type: 'checkbox',
304
+ name: 'selected',
305
+ message: 'Select files to remove (space to select, enter to continue):',
306
+ choices: unknownFiles.map((f) => ({ name: f.name, value: f.value })),
173
307
  },
174
308
  ]);
175
- return confirm;
309
+
310
+ return selected;
176
311
  }
177
312
 
178
313
  /**
179
- * Check MCP servers - find servers not in Flow registry
314
+ * Show final summary before execution
180
315
  */
181
- export async function checkMCPServers(cwd: string): Promise<string[]> {
182
- const mcpPath = path.join(cwd, '.mcp.json');
316
+ export function showFinalSummary(
317
+ manifest: SyncManifest,
318
+ selectedUnknowns: string[]
319
+ ): void {
320
+ console.log(chalk.cyan.bold('\n━━━ 📋 Final Summary\n'));
321
+
322
+ // Will delete + reinstall
323
+ const flowFiles = [
324
+ ...manifest.agents.inFlow,
325
+ ...manifest.slashCommands.inFlow,
326
+ ...manifest.rules.inFlow,
327
+ ];
328
+
329
+ if (flowFiles.length > 0 || manifest.mcpServers.inRegistry.length > 0) {
330
+ console.log(chalk.yellow('Delete + reinstall:\n'));
331
+ flowFiles.forEach((file) => {
332
+ console.log(chalk.dim(` - ${path.basename(file)}`));
333
+ });
334
+ if (manifest.mcpServers.inRegistry.length > 0) {
335
+ manifest.mcpServers.inRegistry.forEach((server) => {
336
+ console.log(chalk.dim(` - MCP: ${server}`));
337
+ });
338
+ }
339
+ console.log('');
340
+ }
183
341
 
184
- if (!fs.existsSync(mcpPath)) {
185
- return [];
342
+ // Will remove (selected unknowns)
343
+ if (selectedUnknowns.length > 0) {
344
+ console.log(chalk.red('Remove (selected):\n'));
345
+ selectedUnknowns.forEach((file) => {
346
+ const name = file.includes('/') ? path.basename(file) : file;
347
+ console.log(chalk.dim(` - ${name}`));
348
+ });
349
+ console.log('');
186
350
  }
187
351
 
188
- try {
189
- const content = await fs.promises.readFile(mcpPath, 'utf-8');
190
- const mcpConfig = JSON.parse(content);
352
+ // Will preserve
353
+ const preservedUnknowns = [
354
+ ...manifest.agents.unknown,
355
+ ...manifest.slashCommands.unknown,
356
+ ...manifest.rules.unknown,
357
+ ...manifest.mcpServers.notInRegistry,
358
+ ].filter((file) => !selectedUnknowns.includes(file));
359
+
360
+ if (preservedUnknowns.length > 0) {
361
+ console.log(chalk.green('Preserve:\n'));
362
+ preservedUnknowns.forEach((file) => {
363
+ const name = file.includes('/') ? path.basename(file) : file;
364
+ console.log(chalk.dim(` - ${name}`));
365
+ });
366
+ console.log('');
367
+ }
368
+ }
191
369
 
192
- if (!mcpConfig.mcpServers) {
193
- return [];
370
+ /**
371
+ * Execute sync - delete Flow templates and selected unknowns
372
+ */
373
+ export async function executeSyncDelete(
374
+ manifest: SyncManifest,
375
+ selectedUnknowns: string[]
376
+ ): Promise<{ templates: number; unknowns: number }> {
377
+ const flowFiles = [
378
+ ...manifest.agents.inFlow,
379
+ ...manifest.slashCommands.inFlow,
380
+ ...manifest.rules.inFlow,
381
+ ];
382
+
383
+ let templatesDeleted = 0;
384
+ let unknownsDeleted = 0;
385
+
386
+ // Delete Flow templates
387
+ for (const file of flowFiles) {
388
+ try {
389
+ await fs.promises.unlink(file);
390
+ templatesDeleted++;
391
+ } catch (error) {
392
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
393
+ console.warn(chalk.yellow(`⚠ Failed to delete: ${file}`));
394
+ }
194
395
  }
396
+ }
195
397
 
196
- const installedServers = Object.keys(mcpConfig.mcpServers);
197
- const registryServers = Object.keys(MCP_SERVER_REGISTRY);
398
+ // Delete selected unknown files
399
+ for (const file of selectedUnknowns) {
400
+ // Skip MCP servers (handled separately)
401
+ if (!file.includes('/')) continue;
198
402
 
199
- // Find servers not in registry
200
- return installedServers.filter(id => !registryServers.includes(id));
201
- } catch (error) {
202
- console.warn(chalk.yellow('⚠ Failed to read .mcp.json'));
203
- return [];
403
+ try {
404
+ await fs.promises.unlink(file);
405
+ unknownsDeleted++;
406
+ } catch (error) {
407
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
408
+ console.warn(chalk.yellow(`⚠ Failed to delete: ${file}`));
409
+ }
410
+ }
204
411
  }
412
+
413
+ return { templates: templatesDeleted, unknowns: unknownsDeleted };
205
414
  }
206
415
 
207
416
  /**
208
- * Select servers to remove
417
+ * Confirm sync with user
209
418
  */
210
- export async function selectServersToRemove(servers: string[]): Promise<string[]> {
419
+ export async function confirmSync(): Promise<boolean> {
211
420
  const { default: inquirer } = await import('inquirer');
212
- const { selected } = await inquirer.prompt([
421
+ const { confirm } = await inquirer.prompt([
213
422
  {
214
- type: 'checkbox',
215
- name: 'selected',
216
- message: '選擇要刪除既 servers:',
217
- choices: servers.map(s => ({ name: s, value: s })),
423
+ type: 'confirm',
424
+ name: 'confirm',
425
+ message: 'Proceed with sync?',
426
+ default: false,
218
427
  },
219
428
  ]);
220
- return selected;
429
+ return confirm;
221
430
  }
222
431
 
223
432
  /**