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