link-agents 0.9.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/AGENTS.md +127 -0
- package/README.md +93 -0
- package/cursor-rules-notes.md +23 -0
- package/dist/cli/interactive.d.ts +9 -0
- package/dist/cli/interactive.d.ts.map +1 -0
- package/dist/cli/interactive.js +1176 -0
- package/dist/cli/interactive.js.map +1 -0
- package/dist/cli/options.d.ts +3 -0
- package/dist/cli/options.d.ts.map +1 -0
- package/dist/cli/options.js +107 -0
- package/dist/cli/options.js.map +1 -0
- package/dist/cli/options.spec.d.ts +2 -0
- package/dist/cli/options.spec.d.ts.map +1 -0
- package/dist/cli/options.spec.js +74 -0
- package/dist/cli/options.spec.js.map +1 -0
- package/dist/clients/definitions.d.ts +5 -0
- package/dist/clients/definitions.d.ts.map +1 -0
- package/dist/clients/definitions.js +82 -0
- package/dist/clients/definitions.js.map +1 -0
- package/dist/clients/definitions.spec.d.ts +2 -0
- package/dist/clients/definitions.spec.d.ts.map +1 -0
- package/dist/clients/definitions.spec.js +135 -0
- package/dist/clients/definitions.spec.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +81 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/restore.d.ts +3 -0
- package/dist/commands/restore.d.ts.map +1 -0
- package/dist/commands/restore.js +36 -0
- package/dist/commands/restore.js.map +1 -0
- package/dist/commands/sync.d.ts +3 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +193 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +98 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/apply.d.ts +22 -0
- package/dist/utils/apply.d.ts.map +1 -0
- package/dist/utils/apply.js +215 -0
- package/dist/utils/apply.js.map +1 -0
- package/dist/utils/bootstrap.d.ts +18 -0
- package/dist/utils/bootstrap.d.ts.map +1 -0
- package/dist/utils/bootstrap.js +31 -0
- package/dist/utils/bootstrap.js.map +1 -0
- package/dist/utils/bootstrap.spec.d.ts +2 -0
- package/dist/utils/bootstrap.spec.d.ts.map +1 -0
- package/dist/utils/bootstrap.spec.js +92 -0
- package/dist/utils/bootstrap.spec.js.map +1 -0
- package/dist/utils/canonical.d.ts +17 -0
- package/dist/utils/canonical.d.ts.map +1 -0
- package/dist/utils/canonical.js +136 -0
- package/dist/utils/canonical.js.map +1 -0
- package/dist/utils/canonicalState.d.ts +19 -0
- package/dist/utils/canonicalState.d.ts.map +1 -0
- package/dist/utils/canonicalState.js +21 -0
- package/dist/utils/canonicalState.js.map +1 -0
- package/dist/utils/cursorHistory.d.ts +7 -0
- package/dist/utils/cursorHistory.d.ts.map +1 -0
- package/dist/utils/cursorHistory.js +54 -0
- package/dist/utils/cursorHistory.js.map +1 -0
- package/dist/utils/cursorPaths.d.ts +3 -0
- package/dist/utils/cursorPaths.d.ts.map +1 -0
- package/dist/utils/cursorPaths.js +17 -0
- package/dist/utils/cursorPaths.js.map +1 -0
- package/dist/utils/discovery.d.ts +8 -0
- package/dist/utils/discovery.d.ts.map +1 -0
- package/dist/utils/discovery.js +93 -0
- package/dist/utils/discovery.js.map +1 -0
- package/dist/utils/frontmatter.d.ts +32 -0
- package/dist/utils/frontmatter.d.ts.map +1 -0
- package/dist/utils/frontmatter.js +263 -0
- package/dist/utils/frontmatter.js.map +1 -0
- package/dist/utils/frontmatter.spec.d.ts +2 -0
- package/dist/utils/frontmatter.spec.d.ts.map +1 -0
- package/dist/utils/frontmatter.spec.js +264 -0
- package/dist/utils/frontmatter.spec.js.map +1 -0
- package/dist/utils/fs.d.ts +27 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +137 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/fs.spec.d.ts +2 -0
- package/dist/utils/fs.spec.d.ts.map +1 -0
- package/dist/utils/fs.spec.js +73 -0
- package/dist/utils/fs.spec.js.map +1 -0
- package/dist/utils/gitignore.d.ts +10 -0
- package/dist/utils/gitignore.d.ts.map +1 -0
- package/dist/utils/gitignore.js +63 -0
- package/dist/utils/gitignore.js.map +1 -0
- package/dist/utils/manifest.d.ts +28 -0
- package/dist/utils/manifest.d.ts.map +1 -0
- package/dist/utils/manifest.js +89 -0
- package/dist/utils/manifest.js.map +1 -0
- package/dist/utils/mcp.d.ts +73 -0
- package/dist/utils/mcp.d.ts.map +1 -0
- package/dist/utils/mcp.js +529 -0
- package/dist/utils/mcp.js.map +1 -0
- package/dist/utils/mcp.spec.d.ts +2 -0
- package/dist/utils/mcp.spec.d.ts.map +1 -0
- package/dist/utils/mcp.spec.js +488 -0
- package/dist/utils/mcp.spec.js.map +1 -0
- package/dist/utils/merge.d.ts +17 -0
- package/dist/utils/merge.d.ts.map +1 -0
- package/dist/utils/merge.js +45 -0
- package/dist/utils/merge.js.map +1 -0
- package/dist/utils/merge.spec.d.ts +2 -0
- package/dist/utils/merge.spec.d.ts.map +1 -0
- package/dist/utils/merge.spec.js +134 -0
- package/dist/utils/merge.spec.js.map +1 -0
- package/dist/utils/paths.d.ts +11 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +164 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/paths.spec.d.ts +2 -0
- package/dist/utils/paths.spec.d.ts.map +1 -0
- package/dist/utils/paths.spec.js +282 -0
- package/dist/utils/paths.spec.js.map +1 -0
- package/dist/utils/plan.d.ts +7 -0
- package/dist/utils/plan.d.ts.map +1 -0
- package/dist/utils/plan.js +118 -0
- package/dist/utils/plan.js.map +1 -0
- package/dist/utils/plan.spec.d.ts +2 -0
- package/dist/utils/plan.spec.d.ts.map +1 -0
- package/dist/utils/plan.spec.js +420 -0
- package/dist/utils/plan.spec.js.map +1 -0
- package/dist/utils/reporting.d.ts +21 -0
- package/dist/utils/reporting.d.ts.map +1 -0
- package/dist/utils/reporting.js +82 -0
- package/dist/utils/reporting.js.map +1 -0
- package/dist/utils/reporting.spec.d.ts +2 -0
- package/dist/utils/reporting.spec.d.ts.map +1 -0
- package/dist/utils/reporting.spec.js +78 -0
- package/dist/utils/reporting.spec.js.map +1 -0
- package/dist/utils/reset.d.ts +14 -0
- package/dist/utils/reset.d.ts.map +1 -0
- package/dist/utils/reset.js +81 -0
- package/dist/utils/reset.js.map +1 -0
- package/dist/utils/revert.d.ts +30 -0
- package/dist/utils/revert.d.ts.map +1 -0
- package/dist/utils/revert.js +89 -0
- package/dist/utils/revert.js.map +1 -0
- package/dist/utils/revert.spec.d.ts +2 -0
- package/dist/utils/revert.spec.d.ts.map +1 -0
- package/dist/utils/revert.spec.js +102 -0
- package/dist/utils/revert.spec.js.map +1 -0
- package/dist/utils/similarity.d.ts +14 -0
- package/dist/utils/similarity.d.ts.map +1 -0
- package/dist/utils/similarity.js +70 -0
- package/dist/utils/similarity.js.map +1 -0
- package/dist/utils/similarity.spec.d.ts +2 -0
- package/dist/utils/similarity.spec.d.ts.map +1 -0
- package/dist/utils/similarity.spec.js +62 -0
- package/dist/utils/similarity.spec.js.map +1 -0
- package/dist/utils/snapshots.d.ts +21 -0
- package/dist/utils/snapshots.d.ts.map +1 -0
- package/dist/utils/snapshots.js +81 -0
- package/dist/utils/snapshots.js.map +1 -0
- package/dist/utils/snapshots.spec.d.ts +2 -0
- package/dist/utils/snapshots.spec.d.ts.map +1 -0
- package/dist/utils/snapshots.spec.js +56 -0
- package/dist/utils/snapshots.spec.js.map +1 -0
- package/dist/utils/syncFilters.d.ts +3 -0
- package/dist/utils/syncFilters.d.ts.map +1 -0
- package/dist/utils/syncFilters.js +8 -0
- package/dist/utils/syncFilters.js.map +1 -0
- package/dist/utils/syncRuntime.d.ts +3 -0
- package/dist/utils/syncRuntime.d.ts.map +1 -0
- package/dist/utils/syncRuntime.js +31 -0
- package/dist/utils/syncRuntime.js.map +1 -0
- package/dist/utils/validation.d.ts +3 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +19 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/utils/validation.spec.d.ts +2 -0
- package/dist/utils/validation.spec.d.ts.map +1 -0
- package/dist/utils/validation.spec.js +36 -0
- package/dist/utils/validation.spec.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,1176 @@
|
|
|
1
|
+
import { intro, outro, select, multiselect, confirm, spinner, note, } from "@clack/prompts";
|
|
2
|
+
import checkboxPlus from "inquirer-checkbox-plus-plus";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { discoverAssets } from "../utils/discovery.js";
|
|
5
|
+
import { fileExists, commandExists, readFileSafe, hashContent, } from "../utils/fs.js";
|
|
6
|
+
import { mergeMcpAssets, parseMcpConfig, detectMcpFormat, serializeMcpConfig, formatEnvForDisplay, compareServerConfigs, validateMcpConfig, getMcpCommands, findRemovedServers, } from "../utils/mcp.js";
|
|
7
|
+
import { calculateSimilarity, getSimilarityLabel, formatRelativeTime, } from "../utils/similarity.js";
|
|
8
|
+
import { remapRelativePathForTarget, buildTargetAbsolutePath, } from "../utils/paths.js";
|
|
9
|
+
import { shouldSkipTargetAsset } from "../utils/syncFilters.js";
|
|
10
|
+
/** Format asset count with type breakdown for display */
|
|
11
|
+
function formatAssetSummary(assets) {
|
|
12
|
+
if (assets.length === 0)
|
|
13
|
+
return "empty";
|
|
14
|
+
const byType = {};
|
|
15
|
+
for (const a of assets) {
|
|
16
|
+
byType[a.type] = (byType[a.type] || 0) + 1;
|
|
17
|
+
}
|
|
18
|
+
const typeLabels = {
|
|
19
|
+
agents: "agent",
|
|
20
|
+
commands: "cmd",
|
|
21
|
+
rules: "rule",
|
|
22
|
+
skills: "skill",
|
|
23
|
+
mcp: "mcp",
|
|
24
|
+
prompts: "prompt",
|
|
25
|
+
};
|
|
26
|
+
const parts = [];
|
|
27
|
+
for (const [type, count] of Object.entries(byType)) {
|
|
28
|
+
const label = typeLabels[type] || type;
|
|
29
|
+
parts.push(`${count} ${label}${count > 1 ? "s" : ""}`);
|
|
30
|
+
}
|
|
31
|
+
// If too many parts, show total with top 2 types
|
|
32
|
+
if (parts.length > 3) {
|
|
33
|
+
const total = assets.length;
|
|
34
|
+
const topTypes = Object.entries(byType)
|
|
35
|
+
.sort((a, b) => b[1] - a[1])
|
|
36
|
+
.slice(0, 2)
|
|
37
|
+
.map(([type, count]) => {
|
|
38
|
+
const label = typeLabels[type] || type;
|
|
39
|
+
return `${count} ${label}${count > 1 ? "s" : ""}`;
|
|
40
|
+
});
|
|
41
|
+
return `${total} files: ${topTypes.join(", ")}...`;
|
|
42
|
+
}
|
|
43
|
+
return parts.join(", ");
|
|
44
|
+
}
|
|
45
|
+
export async function runInteractiveFlow(defs, options) {
|
|
46
|
+
intro(chalk.bold("agsync"));
|
|
47
|
+
const scope = options.scope === "all" ? await selectScope() : options.scope ?? "all";
|
|
48
|
+
if (typeof scope === "symbol") {
|
|
49
|
+
outro("Cancelled.");
|
|
50
|
+
return { proceed: false, entries: [], scope: "all", direction: "sync" };
|
|
51
|
+
}
|
|
52
|
+
// Select direction FIRST (before scanning shows diff info)
|
|
53
|
+
const direction = options.direction && options.direction !== "sync"
|
|
54
|
+
? options.direction
|
|
55
|
+
: await selectDirection(scope);
|
|
56
|
+
if (typeof direction === "symbol") {
|
|
57
|
+
outro("Cancelled.");
|
|
58
|
+
return { proceed: false, entries: [], scope, direction: "sync" };
|
|
59
|
+
}
|
|
60
|
+
const resolvedDirection = direction;
|
|
61
|
+
const s = spinner();
|
|
62
|
+
s.start("Scanning for assets...");
|
|
63
|
+
const scanResults = await scanAllClients(defs, scope);
|
|
64
|
+
s.stop("Scan complete.");
|
|
65
|
+
const projectAssets = scope === "global"
|
|
66
|
+
? []
|
|
67
|
+
: scanResults.find((r) => r.client === "project")?.assets ?? [];
|
|
68
|
+
const globalAssets = scanResults
|
|
69
|
+
.filter((r) => r.client !== "project" && r.found)
|
|
70
|
+
.flatMap((r) => r.assets);
|
|
71
|
+
const allAssets = [...projectAssets, ...globalAssets];
|
|
72
|
+
if (allAssets.length === 0) {
|
|
73
|
+
note("No assets found. Create an AGENTS.md or rules to get started.", "Empty");
|
|
74
|
+
outro("Nothing to sync.");
|
|
75
|
+
return { proceed: false, entries: [], scope, direction: "sync" };
|
|
76
|
+
}
|
|
77
|
+
// Detect conflicts based on direction and scope
|
|
78
|
+
// Push: project is authoritative, only detect conflicts between global clients
|
|
79
|
+
// Pull: project is target, detect conflicts between global clients
|
|
80
|
+
// Sync: detect conflicts between all (but only globalAssets if scope is global)
|
|
81
|
+
const assetsForConflictDetection = resolvedDirection === "push"
|
|
82
|
+
? globalAssets
|
|
83
|
+
: resolvedDirection === "pull"
|
|
84
|
+
? globalAssets
|
|
85
|
+
: scope === "global"
|
|
86
|
+
? globalAssets
|
|
87
|
+
: allAssets;
|
|
88
|
+
const conflicts = detectConflicts(assetsForConflictDetection);
|
|
89
|
+
if (conflicts.length > 0) {
|
|
90
|
+
console.log();
|
|
91
|
+
console.log(chalk.bold.yellow(`Found ${conflicts.length} conflict(s):`));
|
|
92
|
+
for (const conflict of conflicts) {
|
|
93
|
+
const resolution = await resolveConflict(conflict);
|
|
94
|
+
if (typeof resolution === "symbol") {
|
|
95
|
+
outro("Cancelled.");
|
|
96
|
+
return { proceed: false, entries: [], scope, direction: "sync" };
|
|
97
|
+
}
|
|
98
|
+
conflict.resolution = resolution;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Now select target clients with diff info
|
|
102
|
+
const targetClients = await selectTargetClients(scanResults, scope, resolvedDirection, allAssets, conflicts, defs, options);
|
|
103
|
+
if (typeof targetClients === "symbol" || targetClients.length === 0) {
|
|
104
|
+
outro("Cancelled.");
|
|
105
|
+
return { proceed: false, entries: [], scope, direction: "sync" };
|
|
106
|
+
}
|
|
107
|
+
const plan = buildPlanFromConflicts(allAssets, conflicts, targetClients, defs, resolvedDirection, options);
|
|
108
|
+
if (plan.length === 0) {
|
|
109
|
+
note("All clients are already in sync.", "Up to date");
|
|
110
|
+
outro("Nothing to do.");
|
|
111
|
+
return { proceed: false, entries: [], scope, direction: resolvedDirection };
|
|
112
|
+
}
|
|
113
|
+
// Review all assets in a unified flow
|
|
114
|
+
const reviewedPlan = await reviewAllAssets(plan, allAssets);
|
|
115
|
+
if (typeof reviewedPlan === "symbol") {
|
|
116
|
+
outro("Cancelled.");
|
|
117
|
+
return { proceed: false, entries: [], scope, direction: resolvedDirection };
|
|
118
|
+
}
|
|
119
|
+
if (reviewedPlan.length === 0) {
|
|
120
|
+
note("All changes filtered out.", "Nothing to do");
|
|
121
|
+
outro("Nothing to apply.");
|
|
122
|
+
return { proceed: false, entries: [], scope, direction: resolvedDirection };
|
|
123
|
+
}
|
|
124
|
+
console.log();
|
|
125
|
+
console.log(chalk.bold("Planned changes:"));
|
|
126
|
+
for (const entry of reviewedPlan) {
|
|
127
|
+
const icon = entry.action === "create" ? chalk.green("+") : chalk.blue("~");
|
|
128
|
+
console.log(` ${icon} ${entry.targetClient} :: ${entry.targetRelativePath ?? entry.asset.relativePath}`);
|
|
129
|
+
}
|
|
130
|
+
console.log();
|
|
131
|
+
if (options.link === undefined) {
|
|
132
|
+
const writeMode = await select({
|
|
133
|
+
message: "How should files be written?",
|
|
134
|
+
options: [
|
|
135
|
+
{
|
|
136
|
+
value: "symlink",
|
|
137
|
+
label: "Symlink",
|
|
138
|
+
hint: "Recommended when target can point to the source file directly",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
value: "copy",
|
|
142
|
+
label: "Copy",
|
|
143
|
+
hint: "Write independent file copies instead of symlinks",
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
initialValue: "symlink",
|
|
147
|
+
});
|
|
148
|
+
if (typeof writeMode === "symbol") {
|
|
149
|
+
outro("Cancelled.");
|
|
150
|
+
return {
|
|
151
|
+
proceed: false,
|
|
152
|
+
entries: [],
|
|
153
|
+
scope,
|
|
154
|
+
direction: resolvedDirection,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
options.link = writeMode === "symlink";
|
|
158
|
+
}
|
|
159
|
+
const confirmed = await confirm({
|
|
160
|
+
message: `Apply ${reviewedPlan.length} change(s)?`,
|
|
161
|
+
active: "Yes",
|
|
162
|
+
inactive: "No",
|
|
163
|
+
});
|
|
164
|
+
if (!confirmed || typeof confirmed === "symbol") {
|
|
165
|
+
outro("Cancelled.");
|
|
166
|
+
return { proceed: false, entries: [], scope, direction: resolvedDirection };
|
|
167
|
+
}
|
|
168
|
+
outro("Applying changes...");
|
|
169
|
+
return {
|
|
170
|
+
proceed: true,
|
|
171
|
+
entries: reviewedPlan,
|
|
172
|
+
scope,
|
|
173
|
+
direction: resolvedDirection,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function selectScope() {
|
|
177
|
+
const result = await select({
|
|
178
|
+
message: "What would you like to sync?",
|
|
179
|
+
options: [
|
|
180
|
+
{
|
|
181
|
+
value: "global",
|
|
182
|
+
label: "Global configs",
|
|
183
|
+
hint: "~/.cursor, ~/.claude, ~/.codex, etc.",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
value: "project",
|
|
187
|
+
label: "Project files",
|
|
188
|
+
hint: "./AGENTS.md, ./rules/*, etc.",
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
async function selectDirection(scope) {
|
|
195
|
+
if (scope === "global") {
|
|
196
|
+
return "sync";
|
|
197
|
+
}
|
|
198
|
+
const result = await select({
|
|
199
|
+
message: "Sync direction:",
|
|
200
|
+
options: [
|
|
201
|
+
{
|
|
202
|
+
value: "push",
|
|
203
|
+
label: "Project → Global",
|
|
204
|
+
hint: "Push project rules to all clients",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
value: "pull",
|
|
208
|
+
label: "Global → Project",
|
|
209
|
+
hint: "Pull client rules into project",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
value: "sync",
|
|
213
|
+
label: "Merge all",
|
|
214
|
+
hint: "Combine and sync everywhere",
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
});
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
async function selectTargetClients(scanResults, scope, direction, allAssets, conflicts, defs, options) {
|
|
221
|
+
// For sync mode, filter based on scope
|
|
222
|
+
// For push/pull, direction determines which clients are targets
|
|
223
|
+
const availableClients = scanResults.filter((r) => {
|
|
224
|
+
if (scope === "global")
|
|
225
|
+
return r.client !== "project";
|
|
226
|
+
// For project scope or all, show all clients
|
|
227
|
+
return true;
|
|
228
|
+
});
|
|
229
|
+
// Calculate diff for each client
|
|
230
|
+
function getDiffLabel(client) {
|
|
231
|
+
// Calculate what would be created/updated for this client
|
|
232
|
+
const plan = buildPlanFromConflicts(allAssets, conflicts, [client.client], defs, direction, options);
|
|
233
|
+
const creates = plan.filter((p) => p.action === "create").length;
|
|
234
|
+
const updates = plan.filter((p) => p.action === "update").length;
|
|
235
|
+
if (creates === 0 && updates === 0) {
|
|
236
|
+
return `${client.assets.length} files, no changes`;
|
|
237
|
+
}
|
|
238
|
+
const parts = [];
|
|
239
|
+
if (creates > 0)
|
|
240
|
+
parts.push(chalk.green(`+${creates}`));
|
|
241
|
+
if (updates > 0)
|
|
242
|
+
parts.push(chalk.blue(`~${updates}`));
|
|
243
|
+
return `${client.assets.length} files, ${parts.join(" ")}`;
|
|
244
|
+
}
|
|
245
|
+
const legend = chalk.dim("(+ new, ~ update)");
|
|
246
|
+
if (direction === "push") {
|
|
247
|
+
const globalClients = scanResults.filter((r) => r.client !== "project");
|
|
248
|
+
const result = await multiselect({
|
|
249
|
+
message: `Select target clients: ${legend}`,
|
|
250
|
+
options: globalClients.map((r) => ({
|
|
251
|
+
value: r.client,
|
|
252
|
+
label: `${r.displayName} (${getDiffLabel(r)})`,
|
|
253
|
+
})),
|
|
254
|
+
initialValues: globalClients.filter((r) => r.found).map((r) => r.client),
|
|
255
|
+
});
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
if (direction === "pull") {
|
|
259
|
+
return ["project"];
|
|
260
|
+
}
|
|
261
|
+
const result = await multiselect({
|
|
262
|
+
message: `Select target clients: ${legend}`,
|
|
263
|
+
options: availableClients.map((r) => ({
|
|
264
|
+
value: r.client,
|
|
265
|
+
label: `${r.displayName} (${getDiffLabel(r)})`,
|
|
266
|
+
})),
|
|
267
|
+
initialValues: availableClients.filter((r) => r.found).map((r) => r.client),
|
|
268
|
+
});
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
async function scanAllClients(defs, scope) {
|
|
272
|
+
const results = [];
|
|
273
|
+
for (const def of defs) {
|
|
274
|
+
// Always scan all clients to get full picture for diffing
|
|
275
|
+
// Scope filtering happens in client selection, not scanning
|
|
276
|
+
const exists = await fileExists(def.root);
|
|
277
|
+
if (!exists) {
|
|
278
|
+
results.push({
|
|
279
|
+
client: def.name,
|
|
280
|
+
displayName: def.displayName,
|
|
281
|
+
found: false,
|
|
282
|
+
assets: [],
|
|
283
|
+
root: def.root,
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const assets = await discoverAssets([def], {});
|
|
288
|
+
results.push({
|
|
289
|
+
client: def.name,
|
|
290
|
+
displayName: def.displayName,
|
|
291
|
+
found: true,
|
|
292
|
+
assets,
|
|
293
|
+
root: def.root,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return results;
|
|
297
|
+
}
|
|
298
|
+
function detectConflicts(assets) {
|
|
299
|
+
const byKey = new Map();
|
|
300
|
+
for (const asset of assets) {
|
|
301
|
+
const key = `${asset.type}::${asset.canonicalPath ?? asset.relativePath}`;
|
|
302
|
+
const existing = byKey.get(key) ?? [];
|
|
303
|
+
existing.push(asset);
|
|
304
|
+
byKey.set(key, existing);
|
|
305
|
+
}
|
|
306
|
+
const conflicts = [];
|
|
307
|
+
for (const [key, versions] of byKey.entries()) {
|
|
308
|
+
const uniqueHashes = new Set(versions.map((v) => v.hash));
|
|
309
|
+
if (uniqueHashes.size > 1) {
|
|
310
|
+
// Sort by modification time, newest first
|
|
311
|
+
const sorted = versions.slice().sort((a, b) => {
|
|
312
|
+
const timeA = a.modifiedAt?.getTime() ?? 0;
|
|
313
|
+
const timeB = b.modifiedAt?.getTime() ?? 0;
|
|
314
|
+
return timeB - timeA;
|
|
315
|
+
});
|
|
316
|
+
conflicts.push({
|
|
317
|
+
canonicalKey: key,
|
|
318
|
+
type: versions[0].type,
|
|
319
|
+
versions: sorted,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return conflicts;
|
|
324
|
+
}
|
|
325
|
+
async function resolveConflict(conflict) {
|
|
326
|
+
const canMerge = conflict.type === "agents" || conflict.type === "mcp";
|
|
327
|
+
const [, filePath] = conflict.canonicalKey.split("::");
|
|
328
|
+
// Calculate similarity between first two versions
|
|
329
|
+
const similarity = conflict.versions.length >= 2
|
|
330
|
+
? calculateSimilarity(conflict.versions[0].content, conflict.versions[1].content)
|
|
331
|
+
: 1;
|
|
332
|
+
// Auto-resolve if nearly identical (>=95%) - use newest version
|
|
333
|
+
if (similarity >= 0.95) {
|
|
334
|
+
conflict.selectedVersion = conflict.versions[0]; // Already sorted by time, newest first
|
|
335
|
+
return "source";
|
|
336
|
+
}
|
|
337
|
+
const similarityLabel = getSimilarityLabel(similarity);
|
|
338
|
+
const similarityPct = Math.round(similarity * 100);
|
|
339
|
+
const options = [];
|
|
340
|
+
// For MCP conflicts, put "Merge" first as the default (most common choice)
|
|
341
|
+
if (canMerge) {
|
|
342
|
+
options.push({
|
|
343
|
+
value: "merge",
|
|
344
|
+
label: "Merge (combine all servers)",
|
|
345
|
+
hint: "recommended",
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
// Add client version options
|
|
349
|
+
for (const v of conflict.versions) {
|
|
350
|
+
const clientLabel = v.client === "project" ? "local (./)" : v.client;
|
|
351
|
+
options.push({
|
|
352
|
+
value: v.client,
|
|
353
|
+
label: `Use ${clientLabel} (${(v.content.length / 1024).toFixed(1)}kb, ${formatRelativeTime(v.modifiedAt)})`,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
// Add rename option for non-mergeable conflicts
|
|
357
|
+
if (!canMerge) {
|
|
358
|
+
options.push({
|
|
359
|
+
value: "rename",
|
|
360
|
+
label: "Keep both (rename)",
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
options.push({
|
|
364
|
+
value: "skip",
|
|
365
|
+
label: "Skip (keep as-is)",
|
|
366
|
+
});
|
|
367
|
+
const result = await select({
|
|
368
|
+
message: `${filePath} - ${similarityPct}% ${similarityLabel}`,
|
|
369
|
+
options,
|
|
370
|
+
});
|
|
371
|
+
if (typeof result === "symbol")
|
|
372
|
+
return result;
|
|
373
|
+
if (result === "rename")
|
|
374
|
+
return "rename";
|
|
375
|
+
if (result === "skip")
|
|
376
|
+
return "skip";
|
|
377
|
+
if (result === "merge") {
|
|
378
|
+
// For MCP merge, immediately ask which servers to include
|
|
379
|
+
if (conflict.type === "mcp") {
|
|
380
|
+
const selectedServers = await selectMcpServersForMerge(conflict.versions);
|
|
381
|
+
if (typeof selectedServers === "symbol")
|
|
382
|
+
return selectedServers;
|
|
383
|
+
// Store selected servers in conflict metadata for later use
|
|
384
|
+
conflict.resolvedContent = selectedServers;
|
|
385
|
+
}
|
|
386
|
+
return "merge";
|
|
387
|
+
}
|
|
388
|
+
conflict.selectedVersion = conflict.versions.find((v) => v.client === result);
|
|
389
|
+
const isSource = result === conflict.versions[0].client;
|
|
390
|
+
return isSource ? "source" : "target";
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Let user select which MCP servers to include in merge
|
|
394
|
+
*/
|
|
395
|
+
async function selectMcpServersForMerge(versions) {
|
|
396
|
+
// Collect all servers from all versions
|
|
397
|
+
const serversByName = new Map();
|
|
398
|
+
for (const version of versions) {
|
|
399
|
+
const format = detectMcpFormat(version.path);
|
|
400
|
+
const config = parseMcpConfig(version.content, format);
|
|
401
|
+
if (!config?.mcpServers)
|
|
402
|
+
continue;
|
|
403
|
+
for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {
|
|
404
|
+
const list = serversByName.get(serverName) ?? [];
|
|
405
|
+
list.push({ client: version.client, config: serverConfig });
|
|
406
|
+
serversByName.set(serverName, list);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (serversByName.size === 0) {
|
|
410
|
+
return "{}"; // Empty config
|
|
411
|
+
}
|
|
412
|
+
// Build options for multiselect
|
|
413
|
+
const options = [];
|
|
414
|
+
for (const [serverName, sources] of serversByName.entries()) {
|
|
415
|
+
const sourceClients = sources.map((s) => s.client).join(", ");
|
|
416
|
+
options.push({
|
|
417
|
+
value: serverName,
|
|
418
|
+
label: serverName,
|
|
419
|
+
hint: `from ${sourceClients}`,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
console.log();
|
|
423
|
+
const selected = await multiselect({
|
|
424
|
+
message: "Select MCP servers to include:",
|
|
425
|
+
options,
|
|
426
|
+
initialValues: Array.from(serversByName.keys()),
|
|
427
|
+
});
|
|
428
|
+
if (typeof selected === "symbol")
|
|
429
|
+
return selected;
|
|
430
|
+
// Build merged config with selected servers
|
|
431
|
+
const mergedServers = {};
|
|
432
|
+
for (const serverName of selected) {
|
|
433
|
+
const sources = serversByName.get(serverName);
|
|
434
|
+
if (sources && sources.length > 0) {
|
|
435
|
+
// Use first source's config (could add conflict resolution per-server later)
|
|
436
|
+
mergedServers[serverName] = sources[0].config;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Serialize back to JSON
|
|
440
|
+
return JSON.stringify({ mcpServers: mergedServers }, null, 2);
|
|
441
|
+
}
|
|
442
|
+
function buildPlanFromConflicts(allAssets, conflicts, targetClients, defs, direction, options) {
|
|
443
|
+
const plan = [];
|
|
444
|
+
const conflictKeys = new Set(conflicts.map((c) => c.canonicalKey));
|
|
445
|
+
// Build index of what each client already has (by canonical path and hash)
|
|
446
|
+
const clientAssets = new Map(); // client -> (canonicalPath -> hash)
|
|
447
|
+
for (const asset of allAssets) {
|
|
448
|
+
const canonical = asset.canonicalPath ?? asset.relativePath;
|
|
449
|
+
if (!clientAssets.has(asset.client)) {
|
|
450
|
+
clientAssets.set(asset.client, new Map());
|
|
451
|
+
}
|
|
452
|
+
clientAssets.get(asset.client).set(canonical, asset.hash);
|
|
453
|
+
}
|
|
454
|
+
const resolvedAssets = new Map();
|
|
455
|
+
for (const asset of allAssets) {
|
|
456
|
+
const key = `${asset.type}::${asset.canonicalPath ?? asset.relativePath}`;
|
|
457
|
+
if (conflictKeys.has(key))
|
|
458
|
+
continue;
|
|
459
|
+
if (!resolvedAssets.has(key)) {
|
|
460
|
+
resolvedAssets.set(key, asset);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
for (const conflict of conflicts) {
|
|
464
|
+
if (conflict.resolution === "skip")
|
|
465
|
+
continue;
|
|
466
|
+
if (conflict.resolution === "merge") {
|
|
467
|
+
let merged;
|
|
468
|
+
if (conflict.type === "mcp" && conflict.resolvedContent) {
|
|
469
|
+
// Use pre-selected MCP servers from conflict resolution
|
|
470
|
+
merged = conflict.resolvedContent;
|
|
471
|
+
}
|
|
472
|
+
else if (conflict.type === "mcp") {
|
|
473
|
+
// Fallback to smart MCP merging
|
|
474
|
+
const mcpMerged = mergeMcpAssets(conflict.versions);
|
|
475
|
+
merged =
|
|
476
|
+
mcpMerged ??
|
|
477
|
+
conflict.versions.map((v) => v.content).join("\n\n---\n\n");
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
// Simple text concatenation for agents files
|
|
481
|
+
merged = conflict.versions.map((v) => v.content).join("\n\n---\n\n");
|
|
482
|
+
}
|
|
483
|
+
const base = conflict.versions[0];
|
|
484
|
+
resolvedAssets.set(conflict.canonicalKey, {
|
|
485
|
+
...base,
|
|
486
|
+
content: merged,
|
|
487
|
+
hash: hashContent(merged),
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
else if (conflict.resolution === "rename") {
|
|
491
|
+
for (const version of conflict.versions) {
|
|
492
|
+
const renamedPath = addClientSuffix(version.canonicalPath ?? version.relativePath, version.client);
|
|
493
|
+
const renamedKey = `${version.type}::${renamedPath}`;
|
|
494
|
+
resolvedAssets.set(renamedKey, {
|
|
495
|
+
...version,
|
|
496
|
+
canonicalPath: renamedPath,
|
|
497
|
+
relativePath: renamedPath,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
const winner = conflict.selectedVersion ??
|
|
503
|
+
(conflict.resolution === "source"
|
|
504
|
+
? conflict.versions[0]
|
|
505
|
+
: conflict.versions[1]);
|
|
506
|
+
if (winner) {
|
|
507
|
+
resolvedAssets.set(conflict.canonicalKey, winner);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
for (const [key, asset] of resolvedAssets.entries()) {
|
|
512
|
+
for (const clientName of targetClients) {
|
|
513
|
+
if (clientName === asset.client)
|
|
514
|
+
continue;
|
|
515
|
+
// Apply direction filtering
|
|
516
|
+
if (direction === "push" && asset.client !== "project") {
|
|
517
|
+
// In push mode, only sync FROM project to global clients
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (direction === "pull" && clientName !== "project") {
|
|
521
|
+
// In pull mode, only sync TO project
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
const def = defs.find((d) => d.name === clientName);
|
|
525
|
+
if (!def)
|
|
526
|
+
continue;
|
|
527
|
+
if (shouldSkipTargetAsset(options, clientName, asset)) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const supportsType = def.assets.some((a) => a.type === asset.type);
|
|
531
|
+
if (!supportsType)
|
|
532
|
+
continue;
|
|
533
|
+
const canonical = asset.canonicalPath ?? asset.relativePath;
|
|
534
|
+
// Check if target already has this file with same content
|
|
535
|
+
const targetAssets = clientAssets.get(clientName);
|
|
536
|
+
if (targetAssets) {
|
|
537
|
+
const existingHash = targetAssets.get(canonical);
|
|
538
|
+
if (existingHash === asset.hash) {
|
|
539
|
+
// Target already has identical content, skip
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Remap path for target client (e.g., MCP configs have different filenames per client)
|
|
544
|
+
const targetRelative = remapRelativePathForTarget(asset, clientName, canonical, defs);
|
|
545
|
+
const targetPath = buildTargetAbsolutePath(def.root, targetRelative);
|
|
546
|
+
plan.push({
|
|
547
|
+
asset,
|
|
548
|
+
targetClient: clientName,
|
|
549
|
+
targetPath,
|
|
550
|
+
targetRelativePath: targetRelative,
|
|
551
|
+
action: targetAssets?.has(canonical) ? "update" : "create",
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return plan;
|
|
556
|
+
}
|
|
557
|
+
function addClientSuffix(filePath, client) {
|
|
558
|
+
const lastDot = filePath.lastIndexOf(".");
|
|
559
|
+
if (lastDot === -1) {
|
|
560
|
+
return `${filePath}-${client}`;
|
|
561
|
+
}
|
|
562
|
+
const name = filePath.slice(0, lastDot);
|
|
563
|
+
const ext = filePath.slice(lastDot);
|
|
564
|
+
return `${name}-${client}${ext}`;
|
|
565
|
+
}
|
|
566
|
+
const ASSET_TYPE_LABELS = {
|
|
567
|
+
mcp: "MCP Servers",
|
|
568
|
+
commands: "Commands",
|
|
569
|
+
agents: "Agents",
|
|
570
|
+
rules: "Rules",
|
|
571
|
+
skills: "Skills",
|
|
572
|
+
prompts: "Prompts",
|
|
573
|
+
};
|
|
574
|
+
/** Check if asset is the root instructions file (AGENTS.md at root, not in agents/ folder) */
|
|
575
|
+
function isRootInstructionsFile(assetType, canonicalPath) {
|
|
576
|
+
return assetType === "agents" && canonicalPath === "AGENTS.md";
|
|
577
|
+
}
|
|
578
|
+
/** Format display name for assets */
|
|
579
|
+
function formatAssetDisplayName(assetType, canonicalPath) {
|
|
580
|
+
// For agents in agents/ folder, strip the prefix for cleaner display
|
|
581
|
+
if (assetType === "agents" && canonicalPath.startsWith("agents/")) {
|
|
582
|
+
return canonicalPath.slice(7); // Remove "agents/" prefix
|
|
583
|
+
}
|
|
584
|
+
return canonicalPath;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Unified review flow for all asset types
|
|
588
|
+
* 1. Resolve conflicts (ask user to pick version when same asset differs)
|
|
589
|
+
* 2. Show single grouped multiselect for all assets
|
|
590
|
+
*/
|
|
591
|
+
async function reviewAllAssets(plan, allAssets) {
|
|
592
|
+
if (plan.length === 0)
|
|
593
|
+
return plan;
|
|
594
|
+
// Separate MCP from other assets
|
|
595
|
+
// MCP is handled during conflict resolution (merge step), so we just pass it through
|
|
596
|
+
const mcpEntries = plan.filter((e) => e.asset.type === "mcp");
|
|
597
|
+
const otherEntries = plan.filter((e) => e.asset.type !== "mcp");
|
|
598
|
+
// Process other assets and resolve conflicts
|
|
599
|
+
const otherResult = await resolveAssetConflicts(otherEntries);
|
|
600
|
+
if (typeof otherResult === "symbol")
|
|
601
|
+
return otherResult;
|
|
602
|
+
const allResolved = [...otherResult.resolved];
|
|
603
|
+
// If nothing to select (and no MCP), return early
|
|
604
|
+
if (allResolved.length === 0 && mcpEntries.length === 0) {
|
|
605
|
+
return [];
|
|
606
|
+
}
|
|
607
|
+
// If only MCP entries and no other assets, just return MCP entries
|
|
608
|
+
if (allResolved.length === 0 && mcpEntries.length > 0) {
|
|
609
|
+
return mcpEntries;
|
|
610
|
+
}
|
|
611
|
+
// If only one non-MCP asset and no conflicts, skip selection
|
|
612
|
+
if (allResolved.length === 1 && !otherResult.hadConflicts) {
|
|
613
|
+
// Include MCP entries directly since they're already resolved
|
|
614
|
+
return [
|
|
615
|
+
...mcpEntries,
|
|
616
|
+
...otherEntries.filter((e) => e.asset.canonicalPath === allResolved[0].name ||
|
|
617
|
+
e.asset.relativePath === allResolved[0].name),
|
|
618
|
+
];
|
|
619
|
+
}
|
|
620
|
+
// Step 3: Separate root instructions (AGENTS.md) from sub-agents
|
|
621
|
+
const isRootInstructions = (type, name) => type === "agents" &&
|
|
622
|
+
(name === "AGENTS.md" || name.toUpperCase() === "AGENTS.MD");
|
|
623
|
+
const byType = new Map();
|
|
624
|
+
for (const resolved of allResolved) {
|
|
625
|
+
// Skip root instructions from resolved - we'll handle them separately
|
|
626
|
+
if (isRootInstructions(resolved.type, resolved.name)) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const list = byType.get(resolved.type) ?? [];
|
|
630
|
+
list.push(resolved);
|
|
631
|
+
byType.set(resolved.type, list);
|
|
632
|
+
}
|
|
633
|
+
const selectedResolved = [];
|
|
634
|
+
// Step 4: Handle root instructions - only if there are plan entries for AGENTS.md
|
|
635
|
+
const rootPlanEntries = plan.filter((e) => isRootInstructions(e.asset.type, e.asset.canonicalPath ?? e.asset.relativePath));
|
|
636
|
+
if (rootPlanEntries.length > 0) {
|
|
637
|
+
// Collect all available sources for AGENTS.md
|
|
638
|
+
const rootSources = allAssets.filter((a) => a.type === "agents" &&
|
|
639
|
+
(a.canonicalPath === "AGENTS.md" || a.relativePath === "AGENTS.md"));
|
|
640
|
+
if (rootSources.length > 1) {
|
|
641
|
+
// Multiple sources - ask user which to use
|
|
642
|
+
console.log();
|
|
643
|
+
// Sort by modification time (most recent first)
|
|
644
|
+
const sortedSources = [...rootSources].sort((a, b) => {
|
|
645
|
+
const timeA = a.modifiedAt?.getTime() ?? 0;
|
|
646
|
+
const timeB = b.modifiedAt?.getTime() ?? 0;
|
|
647
|
+
return timeB - timeA; // Most recent first
|
|
648
|
+
});
|
|
649
|
+
const options = sortedSources.map((a) => {
|
|
650
|
+
const sizeKb = (a.content.length / 1024).toFixed(1);
|
|
651
|
+
const relTime = formatRelativeTime(a.modifiedAt);
|
|
652
|
+
return {
|
|
653
|
+
value: a.client,
|
|
654
|
+
label: a.client,
|
|
655
|
+
hint: `${sizeKb}kb, ${relTime}`,
|
|
656
|
+
};
|
|
657
|
+
});
|
|
658
|
+
options.push({ value: "__skip__", label: "Skip", hint: "" });
|
|
659
|
+
const sourceChoice = await select({
|
|
660
|
+
message: "AGENTS.md source:",
|
|
661
|
+
options,
|
|
662
|
+
initialValue: sortedSources[0].client, // Default to most recently modified
|
|
663
|
+
});
|
|
664
|
+
if (typeof sourceChoice === "symbol")
|
|
665
|
+
return sourceChoice;
|
|
666
|
+
if (sourceChoice !== "__skip__") {
|
|
667
|
+
const selectedSource = rootSources.find((a) => a.client === sourceChoice);
|
|
668
|
+
if (selectedSource) {
|
|
669
|
+
const sizeKb = (selectedSource.content.length / 1024).toFixed(1);
|
|
670
|
+
selectedResolved.push({
|
|
671
|
+
name: "AGENTS.md",
|
|
672
|
+
type: "agents",
|
|
673
|
+
version: {
|
|
674
|
+
client: selectedSource.client,
|
|
675
|
+
asset: selectedSource,
|
|
676
|
+
entry: rootPlanEntries[0],
|
|
677
|
+
},
|
|
678
|
+
label: `${selectedSource.client} (${sizeKb}kb)`,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
else if (rootSources.length === 1) {
|
|
684
|
+
// Single source - auto-include it (already part of plan)
|
|
685
|
+
const source = rootSources[0];
|
|
686
|
+
const sizeKb = (source.content.length / 1024).toFixed(1);
|
|
687
|
+
selectedResolved.push({
|
|
688
|
+
name: "AGENTS.md",
|
|
689
|
+
type: "agents",
|
|
690
|
+
version: {
|
|
691
|
+
client: source.client,
|
|
692
|
+
asset: source,
|
|
693
|
+
entry: rootPlanEntries[0],
|
|
694
|
+
},
|
|
695
|
+
label: `${source.client} (${sizeKb}kb)`,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// Step 5: Process each type in a logical order
|
|
700
|
+
// Note: MCP is NOT included here - it's handled during conflict resolution (merge step)
|
|
701
|
+
const typeOrder = [
|
|
702
|
+
"agents",
|
|
703
|
+
"commands",
|
|
704
|
+
"prompts",
|
|
705
|
+
"rules",
|
|
706
|
+
"skills",
|
|
707
|
+
];
|
|
708
|
+
// Threshold for using searchable multiselect vs regular multiselect
|
|
709
|
+
// Lower threshold for better UX - searchable is always nicer for lists > 10
|
|
710
|
+
const SEARCHABLE_THRESHOLD = 10;
|
|
711
|
+
for (const assetType of typeOrder) {
|
|
712
|
+
const assets = byType.get(assetType);
|
|
713
|
+
if (!assets || assets.length === 0)
|
|
714
|
+
continue;
|
|
715
|
+
// Deduplicate by name (same command from multiple sources should appear once)
|
|
716
|
+
const uniqueAssets = new Map();
|
|
717
|
+
for (const a of assets) {
|
|
718
|
+
if (!uniqueAssets.has(a.name)) {
|
|
719
|
+
uniqueAssets.set(a.name, a);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const deduped = Array.from(uniqueAssets.values());
|
|
723
|
+
// Sort alphabetically
|
|
724
|
+
deduped.sort((a, b) => a.name.localeCompare(b.name));
|
|
725
|
+
console.log();
|
|
726
|
+
let selectedNames;
|
|
727
|
+
if (deduped.length > SEARCHABLE_THRESHOLD) {
|
|
728
|
+
// Use searchable multiselect for large lists
|
|
729
|
+
const choices = deduped.map((a) => {
|
|
730
|
+
const displayName = formatAssetDisplayName(assetType, a.name);
|
|
731
|
+
return {
|
|
732
|
+
name: `${displayName} ${chalk.dim(`(${a.label})`)}`,
|
|
733
|
+
value: a.name,
|
|
734
|
+
short: displayName.split("/").pop() ?? displayName,
|
|
735
|
+
};
|
|
736
|
+
});
|
|
737
|
+
const selected = await checkboxPlus({
|
|
738
|
+
message: `${ASSET_TYPE_LABELS[assetType]} (${deduped.length}) - type to filter:`,
|
|
739
|
+
searchable: true,
|
|
740
|
+
highlight: true,
|
|
741
|
+
pageSize: 12,
|
|
742
|
+
default: deduped.map((a) => a.name), // Select all by default
|
|
743
|
+
source: async (_answers, input) => {
|
|
744
|
+
if (!input)
|
|
745
|
+
return choices;
|
|
746
|
+
const lower = input.toLowerCase();
|
|
747
|
+
return choices.filter((c) => c.value.toLowerCase().includes(lower));
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
// checkboxPlus returns array of values or throws on cancel
|
|
751
|
+
if (!Array.isArray(selected)) {
|
|
752
|
+
return Symbol("cancel");
|
|
753
|
+
}
|
|
754
|
+
selectedNames = new Set(selected);
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
// Use regular multiselect for small lists
|
|
758
|
+
const options = deduped.map((a) => ({
|
|
759
|
+
value: a.name,
|
|
760
|
+
label: formatAssetDisplayName(assetType, a.name),
|
|
761
|
+
hint: a.label,
|
|
762
|
+
}));
|
|
763
|
+
const selected = await multiselect({
|
|
764
|
+
message: `${ASSET_TYPE_LABELS[assetType]} (${deduped.length}):`,
|
|
765
|
+
options,
|
|
766
|
+
initialValues: deduped.map((a) => a.name),
|
|
767
|
+
maxItems: 15,
|
|
768
|
+
});
|
|
769
|
+
if (typeof selected === "symbol")
|
|
770
|
+
return selected;
|
|
771
|
+
selectedNames = new Set(selected);
|
|
772
|
+
}
|
|
773
|
+
for (const asset of deduped) {
|
|
774
|
+
if (selectedNames.has(asset.name)) {
|
|
775
|
+
selectedResolved.push(asset);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (selectedResolved.length === 0 && mcpEntries.length === 0) {
|
|
780
|
+
return [];
|
|
781
|
+
}
|
|
782
|
+
// Build final plan for non-MCP entries
|
|
783
|
+
const finalPlan = buildFinalPlanSimple(selectedResolved, otherEntries);
|
|
784
|
+
// Add MCP entries directly (already resolved during conflict resolution)
|
|
785
|
+
return [...mcpEntries, ...finalPlan];
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Build final plan from selected assets (simplified, no MCP handling)
|
|
789
|
+
*/
|
|
790
|
+
function buildFinalPlanSimple(selectedResolved, entries) {
|
|
791
|
+
const selectedPaths = new Set(selectedResolved.map((r) => `${r.type}::${r.name}`));
|
|
792
|
+
return entries.filter((e) => {
|
|
793
|
+
const key = `${e.asset.type}::${e.asset.canonicalPath ?? e.asset.relativePath}`;
|
|
794
|
+
return selectedPaths.has(key);
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Resolve MCP server conflicts and prepare for selection
|
|
799
|
+
*/
|
|
800
|
+
async function resolveMcpConflicts(mcpEntries) {
|
|
801
|
+
const resolved = [];
|
|
802
|
+
const serverChoices = new Map();
|
|
803
|
+
let hadConflicts = false;
|
|
804
|
+
if (mcpEntries.length === 0) {
|
|
805
|
+
return { resolved, serverChoices, hadConflicts };
|
|
806
|
+
}
|
|
807
|
+
// Collect servers by name
|
|
808
|
+
const serverVersions = new Map();
|
|
809
|
+
const seenServerClient = new Set();
|
|
810
|
+
for (const entry of mcpEntries) {
|
|
811
|
+
const format = detectMcpFormat(entry.asset.path);
|
|
812
|
+
const config = parseMcpConfig(entry.asset.content, format);
|
|
813
|
+
if (!config?.mcpServers)
|
|
814
|
+
continue;
|
|
815
|
+
for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {
|
|
816
|
+
const key = `${serverName}::${entry.asset.client}`;
|
|
817
|
+
if (seenServerClient.has(key))
|
|
818
|
+
continue;
|
|
819
|
+
seenServerClient.add(key);
|
|
820
|
+
const versions = serverVersions.get(serverName) ?? [];
|
|
821
|
+
versions.push({
|
|
822
|
+
client: entry.asset.client,
|
|
823
|
+
config: serverConfig,
|
|
824
|
+
entry,
|
|
825
|
+
format,
|
|
826
|
+
fullConfig: config,
|
|
827
|
+
});
|
|
828
|
+
serverVersions.set(serverName, versions);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// Process each server
|
|
832
|
+
for (const [serverName, versions] of serverVersions.entries()) {
|
|
833
|
+
if (versions.length === 1) {
|
|
834
|
+
const version = versions[0];
|
|
835
|
+
const envDisplay = formatEnvForDisplay(version.config.env);
|
|
836
|
+
serverChoices.set(serverName, version);
|
|
837
|
+
resolved.push({
|
|
838
|
+
name: serverName,
|
|
839
|
+
type: "mcp",
|
|
840
|
+
version: {
|
|
841
|
+
client: version.client,
|
|
842
|
+
asset: version.entry.asset,
|
|
843
|
+
entry: version.entry,
|
|
844
|
+
},
|
|
845
|
+
label: `${version.client} - ${envDisplay}`,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
// Check if versions differ
|
|
850
|
+
const first = versions[0];
|
|
851
|
+
let hasDifferences = false;
|
|
852
|
+
for (let i = 1; i < versions.length; i++) {
|
|
853
|
+
const comparison = compareServerConfigs(first.config, versions[i].config);
|
|
854
|
+
if (!comparison.same) {
|
|
855
|
+
hasDifferences = true;
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (!hasDifferences) {
|
|
860
|
+
const envDisplay = formatEnvForDisplay(first.config.env);
|
|
861
|
+
serverChoices.set(serverName, first);
|
|
862
|
+
resolved.push({
|
|
863
|
+
name: serverName,
|
|
864
|
+
type: "mcp",
|
|
865
|
+
version: {
|
|
866
|
+
client: first.client,
|
|
867
|
+
asset: first.entry.asset,
|
|
868
|
+
entry: first.entry,
|
|
869
|
+
},
|
|
870
|
+
label: `${versions.map((v) => v.client).join(", ")} - ${envDisplay}`,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
hadConflicts = true;
|
|
875
|
+
// Show conflict and ask user
|
|
876
|
+
console.log();
|
|
877
|
+
console.log(chalk.yellow(`MCP server "${serverName}" differs across clients:`));
|
|
878
|
+
for (const version of versions) {
|
|
879
|
+
const envDisplay = formatEnvForDisplay(version.config.env);
|
|
880
|
+
console.log(` ${chalk.gray(version.client)}: ${version.config.command ?? "?"} - ${chalk.dim(envDisplay)}`);
|
|
881
|
+
}
|
|
882
|
+
const versionChoice = await select({
|
|
883
|
+
message: `Which "${serverName}" config to use?`,
|
|
884
|
+
options: [
|
|
885
|
+
...versions.map((v) => ({
|
|
886
|
+
value: v.client,
|
|
887
|
+
label: `${v.client} (${v.config.command ?? "?"})`,
|
|
888
|
+
hint: formatEnvForDisplay(v.config.env),
|
|
889
|
+
})),
|
|
890
|
+
{ value: "__skip__", label: "Skip this server" },
|
|
891
|
+
],
|
|
892
|
+
});
|
|
893
|
+
if (typeof versionChoice === "symbol")
|
|
894
|
+
return versionChoice;
|
|
895
|
+
if (versionChoice !== "__skip__") {
|
|
896
|
+
const selected = versions.find((v) => v.client === versionChoice);
|
|
897
|
+
if (selected) {
|
|
898
|
+
const envDisplay = formatEnvForDisplay(selected.config.env);
|
|
899
|
+
serverChoices.set(serverName, selected);
|
|
900
|
+
resolved.push({
|
|
901
|
+
name: serverName,
|
|
902
|
+
type: "mcp",
|
|
903
|
+
version: {
|
|
904
|
+
client: selected.client,
|
|
905
|
+
asset: selected.entry.asset,
|
|
906
|
+
entry: selected.entry,
|
|
907
|
+
},
|
|
908
|
+
label: `${selected.client} - ${envDisplay}`,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return { resolved, serverChoices, hadConflicts };
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Resolve conflicts for non-MCP assets
|
|
919
|
+
*/
|
|
920
|
+
async function resolveAssetConflicts(entries) {
|
|
921
|
+
const resolved = [];
|
|
922
|
+
let hadConflicts = false;
|
|
923
|
+
if (entries.length === 0) {
|
|
924
|
+
return { resolved, hadConflicts };
|
|
925
|
+
}
|
|
926
|
+
// Group by type and canonical path (not name, which may differ between clients)
|
|
927
|
+
const assetVersions = new Map();
|
|
928
|
+
const seenAssetClient = new Set();
|
|
929
|
+
for (const entry of entries) {
|
|
930
|
+
const canonicalPath = entry.asset.canonicalPath ?? entry.asset.relativePath;
|
|
931
|
+
const key = `${entry.asset.type}::${canonicalPath}::${entry.asset.client}`;
|
|
932
|
+
if (seenAssetClient.has(key))
|
|
933
|
+
continue;
|
|
934
|
+
seenAssetClient.add(key);
|
|
935
|
+
const mapKey = `${entry.asset.type}::${canonicalPath}`;
|
|
936
|
+
const versions = assetVersions.get(mapKey) ?? [];
|
|
937
|
+
versions.push({
|
|
938
|
+
client: entry.asset.client,
|
|
939
|
+
asset: entry.asset,
|
|
940
|
+
entry,
|
|
941
|
+
});
|
|
942
|
+
assetVersions.set(mapKey, versions);
|
|
943
|
+
}
|
|
944
|
+
// Process each asset
|
|
945
|
+
for (const [mapKey, versions] of assetVersions.entries()) {
|
|
946
|
+
const [assetType, ...pathParts] = mapKey.split("::");
|
|
947
|
+
const canonicalPath = pathParts.join("::");
|
|
948
|
+
const type = assetType;
|
|
949
|
+
if (versions.length === 1) {
|
|
950
|
+
const version = versions[0];
|
|
951
|
+
const sizeKb = (version.asset.content.length / 1024).toFixed(1);
|
|
952
|
+
resolved.push({
|
|
953
|
+
name: canonicalPath,
|
|
954
|
+
type,
|
|
955
|
+
version,
|
|
956
|
+
label: `${version.client} (${sizeKb}kb)`,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
// Check if versions differ
|
|
961
|
+
const first = versions[0];
|
|
962
|
+
let hasDifferences = false;
|
|
963
|
+
for (let i = 1; i < versions.length; i++) {
|
|
964
|
+
if (versions[i].asset.hash !== first.asset.hash) {
|
|
965
|
+
hasDifferences = true;
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (!hasDifferences) {
|
|
970
|
+
const sizeKb = (first.asset.content.length / 1024).toFixed(1);
|
|
971
|
+
resolved.push({
|
|
972
|
+
name: canonicalPath,
|
|
973
|
+
type,
|
|
974
|
+
version: first,
|
|
975
|
+
label: `${versions.map((v) => v.client).join(", ")} (${sizeKb}kb)`,
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
hadConflicts = true;
|
|
980
|
+
// Sort by modification time (most recent first)
|
|
981
|
+
const sortedVersions = [...versions].sort((a, b) => {
|
|
982
|
+
const timeA = a.asset.modifiedAt?.getTime() ?? 0;
|
|
983
|
+
const timeB = b.asset.modifiedAt?.getTime() ?? 0;
|
|
984
|
+
return timeB - timeA;
|
|
985
|
+
});
|
|
986
|
+
// Show conflict
|
|
987
|
+
console.log();
|
|
988
|
+
console.log(chalk.yellow(`"${canonicalPath}" (${type}) differs across clients:`));
|
|
989
|
+
for (const version of sortedVersions) {
|
|
990
|
+
const sizeKb = (version.asset.content.length / 1024).toFixed(1);
|
|
991
|
+
const relTime = formatRelativeTime(version.asset.modifiedAt);
|
|
992
|
+
console.log(` ${chalk.gray(version.client)}: ${sizeKb}kb, ${relTime}`);
|
|
993
|
+
}
|
|
994
|
+
const similarity = calculateSimilarity(sortedVersions[0].asset.content, sortedVersions[1].asset.content);
|
|
995
|
+
console.log(chalk.dim(` Similarity: ${Math.round(similarity * 100)}% ${getSimilarityLabel(similarity)}`));
|
|
996
|
+
const versionChoice = await select({
|
|
997
|
+
message: `Which "${canonicalPath}" to use?`,
|
|
998
|
+
initialValue: sortedVersions[0].client, // Default to most recently modified
|
|
999
|
+
options: [
|
|
1000
|
+
...sortedVersions.map((v) => ({
|
|
1001
|
+
value: v.client,
|
|
1002
|
+
label: `${v.client} (${(v.asset.content.length / 1024).toFixed(1)}kb)`,
|
|
1003
|
+
hint: formatRelativeTime(v.asset.modifiedAt),
|
|
1004
|
+
})),
|
|
1005
|
+
{ value: "__skip__", label: "Skip" },
|
|
1006
|
+
],
|
|
1007
|
+
});
|
|
1008
|
+
if (typeof versionChoice === "symbol")
|
|
1009
|
+
return versionChoice;
|
|
1010
|
+
if (versionChoice !== "__skip__") {
|
|
1011
|
+
const selected = sortedVersions.find((v) => v.client === versionChoice);
|
|
1012
|
+
if (selected) {
|
|
1013
|
+
const sizeKb = (selected.asset.content.length / 1024).toFixed(1);
|
|
1014
|
+
resolved.push({
|
|
1015
|
+
name: canonicalPath,
|
|
1016
|
+
type,
|
|
1017
|
+
version: selected,
|
|
1018
|
+
label: `${selected.client} (${sizeKb}kb)`,
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return { resolved, hadConflicts };
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Build final plan from selected assets
|
|
1029
|
+
*/
|
|
1030
|
+
function buildFinalPlan(selectedResolved, mcpServerChoices, mcpEntries, otherEntries) {
|
|
1031
|
+
const finalPlan = [];
|
|
1032
|
+
// Handle MCP entries specially - need to rebuild config with selected servers
|
|
1033
|
+
const selectedMcpServers = new Set(selectedResolved.filter((r) => r.type === "mcp").map((r) => r.name));
|
|
1034
|
+
if (selectedMcpServers.size > 0) {
|
|
1035
|
+
// Group MCP entries by target client
|
|
1036
|
+
const mcpByTarget = new Map();
|
|
1037
|
+
for (const entry of mcpEntries) {
|
|
1038
|
+
const entries = mcpByTarget.get(entry.targetClient) ?? [];
|
|
1039
|
+
entries.push(entry);
|
|
1040
|
+
mcpByTarget.set(entry.targetClient, entries);
|
|
1041
|
+
}
|
|
1042
|
+
for (const [targetClient, entries] of mcpByTarget.entries()) {
|
|
1043
|
+
const mergedServers = {};
|
|
1044
|
+
for (const serverName of selectedMcpServers) {
|
|
1045
|
+
const choice = mcpServerChoices.get(serverName);
|
|
1046
|
+
if (choice) {
|
|
1047
|
+
mergedServers[serverName] = choice.config;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
if (Object.keys(mergedServers).length === 0)
|
|
1051
|
+
continue;
|
|
1052
|
+
const firstEntry = entries[0];
|
|
1053
|
+
const format = detectMcpFormat(firstEntry.asset.path);
|
|
1054
|
+
const baseConfig = parseMcpConfig(firstEntry.asset.content, format) ?? {};
|
|
1055
|
+
const finalConfig = {
|
|
1056
|
+
...baseConfig,
|
|
1057
|
+
mcpServers: mergedServers,
|
|
1058
|
+
};
|
|
1059
|
+
const serializedContent = serializeMcpConfig(finalConfig, format);
|
|
1060
|
+
finalPlan.push({
|
|
1061
|
+
...firstEntry,
|
|
1062
|
+
asset: {
|
|
1063
|
+
...firstEntry.asset,
|
|
1064
|
+
content: serializedContent,
|
|
1065
|
+
hash: hashContent(serializedContent),
|
|
1066
|
+
},
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
// Handle other entries - use canonical paths for matching (r.name is now canonicalPath)
|
|
1071
|
+
const selectedOther = new Map();
|
|
1072
|
+
for (const r of selectedResolved) {
|
|
1073
|
+
if (r.type !== "mcp") {
|
|
1074
|
+
selectedOther.set(`${r.type}::${r.name}`, r);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const processedTargets = new Set();
|
|
1078
|
+
for (const entry of otherEntries) {
|
|
1079
|
+
const canonicalPath = entry.asset.canonicalPath ?? entry.asset.relativePath;
|
|
1080
|
+
const key = `${entry.asset.type}::${canonicalPath}`;
|
|
1081
|
+
const resolved = selectedOther.get(key);
|
|
1082
|
+
if (!resolved)
|
|
1083
|
+
continue;
|
|
1084
|
+
const targetKey = `${key}::${entry.targetClient}`;
|
|
1085
|
+
if (processedTargets.has(targetKey))
|
|
1086
|
+
continue;
|
|
1087
|
+
processedTargets.add(targetKey);
|
|
1088
|
+
// Use resolved version's content
|
|
1089
|
+
if (entry.asset.client === resolved.version.client) {
|
|
1090
|
+
finalPlan.push(entry);
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
finalPlan.push({
|
|
1094
|
+
...entry,
|
|
1095
|
+
asset: resolved.version.asset,
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return finalPlan;
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Validate MCP configs and warn about potential issues
|
|
1103
|
+
*/
|
|
1104
|
+
async function validateAndWarnMcp(newEntries, originalEntries) {
|
|
1105
|
+
const warnings = [];
|
|
1106
|
+
const errors = [];
|
|
1107
|
+
const missingCommands = new Set();
|
|
1108
|
+
// Validate each new MCP config
|
|
1109
|
+
for (const entry of newEntries) {
|
|
1110
|
+
const format = detectMcpFormat(entry.asset.path);
|
|
1111
|
+
const validation = validateMcpConfig(entry.asset.content, format);
|
|
1112
|
+
for (const err of validation.errors) {
|
|
1113
|
+
errors.push(`${entry.targetClient}: ${err}`);
|
|
1114
|
+
}
|
|
1115
|
+
for (const warn of validation.warnings) {
|
|
1116
|
+
warnings.push(`${entry.targetClient}: ${warn}`);
|
|
1117
|
+
}
|
|
1118
|
+
// Check if commands exist
|
|
1119
|
+
const config = parseMcpConfig(entry.asset.content, format);
|
|
1120
|
+
if (config) {
|
|
1121
|
+
const commands = getMcpCommands(config);
|
|
1122
|
+
for (const cmd of commands) {
|
|
1123
|
+
const exists = await commandExists(cmd);
|
|
1124
|
+
if (!exists) {
|
|
1125
|
+
missingCommands.add(cmd);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
// Check for servers being removed from targets
|
|
1131
|
+
for (const newEntry of newEntries) {
|
|
1132
|
+
const format = detectMcpFormat(newEntry.asset.path);
|
|
1133
|
+
const newConfig = parseMcpConfig(newEntry.asset.content, format);
|
|
1134
|
+
if (!newConfig)
|
|
1135
|
+
continue;
|
|
1136
|
+
// Find original config for same target client
|
|
1137
|
+
for (const origEntry of originalEntries) {
|
|
1138
|
+
if (origEntry.targetClient !== newEntry.targetClient)
|
|
1139
|
+
continue;
|
|
1140
|
+
// Read existing file at target to check what would be removed
|
|
1141
|
+
const existingContent = await readFileSafe(newEntry.targetPath);
|
|
1142
|
+
if (!existingContent)
|
|
1143
|
+
continue;
|
|
1144
|
+
// Use target file's format, not source
|
|
1145
|
+
const targetFormat = detectMcpFormat(newEntry.targetPath);
|
|
1146
|
+
const existingConfig = parseMcpConfig(existingContent, targetFormat);
|
|
1147
|
+
if (!existingConfig)
|
|
1148
|
+
continue;
|
|
1149
|
+
const removed = findRemovedServers(newConfig, existingConfig);
|
|
1150
|
+
for (const serverName of removed) {
|
|
1151
|
+
warnings.push(`${newEntry.targetClient}: server "${serverName}" will be removed`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
// Display warnings
|
|
1156
|
+
if (missingCommands.size > 0) {
|
|
1157
|
+
console.log();
|
|
1158
|
+
console.log(chalk.yellow(`Warning: Commands not found in PATH: ${Array.from(missingCommands).join(", ")}`));
|
|
1159
|
+
console.log(chalk.dim(" These MCP servers may not work correctly."));
|
|
1160
|
+
}
|
|
1161
|
+
if (warnings.length > 0) {
|
|
1162
|
+
console.log();
|
|
1163
|
+
console.log(chalk.yellow("Warnings:"));
|
|
1164
|
+
for (const warn of warnings) {
|
|
1165
|
+
console.log(chalk.yellow(` - ${warn}`));
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (errors.length > 0) {
|
|
1169
|
+
console.log();
|
|
1170
|
+
console.log(chalk.red("Errors:"));
|
|
1171
|
+
for (const err of errors) {
|
|
1172
|
+
console.log(chalk.red(` - ${err}`));
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
//# sourceMappingURL=interactive.js.map
|