@sylphx/flow 1.3.1 → 1.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @sylphx/flow
2
2
 
3
+ ## 1.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Complete sync redesign with intelligent file categorization:
8
+ - Categorize all files: agents, commands, rules, MCP servers
9
+ - Separate Flow templates (auto-sync) from unknown files (user decides)
10
+ - New flow: preview → select unknowns → summary → confirm → execute
11
+ - Preserve user custom files by default (no accidental deletion)
12
+ - Multi-select UI for unknown files
13
+ - Clear visibility: what syncs, what's removed, what's preserved
14
+ - Remove all Chinese text (English only)
15
+
3
16
  ## 1.3.1
4
17
 
5
18
  ### 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.0",
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,45 @@ 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
51
- if (target.config.rulesFile) {
52
- const rulesPath = path.join(cwd, target.config.rulesFile);
53
- if (fs.existsSync(rulesPath)) {
54
- manifest.rules.push(rulesPath);
93
+ // Rules files - check individual files
94
+ const rulesDir = path.dirname(target.config.rulesFile || '');
95
+ if (rulesDir && fs.existsSync(path.join(cwd, rulesDir))) {
96
+ const files = fs.readdirSync(path.join(cwd, rulesDir), { withFileTypes: true });
97
+ const ruleFiles = files
98
+ .filter((f) => f.isFile() && f.name.endsWith('.md'))
99
+ .map((f) => path.join(cwd, rulesDir, f.name));
100
+
101
+ manifest.rules = categorizeFiles(ruleFiles, FLOW_RULES);
102
+ }
103
+
104
+ // MCP servers
105
+ const mcpPath = path.join(cwd, '.mcp.json');
106
+ if (fs.existsSync(mcpPath)) {
107
+ try {
108
+ const content = await fs.promises.readFile(mcpPath, 'utf-8');
109
+ const mcpConfig = JSON.parse(content);
110
+
111
+ if (mcpConfig.mcpServers) {
112
+ const installedServers = Object.keys(mcpConfig.mcpServers);
113
+ const registryServers = Object.keys(MCP_SERVER_REGISTRY);
114
+
115
+ manifest.mcpServers.inRegistry = installedServers.filter(id =>
116
+ registryServers.includes(id)
117
+ );
118
+ manifest.mcpServers.notInRegistry = installedServers.filter(id =>
119
+ !registryServers.includes(id)
120
+ );
121
+ }
122
+ } catch (error) {
123
+ console.warn(chalk.yellow('⚠ Failed to read .mcp.json'));
55
124
  }
56
125
  }
57
126
 
@@ -68,156 +137,283 @@ export async function buildSyncManifest(cwd: string, target: Target): Promise<Sy
68
137
  }
69
138
 
70
139
  /**
71
- * Show sync preview - what will be deleted
140
+ * Show sync preview with categorization
72
141
  */
73
- export function showSyncPreview(
74
- manifest: SyncManifest,
75
- cwd: string,
76
- nonRegistryServers: string[]
77
- ): void {
78
- console.log(chalk.cyan.bold('📋 Sync Preview\n'));
142
+ export function showSyncPreview(manifest: SyncManifest, cwd: string): void {
143
+ console.log(chalk.cyan.bold('━━━ 🔄 Sync Preview\n'));
79
144
 
80
- // Template files section
81
- const allFiles = [...manifest.agents, ...manifest.slashCommands, ...manifest.rules];
145
+ // Will sync section
146
+ const hasFlowFiles =
147
+ manifest.agents.inFlow.length > 0 ||
148
+ manifest.slashCommands.inFlow.length > 0 ||
149
+ manifest.rules.inFlow.length > 0 ||
150
+ manifest.mcpServers.inRegistry.length > 0;
82
151
 
83
- if (allFiles.length > 0) {
84
- console.log(chalk.yellow('🔄 Templates (delete + reinstall):\n'));
152
+ if (hasFlowFiles) {
153
+ console.log(chalk.green('Will sync (delete + reinstall):\n'));
85
154
 
86
- if (manifest.agents.length > 0) {
155
+ if (manifest.agents.inFlow.length > 0) {
87
156
  console.log(chalk.dim(' Agents:'));
88
- manifest.agents.forEach((file) => {
89
- const relative = path.relative(cwd, file);
90
- console.log(chalk.dim(` - ${relative}`));
157
+ manifest.agents.inFlow.forEach((file) => {
158
+ console.log(chalk.dim(` ✓ ${path.basename(file)}`));
91
159
  });
92
160
  console.log('');
93
161
  }
94
162
 
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}`));
163
+ if (manifest.slashCommands.inFlow.length > 0) {
164
+ console.log(chalk.dim(' Commands:'));
165
+ manifest.slashCommands.inFlow.forEach((file) => {
166
+ console.log(chalk.dim(` ✓ ${path.basename(file)}`));
100
167
  });
101
168
  console.log('');
102
169
  }
103
170
 
104
- if (manifest.rules.length > 0) {
171
+ if (manifest.rules.inFlow.length > 0) {
105
172
  console.log(chalk.dim(' Rules:'));
106
- manifest.rules.forEach((file) => {
107
- const relative = path.relative(cwd, file);
108
- console.log(chalk.dim(` - ${relative}`));
173
+ manifest.rules.inFlow.forEach((file) => {
174
+ console.log(chalk.dim(` ✓ ${path.basename(file)}`));
175
+ });
176
+ console.log('');
177
+ }
178
+
179
+ if (manifest.mcpServers.inRegistry.length > 0) {
180
+ console.log(chalk.dim(' MCP Servers:'));
181
+ manifest.mcpServers.inRegistry.forEach((server) => {
182
+ console.log(chalk.dim(` ✓ ${server}`));
109
183
  });
110
184
  console.log('');
111
185
  }
112
- } else {
113
- console.log(chalk.yellow('🔄 Templates: None found\n'));
114
186
  }
115
187
 
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'));
188
+ // Unknown files section
189
+ const hasUnknownFiles =
190
+ manifest.agents.unknown.length > 0 ||
191
+ manifest.slashCommands.unknown.length > 0 ||
192
+ manifest.rules.unknown.length > 0 ||
193
+ manifest.mcpServers.notInRegistry.length > 0;
194
+
195
+ if (hasUnknownFiles) {
196
+ console.log(chalk.yellow('Unknown files (not in Flow templates):\n'));
197
+
198
+ if (manifest.agents.unknown.length > 0) {
199
+ console.log(chalk.dim(' Agents:'));
200
+ manifest.agents.unknown.forEach((file) => {
201
+ console.log(chalk.dim(` ? ${path.basename(file)}`));
202
+ });
203
+ console.log('');
204
+ }
205
+
206
+ if (manifest.slashCommands.unknown.length > 0) {
207
+ console.log(chalk.dim(' Commands:'));
208
+ manifest.slashCommands.unknown.forEach((file) => {
209
+ console.log(chalk.dim(` ? ${path.basename(file)}`));
210
+ });
211
+ console.log('');
212
+ }
213
+
214
+ if (manifest.rules.unknown.length > 0) {
215
+ console.log(chalk.dim(' Rules:'));
216
+ manifest.rules.unknown.forEach((file) => {
217
+ console.log(chalk.dim(` ? ${path.basename(file)}`));
218
+ });
219
+ console.log('');
220
+ }
221
+
222
+ if (manifest.mcpServers.notInRegistry.length > 0) {
223
+ console.log(chalk.dim(' MCP Servers:'));
224
+ manifest.mcpServers.notInRegistry.forEach((server) => {
225
+ console.log(chalk.dim(` ? ${server}`));
226
+ });
227
+ console.log('');
228
+ }
125
229
  } else {
126
- console.log(chalk.green('✓ MCP Servers: All in registry\n'));
230
+ console.log(chalk.green('✓ No unknown files\n'));
127
231
  }
128
232
 
129
- // Preserved files section
130
- console.log(chalk.green('Preserved:\n'));
233
+ // Preserved section
234
+ console.log(chalk.green('Preserved:\n'));
131
235
  manifest.preserve.forEach((file) => {
132
236
  const relative = path.relative(cwd, file);
133
237
  if (fs.existsSync(file)) {
134
- console.log(chalk.dim(` - ${relative}`));
238
+ console.log(chalk.dim(` ${relative}`));
135
239
  }
136
240
  });
137
241
  console.log('');
138
242
  }
139
243
 
140
244
  /**
141
- * Execute sync - delete template files
245
+ * Select unknown files to remove
142
246
  */
143
- export async function executeSyncDelete(manifest: SyncManifest): Promise<number> {
144
- const allFiles = [...manifest.agents, ...manifest.slashCommands, ...manifest.rules];
145
- let deletedCount = 0;
247
+ export async function selectUnknownFilesToRemove(manifest: SyncManifest): Promise<string[]> {
248
+ const unknownFiles: Array<{ name: string; value: string; type: string }> = [];
249
+
250
+ // Collect all unknown files
251
+ manifest.agents.unknown.forEach((file) => {
252
+ unknownFiles.push({
253
+ name: `Agents: ${path.basename(file)}`,
254
+ value: file,
255
+ type: 'agent',
256
+ });
257
+ });
146
258
 
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
- }
259
+ manifest.slashCommands.unknown.forEach((file) => {
260
+ unknownFiles.push({
261
+ name: `Commands: ${path.basename(file)}`,
262
+ value: file,
263
+ type: 'command',
264
+ });
265
+ });
158
266
 
159
- return deletedCount;
160
- }
267
+ manifest.rules.unknown.forEach((file) => {
268
+ unknownFiles.push({
269
+ name: `Rules: ${path.basename(file)}`,
270
+ value: file,
271
+ type: 'rule',
272
+ });
273
+ });
274
+
275
+ manifest.mcpServers.notInRegistry.forEach((server) => {
276
+ unknownFiles.push({
277
+ name: `MCP: ${server}`,
278
+ value: server,
279
+ type: 'mcp',
280
+ });
281
+ });
282
+
283
+ if (unknownFiles.length === 0) {
284
+ return [];
285
+ }
161
286
 
162
- /**
163
- * Confirm sync with user
164
- */
165
- export async function confirmSync(): Promise<boolean> {
166
287
  const { default: inquirer } = await import('inquirer');
167
- const { confirm } = await inquirer.prompt([
288
+ const { selected } = await inquirer.prompt([
168
289
  {
169
- type: 'confirm',
170
- name: 'confirm',
171
- message: 'Proceed with sync? This will delete the files listed above.',
172
- default: false,
290
+ type: 'checkbox',
291
+ name: 'selected',
292
+ message: 'Select files to remove (space to select, enter to continue):',
293
+ choices: unknownFiles.map((f) => ({ name: f.name, value: f.value })),
173
294
  },
174
295
  ]);
175
- return confirm;
296
+
297
+ return selected;
176
298
  }
177
299
 
178
300
  /**
179
- * Check MCP servers - find servers not in Flow registry
301
+ * Show final summary before execution
180
302
  */
181
- export async function checkMCPServers(cwd: string): Promise<string[]> {
182
- const mcpPath = path.join(cwd, '.mcp.json');
303
+ export function showFinalSummary(
304
+ manifest: SyncManifest,
305
+ selectedUnknowns: string[]
306
+ ): void {
307
+ console.log(chalk.cyan.bold('\n━━━ 📋 Final Summary\n'));
308
+
309
+ // Will delete + reinstall
310
+ const flowFiles = [
311
+ ...manifest.agents.inFlow,
312
+ ...manifest.slashCommands.inFlow,
313
+ ...manifest.rules.inFlow,
314
+ ];
315
+
316
+ if (flowFiles.length > 0 || manifest.mcpServers.inRegistry.length > 0) {
317
+ console.log(chalk.yellow('Delete + reinstall:\n'));
318
+ flowFiles.forEach((file) => {
319
+ console.log(chalk.dim(` - ${path.basename(file)}`));
320
+ });
321
+ if (manifest.mcpServers.inRegistry.length > 0) {
322
+ manifest.mcpServers.inRegistry.forEach((server) => {
323
+ console.log(chalk.dim(` - MCP: ${server}`));
324
+ });
325
+ }
326
+ console.log('');
327
+ }
183
328
 
184
- if (!fs.existsSync(mcpPath)) {
185
- return [];
329
+ // Will remove (selected unknowns)
330
+ if (selectedUnknowns.length > 0) {
331
+ console.log(chalk.red('Remove (selected):\n'));
332
+ selectedUnknowns.forEach((file) => {
333
+ const name = file.includes('/') ? path.basename(file) : file;
334
+ console.log(chalk.dim(` - ${name}`));
335
+ });
336
+ console.log('');
186
337
  }
187
338
 
188
- try {
189
- const content = await fs.promises.readFile(mcpPath, 'utf-8');
190
- const mcpConfig = JSON.parse(content);
339
+ // Will preserve
340
+ const preservedUnknowns = [
341
+ ...manifest.agents.unknown,
342
+ ...manifest.slashCommands.unknown,
343
+ ...manifest.rules.unknown,
344
+ ...manifest.mcpServers.notInRegistry,
345
+ ].filter((file) => !selectedUnknowns.includes(file));
346
+
347
+ if (preservedUnknowns.length > 0) {
348
+ console.log(chalk.green('Preserve:\n'));
349
+ preservedUnknowns.forEach((file) => {
350
+ const name = file.includes('/') ? path.basename(file) : file;
351
+ console.log(chalk.dim(` - ${name}`));
352
+ });
353
+ console.log('');
354
+ }
355
+ }
191
356
 
192
- if (!mcpConfig.mcpServers) {
193
- return [];
357
+ /**
358
+ * Execute sync - delete Flow templates and selected unknowns
359
+ */
360
+ export async function executeSyncDelete(
361
+ manifest: SyncManifest,
362
+ selectedUnknowns: string[]
363
+ ): Promise<{ templates: number; unknowns: number }> {
364
+ const flowFiles = [
365
+ ...manifest.agents.inFlow,
366
+ ...manifest.slashCommands.inFlow,
367
+ ...manifest.rules.inFlow,
368
+ ];
369
+
370
+ let templatesDeleted = 0;
371
+ let unknownsDeleted = 0;
372
+
373
+ // Delete Flow templates
374
+ for (const file of flowFiles) {
375
+ try {
376
+ await fs.promises.unlink(file);
377
+ templatesDeleted++;
378
+ } catch (error) {
379
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
380
+ console.warn(chalk.yellow(`⚠ Failed to delete: ${file}`));
381
+ }
194
382
  }
383
+ }
195
384
 
196
- const installedServers = Object.keys(mcpConfig.mcpServers);
197
- const registryServers = Object.keys(MCP_SERVER_REGISTRY);
385
+ // Delete selected unknown files
386
+ for (const file of selectedUnknowns) {
387
+ // Skip MCP servers (handled separately)
388
+ if (!file.includes('/')) continue;
198
389
 
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 [];
390
+ try {
391
+ await fs.promises.unlink(file);
392
+ unknownsDeleted++;
393
+ } catch (error) {
394
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
395
+ console.warn(chalk.yellow(`⚠ Failed to delete: ${file}`));
396
+ }
397
+ }
204
398
  }
399
+
400
+ return { templates: templatesDeleted, unknowns: unknownsDeleted };
205
401
  }
206
402
 
207
403
  /**
208
- * Select servers to remove
404
+ * Confirm sync with user
209
405
  */
210
- export async function selectServersToRemove(servers: string[]): Promise<string[]> {
406
+ export async function confirmSync(): Promise<boolean> {
211
407
  const { default: inquirer } = await import('inquirer');
212
- const { selected } = await inquirer.prompt([
408
+ const { confirm } = await inquirer.prompt([
213
409
  {
214
- type: 'checkbox',
215
- name: 'selected',
216
- message: '選擇要刪除既 servers:',
217
- choices: servers.map(s => ({ name: s, value: s })),
410
+ type: 'confirm',
411
+ name: 'confirm',
412
+ message: 'Proceed with sync?',
413
+ default: false,
218
414
  },
219
415
  ]);
220
- return selected;
416
+ return confirm;
221
417
  }
222
418
 
223
419
  /**