@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 +23 -0
- package/package.json +1 -1
- package/src/commands/flow-command.ts +52 -28
- package/src/utils/sync-utils.ts +310 -118
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
|
@@ -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
|
-
|
|
407
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
736
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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);
|
package/src/utils/sync-utils.ts
CHANGED
|
@@ -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
|
-
*
|
|
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:
|
|
12
|
-
slashCommands:
|
|
13
|
-
rules:
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
140
|
+
* Show sync preview with categorization
|
|
74
141
|
*/
|
|
75
142
|
export function showSyncPreview(manifest: SyncManifest, cwd: string): void {
|
|
76
|
-
console.log(chalk.cyan.bold('
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
console.log(
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
manifest.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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(`
|
|
238
|
+
console.log(chalk.dim(` ${relative}`));
|
|
119
239
|
}
|
|
120
240
|
});
|
|
121
241
|
console.log('');
|
|
122
242
|
}
|
|
123
243
|
|
|
124
244
|
/**
|
|
125
|
-
*
|
|
245
|
+
* Select unknown files to remove
|
|
126
246
|
*/
|
|
127
|
-
export async function
|
|
128
|
-
const
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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 {
|
|
288
|
+
const { selected } = await inquirer.prompt([
|
|
152
289
|
{
|
|
153
|
-
type: '
|
|
154
|
-
name: '
|
|
155
|
-
message: '
|
|
156
|
-
|
|
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
|
-
|
|
296
|
+
|
|
297
|
+
return selected;
|
|
160
298
|
}
|
|
161
299
|
|
|
162
300
|
/**
|
|
163
|
-
*
|
|
301
|
+
* Show final summary before execution
|
|
164
302
|
*/
|
|
165
|
-
export
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
*
|
|
358
|
+
* Execute sync - delete Flow templates and selected unknowns
|
|
193
359
|
*/
|
|
194
|
-
export function
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
404
|
+
* Confirm sync with user
|
|
213
405
|
*/
|
|
214
|
-
export async function
|
|
406
|
+
export async function confirmSync(): Promise<boolean> {
|
|
215
407
|
const { default: inquirer } = await import('inquirer');
|
|
216
|
-
const {
|
|
408
|
+
const { confirm } = await inquirer.prompt([
|
|
217
409
|
{
|
|
218
|
-
type: '
|
|
219
|
-
name: '
|
|
220
|
-
message: '
|
|
221
|
-
|
|
410
|
+
type: 'confirm',
|
|
411
|
+
name: 'confirm',
|
|
412
|
+
message: 'Proceed with sync?',
|
|
413
|
+
default: false,
|
|
222
414
|
},
|
|
223
415
|
]);
|
|
224
|
-
return
|
|
416
|
+
return confirm;
|
|
225
417
|
}
|
|
226
418
|
|
|
227
419
|
/**
|