@sylphx/flow 1.3.0 → 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,28 @@
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
+
16
+ ## 1.3.1
17
+
18
+ ### Patch Changes
19
+
20
+ - Redesign sync flow for better clarity:
21
+ - Remove duplicate config files in preserved list
22
+ - Show MCP check in preview upfront (not after confirmation)
23
+ - Combined preview: templates + MCP servers + preserved files
24
+ - Clear sections with emojis for easy scanning
25
+
3
26
  ## 1.3.0
4
27
 
5
28
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/flow",
3
- "version": "1.3.0",
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 } = 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
+ // Show preview
397
398
  console.log(chalk.cyan.bold('━━━ 🔄 Synchronizing Files\n'));
398
399
  showSyncPreview(manifest, process.cwd());
399
400
 
401
+ // Select unknown files to remove
402
+ const selectedUnknowns = await selectUnknownFilesToRemove(manifest);
403
+
404
+ // Show final summary
405
+ showFinalSummary(manifest, selectedUnknowns);
406
+
407
+ // Confirm
400
408
  const confirmed = await confirmSync();
401
409
  if (!confirmed) {
402
410
  console.log(chalk.yellow('\n✗ Sync cancelled\n'));
403
411
  process.exit(0);
404
412
  }
405
413
 
406
- const deletedCount = await executeSyncDelete(manifest);
407
- console.log(chalk.green(`\n✓ Deleted ${deletedCount} files\n`));
408
-
409
- // Check MCP servers
410
- const { checkMCPServers, showNonRegistryServers, selectServersToRemove, removeMCPServers } = await import('../utils/sync-utils.js');
411
- const nonRegistryServers = await checkMCPServers(process.cwd());
414
+ // Execute deletion
415
+ const { templates, unknowns } = await executeSyncDelete(manifest, selectedUnknowns);
412
416
 
413
- if (nonRegistryServers.length > 0) {
414
- showNonRegistryServers(nonRegistryServers);
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(`\n✓ 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 } = 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
 
738
+ // Show preview
726
739
  console.log(chalk.cyan.bold('━━━ 🔄 Synchronizing Files\n'));
727
740
  showSyncPreview(manifest, process.cwd());
728
741
 
742
+ // Select unknown files to remove
743
+ const selectedUnknowns = await selectUnknownFilesToRemove(manifest);
744
+
745
+ // Show final summary
746
+ showFinalSummary(manifest, selectedUnknowns);
747
+
748
+ // Confirm
729
749
  const confirmed = await confirmSync();
730
750
  if (!confirmed) {
731
751
  console.log(chalk.yellow('\n✗ Sync cancelled\n'));
732
752
  process.exit(0);
733
753
  }
734
754
 
735
- const deletedCount = await executeSyncDelete(manifest);
736
- console.log(chalk.green(`\n✓ Deleted ${deletedCount} files\n`));
737
-
738
- // Check MCP servers
739
- const { checkMCPServers, showNonRegistryServers, selectServersToRemove, removeMCPServers } = await import('../utils/sync-utils.js');
740
- const nonRegistryServers = await checkMCPServers(process.cwd());
755
+ // Execute deletion
756
+ const { templates, unknowns } = await executeSyncDelete(manifest, selectedUnknowns);
741
757
 
742
- if (nonRegistryServers.length > 0) {
743
- showNonRegistryServers(nonRegistryServers);
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(`\n✓ 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
 
@@ -60,8 +129,6 @@ export async function buildSyncManifest(cwd: string, target: Target): Promise<Sy
60
129
  '.sylphx-flow/',
61
130
  '.secrets/',
62
131
  target.config.configFile || '',
63
- '.mcp.json',
64
- 'opencode.jsonc',
65
132
  ]
66
133
  .filter(Boolean)
67
134
  .map((p) => path.join(cwd, p));
@@ -70,158 +137,283 @@ export async function buildSyncManifest(cwd: string, target: Target): Promise<Sy
70
137
  }
71
138
 
72
139
  /**
73
- * Show sync preview - what will be deleted
140
+ * Show sync preview with categorization
74
141
  */
75
142
  export function showSyncPreview(manifest: SyncManifest, cwd: string): void {
76
- console.log(chalk.cyan.bold('📋 Sync Preview\n'));
77
- console.log(chalk.dim('The following files will be deleted and re-installed:\n'));
143
+ console.log(chalk.cyan.bold('━━━ 🔄 Sync Preview\n'));
144
+
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;
151
+
152
+ if (hasFlowFiles) {
153
+ console.log(chalk.green('Will sync (delete + reinstall):\n'));
154
+
155
+ if (manifest.agents.inFlow.length > 0) {
156
+ console.log(chalk.dim(' Agents:'));
157
+ manifest.agents.inFlow.forEach((file) => {
158
+ console.log(chalk.dim(` ✓ ${path.basename(file)}`));
159
+ });
160
+ console.log('');
161
+ }
78
162
 
79
- const allFiles = [...manifest.agents, ...manifest.slashCommands, ...manifest.rules];
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)}`));
167
+ });
168
+ console.log('');
169
+ }
80
170
 
81
- if (allFiles.length === 0) {
82
- console.log(chalk.yellow(' No template files found\n'));
83
- return;
84
- }
171
+ if (manifest.rules.inFlow.length > 0) {
172
+ console.log(chalk.dim(' Rules:'));
173
+ manifest.rules.inFlow.forEach((file) => {
174
+ console.log(chalk.dim(` ✓ ${path.basename(file)}`));
175
+ });
176
+ console.log('');
177
+ }
85
178
 
86
- // Group by type
87
- if (manifest.agents.length > 0) {
88
- console.log(chalk.cyan(' Agents:'));
89
- manifest.agents.forEach((file) => {
90
- const relative = path.relative(cwd, file);
91
- console.log(chalk.dim(` - ${relative}`));
92
- });
93
- console.log('');
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}`));
183
+ });
184
+ console.log('');
185
+ }
94
186
  }
95
187
 
96
- if (manifest.slashCommands.length > 0) {
97
- console.log(chalk.cyan(' Slash Commands:'));
98
- manifest.slashCommands.forEach((file) => {
99
- const relative = path.relative(cwd, file);
100
- console.log(chalk.dim(` - ${relative}`));
101
- });
102
- console.log('');
103
- }
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
+ }
104
205
 
105
- if (manifest.rules.length > 0) {
106
- console.log(chalk.cyan(' Rules:'));
107
- manifest.rules.forEach((file) => {
108
- const relative = path.relative(cwd, file);
109
- console.log(chalk.dim(` - ${relative}`));
110
- });
111
- console.log('');
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
+ }
229
+ } else {
230
+ console.log(chalk.green('✓ No unknown files\n'));
112
231
  }
113
232
 
114
- console.log(chalk.green('✓ Preserved:'));
233
+ // Preserved section
234
+ console.log(chalk.green('Preserved:\n'));
115
235
  manifest.preserve.forEach((file) => {
116
236
  const relative = path.relative(cwd, file);
117
237
  if (fs.existsSync(file)) {
118
- console.log(chalk.dim(` - ${relative}`));
238
+ console.log(chalk.dim(` ${relative}`));
119
239
  }
120
240
  });
121
241
  console.log('');
122
242
  }
123
243
 
124
244
  /**
125
- * Execute sync - delete template files
245
+ * Select unknown files to remove
126
246
  */
127
- export async function executeSyncDelete(manifest: SyncManifest): Promise<number> {
128
- const allFiles = [...manifest.agents, ...manifest.slashCommands, ...manifest.rules];
129
- 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
+ });
130
258
 
131
- for (const file of allFiles) {
132
- try {
133
- await fs.promises.unlink(file);
134
- deletedCount++;
135
- } catch (error) {
136
- // Ignore if file doesn't exist
137
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
138
- console.warn(chalk.yellow(`⚠ Failed to delete: ${file}`));
139
- }
140
- }
141
- }
259
+ manifest.slashCommands.unknown.forEach((file) => {
260
+ unknownFiles.push({
261
+ name: `Commands: ${path.basename(file)}`,
262
+ value: file,
263
+ type: 'command',
264
+ });
265
+ });
142
266
 
143
- return deletedCount;
144
- }
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
+ }
145
286
 
146
- /**
147
- * Confirm sync with user
148
- */
149
- export async function confirmSync(): Promise<boolean> {
150
287
  const { default: inquirer } = await import('inquirer');
151
- const { confirm } = await inquirer.prompt([
288
+ const { selected } = await inquirer.prompt([
152
289
  {
153
- type: 'confirm',
154
- name: 'confirm',
155
- message: 'Proceed with sync? This will delete the files listed above.',
156
- 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 })),
157
294
  },
158
295
  ]);
159
- return confirm;
296
+
297
+ return selected;
160
298
  }
161
299
 
162
300
  /**
163
- * Check MCP servers - find servers not in Flow registry
301
+ * Show final summary before execution
164
302
  */
165
- export async function checkMCPServers(cwd: string): Promise<string[]> {
166
- const mcpPath = path.join(cwd, '.mcp.json');
167
-
168
- if (!fs.existsSync(mcpPath)) {
169
- return [];
170
- }
171
-
172
- try {
173
- const content = await fs.promises.readFile(mcpPath, 'utf-8');
174
- const mcpConfig = JSON.parse(content);
175
-
176
- if (!mcpConfig.mcpServers) {
177
- return [];
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
+ });
178
325
  }
326
+ console.log('');
327
+ }
179
328
 
180
- const installedServers = Object.keys(mcpConfig.mcpServers);
181
- const registryServers = Object.keys(MCP_SERVER_REGISTRY);
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('');
337
+ }
182
338
 
183
- // Find servers not in registry
184
- return installedServers.filter(id => !registryServers.includes(id));
185
- } catch (error) {
186
- console.warn(chalk.yellow('⚠ Failed to read .mcp.json'));
187
- return [];
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('');
188
354
  }
189
355
  }
190
356
 
191
357
  /**
192
- * Show non-registry servers
358
+ * Execute sync - delete Flow templates and selected unknowns
193
359
  */
194
- export function showNonRegistryServers(servers: string[]): void {
195
- if (servers.length === 0) {
196
- return;
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
+ }
382
+ }
197
383
  }
198
384
 
199
- console.log(chalk.cyan.bold('\n📋 MCP Registry Check\n'));
200
- console.log(chalk.yellow('⚠️ 以下 MCP servers 唔係 Flow registry 入面:\n'));
385
+ // Delete selected unknown files
386
+ for (const file of selectedUnknowns) {
387
+ // Skip MCP servers (handled separately)
388
+ if (!file.includes('/')) continue;
201
389
 
202
- servers.forEach(server => {
203
- console.log(chalk.dim(` - ${server}`));
204
- });
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
+ }
398
+ }
205
399
 
206
- console.log(chalk.dim('\n可能原因:'));
207
- console.log(chalk.dim(' 1. Flow registry 已移除'));
208
- console.log(chalk.dim(' 2. 你自己手動安裝\n'));
400
+ return { templates: templatesDeleted, unknowns: unknownsDeleted };
209
401
  }
210
402
 
211
403
  /**
212
- * Select servers to remove
404
+ * Confirm sync with user
213
405
  */
214
- export async function selectServersToRemove(servers: string[]): Promise<string[]> {
406
+ export async function confirmSync(): Promise<boolean> {
215
407
  const { default: inquirer } = await import('inquirer');
216
- const { selected } = await inquirer.prompt([
408
+ const { confirm } = await inquirer.prompt([
217
409
  {
218
- type: 'checkbox',
219
- name: 'selected',
220
- message: '選擇要刪除既 servers:',
221
- choices: servers.map(s => ({ name: s, value: s })),
410
+ type: 'confirm',
411
+ name: 'confirm',
412
+ message: 'Proceed with sync?',
413
+ default: false,
222
414
  },
223
415
  ]);
224
- return selected;
416
+ return confirm;
225
417
  }
226
418
 
227
419
  /**