@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 +13 -0
- package/package.json +1 -1
- package/src/commands/flow-command.ts +54 -30
- package/src/utils/sync-utils.ts +302 -106
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
|
@@ -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,
|
|
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
|
-
//
|
|
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()
|
|
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
|
-
//
|
|
410
|
-
const
|
|
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
|
-
//
|
|
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,
|
|
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
|
-
//
|
|
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()
|
|
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
|
-
//
|
|
739
|
-
const
|
|
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
|
-
//
|
|
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
|
|
|
@@ -68,156 +137,283 @@ export async function buildSyncManifest(cwd: string, target: Target): Promise<Sy
|
|
|
68
137
|
}
|
|
69
138
|
|
|
70
139
|
/**
|
|
71
|
-
* Show sync preview
|
|
140
|
+
* Show sync preview with categorization
|
|
72
141
|
*/
|
|
73
|
-
export function showSyncPreview(
|
|
74
|
-
|
|
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
|
-
//
|
|
81
|
-
const
|
|
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 (
|
|
84
|
-
console.log(chalk.
|
|
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
|
-
|
|
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('
|
|
97
|
-
manifest.slashCommands.forEach((file) => {
|
|
98
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
console.log(chalk.
|
|
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('✓
|
|
230
|
+
console.log(chalk.green('✓ No unknown files\n'));
|
|
127
231
|
}
|
|
128
232
|
|
|
129
|
-
// Preserved
|
|
130
|
-
console.log(chalk.green('
|
|
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(`
|
|
238
|
+
console.log(chalk.dim(` ${relative}`));
|
|
135
239
|
}
|
|
136
240
|
});
|
|
137
241
|
console.log('');
|
|
138
242
|
}
|
|
139
243
|
|
|
140
244
|
/**
|
|
141
|
-
*
|
|
245
|
+
* Select unknown files to remove
|
|
142
246
|
*/
|
|
143
|
-
export async function
|
|
144
|
-
const
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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 {
|
|
288
|
+
const { selected } = await inquirer.prompt([
|
|
168
289
|
{
|
|
169
|
-
type: '
|
|
170
|
-
name: '
|
|
171
|
-
message: '
|
|
172
|
-
|
|
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
|
-
|
|
296
|
+
|
|
297
|
+
return selected;
|
|
176
298
|
}
|
|
177
299
|
|
|
178
300
|
/**
|
|
179
|
-
*
|
|
301
|
+
* Show final summary before execution
|
|
180
302
|
*/
|
|
181
|
-
export
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
*
|
|
404
|
+
* Confirm sync with user
|
|
209
405
|
*/
|
|
210
|
-
export async function
|
|
406
|
+
export async function confirmSync(): Promise<boolean> {
|
|
211
407
|
const { default: inquirer } = await import('inquirer');
|
|
212
|
-
const {
|
|
408
|
+
const { confirm } = await inquirer.prompt([
|
|
213
409
|
{
|
|
214
|
-
type: '
|
|
215
|
-
name: '
|
|
216
|
-
message: '
|
|
217
|
-
|
|
410
|
+
type: 'confirm',
|
|
411
|
+
name: 'confirm',
|
|
412
|
+
message: 'Proceed with sync?',
|
|
413
|
+
default: false,
|
|
218
414
|
},
|
|
219
415
|
]);
|
|
220
|
-
return
|
|
416
|
+
return confirm;
|
|
221
417
|
}
|
|
222
418
|
|
|
223
419
|
/**
|