agentloom 0.1.4 → 0.1.6
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/README.md +92 -6
- package/dist/cli.js +11 -4
- package/dist/commands/add.js +14 -0
- package/dist/commands/delete.js +89 -3
- package/dist/commands/entity-utils.js +3 -0
- package/dist/commands/find.js +146 -12
- package/dist/commands/rule.d.ts +2 -0
- package/dist/commands/rule.js +86 -0
- package/dist/commands/sync.js +13 -4
- package/dist/commands/update.js +90 -7
- package/dist/core/agents.js +1 -1
- package/dist/core/argv.js +2 -0
- package/dist/core/commands.d.ts +12 -0
- package/dist/core/commands.js +106 -6
- package/dist/core/copy.js +12 -5
- package/dist/core/importer.d.ts +10 -0
- package/dist/core/importer.js +629 -13
- package/dist/core/lockfile.js +8 -0
- package/dist/core/manifest.js +123 -6
- package/dist/core/migration.js +655 -66
- package/dist/core/provider-entity-validation.d.ts +8 -0
- package/dist/core/provider-entity-validation.js +34 -0
- package/dist/core/provider-paths.d.ts +8 -1
- package/dist/core/provider-paths.js +69 -5
- package/dist/core/router.js +7 -1
- package/dist/core/rules.d.ts +34 -0
- package/dist/core/rules.js +149 -0
- package/dist/core/scope.js +1 -0
- package/dist/core/skills.d.ts +1 -0
- package/dist/core/skills.js +21 -2
- package/dist/core/sources.d.ts +2 -0
- package/dist/core/sources.js +34 -5
- package/dist/core/telemetry.d.ts +1 -1
- package/dist/core/telemetry.js +16 -0
- package/dist/sync/index.js +376 -18
- package/dist/types.d.ts +5 -1
- package/package.json +1 -1
package/dist/core/migration.js
CHANGED
|
@@ -4,15 +4,18 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
4
4
|
import { cancel, isCancel, select } from "@clack/prompts";
|
|
5
5
|
import TOML from "@iarna/toml";
|
|
6
6
|
import matter from "gray-matter";
|
|
7
|
+
import YAML from "yaml";
|
|
7
8
|
import { buildAgentMarkdown, parseAgentsDir } from "./agents.js";
|
|
8
|
-
import { parseCommandsDir } from "./commands.js";
|
|
9
|
+
import { normalizeCommandArgumentsForCanonical, parseCommandsDir, } from "./commands.js";
|
|
9
10
|
import { ensureDir, isObject, readJsonIfExists, slugify } from "./fs.js";
|
|
10
11
|
import { readLockfile, writeLockfile } from "./lockfile.js";
|
|
11
12
|
import { readManifest, writeManifest } from "./manifest.js";
|
|
12
13
|
import { readCanonicalMcp, writeCanonicalMcp } from "./mcp.js";
|
|
13
|
-
import { getClaudeMcpPath, getCodexConfigPath, getCopilotMcpPath, getCursorMcpPath, getGeminiSettingsPath, getOpenCodeConfigPath, getPiMcpPath, getProviderAgentsDir, getProviderCommandsDir, getProviderSkillsPaths, } from "./provider-paths.js";
|
|
14
|
+
import { getClaudeMcpPath, getCodexConfigPath, getCopilotMcpPath, getCursorRulesDir, getCursorMcpPath, getGeminiSettingsPath, getOpenCodeConfigPath, getPiMcpPath, getProviderAgentsDir, getProviderCommandsDir, getRuleInstructionPaths, getProviderSkillsPaths, } from "./provider-paths.js";
|
|
14
15
|
import { readSettings, writeSettings } from "./settings.js";
|
|
15
|
-
import { applySkillProviderSideEffects, copySkillArtifacts, parseSkillsDir, skillContentMatchesTarget, } from "./skills.js";
|
|
16
|
+
import { applySkillProviderSideEffects, copySkillArtifacts, getLegacyCopilotSkillDirs, parseSkillsDir, skillContentMatchesTarget, } from "./skills.js";
|
|
17
|
+
import { parseManagedRuleBlocks, parseRuleMarkdown, stripRuleFileExtension, } from "./rules.js";
|
|
18
|
+
import { isProviderEntityFileName } from "./provider-entity-validation.js";
|
|
16
19
|
import { ALL_PROVIDERS } from "../types.js";
|
|
17
20
|
const PROVIDER_NAME_KEYS = new Set(ALL_PROVIDERS);
|
|
18
21
|
export class MigrationConflictError extends Error {
|
|
@@ -29,6 +32,7 @@ export function createEmptyMigrationSummary(providers, target) {
|
|
|
29
32
|
agent: { detected: 0, imported: 0, conflicts: 0, skipped: 0 },
|
|
30
33
|
command: { detected: 0, imported: 0, conflicts: 0, skipped: 0 },
|
|
31
34
|
mcp: { detected: 0, imported: 0, conflicts: 0, skipped: 0 },
|
|
35
|
+
rule: { detected: 0, imported: 0, conflicts: 0, skipped: 0 },
|
|
32
36
|
skill: { detected: 0, imported: 0, conflicts: 0, skipped: 0 },
|
|
33
37
|
},
|
|
34
38
|
};
|
|
@@ -37,6 +41,7 @@ export function initializeCanonicalLayout(paths, providers) {
|
|
|
37
41
|
ensureDir(paths.agentsRoot);
|
|
38
42
|
ensureDir(paths.agentsDir);
|
|
39
43
|
ensureDir(paths.commandsDir);
|
|
44
|
+
ensureDir(paths.rulesDir);
|
|
40
45
|
ensureDir(paths.skillsDir);
|
|
41
46
|
if (!fs.existsSync(paths.mcpPath)) {
|
|
42
47
|
writeCanonicalMcp({ mcpPath: paths.mcpPath }, { version: 1, mcpServers: {} });
|
|
@@ -84,6 +89,9 @@ export async function migrateProviderStateToCanonical(options) {
|
|
|
84
89
|
if (includesTarget(options.target, "mcp")) {
|
|
85
90
|
await migrateMcp(options, summary.entities.mcp);
|
|
86
91
|
}
|
|
92
|
+
if (includesTarget(options.target, "rule")) {
|
|
93
|
+
await migrateRules(options, summary.entities.rule);
|
|
94
|
+
}
|
|
87
95
|
if (includesTarget(options.target, "skill")) {
|
|
88
96
|
await migrateSkills(options, summary.entities.skill);
|
|
89
97
|
}
|
|
@@ -91,7 +99,7 @@ export async function migrateProviderStateToCanonical(options) {
|
|
|
91
99
|
}
|
|
92
100
|
export function formatMigrationSummary(summary) {
|
|
93
101
|
const lines = ["Migration summary (provider -> canonical):"];
|
|
94
|
-
for (const entity of ["agent", "command", "mcp", "skill"]) {
|
|
102
|
+
for (const entity of ["agent", "command", "mcp", "rule", "skill"]) {
|
|
95
103
|
const row = summary.entities[entity];
|
|
96
104
|
lines.push(`${entity}: detected=${row.detected}, imported=${row.imported}, conflicts=${row.conflicts}, skipped=${row.skipped}`);
|
|
97
105
|
}
|
|
@@ -102,8 +110,13 @@ async function migrateAgents(options, summary) {
|
|
|
102
110
|
const canonicalByKey = new Map();
|
|
103
111
|
for (const agent of canonicalAgents) {
|
|
104
112
|
const key = agentKey(agent.name, agent.fileName);
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
// Always let file-derived keys win over name aliases from earlier entries.
|
|
114
|
+
canonicalByKey.set(key, agent);
|
|
115
|
+
}
|
|
116
|
+
for (const agent of canonicalAgents) {
|
|
117
|
+
const nameKey = slugify(agent.name);
|
|
118
|
+
if (nameKey && !canonicalByKey.has(nameKey)) {
|
|
119
|
+
canonicalByKey.set(nameKey, agent);
|
|
107
120
|
}
|
|
108
121
|
}
|
|
109
122
|
const recordsByKey = new Map();
|
|
@@ -118,10 +131,11 @@ async function migrateAgents(options, summary) {
|
|
|
118
131
|
recordsByKey.set(record.key, next);
|
|
119
132
|
}
|
|
120
133
|
}
|
|
134
|
+
mergeAgentRecordsByName(recordsByKey);
|
|
121
135
|
for (const [key, records] of recordsByKey.entries()) {
|
|
122
136
|
if (records.length === 0)
|
|
123
137
|
continue;
|
|
124
|
-
const canonical = canonicalByKey
|
|
138
|
+
const canonical = resolveCanonicalAgentForMerge(canonicalByKey, key, records);
|
|
125
139
|
const existingRaw = canonical
|
|
126
140
|
? fs.readFileSync(canonical.sourcePath, "utf8")
|
|
127
141
|
: null;
|
|
@@ -152,41 +166,192 @@ async function migrateAgents(options, summary) {
|
|
|
152
166
|
summary.imported += 1;
|
|
153
167
|
}
|
|
154
168
|
}
|
|
169
|
+
function mergeAgentRecordsByName(recordsByKey) {
|
|
170
|
+
const groupKeyByName = new Map();
|
|
171
|
+
for (const [key, records] of [...recordsByKey.entries()]) {
|
|
172
|
+
if (!recordsByKey.has(key))
|
|
173
|
+
continue;
|
|
174
|
+
let targetKey = key;
|
|
175
|
+
for (const record of records) {
|
|
176
|
+
const nameKey = slugify(record.name);
|
|
177
|
+
if (!nameKey)
|
|
178
|
+
continue;
|
|
179
|
+
const existingKey = groupKeyByName.get(nameKey);
|
|
180
|
+
if (!existingKey || existingKey === targetKey) {
|
|
181
|
+
groupKeyByName.set(nameKey, targetKey);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const existing = recordsByKey.get(existingKey) ?? [];
|
|
185
|
+
const current = recordsByKey.get(targetKey) ?? [];
|
|
186
|
+
existing.push(...current);
|
|
187
|
+
recordsByKey.set(existingKey, existing);
|
|
188
|
+
recordsByKey.delete(targetKey);
|
|
189
|
+
targetKey = existingKey;
|
|
190
|
+
targetKey = rekeyMergedAgentGroup(recordsByKey, targetKey, nameKey);
|
|
191
|
+
groupKeyByName.set(nameKey, targetKey);
|
|
192
|
+
}
|
|
193
|
+
const merged = recordsByKey.get(targetKey) ?? [];
|
|
194
|
+
for (const record of merged) {
|
|
195
|
+
const nameKey = slugify(record.name);
|
|
196
|
+
if (nameKey) {
|
|
197
|
+
groupKeyByName.set(nameKey, targetKey);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function rekeyMergedAgentGroup(recordsByKey, currentKey, nameKey) {
|
|
203
|
+
if (!nameKey || currentKey === nameKey) {
|
|
204
|
+
return currentKey;
|
|
205
|
+
}
|
|
206
|
+
const current = recordsByKey.get(currentKey);
|
|
207
|
+
if (!current) {
|
|
208
|
+
return currentKey;
|
|
209
|
+
}
|
|
210
|
+
const existingNameKeyGroup = recordsByKey.get(nameKey);
|
|
211
|
+
if (existingNameKeyGroup && existingNameKeyGroup !== current) {
|
|
212
|
+
existingNameKeyGroup.push(...current);
|
|
213
|
+
recordsByKey.set(nameKey, existingNameKeyGroup);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
recordsByKey.set(nameKey, current);
|
|
217
|
+
}
|
|
218
|
+
recordsByKey.delete(currentKey);
|
|
219
|
+
return nameKey;
|
|
220
|
+
}
|
|
221
|
+
function resolveCanonicalAgentForMerge(canonicalByKey, key, records) {
|
|
222
|
+
const direct = canonicalByKey.get(key);
|
|
223
|
+
if (direct) {
|
|
224
|
+
return direct;
|
|
225
|
+
}
|
|
226
|
+
for (const record of records) {
|
|
227
|
+
const nameKey = slugify(record.name);
|
|
228
|
+
if (!nameKey)
|
|
229
|
+
continue;
|
|
230
|
+
const match = canonicalByKey.get(nameKey);
|
|
231
|
+
if (match) {
|
|
232
|
+
return match;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
155
237
|
function readMarkdownProviderAgents(paths, provider) {
|
|
156
|
-
const
|
|
238
|
+
const records = [];
|
|
239
|
+
const seenSourcePaths = new Set();
|
|
240
|
+
const candidateDirs = [
|
|
241
|
+
{
|
|
242
|
+
path: getProviderAgentsDir(paths, provider),
|
|
243
|
+
sourcePriority: 0,
|
|
244
|
+
fallback: false,
|
|
245
|
+
},
|
|
246
|
+
];
|
|
247
|
+
// Keep backward compatibility with historical global Copilot output.
|
|
248
|
+
if (provider === "copilot" && paths.scope === "global") {
|
|
249
|
+
candidateDirs.push({
|
|
250
|
+
path: path.join(paths.homeDir, ".github", "agents"),
|
|
251
|
+
sourcePriority: 1,
|
|
252
|
+
fallback: true,
|
|
253
|
+
});
|
|
254
|
+
candidateDirs.push({
|
|
255
|
+
path: path.join(paths.homeDir, ".vscode", "chatmodes"),
|
|
256
|
+
sourcePriority: 2,
|
|
257
|
+
fallback: true,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
const entriesByPath = new Map();
|
|
261
|
+
for (const candidateDir of candidateDirs) {
|
|
262
|
+
entriesByPath.set(candidateDir.path, listProviderAgentEntries(candidateDir.path, provider));
|
|
263
|
+
}
|
|
264
|
+
for (const candidateDir of candidateDirs) {
|
|
265
|
+
const dirPath = candidateDir.path;
|
|
266
|
+
const entries = entriesByPath.get(dirPath) ?? [];
|
|
267
|
+
for (const fileName of entries) {
|
|
268
|
+
const sourcePath = path.join(dirPath, fileName);
|
|
269
|
+
if (seenSourcePaths.has(sourcePath))
|
|
270
|
+
continue;
|
|
271
|
+
seenSourcePaths.add(sourcePath);
|
|
272
|
+
const raw = fs.readFileSync(sourcePath, "utf8");
|
|
273
|
+
const parsed = matter(raw);
|
|
274
|
+
const data = isObject(parsed.data) ? parsed.data : {};
|
|
275
|
+
const parsedName = typeof data.name === "string" && data.name.trim().length > 0
|
|
276
|
+
? data.name.trim()
|
|
277
|
+
: guessAgentNameFromFile(fileName);
|
|
278
|
+
const parsedDescription = typeof data.description === "string" &&
|
|
279
|
+
data.description.trim().length > 0
|
|
280
|
+
? data.description.trim()
|
|
281
|
+
: `Migrated from ${provider}`;
|
|
282
|
+
const nameKey = providerAgentNameKey(parsedName, fileName);
|
|
283
|
+
const key = agentKey(parsedName, fileName);
|
|
284
|
+
const providerConfig = extractProviderConfigFromAgentFrontmatter(data, provider);
|
|
285
|
+
records.push({
|
|
286
|
+
provider,
|
|
287
|
+
sourcePath,
|
|
288
|
+
fileName,
|
|
289
|
+
sourcePriority: candidateDir.sourcePriority,
|
|
290
|
+
nameKey,
|
|
291
|
+
key,
|
|
292
|
+
name: parsedName,
|
|
293
|
+
description: parsedDescription,
|
|
294
|
+
body: parsed.content.trimStart(),
|
|
295
|
+
providerConfig,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return dedupeProviderAgentRecords(records);
|
|
300
|
+
}
|
|
301
|
+
function listProviderAgentEntries(dirPath, provider) {
|
|
157
302
|
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
|
|
158
303
|
return [];
|
|
159
304
|
}
|
|
160
|
-
|
|
305
|
+
return fs
|
|
161
306
|
.readdirSync(dirPath, { withFileTypes: true })
|
|
162
307
|
.filter((entry) => entry.isFile())
|
|
163
308
|
.map((entry) => entry.name)
|
|
164
|
-
.filter((name) =>
|
|
309
|
+
.filter((name) => isProviderEntityFileName({
|
|
310
|
+
provider,
|
|
311
|
+
entity: "agent",
|
|
312
|
+
fileName: name,
|
|
313
|
+
}))
|
|
165
314
|
.sort();
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
? data.description.trim()
|
|
177
|
-
: `Migrated from ${provider}`;
|
|
178
|
-
const providerConfig = extractProviderConfigFromAgentFrontmatter(data, provider);
|
|
179
|
-
records.push({
|
|
180
|
-
provider,
|
|
181
|
-
sourcePath,
|
|
182
|
-
key: agentKey(parsedName, fileName),
|
|
183
|
-
name: parsedName,
|
|
184
|
-
description: parsedDescription,
|
|
185
|
-
body: parsed.content.trimStart(),
|
|
186
|
-
providerConfig,
|
|
187
|
-
});
|
|
315
|
+
}
|
|
316
|
+
function providerAgentNameKey(name, fileName) {
|
|
317
|
+
return slugify(name) || agentFileKey(fileName);
|
|
318
|
+
}
|
|
319
|
+
function dedupeProviderAgentRecords(records) {
|
|
320
|
+
const recordsByName = new Map();
|
|
321
|
+
for (const record of records) {
|
|
322
|
+
const next = recordsByName.get(record.nameKey) ?? [];
|
|
323
|
+
next.push(record);
|
|
324
|
+
recordsByName.set(record.nameKey, next);
|
|
188
325
|
}
|
|
189
|
-
|
|
326
|
+
const deduped = [];
|
|
327
|
+
for (const [nameKey, bucket] of recordsByName.entries()) {
|
|
328
|
+
if (bucket.length === 1) {
|
|
329
|
+
deduped.push(bucket[0]);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const ranked = [...bucket].sort(compareProviderAgentCollisionPriority);
|
|
333
|
+
const winner = ranked[0];
|
|
334
|
+
const ignored = ranked.slice(1).map((record) => record.sourcePath);
|
|
335
|
+
console.warn(`Warning: Duplicate ${winner.provider} agents share canonical name "${nameKey}". Keeping ${winner.sourcePath} and ignoring ${ignored.join(", ")}.`);
|
|
336
|
+
deduped.push(winner);
|
|
337
|
+
}
|
|
338
|
+
return deduped.sort((a, b) => a.key.localeCompare(b.key) || a.sourcePath.localeCompare(b.sourcePath));
|
|
339
|
+
}
|
|
340
|
+
function compareProviderAgentCollisionPriority(left, right) {
|
|
341
|
+
if (left.sourcePriority !== right.sourcePriority) {
|
|
342
|
+
return left.sourcePriority - right.sourcePriority;
|
|
343
|
+
}
|
|
344
|
+
const leftFileMatchesName = isProviderAgentFileAlignedWithName(left) ? 0 : 1;
|
|
345
|
+
const rightFileMatchesName = isProviderAgentFileAlignedWithName(right)
|
|
346
|
+
? 0
|
|
347
|
+
: 1;
|
|
348
|
+
if (leftFileMatchesName !== rightFileMatchesName) {
|
|
349
|
+
return leftFileMatchesName - rightFileMatchesName;
|
|
350
|
+
}
|
|
351
|
+
return left.sourcePath.localeCompare(right.sourcePath);
|
|
352
|
+
}
|
|
353
|
+
function isProviderAgentFileAlignedWithName(record) {
|
|
354
|
+
return agentFileKey(record.fileName) === record.nameKey;
|
|
190
355
|
}
|
|
191
356
|
function readCodexProviderAgents(paths) {
|
|
192
357
|
const configPath = getCodexConfigPath(paths);
|
|
@@ -215,13 +380,17 @@ function readCodexProviderAgents(paths) {
|
|
|
215
380
|
const roleToml = roleTomlRaw.trim()
|
|
216
381
|
? TOML.parse(roleTomlRaw)
|
|
217
382
|
: {};
|
|
383
|
+
const inlineInstructions = typeof roleToml.developer_instructions === "string"
|
|
384
|
+
? roleToml.developer_instructions.trim()
|
|
385
|
+
: "";
|
|
218
386
|
const instructionRef = roleToml.model_instructions_file;
|
|
219
387
|
const instructionPath = typeof instructionRef === "string" && instructionRef.trim().length > 0
|
|
220
388
|
? resolveCodexPath(path.dirname(roleTomlPath), instructionRef)
|
|
221
389
|
: null;
|
|
222
|
-
const
|
|
223
|
-
? fs.readFileSync(instructionPath, "utf8").trimStart()
|
|
390
|
+
const fileInstructions = instructionPath && fs.existsSync(instructionPath)
|
|
391
|
+
? fs.readFileSync(instructionPath, "utf8").trimStart().trimEnd()
|
|
224
392
|
: "";
|
|
393
|
+
const body = inlineInstructions || fileInstructions;
|
|
225
394
|
const description = isObject(roleEntry) && typeof roleEntry.description === "string"
|
|
226
395
|
? roleEntry.description.trim()
|
|
227
396
|
: roleName;
|
|
@@ -232,19 +401,32 @@ function readCodexProviderAgents(paths) {
|
|
|
232
401
|
if (typeof roleToml.model_reasoning_effort === "string") {
|
|
233
402
|
providerConfig.reasoningEffort = roleToml.model_reasoning_effort;
|
|
234
403
|
}
|
|
404
|
+
if (typeof roleToml.model_reasoning_summary === "string") {
|
|
405
|
+
providerConfig.reasoningSummary = roleToml.model_reasoning_summary;
|
|
406
|
+
}
|
|
407
|
+
if (typeof roleToml.model_verbosity === "string") {
|
|
408
|
+
providerConfig.verbosity = roleToml.model_verbosity;
|
|
409
|
+
}
|
|
235
410
|
if (typeof roleToml.approval_policy === "string") {
|
|
236
411
|
providerConfig.approvalPolicy = roleToml.approval_policy;
|
|
237
412
|
}
|
|
238
413
|
if (typeof roleToml.sandbox_mode === "string") {
|
|
239
414
|
providerConfig.sandboxMode = roleToml.sandbox_mode;
|
|
240
415
|
}
|
|
241
|
-
if (
|
|
416
|
+
if (typeof roleToml.web_search === "boolean") {
|
|
417
|
+
providerConfig.webSearch = roleToml.web_search;
|
|
418
|
+
}
|
|
419
|
+
if (typeof providerConfig.webSearch !== "boolean" &&
|
|
420
|
+
isObject(roleToml.tools) &&
|
|
242
421
|
typeof roleToml.tools.web_search === "boolean") {
|
|
243
422
|
providerConfig.webSearch = roleToml.tools.web_search;
|
|
244
423
|
}
|
|
245
424
|
records.push({
|
|
246
425
|
provider: "codex",
|
|
247
426
|
sourcePath: roleTomlPath,
|
|
427
|
+
fileName: `${roleName}.md`,
|
|
428
|
+
sourcePriority: 0,
|
|
429
|
+
nameKey: providerAgentNameKey(roleName, `${roleName}.md`),
|
|
248
430
|
key: agentKey(roleName, `${roleName}.md`),
|
|
249
431
|
name: roleName,
|
|
250
432
|
description: description || roleName,
|
|
@@ -279,17 +461,24 @@ function guessAgentNameFromFile(fileName) {
|
|
|
279
461
|
const base = fileName.replace(/\.agent\.md$/i, "").replace(/\.md$/i, "");
|
|
280
462
|
return base.trim() || "agent";
|
|
281
463
|
}
|
|
464
|
+
function agentFileKey(fileName) {
|
|
465
|
+
const normalizedFile = fileName
|
|
466
|
+
.replace(/\.agent\.md$/i, "")
|
|
467
|
+
.replace(/\.md$/i, "");
|
|
468
|
+
return slugify(normalizedFile);
|
|
469
|
+
}
|
|
282
470
|
function agentKey(name, fileName) {
|
|
283
|
-
return
|
|
471
|
+
return agentFileKey(fileName) || slugify(name) || "agent";
|
|
284
472
|
}
|
|
285
473
|
async function resolveAgentMerge(options) {
|
|
286
474
|
const records = dedupeAgentRecords(options.records);
|
|
287
475
|
if (records.length === 0) {
|
|
288
476
|
return null;
|
|
289
477
|
}
|
|
290
|
-
|
|
291
|
-
let
|
|
292
|
-
let
|
|
478
|
+
const preferredRecord = choosePreferredProviderAgent(records);
|
|
479
|
+
let name = options.canonical?.name ?? preferredRecord.name;
|
|
480
|
+
let description = options.canonical?.description ?? preferredRecord.description;
|
|
481
|
+
let body = options.canonical?.body ?? preferredRecord.body;
|
|
293
482
|
const frontmatter = options.canonical
|
|
294
483
|
? { ...options.canonical.frontmatter }
|
|
295
484
|
: {
|
|
@@ -297,9 +486,8 @@ async function resolveAgentMerge(options) {
|
|
|
297
486
|
description,
|
|
298
487
|
};
|
|
299
488
|
if (!options.canonical && records.length > 1) {
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
if (different) {
|
|
489
|
+
const allSameBody = records.every((record) => sameNormalizedBody(record.body, records[0].body));
|
|
490
|
+
if (!allSameBody) {
|
|
303
491
|
options.onConflict();
|
|
304
492
|
const chosen = await chooseProviderSource({
|
|
305
493
|
conflictLabel: `agent "${options.key}"`,
|
|
@@ -314,7 +502,7 @@ async function resolveAgentMerge(options) {
|
|
|
314
502
|
}
|
|
315
503
|
if (options.canonical) {
|
|
316
504
|
for (const record of records) {
|
|
317
|
-
if (!
|
|
505
|
+
if (!sameNormalizedBody(record.body, body)) {
|
|
318
506
|
options.onConflict();
|
|
319
507
|
const decision = await resolveCanonicalConflict({
|
|
320
508
|
conflictLabel: `agent "${record.key}" from ${record.provider}`,
|
|
@@ -330,7 +518,14 @@ async function resolveAgentMerge(options) {
|
|
|
330
518
|
}
|
|
331
519
|
}
|
|
332
520
|
for (const record of records) {
|
|
333
|
-
|
|
521
|
+
const providerConfig = cloneRecord(record.providerConfig);
|
|
522
|
+
if (record.name.trim() !== name.trim()) {
|
|
523
|
+
providerConfig.name = record.name;
|
|
524
|
+
}
|
|
525
|
+
if (record.description.trim() !== description.trim()) {
|
|
526
|
+
providerConfig.description = record.description;
|
|
527
|
+
}
|
|
528
|
+
frontmatter[record.provider] = providerConfig;
|
|
334
529
|
}
|
|
335
530
|
frontmatter.name = name;
|
|
336
531
|
frontmatter.description = description;
|
|
@@ -343,6 +538,9 @@ async function resolveAgentMerge(options) {
|
|
|
343
538
|
markdown,
|
|
344
539
|
};
|
|
345
540
|
}
|
|
541
|
+
function choosePreferredProviderAgent(records) {
|
|
542
|
+
return records.find((record) => record.provider === "copilot") ?? records[0];
|
|
543
|
+
}
|
|
346
544
|
function dedupeAgentRecords(records) {
|
|
347
545
|
const unique = [];
|
|
348
546
|
for (const record of records) {
|
|
@@ -353,10 +551,8 @@ function dedupeAgentRecords(records) {
|
|
|
353
551
|
}
|
|
354
552
|
return unique;
|
|
355
553
|
}
|
|
356
|
-
function
|
|
357
|
-
return (left
|
|
358
|
-
left.description.trim() === right.description.trim() &&
|
|
359
|
-
normalizeBody(left.body) === normalizeBody(right.body));
|
|
554
|
+
function sameNormalizedBody(left, right) {
|
|
555
|
+
return normalizeBody(left) === normalizeBody(right);
|
|
360
556
|
}
|
|
361
557
|
async function migrateCommands(options, summary) {
|
|
362
558
|
const canonicalCommands = parseCommandsDir(options.paths.commandsDir);
|
|
@@ -373,11 +569,12 @@ async function migrateCommands(options, summary) {
|
|
|
373
569
|
}
|
|
374
570
|
for (const [fileName, records] of grouped.entries()) {
|
|
375
571
|
const canonical = canonicalByFile.get(fileName);
|
|
376
|
-
let
|
|
572
|
+
let preferredRecord = choosePreferredProviderCommand(records);
|
|
573
|
+
let body = canonical?.body ?? preferredRecord.body;
|
|
377
574
|
let hadConflict = false;
|
|
378
575
|
if (!canonical) {
|
|
379
|
-
const
|
|
380
|
-
if (!
|
|
576
|
+
const allSameBody = records.every((record) => sameNormalizedBody(record.body, records[0].body));
|
|
577
|
+
if (!allSameBody) {
|
|
381
578
|
hadConflict = true;
|
|
382
579
|
summary.conflicts += 1;
|
|
383
580
|
const chosen = await resolveProviderDuplicateConflict({
|
|
@@ -386,13 +583,18 @@ async function migrateCommands(options, summary) {
|
|
|
386
583
|
yes: Boolean(options.yes),
|
|
387
584
|
nonInteractive: Boolean(options.nonInteractive),
|
|
388
585
|
});
|
|
389
|
-
|
|
586
|
+
preferredRecord = chosen;
|
|
587
|
+
body = chosen.body;
|
|
390
588
|
}
|
|
391
589
|
}
|
|
392
590
|
else {
|
|
393
591
|
for (const record of records) {
|
|
394
|
-
if (
|
|
592
|
+
if (canonical.frontmatter?.[record.provider] === false) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (sameNormalizedBody(record.body, body)) {
|
|
395
596
|
continue;
|
|
597
|
+
}
|
|
396
598
|
hadConflict = true;
|
|
397
599
|
summary.conflicts += 1;
|
|
398
600
|
const decision = await resolveCanonicalConflict({
|
|
@@ -401,10 +603,17 @@ async function migrateCommands(options, summary) {
|
|
|
401
603
|
nonInteractive: Boolean(options.nonInteractive),
|
|
402
604
|
});
|
|
403
605
|
if (decision === "provider") {
|
|
404
|
-
|
|
606
|
+
preferredRecord = record;
|
|
607
|
+
body = record.body;
|
|
405
608
|
}
|
|
406
609
|
}
|
|
407
610
|
}
|
|
611
|
+
const frontmatter = mergeCommandFrontmatter({
|
|
612
|
+
canonicalFrontmatter: canonical?.frontmatter,
|
|
613
|
+
records,
|
|
614
|
+
preferredRecord,
|
|
615
|
+
});
|
|
616
|
+
const content = buildCommandMarkdown(frontmatter, body);
|
|
408
617
|
const hasChanged = canonical
|
|
409
618
|
? canonical.content !== content
|
|
410
619
|
: records.length > 0;
|
|
@@ -419,24 +628,222 @@ async function migrateCommands(options, summary) {
|
|
|
419
628
|
summary.imported += 1;
|
|
420
629
|
}
|
|
421
630
|
}
|
|
631
|
+
function choosePreferredProviderCommand(records) {
|
|
632
|
+
const withFrontmatter = records.filter((record) => record.frontmatter);
|
|
633
|
+
if (withFrontmatter.length === 0) {
|
|
634
|
+
return records[0];
|
|
635
|
+
}
|
|
636
|
+
return (withFrontmatter.find((record) => record.provider === "copilot") ??
|
|
637
|
+
withFrontmatter[0]);
|
|
638
|
+
}
|
|
639
|
+
const COMMAND_GENERIC_FRONTMATTER_KEYS = new Set([
|
|
640
|
+
"name",
|
|
641
|
+
"description",
|
|
642
|
+
]);
|
|
643
|
+
function mergeCommandFrontmatter(options) {
|
|
644
|
+
const hasCanonical = options.canonicalFrontmatter !== undefined;
|
|
645
|
+
const merged = options.canonicalFrontmatter
|
|
646
|
+
? cloneRecord(options.canonicalFrontmatter)
|
|
647
|
+
: {};
|
|
648
|
+
const sharedGeneric = new Map();
|
|
649
|
+
for (const key of COMMAND_GENERIC_FRONTMATTER_KEYS) {
|
|
650
|
+
if (merged[key] !== undefined) {
|
|
651
|
+
sharedGeneric.set(key, merged[key]);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (!hasCanonical) {
|
|
655
|
+
const preferredValue = options.preferredRecord.frontmatter?.[key];
|
|
656
|
+
if (preferredValue !== undefined) {
|
|
657
|
+
const cloned = cloneRecord(preferredValue);
|
|
658
|
+
merged[key] = cloned;
|
|
659
|
+
sharedGeneric.set(key, cloned);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
for (const record of options.records) {
|
|
664
|
+
const existingProviderValue = merged[record.provider];
|
|
665
|
+
if (existingProviderValue === false) {
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
const hadProviderObject = isObject(existingProviderValue);
|
|
669
|
+
const existingProviderConfig = hadProviderObject
|
|
670
|
+
? cloneRecord(existingProviderValue)
|
|
671
|
+
: {};
|
|
672
|
+
const providerConfig = existingProviderConfig;
|
|
673
|
+
const data = record.frontmatter ?? {};
|
|
674
|
+
for (const [key, value] of Object.entries(data)) {
|
|
675
|
+
if (PROVIDER_NAME_KEYS.has(key))
|
|
676
|
+
continue;
|
|
677
|
+
if (COMMAND_GENERIC_FRONTMATTER_KEYS.has(key)) {
|
|
678
|
+
if (!sharedGeneric.has(key)) {
|
|
679
|
+
if (hasCanonical) {
|
|
680
|
+
providerConfig[key] = cloneRecord(value);
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
const cloned = cloneRecord(value);
|
|
684
|
+
merged[key] = cloned;
|
|
685
|
+
sharedGeneric.set(key, cloned);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
const sharedValue = sharedGeneric.get(key);
|
|
689
|
+
if (!isDeepStrictEqual(sharedValue, value)) {
|
|
690
|
+
providerConfig[key] = cloneRecord(value);
|
|
691
|
+
}
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const sharedValue = merged[key];
|
|
695
|
+
if (sharedValue !== undefined && isDeepStrictEqual(sharedValue, value)) {
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
providerConfig[key] = cloneRecord(value);
|
|
699
|
+
}
|
|
700
|
+
if (Object.keys(providerConfig).length > 0 || hadProviderObject) {
|
|
701
|
+
merged[record.provider] = providerConfig;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
705
|
+
}
|
|
706
|
+
function buildCommandMarkdown(frontmatter, body) {
|
|
707
|
+
const normalizedBody = body.trimStart();
|
|
708
|
+
if (!frontmatter || Object.keys(frontmatter).length === 0) {
|
|
709
|
+
return normalizedBody.endsWith("\n")
|
|
710
|
+
? normalizedBody
|
|
711
|
+
: `${normalizedBody}\n`;
|
|
712
|
+
}
|
|
713
|
+
const fm = YAML.stringify(frontmatter, { lineWidth: 0 }).trimEnd();
|
|
714
|
+
return `---\n${fm}\n---\n\n${normalizedBody}${normalizedBody.endsWith("\n") ? "" : "\n"}`;
|
|
715
|
+
}
|
|
422
716
|
function readProviderCommands(paths, provider) {
|
|
423
|
-
|
|
717
|
+
// Codex prompts are home-scoped; importing them into local canonical state
|
|
718
|
+
// causes unrelated global prompts to appear in fresh repositories.
|
|
719
|
+
if (provider === "codex" && paths.scope === "local") {
|
|
720
|
+
return [];
|
|
721
|
+
}
|
|
722
|
+
const candidateDirs = [
|
|
723
|
+
{
|
|
724
|
+
path: getProviderCommandsDir(paths, provider),
|
|
725
|
+
sourcePriority: 0,
|
|
726
|
+
},
|
|
727
|
+
];
|
|
728
|
+
if (provider === "copilot" && paths.scope === "global") {
|
|
729
|
+
candidateDirs.push({
|
|
730
|
+
path: path.join(paths.homeDir, ".github", "prompts"),
|
|
731
|
+
sourcePriority: 1,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
const records = [];
|
|
735
|
+
for (const candidateDir of candidateDirs) {
|
|
736
|
+
records.push(...readProviderCommandsFromDir(candidateDir.path, provider, candidateDir.sourcePriority));
|
|
737
|
+
}
|
|
738
|
+
return dedupeProviderCommandRecords(records);
|
|
739
|
+
}
|
|
740
|
+
function readProviderCommandsFromDir(commandsDir, provider, sourcePriority) {
|
|
424
741
|
if (!fs.existsSync(commandsDir) || !fs.statSync(commandsDir).isDirectory()) {
|
|
425
742
|
return [];
|
|
426
743
|
}
|
|
427
|
-
const
|
|
428
|
-
|
|
744
|
+
const markdownFiles = parseCommandsDir(commandsDir).filter((file) => isProviderEntityFileName({
|
|
745
|
+
provider,
|
|
746
|
+
entity: "command",
|
|
747
|
+
fileName: file.fileName,
|
|
748
|
+
}));
|
|
749
|
+
const files = provider === "gemini"
|
|
750
|
+
? [...parseGeminiTomlCommandsForMigration(commandsDir), ...markdownFiles]
|
|
751
|
+
: markdownFiles;
|
|
752
|
+
const dedupedByTargetFile = new Map();
|
|
753
|
+
for (const file of files) {
|
|
754
|
+
const targetFileName = toCanonicalCommandFileName(file.fileName);
|
|
755
|
+
if (!dedupedByTargetFile.has(targetFileName)) {
|
|
756
|
+
dedupedByTargetFile.set(targetFileName, file);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return [...dedupedByTargetFile.entries()].map(([targetFileName, file]) => ({
|
|
429
760
|
provider,
|
|
430
761
|
sourcePath: file.sourcePath,
|
|
431
|
-
|
|
762
|
+
sourcePriority,
|
|
763
|
+
targetFileName,
|
|
432
764
|
content: file.content,
|
|
765
|
+
body: provider === "gemini"
|
|
766
|
+
? normalizeCommandArgumentsForCanonical(file.body, provider)
|
|
767
|
+
: file.body,
|
|
768
|
+
frontmatter: file.frontmatter,
|
|
433
769
|
}));
|
|
434
770
|
}
|
|
771
|
+
function dedupeProviderCommandRecords(records) {
|
|
772
|
+
const recordsByTarget = new Map();
|
|
773
|
+
for (const record of records) {
|
|
774
|
+
const next = recordsByTarget.get(record.targetFileName) ?? [];
|
|
775
|
+
next.push(record);
|
|
776
|
+
recordsByTarget.set(record.targetFileName, next);
|
|
777
|
+
}
|
|
778
|
+
const deduped = [];
|
|
779
|
+
for (const [targetFileName, bucket] of recordsByTarget.entries()) {
|
|
780
|
+
if (bucket.length === 1) {
|
|
781
|
+
deduped.push(bucket[0]);
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const ranked = [...bucket].sort(compareProviderCommandCollisionPriority);
|
|
785
|
+
const winner = ranked[0];
|
|
786
|
+
const ignored = ranked.slice(1).map((record) => record.sourcePath);
|
|
787
|
+
console.warn(`Warning: Duplicate ${winner.provider} commands map to "${targetFileName}". Keeping ${winner.sourcePath} and ignoring ${ignored.join(", ")}.`);
|
|
788
|
+
deduped.push(winner);
|
|
789
|
+
}
|
|
790
|
+
return deduped.sort((left, right) => left.targetFileName.localeCompare(right.targetFileName));
|
|
791
|
+
}
|
|
792
|
+
function compareProviderCommandCollisionPriority(left, right) {
|
|
793
|
+
if (left.sourcePriority !== right.sourcePriority) {
|
|
794
|
+
return left.sourcePriority - right.sourcePriority;
|
|
795
|
+
}
|
|
796
|
+
return left.sourcePath.localeCompare(right.sourcePath);
|
|
797
|
+
}
|
|
798
|
+
function parseGeminiTomlCommandsForMigration(commandsDir) {
|
|
799
|
+
return fs
|
|
800
|
+
.readdirSync(commandsDir, { withFileTypes: true })
|
|
801
|
+
.filter((entry) => entry.isFile())
|
|
802
|
+
.map((entry) => entry.name)
|
|
803
|
+
.filter((fileName) => isProviderEntityFileName({
|
|
804
|
+
provider: "gemini",
|
|
805
|
+
entity: "command",
|
|
806
|
+
fileName,
|
|
807
|
+
}))
|
|
808
|
+
.filter((fileName) => fileName.toLowerCase().endsWith(".toml"))
|
|
809
|
+
.sort((a, b) => a.localeCompare(b))
|
|
810
|
+
.map((fileName) => parseGeminiTomlCommandForMigration(path.join(commandsDir, fileName)))
|
|
811
|
+
.filter((command) => command !== null);
|
|
812
|
+
}
|
|
813
|
+
function parseGeminiTomlCommandForMigration(sourcePath) {
|
|
814
|
+
const raw = fs.readFileSync(sourcePath, "utf8");
|
|
815
|
+
let parsed;
|
|
816
|
+
try {
|
|
817
|
+
parsed = TOML.parse(raw);
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
if (!isObject(parsed) || typeof parsed.prompt !== "string") {
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
const body = normalizeCommandArgumentsForCanonical(parsed.prompt, "gemini");
|
|
826
|
+
const frontmatter = cloneRecord(parsed);
|
|
827
|
+
delete frontmatter.prompt;
|
|
828
|
+
const normalizedFrontmatter = Object.keys(frontmatter).length > 0 ? frontmatter : undefined;
|
|
829
|
+
const fileName = path.basename(sourcePath);
|
|
830
|
+
const content = buildCommandMarkdown(normalizedFrontmatter, body);
|
|
831
|
+
return {
|
|
832
|
+
fileName,
|
|
833
|
+
sourcePath,
|
|
834
|
+
content,
|
|
835
|
+
body,
|
|
836
|
+
frontmatter: normalizedFrontmatter,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
435
839
|
function toCanonicalCommandFileName(fileName) {
|
|
436
840
|
const lower = fileName.toLowerCase();
|
|
437
841
|
if (lower.endsWith(".prompt.md")) {
|
|
438
842
|
return `${fileName.slice(0, -".prompt.md".length)}.md`;
|
|
439
843
|
}
|
|
844
|
+
if (lower.endsWith(".toml")) {
|
|
845
|
+
return `${fileName.slice(0, -".toml".length)}.md`;
|
|
846
|
+
}
|
|
440
847
|
if (lower.endsWith(".mdc")) {
|
|
441
848
|
return `${fileName.slice(0, -".mdc".length)}.md`;
|
|
442
849
|
}
|
|
@@ -644,8 +1051,185 @@ function isCanonicalServerEqual(left, right) {
|
|
|
644
1051
|
return false;
|
|
645
1052
|
return isDeepStrictEqual(normalizeCanonicalServer(left), normalizeCanonicalServer(right));
|
|
646
1053
|
}
|
|
1054
|
+
async function migrateRules(options, summary) {
|
|
1055
|
+
const detectedEntries = [
|
|
1056
|
+
...getCursorRuleMigrationEntries(options),
|
|
1057
|
+
...getManagedInstructionRuleMigrationEntries(options),
|
|
1058
|
+
];
|
|
1059
|
+
const entries = dedupeRuleMigrationEntries(detectedEntries);
|
|
1060
|
+
summary.detected += detectedEntries.length;
|
|
1061
|
+
for (const entry of entries) {
|
|
1062
|
+
const targetPath = path.join(options.paths.rulesDir, `${entry.targetStem}.md`);
|
|
1063
|
+
if (!fs.existsSync(targetPath)) {
|
|
1064
|
+
if (shouldWriteCanonical(options)) {
|
|
1065
|
+
ensureDir(options.paths.rulesDir);
|
|
1066
|
+
fs.writeFileSync(targetPath, entry.canonicalContent, "utf8");
|
|
1067
|
+
}
|
|
1068
|
+
summary.imported += 1;
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
const existing = fs.readFileSync(targetPath, "utf8");
|
|
1072
|
+
if (existing === entry.canonicalContent) {
|
|
1073
|
+
summary.skipped += 1;
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
summary.conflicts += 1;
|
|
1077
|
+
const decision = await resolveCanonicalConflict({
|
|
1078
|
+
conflictLabel: entry.conflictLabel,
|
|
1079
|
+
yes: Boolean(options.yes),
|
|
1080
|
+
nonInteractive: Boolean(options.nonInteractive),
|
|
1081
|
+
});
|
|
1082
|
+
if (decision === "canonical") {
|
|
1083
|
+
summary.skipped += 1;
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
if (shouldWriteCanonical(options)) {
|
|
1087
|
+
ensureDir(options.paths.rulesDir);
|
|
1088
|
+
fs.writeFileSync(targetPath, entry.canonicalContent, "utf8");
|
|
1089
|
+
}
|
|
1090
|
+
summary.imported += 1;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
function dedupeRuleMigrationEntries(entries) {
|
|
1094
|
+
const deduped = [];
|
|
1095
|
+
for (const entry of entries) {
|
|
1096
|
+
const existingIndex = deduped.findIndex((item) => item.targetStem === entry.targetStem);
|
|
1097
|
+
if (existingIndex < 0) {
|
|
1098
|
+
deduped.push(entry);
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
const existing = deduped[existingIndex];
|
|
1102
|
+
if (existing.canonicalContent === entry.canonicalContent) {
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
const preferred = choosePreferredRuleMigrationEntry(existing, entry);
|
|
1106
|
+
if (preferred === existing) {
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
if (preferred === entry) {
|
|
1110
|
+
deduped[existingIndex] = entry;
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
deduped.push(entry);
|
|
1114
|
+
}
|
|
1115
|
+
return deduped;
|
|
1116
|
+
}
|
|
1117
|
+
function choosePreferredRuleMigrationEntry(left, right) {
|
|
1118
|
+
const hasCursor = left.sourceKind === "cursor" || right.sourceKind === "cursor";
|
|
1119
|
+
const hasManaged = left.sourceKind === "managed" || right.sourceKind === "managed";
|
|
1120
|
+
if (!hasCursor || !hasManaged) {
|
|
1121
|
+
return undefined;
|
|
1122
|
+
}
|
|
1123
|
+
try {
|
|
1124
|
+
const leftParsed = parseRuleMarkdown(left.canonicalContent, left.conflictLabel);
|
|
1125
|
+
const rightParsed = parseRuleMarkdown(right.canonicalContent, right.conflictLabel);
|
|
1126
|
+
if (leftParsed.name !== rightParsed.name) {
|
|
1127
|
+
return undefined;
|
|
1128
|
+
}
|
|
1129
|
+
if (!sameNormalizedBody(leftParsed.body, rightParsed.body)) {
|
|
1130
|
+
return undefined;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
catch {
|
|
1134
|
+
return undefined;
|
|
1135
|
+
}
|
|
1136
|
+
return left.sourceKind === "cursor" ? left : right;
|
|
1137
|
+
}
|
|
1138
|
+
function getCursorRuleMigrationEntries(options) {
|
|
1139
|
+
if (!options.providers.includes("cursor")) {
|
|
1140
|
+
return [];
|
|
1141
|
+
}
|
|
1142
|
+
if (options.paths.scope !== "local") {
|
|
1143
|
+
return [];
|
|
1144
|
+
}
|
|
1145
|
+
const cursorRulesDir = getCursorRulesDir(options.paths);
|
|
1146
|
+
if (!fs.existsSync(cursorRulesDir) ||
|
|
1147
|
+
!fs.statSync(cursorRulesDir).isDirectory()) {
|
|
1148
|
+
return [];
|
|
1149
|
+
}
|
|
1150
|
+
return fs
|
|
1151
|
+
.readdirSync(cursorRulesDir, { withFileTypes: true })
|
|
1152
|
+
.filter((entry) => entry.isFile())
|
|
1153
|
+
.map((entry) => entry.name)
|
|
1154
|
+
.filter((name) => /\.(md|mdc)$/i.test(name))
|
|
1155
|
+
.filter((name) => stripRuleFileExtension(name).toLowerCase() !== "readme")
|
|
1156
|
+
.sort((a, b) => a.localeCompare(b))
|
|
1157
|
+
.map((fileName) => {
|
|
1158
|
+
const sourcePath = path.join(cursorRulesDir, fileName);
|
|
1159
|
+
const raw = fs.readFileSync(sourcePath, "utf8");
|
|
1160
|
+
const targetStem = slugify(stripRuleFileExtension(fileName)) || "rule";
|
|
1161
|
+
return {
|
|
1162
|
+
canonicalContent: toCanonicalRuleMarkdown(raw, fileName, sourcePath),
|
|
1163
|
+
conflictLabel: `rule "${targetStem}" from cursor`,
|
|
1164
|
+
sourceKind: "cursor",
|
|
1165
|
+
targetStem,
|
|
1166
|
+
};
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
function getManagedInstructionRuleMigrationEntries(options) {
|
|
1170
|
+
const instructionPaths = getManagedInstructionRulePaths(options).sort((left, right) => left.localeCompare(right));
|
|
1171
|
+
const entries = [];
|
|
1172
|
+
for (const instructionPath of instructionPaths) {
|
|
1173
|
+
if (!fs.existsSync(instructionPath) ||
|
|
1174
|
+
!fs.statSync(instructionPath).isFile()) {
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
const raw = fs.readFileSync(instructionPath, "utf8");
|
|
1178
|
+
for (const block of parseManagedRuleBlocks(raw)) {
|
|
1179
|
+
const targetStem = slugify(block.id) || "rule";
|
|
1180
|
+
entries.push({
|
|
1181
|
+
canonicalContent: toCanonicalRuleMarkdownFromManagedBlock(block),
|
|
1182
|
+
conflictLabel: `rule "${targetStem}" from ${path.basename(instructionPath)}`,
|
|
1183
|
+
sourceKind: "managed",
|
|
1184
|
+
targetStem,
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return entries;
|
|
1189
|
+
}
|
|
1190
|
+
function getManagedInstructionRulePaths(options) {
|
|
1191
|
+
const instructionPaths = getRuleInstructionPaths(options.paths, options.providers);
|
|
1192
|
+
if (options.paths.scope === "global" &&
|
|
1193
|
+
options.providers.includes("copilot")) {
|
|
1194
|
+
const legacyCopilotInstructionPath = path.join(options.paths.homeDir, ".github", "copilot-instructions.md");
|
|
1195
|
+
if (fs.existsSync(legacyCopilotInstructionPath) &&
|
|
1196
|
+
fs.statSync(legacyCopilotInstructionPath).isFile()) {
|
|
1197
|
+
instructionPaths.push(legacyCopilotInstructionPath);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return [...new Set(instructionPaths)];
|
|
1201
|
+
}
|
|
1202
|
+
function toCanonicalRuleMarkdown(raw, fileName, sourcePath) {
|
|
1203
|
+
try {
|
|
1204
|
+
const parsed = parseRuleMarkdown(raw, sourcePath);
|
|
1205
|
+
const fm = YAML.stringify(parsed.frontmatter, { lineWidth: 0 }).trimEnd();
|
|
1206
|
+
return `---\n${fm}\n---\n\n${parsed.body}${parsed.body.endsWith("\n") ? "" : "\n"}`;
|
|
1207
|
+
}
|
|
1208
|
+
catch {
|
|
1209
|
+
const parsed = matter(raw);
|
|
1210
|
+
const data = isObject(parsed.data)
|
|
1211
|
+
? cloneRecord(parsed.data)
|
|
1212
|
+
: {};
|
|
1213
|
+
if (typeof data.name !== "string" || data.name.trim() === "") {
|
|
1214
|
+
data.name = stripRuleFileExtension(fileName);
|
|
1215
|
+
}
|
|
1216
|
+
const fm = YAML.stringify(data, { lineWidth: 0 }).trimEnd();
|
|
1217
|
+
const body = parsed.content.trimStart();
|
|
1218
|
+
return `---\n${fm}\n---\n\n${body}${body.endsWith("\n") ? "" : "\n"}`;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
function toCanonicalRuleMarkdownFromManagedBlock(block) {
|
|
1222
|
+
const fm = YAML.stringify({ name: block.name }, { lineWidth: 0 }).trimEnd();
|
|
1223
|
+
const body = block.body.trim();
|
|
1224
|
+
return `---\n${fm}\n---\n\n${body}${body.endsWith("\n") ? "" : "\n"}`;
|
|
1225
|
+
}
|
|
647
1226
|
async function migrateSkills(options, summary) {
|
|
648
|
-
const providerSkillDirs = getProviderSkillsPaths(options.paths, options.providers)
|
|
1227
|
+
const providerSkillDirs = getProviderSkillsPaths(options.paths, options.providers);
|
|
1228
|
+
if (options.providers.includes("copilot")) {
|
|
1229
|
+
providerSkillDirs.push(...getLegacyCopilotSkillDirs(options.paths));
|
|
1230
|
+
}
|
|
1231
|
+
const legacyCopilotSkillDirs = new Set(getLegacyCopilotSkillDirs(options.paths));
|
|
1232
|
+
const existingProviderSkillDirs = [...new Set(providerSkillDirs)]
|
|
649
1233
|
.filter((dirPath) => fs.existsSync(dirPath))
|
|
650
1234
|
.filter((dirPath) => {
|
|
651
1235
|
try {
|
|
@@ -655,12 +1239,17 @@ async function migrateSkills(options, summary) {
|
|
|
655
1239
|
return false;
|
|
656
1240
|
}
|
|
657
1241
|
});
|
|
658
|
-
for (const providerSkillsDir of
|
|
659
|
-
const providerLabel =
|
|
660
|
-
? "
|
|
661
|
-
: providerSkillsDir.includes(`${path.sep}.
|
|
662
|
-
? "
|
|
663
|
-
:
|
|
1242
|
+
for (const providerSkillsDir of existingProviderSkillDirs) {
|
|
1243
|
+
const providerLabel = legacyCopilotSkillDirs.has(providerSkillsDir)
|
|
1244
|
+
? "copilot"
|
|
1245
|
+
: providerSkillsDir.includes(`${path.sep}.cursor${path.sep}`)
|
|
1246
|
+
? "cursor"
|
|
1247
|
+
: providerSkillsDir.includes(`${path.sep}.github${path.sep}`) ||
|
|
1248
|
+
providerSkillsDir.includes(`${path.sep}.copilot${path.sep}`)
|
|
1249
|
+
? "copilot"
|
|
1250
|
+
: providerSkillsDir.includes(`${path.sep}.pi${path.sep}`)
|
|
1251
|
+
? "pi"
|
|
1252
|
+
: "claude";
|
|
664
1253
|
const skills = parseSkillsDir(providerSkillsDir);
|
|
665
1254
|
summary.detected += skills.length;
|
|
666
1255
|
for (const skill of skills) {
|