agentloom 0.1.5 → 0.1.7
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/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 -12
- package/dist/core/importer.d.ts +10 -0
- package/dist/core/importer.js +941 -46
- package/dist/core/lockfile.js +8 -0
- package/dist/core/manifest.js +1 -1
- package/dist/core/migration.js +650 -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 +17 -3
- 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 +4 -0
- package/dist/core/skills.js +47 -12
- package/dist/core/sources.d.ts +7 -0
- package/dist/core/sources.js +146 -22
- package/dist/core/telemetry.d.ts +1 -1
- package/dist/core/telemetry.js +16 -0
- package/dist/sync/index.js +403 -17
- package/dist/types.d.ts +5 -1
- package/package.json +1 -1
package/dist/core/importer.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { cancel, isCancel, multiselect, select, text as promptText, } from "@clack/prompts";
|
|
4
|
+
import TOML from "@iarna/toml";
|
|
5
|
+
import matter from "gray-matter";
|
|
6
|
+
import YAML from "yaml";
|
|
4
7
|
import { buildAgentMarkdown, parseAgentsDir, targetFileNameForAgent, } from "./agents.js";
|
|
5
|
-
import { normalizeCommandSelector, parseCommandsDir, resolveCommandSelections, } from "./commands.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
+
import { normalizeCommandArgumentsForCanonical, normalizeCommandSelector, parseCommandsDir, resolveCommandSelections, } from "./commands.js";
|
|
9
|
+
import { normalizeRuleSelector, parseRulesDir, resolveRuleSelections, stripRuleFileExtension, } from "./rules.js";
|
|
10
|
+
import { applySkillProviderSideEffects, copySkillArtifacts, normalizeSkillSelector, parseSkillsDir, resolveSkillSelector, resolveSkillSelections, skillContentMatchesTarget, } from "./skills.js";
|
|
11
|
+
import { ensureDir, hashContent, isObject, readJsonIfExists, relativePosix, slugify, writeTextAtomic, } from "./fs.js";
|
|
8
12
|
import { ALL_PROVIDERS } from "../types.js";
|
|
9
13
|
import { readLockfile, upsertLockEntry, writeLockfile } from "./lockfile.js";
|
|
10
14
|
import { readCanonicalMcp, writeCanonicalMcp } from "./mcp.js";
|
|
11
|
-
import {
|
|
15
|
+
import { discoverPluginSourceRoots, discoverSourceAgentsDirs, discoverSourceCommandsDirs, discoverSourceMcpPaths, discoverSourceRulesDirs, discoverSourceSkillsDirs, prepareSource, } from "./sources.js";
|
|
16
|
+
import { isProviderEntityFileName } from "./provider-entity-validation.js";
|
|
12
17
|
export class NonInteractiveConflictError extends Error {
|
|
13
18
|
constructor(message) {
|
|
14
19
|
super(message);
|
|
@@ -24,10 +29,13 @@ export async function importSource(options) {
|
|
|
24
29
|
const requireAgents = options.requireAgents ?? shouldImportAgents;
|
|
25
30
|
const shouldImportCommands = options.importCommands ?? true;
|
|
26
31
|
const shouldImportMcp = options.importMcp ?? true;
|
|
32
|
+
const shouldImportRules = options.importRules ?? false;
|
|
33
|
+
const requireRules = options.requireRules ?? shouldImportRules;
|
|
27
34
|
const shouldImportSkills = options.importSkills ?? false;
|
|
28
35
|
if (!shouldImportAgents &&
|
|
29
36
|
!shouldImportCommands &&
|
|
30
37
|
!shouldImportMcp &&
|
|
38
|
+
!shouldImportRules &&
|
|
31
39
|
!shouldImportSkills) {
|
|
32
40
|
throw new Error("No import targets selected.");
|
|
33
41
|
}
|
|
@@ -43,50 +51,77 @@ export async function importSource(options) {
|
|
|
43
51
|
? `${options.source} (subdir: ${options.subdir})`
|
|
44
52
|
: options.source;
|
|
45
53
|
try {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const sourceSkillsDir = shouldImportSkills
|
|
56
|
-
? discoverSourceSkillsDir(prepared.importRoot)
|
|
57
|
-
: null;
|
|
58
|
-
const sourceAgents = sourceAgentsDir ? parseAgentsDir(sourceAgentsDir) : [];
|
|
59
|
-
const sourceCommands = sourceCommandsDir
|
|
60
|
-
? parseCommandsDir(sourceCommandsDir)
|
|
54
|
+
const pluginSourceRoots = discoverPluginSourceRoots(prepared.importRoot);
|
|
55
|
+
const sourceAgentsDirs = shouldImportAgents
|
|
56
|
+
? discoverSourceAgentsDirs(prepared.importRoot)
|
|
57
|
+
: [];
|
|
58
|
+
const sourceCommandsDirs = shouldImportCommands
|
|
59
|
+
? discoverSourceCommandsDirs(prepared.importRoot)
|
|
60
|
+
: [];
|
|
61
|
+
const sourceMcpPaths = shouldImportMcp
|
|
62
|
+
? discoverSourceMcpPaths(prepared.importRoot)
|
|
61
63
|
: [];
|
|
62
|
-
const
|
|
63
|
-
?
|
|
64
|
+
const sourceRulesDirs = shouldImportRules
|
|
65
|
+
? discoverSourceRulesDirs(prepared.importRoot)
|
|
66
|
+
: [];
|
|
67
|
+
const sourceSkillsDirs = shouldImportSkills
|
|
68
|
+
? discoverSourceSkillsDirs(prepared.importRoot)
|
|
69
|
+
: [];
|
|
70
|
+
const sourceAgents = sourceAgentsDirs.length > 0
|
|
71
|
+
? parseSourceAgentsForImport(sourceAgentsDirs, pluginSourceRoots)
|
|
72
|
+
: [];
|
|
73
|
+
const sourceCommands = sourceCommandsDirs.length > 0
|
|
74
|
+
? parseSourceCommandsForImport(sourceCommandsDirs, pluginSourceRoots)
|
|
75
|
+
: [];
|
|
76
|
+
const sourceMcp = sourceMcpPaths.length > 0
|
|
77
|
+
? parseSourceMcpForImport(sourceMcpPaths, pluginSourceRoots)
|
|
64
78
|
: null;
|
|
65
|
-
const
|
|
79
|
+
const sourceRules = sourceRulesDirs.length > 0
|
|
80
|
+
? parseSourceRulesForImport(sourceRulesDirs, pluginSourceRoots)
|
|
81
|
+
: [];
|
|
82
|
+
const sourceSkills = sourceSkillsDirs.length > 0
|
|
83
|
+
? parseSourceSkillsForImport(sourceSkillsDirs, pluginSourceRoots)
|
|
84
|
+
: [];
|
|
85
|
+
const sourceAgentsDir = sourceAgentsDirs[0] ?? null;
|
|
86
|
+
const sourceCommandsDir = sourceCommandsDirs[0] ?? null;
|
|
87
|
+
const sourceMcpPath = sourceMcpPaths[0] ?? null;
|
|
88
|
+
const sourceRulesDir = sourceRulesDirs[0] ?? null;
|
|
89
|
+
const sourceSkillsDir = sourceSkillsDirs[0] ?? null;
|
|
66
90
|
const hasExplicitCommandSelection = (options.commandSelectors?.length ?? 0) > 0;
|
|
67
91
|
const isAggregateImport = shouldImportAgents &&
|
|
68
92
|
shouldImportCommands &&
|
|
69
93
|
shouldImportMcp &&
|
|
94
|
+
(options.importRules === undefined || shouldImportRules) &&
|
|
70
95
|
shouldImportSkills;
|
|
71
|
-
if (shouldImportAgents && requireAgents &&
|
|
72
|
-
throw new Error(`No source agents directory found under ${prepared.importRoot} (expected agents/ or .
|
|
96
|
+
if (shouldImportAgents && requireAgents && sourceAgentsDirs.length === 0) {
|
|
97
|
+
throw new Error(`No source agents directory found under ${prepared.importRoot} (expected agents/, .agents/agents/, or .github/agents/, including plugin sources declared in .claude-plugin/marketplace.json).`);
|
|
73
98
|
}
|
|
74
99
|
if (shouldImportAgents && requireAgents && sourceAgents.length === 0) {
|
|
75
100
|
throw new Error(`No agent files found in ${sourceAgentsDir}.`);
|
|
76
101
|
}
|
|
77
|
-
if (shouldImportCommands &&
|
|
78
|
-
|
|
102
|
+
if (shouldImportCommands &&
|
|
103
|
+
options.requireCommands &&
|
|
104
|
+
sourceCommandsDirs.length === 0) {
|
|
105
|
+
throw new Error(`No source commands directory found under ${prepared.importRoot} (expected .agents/commands/, commands/, prompts/, .gemini/commands/, or .github/prompts/, including plugin sources declared in .claude-plugin/marketplace.json).`);
|
|
79
106
|
}
|
|
80
107
|
if (shouldImportCommands &&
|
|
81
108
|
options.requireCommands &&
|
|
82
109
|
sourceCommands.length === 0) {
|
|
83
110
|
throw new Error(`No command files found in ${sourceCommandsDir}.`);
|
|
84
111
|
}
|
|
85
|
-
if (shouldImportMcp && options.requireMcp &&
|
|
86
|
-
throw new Error(`No source mcp.json found under ${prepared.importRoot} (expected mcp.json or .agents/mcp.json).`);
|
|
112
|
+
if (shouldImportMcp && options.requireMcp && sourceMcpPaths.length === 0) {
|
|
113
|
+
throw new Error(`No source mcp.json found under ${prepared.importRoot} (expected mcp.json or .agents/mcp.json, including plugin sources declared in .claude-plugin/marketplace.json).`);
|
|
114
|
+
}
|
|
115
|
+
if (shouldImportRules && requireRules && sourceRulesDirs.length === 0) {
|
|
116
|
+
throw new Error(`No source rules directory found under ${prepared.importRoot} (expected .agents/rules/ or rules/, including plugin sources declared in .claude-plugin/marketplace.json).`);
|
|
117
|
+
}
|
|
118
|
+
if (shouldImportRules && requireRules && sourceRules.length === 0) {
|
|
119
|
+
throw new Error(`No rule files found in ${sourceRulesDir}.`);
|
|
87
120
|
}
|
|
88
|
-
if (shouldImportSkills &&
|
|
89
|
-
|
|
121
|
+
if (shouldImportSkills &&
|
|
122
|
+
options.requireSkills &&
|
|
123
|
+
sourceSkillsDirs.length === 0) {
|
|
124
|
+
throw new Error(`No source skills directory found under ${prepared.importRoot} (expected .agents/skills/, skills/, or root SKILL.md, including plugin sources declared in .claude-plugin/marketplace.json).`);
|
|
90
125
|
}
|
|
91
126
|
if (shouldImportSkills &&
|
|
92
127
|
options.requireSkills &&
|
|
@@ -96,9 +131,10 @@ export async function importSource(options) {
|
|
|
96
131
|
if (isAggregateImport &&
|
|
97
132
|
sourceAgents.length === 0 &&
|
|
98
133
|
sourceCommands.length === 0 &&
|
|
134
|
+
sourceRules.length === 0 &&
|
|
99
135
|
sourceSkills.length === 0 &&
|
|
100
136
|
Object.keys(sourceMcp?.mcpServers ?? {}).length === 0) {
|
|
101
|
-
throw new Error(`No importable entities found in source "${sourceLocation}".\nExpected agents/, .agents/agents/, commands/, .agents/commands/, prompts/, mcp.json/.agents/mcp.json, skills/, .agents/skills/,
|
|
137
|
+
throw new Error(`No importable entities found in source "${sourceLocation}".\nExpected agents/, .agents/agents/, .github/agents/, commands/, .agents/commands/, prompts/, .gemini/commands/, .github/prompts/, mcp.json/.agents/mcp.json, rules/.agents/rules/, skills/, .agents/skills/, root SKILL.md, or plugin sources from .claude-plugin/marketplace.json.`);
|
|
102
138
|
}
|
|
103
139
|
const shouldResolveAgents = shouldImportAgents &&
|
|
104
140
|
(sourceAgents.length > 0 ||
|
|
@@ -145,6 +181,21 @@ export async function importSource(options) {
|
|
|
145
181
|
selectedSourceMcpServers = mcpSelection.selectedServerNames;
|
|
146
182
|
mcpSelectionMode = mcpSelection.selectionMode;
|
|
147
183
|
}
|
|
184
|
+
let selectedRules = [];
|
|
185
|
+
let selectedSourceRules = [];
|
|
186
|
+
let ruleSelectionMode = "all";
|
|
187
|
+
if (shouldImportRules && sourceRulesDir) {
|
|
188
|
+
const ruleSelection = await resolveRulesToImport({
|
|
189
|
+
sourceRules,
|
|
190
|
+
selectors: options.ruleSelectors ?? [],
|
|
191
|
+
promptForRules: options.promptForRules ?? true,
|
|
192
|
+
nonInteractive: Boolean(options.nonInteractive),
|
|
193
|
+
selectionMode: options.selectionMode,
|
|
194
|
+
});
|
|
195
|
+
selectedRules = ruleSelection.selectedRules;
|
|
196
|
+
ruleSelectionMode = ruleSelection.selectionMode;
|
|
197
|
+
selectedSourceRules = selectedRules.map((rule) => rule.fileName);
|
|
198
|
+
}
|
|
148
199
|
let selectedSkills = [];
|
|
149
200
|
let selectedSourceSkills = [];
|
|
150
201
|
let skillSelectionMode = "all";
|
|
@@ -157,8 +208,8 @@ export async function importSource(options) {
|
|
|
157
208
|
selectionMode: options.selectionMode,
|
|
158
209
|
});
|
|
159
210
|
selectedSkills = skillSelection.selectedSkills;
|
|
211
|
+
selectedSourceSkills = skillSelection.selectedSourceSkills;
|
|
160
212
|
skillSelectionMode = skillSelection.selectionMode;
|
|
161
|
-
selectedSourceSkills = selectedSkills.map((skill) => skill.name);
|
|
162
213
|
}
|
|
163
214
|
const importedAgents = [];
|
|
164
215
|
if (shouldImportAgents && selection.selectedAgents.length > 0) {
|
|
@@ -240,6 +291,47 @@ export async function importSource(options) {
|
|
|
240
291
|
}
|
|
241
292
|
importedMcpServers.push(...selectedSourceMcpServers);
|
|
242
293
|
}
|
|
294
|
+
const importedRules = [];
|
|
295
|
+
const telemetryRules = [];
|
|
296
|
+
const importedRuleRenameMap = {};
|
|
297
|
+
if (shouldImportRules && sourceRulesDir) {
|
|
298
|
+
if (selectedRules.length > 0) {
|
|
299
|
+
ensureDir(options.paths.rulesDir);
|
|
300
|
+
}
|
|
301
|
+
for (const [index, rule] of selectedRules.entries()) {
|
|
302
|
+
let targetFileName = rule.fileName;
|
|
303
|
+
const mappedTargetFileName = resolveMappedTargetRuleFileName(rule.fileName, options.ruleRenameMap);
|
|
304
|
+
if (mappedTargetFileName) {
|
|
305
|
+
targetFileName = mappedTargetFileName;
|
|
306
|
+
}
|
|
307
|
+
else if (options.rename &&
|
|
308
|
+
selectedRules.length === 1 &&
|
|
309
|
+
importedAgents.length === 0 &&
|
|
310
|
+
importedCommands.length === 0 &&
|
|
311
|
+
importedMcpServers.length === 0 &&
|
|
312
|
+
selectedSkills.length === 0) {
|
|
313
|
+
targetFileName = `${slugify(options.rename) || "rule"}.md`;
|
|
314
|
+
}
|
|
315
|
+
const resolvedFileName = await resolveRuleConflict({
|
|
316
|
+
targetFileName,
|
|
317
|
+
ruleContent: rule.content,
|
|
318
|
+
paths: options.paths,
|
|
319
|
+
yes: !!options.yes,
|
|
320
|
+
nonInteractive: !!options.nonInteractive,
|
|
321
|
+
promptLabel: `${rule.fileName} (${index + 1}/${selectedRules.length})`,
|
|
322
|
+
});
|
|
323
|
+
if (!resolvedFileName)
|
|
324
|
+
continue;
|
|
325
|
+
const targetPath = path.join(options.paths.rulesDir, resolvedFileName);
|
|
326
|
+
writeTextAtomic(targetPath, rule.content);
|
|
327
|
+
importedRules.push(relativePosix(options.paths.agentsRoot, targetPath));
|
|
328
|
+
telemetryRules.push({
|
|
329
|
+
name: stripRuleFileExtension(rule.fileName),
|
|
330
|
+
filePath: relativePosix(prepared.rootPath, rule.sourcePath),
|
|
331
|
+
});
|
|
332
|
+
importedRuleRenameMap[rule.fileName] = resolvedFileName;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
243
335
|
const importedSkills = [];
|
|
244
336
|
const telemetrySkills = [];
|
|
245
337
|
const importedSkillRenameMap = {};
|
|
@@ -249,21 +341,32 @@ export async function importSource(options) {
|
|
|
249
341
|
ensureDir(options.paths.skillsDir);
|
|
250
342
|
}
|
|
251
343
|
for (const [index, sourceSkill] of selectedSkills.entries()) {
|
|
252
|
-
|
|
253
|
-
const
|
|
344
|
+
const canonicalSkillDirName = slugify(sourceSkill.name) || "skill";
|
|
345
|
+
const legacySkillDirName = slugify(sourceSkill.sourceDirName) || "skill";
|
|
346
|
+
let targetSkillDirName = canonicalSkillDirName;
|
|
347
|
+
const mappedTargetSkillDirName = resolveMappedTargetSkillName(sourceSkill, selectedSkills, options.skillRenameMap);
|
|
254
348
|
if (mappedTargetSkillDirName) {
|
|
255
|
-
targetSkillDirName =
|
|
349
|
+
targetSkillDirName =
|
|
350
|
+
mappedTargetSkillDirName === legacySkillDirName &&
|
|
351
|
+
legacySkillDirName !== canonicalSkillDirName
|
|
352
|
+
? canonicalSkillDirName
|
|
353
|
+
: mappedTargetSkillDirName;
|
|
256
354
|
}
|
|
257
355
|
else if (options.rename &&
|
|
258
356
|
selectedSkills.length === 1 &&
|
|
259
357
|
importedAgents.length === 0 &&
|
|
260
358
|
importedCommands.length === 0 &&
|
|
261
|
-
importedMcpServers.length === 0
|
|
359
|
+
importedMcpServers.length === 0 &&
|
|
360
|
+
importedRules.length === 0) {
|
|
262
361
|
targetSkillDirName = slugify(options.rename) || "skill";
|
|
263
362
|
}
|
|
264
363
|
const resolvedSkillDirName = await resolveSkillConflict({
|
|
265
364
|
sourceSkill,
|
|
266
365
|
targetSkillDirName,
|
|
366
|
+
legacySkillDirName: targetSkillDirName === canonicalSkillDirName
|
|
367
|
+
? legacySkillDirName
|
|
368
|
+
: undefined,
|
|
369
|
+
canonicalSkillDirName,
|
|
267
370
|
paths: options.paths,
|
|
268
371
|
yes: !!options.yes,
|
|
269
372
|
nonInteractive: !!options.nonInteractive,
|
|
@@ -272,10 +375,25 @@ export async function importSource(options) {
|
|
|
272
375
|
if (!resolvedSkillDirName)
|
|
273
376
|
continue;
|
|
274
377
|
const targetSkillDir = path.join(options.paths.skillsDir, resolvedSkillDirName);
|
|
378
|
+
if (resolvedSkillDirName === canonicalSkillDirName) {
|
|
379
|
+
moveLegacySkillDirectoryToCanonicalIfUnchanged({
|
|
380
|
+
sourceSkill,
|
|
381
|
+
legacySkillDirName,
|
|
382
|
+
canonicalSkillDirName,
|
|
383
|
+
paths: options.paths,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
275
386
|
if (!skillContentMatchesTarget(sourceSkill, targetSkillDir)) {
|
|
276
387
|
fs.rmSync(targetSkillDir, { recursive: true, force: true });
|
|
277
388
|
copySkillArtifacts(sourceSkill, targetSkillDir);
|
|
278
389
|
}
|
|
390
|
+
if (resolvedSkillDirName === canonicalSkillDirName) {
|
|
391
|
+
removeLegacySkillDirectory({
|
|
392
|
+
legacySkillDirName,
|
|
393
|
+
canonicalSkillDirName,
|
|
394
|
+
paths: options.paths,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
279
397
|
importedSkills.push(resolvedSkillDirName);
|
|
280
398
|
telemetrySkills.push({
|
|
281
399
|
name: sourceSkill.name,
|
|
@@ -303,10 +421,12 @@ export async function importSource(options) {
|
|
|
303
421
|
selectedSourceCommandFiles.length < sourceCommands.length;
|
|
304
422
|
const selectedSubsetOfSourceMcp = sourceMcpServerNames.length > 0 &&
|
|
305
423
|
selectedSourceMcpServers.length < sourceMcpServerNames.length;
|
|
424
|
+
const selectedSubsetOfSourceRules = sourceRules.length > 0 && selectedSourceRules.length < sourceRules.length;
|
|
306
425
|
const selectedSubsetOfSourceSkills = sourceSkills.length > 0 &&
|
|
307
426
|
selectedSourceSkills.length < sourceSkills.length;
|
|
308
427
|
const shouldPersistCommandSelection = shouldImportCommands && commandSelectionMode === "custom";
|
|
309
428
|
const shouldPersistMcpSelection = shouldImportMcp && mcpSelectionMode === "custom";
|
|
429
|
+
const shouldPersistRuleSelection = shouldImportRules && ruleSelectionMode === "custom";
|
|
310
430
|
const shouldPersistSkillSelection = shouldImportSkills && skillSelectionMode === "custom";
|
|
311
431
|
const selectedSourceCommandsForLock = shouldPersistCommandSelection || selectedSubsetOfSourceCommands
|
|
312
432
|
? selectedSourceCommandFiles
|
|
@@ -314,6 +434,9 @@ export async function importSource(options) {
|
|
|
314
434
|
const selectedSourceMcpServersForLock = shouldPersistMcpSelection || selectedSubsetOfSourceMcp
|
|
315
435
|
? selectedSourceMcpServers
|
|
316
436
|
: undefined;
|
|
437
|
+
const selectedSourceRulesForLock = shouldPersistRuleSelection || selectedSubsetOfSourceRules
|
|
438
|
+
? selectedSourceRules
|
|
439
|
+
: undefined;
|
|
317
440
|
const selectedSourceSkillsForLock = shouldPersistSkillSelection || selectedSubsetOfSourceSkills
|
|
318
441
|
? selectedSourceSkills
|
|
319
442
|
: undefined;
|
|
@@ -321,6 +444,7 @@ export async function importSource(options) {
|
|
|
321
444
|
const isCommandOnlyImport = !shouldImportAgents &&
|
|
322
445
|
shouldImportCommands &&
|
|
323
446
|
!shouldImportMcp &&
|
|
447
|
+
!shouldImportRules &&
|
|
324
448
|
!shouldImportSkills;
|
|
325
449
|
const existingEntry = isCommandOnlyImport
|
|
326
450
|
? findRelaxedCommandEntry(lockfile.entries, {
|
|
@@ -338,7 +462,9 @@ export async function importSource(options) {
|
|
|
338
462
|
: options.agents,
|
|
339
463
|
selectedSourceCommands: selectedSourceCommandsForLock,
|
|
340
464
|
selectedSourceMcpServers: selectedSourceMcpServersForLock,
|
|
465
|
+
selectedSourceRules: selectedSourceRulesForLock,
|
|
341
466
|
selectedSourceSkills: selectedSourceSkillsForLock,
|
|
467
|
+
selectedSkills,
|
|
342
468
|
skillsProviders: skillsProvidersForLock,
|
|
343
469
|
});
|
|
344
470
|
const shouldMergeCommandOnlyEntry = isCommandOnlyImport &&
|
|
@@ -360,6 +486,9 @@ export async function importSource(options) {
|
|
|
360
486
|
const lockImportedMcpServers = shouldImportMcp
|
|
361
487
|
? importedMcpServers
|
|
362
488
|
: (existingEntry?.importedMcpServers ?? []);
|
|
489
|
+
const lockImportedRules = shouldImportRules
|
|
490
|
+
? importedRules
|
|
491
|
+
: (existingEntry?.importedRules ?? []);
|
|
363
492
|
const lockImportedSkills = shouldImportSkills
|
|
364
493
|
? importedSkills
|
|
365
494
|
: (existingEntry?.importedSkills ?? []);
|
|
@@ -392,11 +521,19 @@ export async function importSource(options) {
|
|
|
392
521
|
? [...selectedSourceMcpServers]
|
|
393
522
|
: undefined
|
|
394
523
|
: existingEntry?.selectedSourceMcpServers;
|
|
524
|
+
const lockSelectedSourceRules = shouldImportRules
|
|
525
|
+
? shouldPersistRuleSelection || selectedSubsetOfSourceRules
|
|
526
|
+
? [...selectedSourceRules]
|
|
527
|
+
: undefined
|
|
528
|
+
: existingEntry?.selectedSourceRules;
|
|
395
529
|
const lockSelectedSourceSkills = shouldImportSkills
|
|
396
530
|
? shouldPersistSkillSelection || selectedSubsetOfSourceSkills
|
|
397
531
|
? [...selectedSourceSkills]
|
|
398
532
|
: undefined
|
|
399
533
|
: existingEntry?.selectedSourceSkills;
|
|
534
|
+
const lockRuleRenameMap = shouldImportRules
|
|
535
|
+
? normalizeRuleRenameMap(importedRuleRenameMap)
|
|
536
|
+
: existingEntry?.ruleRenameMap;
|
|
400
537
|
const lockSkillsProviders = shouldImportSkills
|
|
401
538
|
? (skillsProvidersForLock ?? existingEntry?.skillsProviders)
|
|
402
539
|
: existingEntry?.skillsProviders;
|
|
@@ -426,6 +563,9 @@ export async function importSource(options) {
|
|
|
426
563
|
commandRenameMap: lockCommandRenameMap,
|
|
427
564
|
importedMcpServers: lockImportedMcpServers,
|
|
428
565
|
selectedSourceMcpServers: lockSelectedSourceMcpServers,
|
|
566
|
+
importedRules: lockImportedRules,
|
|
567
|
+
selectedSourceRules: lockSelectedSourceRules,
|
|
568
|
+
ruleRenameMap: lockRuleRenameMap,
|
|
429
569
|
importedSkills: lockImportedSkills,
|
|
430
570
|
selectedSourceSkills: lockSelectedSourceSkills,
|
|
431
571
|
skillsProviders: lockSkillsProviders,
|
|
@@ -438,6 +578,9 @@ export async function importSource(options) {
|
|
|
438
578
|
commandRenameMap: lockCommandRenameMap ?? {},
|
|
439
579
|
mcp: lockImportedMcpServers,
|
|
440
580
|
selectedSourceMcpServers: lockSelectedSourceMcpServers ?? [],
|
|
581
|
+
rules: lockImportedRules,
|
|
582
|
+
selectedSourceRules: lockSelectedSourceRules ?? [],
|
|
583
|
+
ruleRenameMap: lockRuleRenameMap ?? {},
|
|
441
584
|
skills: lockImportedSkills,
|
|
442
585
|
selectedSourceSkills: lockSelectedSourceSkills ?? [],
|
|
443
586
|
skillsProviders: lockSkillsProviders ?? [],
|
|
@@ -458,6 +601,9 @@ export async function importSource(options) {
|
|
|
458
601
|
commandRenameMap: lockCommandRenameMap,
|
|
459
602
|
importedMcpServers: lockImportedMcpServers,
|
|
460
603
|
selectedSourceMcpServers: lockSelectedSourceMcpServers,
|
|
604
|
+
importedRules: lockImportedRules,
|
|
605
|
+
selectedSourceRules: lockSelectedSourceRules,
|
|
606
|
+
ruleRenameMap: lockRuleRenameMap,
|
|
461
607
|
importedSkills: lockImportedSkills,
|
|
462
608
|
selectedSourceSkills: lockSelectedSourceSkills,
|
|
463
609
|
skillsProviders: lockSkillsProviders,
|
|
@@ -484,7 +630,9 @@ export async function importSource(options) {
|
|
|
484
630
|
importedAgents,
|
|
485
631
|
importedCommands,
|
|
486
632
|
importedMcpServers,
|
|
633
|
+
importedRules,
|
|
487
634
|
importedSkills,
|
|
635
|
+
telemetryRules: telemetryRules.length > 0 ? telemetryRules : undefined,
|
|
488
636
|
telemetrySkills: telemetrySkills.length > 0 ? telemetrySkills : undefined,
|
|
489
637
|
resolvedCommit: prepared.resolvedCommit,
|
|
490
638
|
};
|
|
@@ -493,6 +641,504 @@ export async function importSource(options) {
|
|
|
493
641
|
prepared.cleanup();
|
|
494
642
|
}
|
|
495
643
|
}
|
|
644
|
+
function parseSourceAgentsForImport(sourceAgentsDirs, pluginSourceRoots) {
|
|
645
|
+
const sourceAgents = sourceAgentsDirs.flatMap((sourceAgentsDir) => {
|
|
646
|
+
if (isGitHubAgentsDir(sourceAgentsDir)) {
|
|
647
|
+
return parseGitHubAgentsDirForImport(sourceAgentsDir);
|
|
648
|
+
}
|
|
649
|
+
return parseAgentsDir(sourceAgentsDir);
|
|
650
|
+
});
|
|
651
|
+
assertNoPluginSourceCollisions({
|
|
652
|
+
entityLabel: "agent",
|
|
653
|
+
pluginSourceRoots,
|
|
654
|
+
entries: sourceAgents.map((agent) => ({
|
|
655
|
+
key: targetFileNameForAgent(agent),
|
|
656
|
+
sourcePath: agent.sourcePath,
|
|
657
|
+
})),
|
|
658
|
+
});
|
|
659
|
+
return sourceAgents;
|
|
660
|
+
}
|
|
661
|
+
function parseSourceCommandsForImport(sourceCommandsDirs, pluginSourceRoots) {
|
|
662
|
+
const sourceCommands = sourceCommandsDirs.flatMap((dirPath) => parseSourceCommandsFromDir(dirPath));
|
|
663
|
+
assertNoPluginSourceCollisions({
|
|
664
|
+
entityLabel: "command",
|
|
665
|
+
pluginSourceRoots,
|
|
666
|
+
entries: sourceCommands.map((command) => ({
|
|
667
|
+
key: toCanonicalCommandFileName(command.fileName),
|
|
668
|
+
sourcePath: command.sourcePath,
|
|
669
|
+
})),
|
|
670
|
+
});
|
|
671
|
+
return mergeCanonicalCommandFiles(sourceCommands);
|
|
672
|
+
}
|
|
673
|
+
function parseSourceMcpForImport(sourceMcpPaths, pluginSourceRoots) {
|
|
674
|
+
const mergedMcpServers = {};
|
|
675
|
+
const seenServerSource = new Map();
|
|
676
|
+
for (const sourceMcpPath of sourceMcpPaths) {
|
|
677
|
+
const sourceMcp = normalizeMcp(readJsonIfExists(sourceMcpPath));
|
|
678
|
+
for (const [serverName, serverConfig] of Object.entries(sourceMcp.mcpServers)) {
|
|
679
|
+
const pluginSourceRoot = resolvePluginSourceRootForPath(sourceMcpPath, pluginSourceRoots);
|
|
680
|
+
const existing = seenServerSource.get(serverName);
|
|
681
|
+
if (existing &&
|
|
682
|
+
existing.pluginSourceRoot &&
|
|
683
|
+
pluginSourceRoot &&
|
|
684
|
+
existing.pluginSourceRoot !== pluginSourceRoot) {
|
|
685
|
+
throw buildPluginCollisionError({
|
|
686
|
+
entityLabel: "mcp server",
|
|
687
|
+
key: serverName,
|
|
688
|
+
sourcePaths: [existing.sourcePath, sourceMcpPath],
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
mergedMcpServers[serverName] = serverConfig;
|
|
692
|
+
seenServerSource.set(serverName, {
|
|
693
|
+
sourcePath: sourceMcpPath,
|
|
694
|
+
pluginSourceRoot,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
version: 1,
|
|
700
|
+
mcpServers: mergedMcpServers,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function parseSourceRulesForImport(sourceRulesDirs, pluginSourceRoots) {
|
|
704
|
+
const sourceRules = sourceRulesDirs.flatMap((sourceRulesDir) => parseRulesDir(sourceRulesDir));
|
|
705
|
+
assertNoPluginSourceCollisions({
|
|
706
|
+
entityLabel: "rule",
|
|
707
|
+
pluginSourceRoots,
|
|
708
|
+
entries: sourceRules.map((rule) => ({
|
|
709
|
+
key: rule.id,
|
|
710
|
+
sourcePath: rule.sourcePath,
|
|
711
|
+
})),
|
|
712
|
+
});
|
|
713
|
+
return sourceRules;
|
|
714
|
+
}
|
|
715
|
+
function parseSourceSkillsForImport(sourceSkillsDirs, pluginSourceRoots) {
|
|
716
|
+
const sourceSkills = sourceSkillsDirs.flatMap((sourceSkillsDir) => parseSkillsDir(sourceSkillsDir));
|
|
717
|
+
assertNoPluginSourceCollisions({
|
|
718
|
+
entityLabel: "skill",
|
|
719
|
+
pluginSourceRoots,
|
|
720
|
+
entries: sourceSkills.map((skill) => ({
|
|
721
|
+
key: normalizeSkillSelector(skill.name),
|
|
722
|
+
sourcePath: skill.skillPath,
|
|
723
|
+
})),
|
|
724
|
+
});
|
|
725
|
+
assertNoDuplicateSkillNames(sourceSkills);
|
|
726
|
+
return sourceSkills;
|
|
727
|
+
}
|
|
728
|
+
function assertNoDuplicateSkillNames(sourceSkills) {
|
|
729
|
+
const byName = new Map();
|
|
730
|
+
for (const skill of sourceSkills) {
|
|
731
|
+
const normalizedName = normalizeSkillSelector(skill.name);
|
|
732
|
+
if (!normalizedName)
|
|
733
|
+
continue;
|
|
734
|
+
const matches = byName.get(normalizedName) ?? [];
|
|
735
|
+
matches.push({
|
|
736
|
+
name: skill.name,
|
|
737
|
+
sourcePath: skill.skillPath,
|
|
738
|
+
});
|
|
739
|
+
byName.set(normalizedName, matches);
|
|
740
|
+
}
|
|
741
|
+
for (const matches of byName.values()) {
|
|
742
|
+
const sourcePaths = [...new Set(matches.map((item) => item.sourcePath))];
|
|
743
|
+
if (sourcePaths.length < 2) {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
const locations = sourcePaths
|
|
747
|
+
.map((sourcePath) => `- ${sourcePath}`)
|
|
748
|
+
.join("\n");
|
|
749
|
+
throw new Error(`Conflicting skill "${matches[0]?.name ?? "unknown"}" found in source:\n${locations}\nEnsure each SKILL.md frontmatter name is unique.`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function assertNoPluginSourceCollisions(options) {
|
|
753
|
+
if (options.pluginSourceRoots.length === 0 || options.entries.length === 0) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const byKey = new Map();
|
|
757
|
+
for (const entry of options.entries) {
|
|
758
|
+
const normalizedKey = entry.key.trim().toLowerCase();
|
|
759
|
+
if (!normalizedKey)
|
|
760
|
+
continue;
|
|
761
|
+
const pluginSourceRoot = resolvePluginSourceRootForPath(entry.sourcePath, options.pluginSourceRoots);
|
|
762
|
+
const group = byKey.get(normalizedKey) ?? [];
|
|
763
|
+
group.push({
|
|
764
|
+
key: entry.key,
|
|
765
|
+
sourcePath: entry.sourcePath,
|
|
766
|
+
pluginSourceRoot,
|
|
767
|
+
});
|
|
768
|
+
byKey.set(normalizedKey, group);
|
|
769
|
+
}
|
|
770
|
+
for (const matches of byKey.values()) {
|
|
771
|
+
const pluginRoots = [
|
|
772
|
+
...new Set(matches
|
|
773
|
+
.map((item) => item.pluginSourceRoot)
|
|
774
|
+
.filter((item) => Boolean(item))),
|
|
775
|
+
];
|
|
776
|
+
if (pluginRoots.length < 2) {
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
throw buildPluginCollisionError({
|
|
780
|
+
entityLabel: options.entityLabel,
|
|
781
|
+
key: matches[0]?.key ?? "unknown",
|
|
782
|
+
sourcePaths: matches.map((item) => item.sourcePath),
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function buildPluginCollisionError(options) {
|
|
787
|
+
const locations = [...new Set(options.sourcePaths)]
|
|
788
|
+
.map((sourcePath) => `- ${sourcePath}`)
|
|
789
|
+
.join("\n");
|
|
790
|
+
return new Error(`Conflicting ${options.entityLabel} "${options.key}" found across plugin sources declared in .claude-plugin/marketplace.json:\n${locations}\nUse --subdir to import a single plugin source.`);
|
|
791
|
+
}
|
|
792
|
+
function resolvePluginSourceRootForPath(sourcePath, pluginSourceRoots) {
|
|
793
|
+
for (const pluginSourceRoot of [...pluginSourceRoots].sort((left, right) => right.length - left.length)) {
|
|
794
|
+
const normalizedRoot = path.resolve(pluginSourceRoot);
|
|
795
|
+
const normalizedPath = path.resolve(sourcePath);
|
|
796
|
+
if (normalizedPath === normalizedRoot ||
|
|
797
|
+
normalizedPath.startsWith(`${normalizedRoot}${path.sep}`)) {
|
|
798
|
+
return normalizedRoot;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
function parseSourceCommandsFromDir(sourceCommandsDir) {
|
|
804
|
+
if (isGeminiCommandsDir(sourceCommandsDir)) {
|
|
805
|
+
const geminiMarkdownCommands = parseCommandsDir(sourceCommandsDir).filter((command) => isProviderEntityFileName({
|
|
806
|
+
provider: "gemini",
|
|
807
|
+
entity: "command",
|
|
808
|
+
fileName: command.fileName,
|
|
809
|
+
}));
|
|
810
|
+
const parsedCommands = mergeCommandsByCanonicalFileName([
|
|
811
|
+
...parseGeminiTomlCommandsForImport(sourceCommandsDir),
|
|
812
|
+
...geminiMarkdownCommands,
|
|
813
|
+
]);
|
|
814
|
+
return parsedCommands.map((command) => normalizeGeminiCommandForImport(command));
|
|
815
|
+
}
|
|
816
|
+
const commands = parseCommandsDir(sourceCommandsDir);
|
|
817
|
+
if (!isGitHubPromptsDir(sourceCommandsDir)) {
|
|
818
|
+
return commands;
|
|
819
|
+
}
|
|
820
|
+
return commands
|
|
821
|
+
.filter((command) => isProviderEntityFileName({
|
|
822
|
+
provider: "copilot",
|
|
823
|
+
entity: "command",
|
|
824
|
+
fileName: command.fileName,
|
|
825
|
+
}))
|
|
826
|
+
.map((command) => normalizeGitHubPromptForImport(command));
|
|
827
|
+
}
|
|
828
|
+
function mergeCommandsByCanonicalFileName(commands) {
|
|
829
|
+
const byFileName = new Map();
|
|
830
|
+
for (const command of commands) {
|
|
831
|
+
if (!byFileName.has(command.fileName)) {
|
|
832
|
+
byFileName.set(command.fileName, command);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return [...byFileName.values()];
|
|
836
|
+
}
|
|
837
|
+
function isGitHubAgentsDir(sourceAgentsDir) {
|
|
838
|
+
return (path.basename(sourceAgentsDir).toLowerCase() === "agents" &&
|
|
839
|
+
path.basename(path.dirname(sourceAgentsDir)).toLowerCase() === ".github");
|
|
840
|
+
}
|
|
841
|
+
function isGitHubPromptsDir(sourceCommandsDir) {
|
|
842
|
+
return (path.basename(sourceCommandsDir).toLowerCase() === "prompts" &&
|
|
843
|
+
path.basename(path.dirname(sourceCommandsDir)).toLowerCase() === ".github");
|
|
844
|
+
}
|
|
845
|
+
function isGeminiCommandsDir(sourceCommandsDir) {
|
|
846
|
+
return (path.basename(sourceCommandsDir).toLowerCase() === "commands" &&
|
|
847
|
+
path.basename(path.dirname(sourceCommandsDir)).toLowerCase() === ".gemini");
|
|
848
|
+
}
|
|
849
|
+
function parseGeminiTomlCommandsForImport(sourceCommandsDir) {
|
|
850
|
+
return fs
|
|
851
|
+
.readdirSync(sourceCommandsDir, { withFileTypes: true })
|
|
852
|
+
.filter((entry) => entry.isFile())
|
|
853
|
+
.map((entry) => entry.name)
|
|
854
|
+
.filter((fileName) => isProviderEntityFileName({
|
|
855
|
+
provider: "gemini",
|
|
856
|
+
entity: "command",
|
|
857
|
+
fileName,
|
|
858
|
+
}))
|
|
859
|
+
.filter((fileName) => fileName.toLowerCase().endsWith(".toml"))
|
|
860
|
+
.sort((a, b) => a.localeCompare(b))
|
|
861
|
+
.map((fileName) => parseGeminiTomlCommandForImport(path.join(sourceCommandsDir, fileName)))
|
|
862
|
+
.filter((command) => command !== null);
|
|
863
|
+
}
|
|
864
|
+
function mergeCanonicalCommandFiles(commands) {
|
|
865
|
+
const merged = new Map();
|
|
866
|
+
for (const command of commands) {
|
|
867
|
+
const fileName = toCanonicalCommandFileName(command.fileName);
|
|
868
|
+
const normalizedCommand = fileName === command.fileName ? command : { ...command, fileName };
|
|
869
|
+
const existing = merged.get(fileName);
|
|
870
|
+
if (!existing) {
|
|
871
|
+
merged.set(fileName, normalizedCommand);
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
const existingHasBody = normalizeImportedCommandBody(existing.body).length > 0;
|
|
875
|
+
const incomingHasBody = normalizeImportedCommandBody(normalizedCommand.body).length > 0;
|
|
876
|
+
if (existingHasBody &&
|
|
877
|
+
incomingHasBody &&
|
|
878
|
+
!sameNormalizedImportedCommandBody(existing.body, normalizedCommand.body)) {
|
|
879
|
+
throw new Error(`Conflicting command bodies found for "${fileName}" in ${existing.sourcePath} and ${normalizedCommand.sourcePath}. Align the provider-specific prompts before importing, or import a single provider directory with --subdir.`);
|
|
880
|
+
}
|
|
881
|
+
const body = existingHasBody ? existing.body : normalizedCommand.body;
|
|
882
|
+
const frontmatter = mergeCommandFrontmatterForImport(existing.frontmatter, normalizedCommand.frontmatter);
|
|
883
|
+
merged.set(fileName, {
|
|
884
|
+
...existing,
|
|
885
|
+
fileName,
|
|
886
|
+
body,
|
|
887
|
+
frontmatter,
|
|
888
|
+
content: buildCommandMarkdownForImport(frontmatter, body),
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
return [...merged.values()];
|
|
892
|
+
}
|
|
893
|
+
function sameNormalizedImportedCommandBody(left, right) {
|
|
894
|
+
return (normalizeImportedCommandBody(left) === normalizeImportedCommandBody(right));
|
|
895
|
+
}
|
|
896
|
+
function normalizeImportedCommandBody(value) {
|
|
897
|
+
return value.trim().replace(/\r\n/g, "\n");
|
|
898
|
+
}
|
|
899
|
+
function mergeCommandFrontmatterForImport(existing, incoming) {
|
|
900
|
+
if (!existing && !incoming) {
|
|
901
|
+
return undefined;
|
|
902
|
+
}
|
|
903
|
+
const merged = existing ? cloneUnknown(existing) : {};
|
|
904
|
+
for (const [key, value] of Object.entries(incoming ?? {})) {
|
|
905
|
+
const current = merged[key];
|
|
906
|
+
if (current === undefined) {
|
|
907
|
+
merged[key] = cloneUnknown(value);
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
if (isObject(current) && isObject(value)) {
|
|
911
|
+
merged[key] = {
|
|
912
|
+
...cloneUnknown(value),
|
|
913
|
+
...cloneUnknown(current),
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
918
|
+
}
|
|
919
|
+
function parseGeminiTomlCommandForImport(sourcePath) {
|
|
920
|
+
const raw = fs.readFileSync(sourcePath, "utf8");
|
|
921
|
+
let parsed;
|
|
922
|
+
try {
|
|
923
|
+
parsed = TOML.parse(raw);
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
if (!isObject(parsed) || typeof parsed.prompt !== "string") {
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
const body = normalizeCommandArgumentsForCanonical(parsed.prompt, "gemini");
|
|
932
|
+
const frontmatter = cloneUnknown(parsed);
|
|
933
|
+
delete frontmatter.prompt;
|
|
934
|
+
const normalizedFrontmatter = Object.keys(frontmatter).length > 0 ? frontmatter : undefined;
|
|
935
|
+
const fileName = toCanonicalCommandFileName(path.basename(sourcePath));
|
|
936
|
+
const content = buildCommandMarkdownForImport(normalizedFrontmatter, body);
|
|
937
|
+
return {
|
|
938
|
+
fileName,
|
|
939
|
+
sourcePath,
|
|
940
|
+
content,
|
|
941
|
+
body,
|
|
942
|
+
frontmatter: normalizedFrontmatter,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
function parseGitHubAgentsDirForImport(sourceAgentsDir) {
|
|
946
|
+
return fs
|
|
947
|
+
.readdirSync(sourceAgentsDir)
|
|
948
|
+
.filter((entry) => isProviderEntityFileName({
|
|
949
|
+
provider: "copilot",
|
|
950
|
+
entity: "agent",
|
|
951
|
+
fileName: entry,
|
|
952
|
+
}))
|
|
953
|
+
.sort((a, b) => a.localeCompare(b))
|
|
954
|
+
.map((entry) => parseGitHubAgentForImport(path.join(sourceAgentsDir, entry)));
|
|
955
|
+
}
|
|
956
|
+
function parseGitHubAgentForImport(sourcePath) {
|
|
957
|
+
const raw = fs.readFileSync(sourcePath, "utf8");
|
|
958
|
+
const parsed = matter(raw);
|
|
959
|
+
const data = isObject(parsed.data)
|
|
960
|
+
? parsed.data
|
|
961
|
+
: {};
|
|
962
|
+
const fileName = path.basename(sourcePath);
|
|
963
|
+
const fallbackName = inferAgentNameFromFile(fileName);
|
|
964
|
+
const name = typeof data.name === "string" && data.name.trim().length > 0
|
|
965
|
+
? data.name.trim()
|
|
966
|
+
: fallbackName;
|
|
967
|
+
const description = typeof data.description === "string" && data.description.trim().length > 0
|
|
968
|
+
? data.description.trim()
|
|
969
|
+
: `Imported from Copilot agent "${name}".`;
|
|
970
|
+
const frontmatter = {
|
|
971
|
+
name,
|
|
972
|
+
description,
|
|
973
|
+
};
|
|
974
|
+
for (const provider of ALL_PROVIDERS) {
|
|
975
|
+
if (provider === "copilot")
|
|
976
|
+
continue;
|
|
977
|
+
const value = data[provider];
|
|
978
|
+
if (value === false || isObject(value)) {
|
|
979
|
+
frontmatter[provider] = cloneUnknown(value);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
const inferredCopilotConfig = {};
|
|
983
|
+
for (const [key, value] of Object.entries(data)) {
|
|
984
|
+
if (key === "name" || key === "description")
|
|
985
|
+
continue;
|
|
986
|
+
if (ALL_PROVIDERS.includes(key))
|
|
987
|
+
continue;
|
|
988
|
+
inferredCopilotConfig[key] = cloneUnknown(value);
|
|
989
|
+
}
|
|
990
|
+
const explicitCopilot = data.copilot;
|
|
991
|
+
if (explicitCopilot === false) {
|
|
992
|
+
frontmatter.copilot = false;
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
const copilotConfig = isObject(explicitCopilot)
|
|
996
|
+
? cloneUnknown(explicitCopilot)
|
|
997
|
+
: {};
|
|
998
|
+
for (const [key, value] of Object.entries(inferredCopilotConfig)) {
|
|
999
|
+
if (!(key in copilotConfig)) {
|
|
1000
|
+
copilotConfig[key] = value;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (Object.keys(copilotConfig).length > 0) {
|
|
1004
|
+
frontmatter.copilot = copilotConfig;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return {
|
|
1008
|
+
name,
|
|
1009
|
+
description,
|
|
1010
|
+
body: parsed.content.trimStart(),
|
|
1011
|
+
frontmatter: frontmatter,
|
|
1012
|
+
sourcePath,
|
|
1013
|
+
fileName,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function normalizeGitHubPromptForImport(command) {
|
|
1017
|
+
const fileName = toCanonicalCommandFileName(command.fileName);
|
|
1018
|
+
if (!command.frontmatter) {
|
|
1019
|
+
return {
|
|
1020
|
+
...command,
|
|
1021
|
+
fileName,
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
const nextFrontmatter = {};
|
|
1025
|
+
for (const provider of ALL_PROVIDERS) {
|
|
1026
|
+
if (provider === "copilot")
|
|
1027
|
+
continue;
|
|
1028
|
+
const value = command.frontmatter[provider];
|
|
1029
|
+
if (value === false || isObject(value)) {
|
|
1030
|
+
nextFrontmatter[provider] = cloneUnknown(value);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
const explicitCopilot = command.frontmatter.copilot;
|
|
1034
|
+
if (explicitCopilot === false) {
|
|
1035
|
+
nextFrontmatter.copilot = false;
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
const copilotConfig = isObject(explicitCopilot)
|
|
1039
|
+
? cloneUnknown(explicitCopilot)
|
|
1040
|
+
: {};
|
|
1041
|
+
for (const [key, value] of Object.entries(command.frontmatter)) {
|
|
1042
|
+
if (ALL_PROVIDERS.includes(key))
|
|
1043
|
+
continue;
|
|
1044
|
+
if (!(key in copilotConfig)) {
|
|
1045
|
+
copilotConfig[key] = cloneUnknown(value);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (Object.keys(copilotConfig).length > 0) {
|
|
1049
|
+
nextFrontmatter.copilot = copilotConfig;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const frontmatter = Object.keys(nextFrontmatter).length > 0 ? nextFrontmatter : undefined;
|
|
1053
|
+
const body = normalizeCommandArgumentsForCanonical(command.body, "copilot").trimStart();
|
|
1054
|
+
return {
|
|
1055
|
+
...command,
|
|
1056
|
+
fileName,
|
|
1057
|
+
body,
|
|
1058
|
+
frontmatter,
|
|
1059
|
+
content: buildCommandMarkdownForImport(frontmatter, body),
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function normalizeGeminiCommandForImport(command) {
|
|
1063
|
+
const fileName = toCanonicalCommandFileName(command.fileName);
|
|
1064
|
+
const body = normalizeCommandArgumentsForCanonical(command.body, "gemini");
|
|
1065
|
+
if (!command.frontmatter) {
|
|
1066
|
+
return {
|
|
1067
|
+
...command,
|
|
1068
|
+
fileName,
|
|
1069
|
+
body,
|
|
1070
|
+
content: buildCommandMarkdownForImport(undefined, body),
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
const nextFrontmatter = {};
|
|
1074
|
+
for (const provider of ALL_PROVIDERS) {
|
|
1075
|
+
if (provider === "gemini")
|
|
1076
|
+
continue;
|
|
1077
|
+
const value = command.frontmatter[provider];
|
|
1078
|
+
if (value === false || isObject(value)) {
|
|
1079
|
+
nextFrontmatter[provider] = cloneUnknown(value);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (typeof command.frontmatter.description === "string") {
|
|
1083
|
+
nextFrontmatter.description = command.frontmatter.description;
|
|
1084
|
+
}
|
|
1085
|
+
const explicitGemini = command.frontmatter.gemini;
|
|
1086
|
+
if (explicitGemini === false) {
|
|
1087
|
+
nextFrontmatter.gemini = false;
|
|
1088
|
+
}
|
|
1089
|
+
else {
|
|
1090
|
+
const geminiConfig = isObject(explicitGemini)
|
|
1091
|
+
? cloneUnknown(explicitGemini)
|
|
1092
|
+
: {};
|
|
1093
|
+
for (const [key, value] of Object.entries(command.frontmatter)) {
|
|
1094
|
+
if (key === "description")
|
|
1095
|
+
continue;
|
|
1096
|
+
if (ALL_PROVIDERS.includes(key))
|
|
1097
|
+
continue;
|
|
1098
|
+
if (!(key in geminiConfig)) {
|
|
1099
|
+
geminiConfig[key] = cloneUnknown(value);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (Object.keys(geminiConfig).length > 0) {
|
|
1103
|
+
nextFrontmatter.gemini = geminiConfig;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
const frontmatter = Object.keys(nextFrontmatter).length > 0 ? nextFrontmatter : undefined;
|
|
1107
|
+
return {
|
|
1108
|
+
...command,
|
|
1109
|
+
fileName,
|
|
1110
|
+
body,
|
|
1111
|
+
frontmatter,
|
|
1112
|
+
content: buildCommandMarkdownForImport(frontmatter, body),
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
function buildCommandMarkdownForImport(frontmatter, body) {
|
|
1116
|
+
if (!frontmatter) {
|
|
1117
|
+
return body.endsWith("\n") ? body : `${body}\n`;
|
|
1118
|
+
}
|
|
1119
|
+
const fm = YAML.stringify(frontmatter, { lineWidth: 0 }).trimEnd();
|
|
1120
|
+
return `---\n${fm}\n---\n\n${body}${body.endsWith("\n") ? "" : "\n"}`;
|
|
1121
|
+
}
|
|
1122
|
+
function inferAgentNameFromFile(fileName) {
|
|
1123
|
+
const base = fileName
|
|
1124
|
+
.replace(/\.agent\.md$/i, "")
|
|
1125
|
+
.replace(/\.md$/i, "")
|
|
1126
|
+
.trim();
|
|
1127
|
+
return base || "agent";
|
|
1128
|
+
}
|
|
1129
|
+
function toCanonicalCommandFileName(fileName) {
|
|
1130
|
+
const lower = fileName.toLowerCase();
|
|
1131
|
+
if (lower.endsWith(".prompt.md")) {
|
|
1132
|
+
return `${fileName.slice(0, -".prompt.md".length)}.md`;
|
|
1133
|
+
}
|
|
1134
|
+
if (lower.endsWith(".toml")) {
|
|
1135
|
+
return `${fileName.slice(0, -".toml".length)}.md`;
|
|
1136
|
+
}
|
|
1137
|
+
return fileName;
|
|
1138
|
+
}
|
|
1139
|
+
function cloneUnknown(value) {
|
|
1140
|
+
return JSON.parse(JSON.stringify(value));
|
|
1141
|
+
}
|
|
496
1142
|
async function resolveAgentsToImport(options) {
|
|
497
1143
|
const requestedAgents = normalizeRequestedAgents(options.requestedAgents);
|
|
498
1144
|
if (requestedAgents && requestedAgents.length > 0) {
|
|
@@ -653,7 +1299,8 @@ function findMatchingLockEntry(entries, key) {
|
|
|
653
1299
|
sameRequestedAgentsForMatch(entry.requestedAgents, key.requestedAgents) &&
|
|
654
1300
|
sameStringSelectionForMatch(entry.selectedSourceCommands, key.selectedSourceCommands) &&
|
|
655
1301
|
sameStringSelectionForMatch(entry.selectedSourceMcpServers, key.selectedSourceMcpServers) &&
|
|
656
|
-
sameStringSelectionForMatch(entry.
|
|
1302
|
+
sameStringSelectionForMatch(entry.selectedSourceRules, key.selectedSourceRules, { wildcardWhenRightIsUndefined: true }) &&
|
|
1303
|
+
sameSkillSelectionForMatch(entry.selectedSourceSkills, key.selectedSourceSkills, key.selectedSkills, { wildcardWhenRightIsUndefined: true }) &&
|
|
657
1304
|
sameStringSelectionForMatch(entry.skillsProviders, key.skillsProviders, {
|
|
658
1305
|
wildcardWhenRightIsUndefined: true,
|
|
659
1306
|
}));
|
|
@@ -667,6 +1314,7 @@ function findRelaxedCommandEntry(entries, key) {
|
|
|
667
1314
|
return undefined;
|
|
668
1315
|
const mixed = matches.find((entry) => entry.importedAgents.length > 0 ||
|
|
669
1316
|
entry.importedMcpServers.length > 0 ||
|
|
1317
|
+
entry.importedRules.length > 0 ||
|
|
670
1318
|
entry.importedSkills.length > 0);
|
|
671
1319
|
return mixed ?? matches[0];
|
|
672
1320
|
}
|
|
@@ -696,6 +1344,39 @@ function sameStringSelectionForMatch(left, right, options = {}) {
|
|
|
696
1344
|
}
|
|
697
1345
|
return normalizedLeft.every((value, index) => value === normalizedRight[index]);
|
|
698
1346
|
}
|
|
1347
|
+
function sameSkillSelectionForMatch(left, right, selectedSkills, options = {}) {
|
|
1348
|
+
if (options.wildcardWhenRightIsUndefined && right === undefined) {
|
|
1349
|
+
return true;
|
|
1350
|
+
}
|
|
1351
|
+
const normalizedLeft = normalizeSkillSelectionsForMatch(left);
|
|
1352
|
+
const normalizedRight = normalizeSkillSelectionsForMatch(right);
|
|
1353
|
+
if (normalizedLeft.length === normalizedRight.length &&
|
|
1354
|
+
normalizedLeft.every((value, index) => value === normalizedRight[index])) {
|
|
1355
|
+
return true;
|
|
1356
|
+
}
|
|
1357
|
+
if (!selectedSkills || normalizedLeft.length !== selectedSkills.length) {
|
|
1358
|
+
return false;
|
|
1359
|
+
}
|
|
1360
|
+
const remainingSelectors = new Set(normalizedLeft);
|
|
1361
|
+
for (const skill of selectedSkills) {
|
|
1362
|
+
const matchedSelector = [
|
|
1363
|
+
normalizeSkillSelector(skill.name),
|
|
1364
|
+
normalizeSkillSelector(skill.sourceDirName),
|
|
1365
|
+
].find((selector) => selector && remainingSelectors.has(selector));
|
|
1366
|
+
if (!matchedSelector) {
|
|
1367
|
+
return false;
|
|
1368
|
+
}
|
|
1369
|
+
remainingSelectors.delete(matchedSelector);
|
|
1370
|
+
}
|
|
1371
|
+
return remainingSelectors.size === 0;
|
|
1372
|
+
}
|
|
1373
|
+
function normalizeSkillSelectionsForMatch(value) {
|
|
1374
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
1375
|
+
return [];
|
|
1376
|
+
return [
|
|
1377
|
+
...new Set(value.map((item) => normalizeSkillSelector(item)).filter(Boolean)),
|
|
1378
|
+
].sort();
|
|
1379
|
+
}
|
|
699
1380
|
function uniqueStrings(values) {
|
|
700
1381
|
return [...new Set(values)];
|
|
701
1382
|
}
|
|
@@ -714,6 +1395,11 @@ function computeTrackedEntitiesForLock(options) {
|
|
|
714
1395
|
(options.selectedSourceMcpServers?.length ?? 0) > 0) {
|
|
715
1396
|
tracked.push("mcp");
|
|
716
1397
|
}
|
|
1398
|
+
if (options.importedRules.length > 0 ||
|
|
1399
|
+
(options.selectedSourceRules?.length ?? 0) > 0 ||
|
|
1400
|
+
Object.keys(options.ruleRenameMap ?? {}).length > 0) {
|
|
1401
|
+
tracked.push("rule");
|
|
1402
|
+
}
|
|
717
1403
|
if (options.importedSkills.length > 0 ||
|
|
718
1404
|
(options.selectedSourceSkills?.length ?? 0) > 0 ||
|
|
719
1405
|
(options.skillsProviders?.length ?? 0) > 0 ||
|
|
@@ -754,6 +1440,44 @@ function resolveMappedTargetFileName(sourceFileName, renameMap) {
|
|
|
754
1440
|
}
|
|
755
1441
|
return undefined;
|
|
756
1442
|
}
|
|
1443
|
+
function normalizeRuleRenameMap(renameMap) {
|
|
1444
|
+
if (!renameMap)
|
|
1445
|
+
return undefined;
|
|
1446
|
+
const normalizedEntries = Object.entries(renameMap)
|
|
1447
|
+
.map(([sourceSelector, importedName]) => {
|
|
1448
|
+
const normalizedSourceSelector = normalizeRuleSelector(sourceSelector);
|
|
1449
|
+
const importedBaseName = path.basename(importedName.trim());
|
|
1450
|
+
if (!normalizedSourceSelector || !importedBaseName) {
|
|
1451
|
+
return null;
|
|
1452
|
+
}
|
|
1453
|
+
const ext = path.extname(importedBaseName);
|
|
1454
|
+
const stem = stripRuleFileExtension(importedBaseName);
|
|
1455
|
+
const normalizedTarget = `${slugify(stem) || "rule"}${ext || ".md"}`;
|
|
1456
|
+
return [normalizedSourceSelector, normalizedTarget];
|
|
1457
|
+
})
|
|
1458
|
+
.filter((entry) => entry !== null);
|
|
1459
|
+
if (normalizedEntries.length === 0)
|
|
1460
|
+
return undefined;
|
|
1461
|
+
return Object.fromEntries(normalizedEntries);
|
|
1462
|
+
}
|
|
1463
|
+
function resolveMappedTargetRuleFileName(sourceFileName, renameMap) {
|
|
1464
|
+
if (!renameMap)
|
|
1465
|
+
return undefined;
|
|
1466
|
+
const normalizedSourceName = normalizeRuleSelector(sourceFileName);
|
|
1467
|
+
for (const [sourceSelector, importedName] of Object.entries(renameMap)) {
|
|
1468
|
+
if (normalizeRuleSelector(sourceSelector) !== normalizedSourceName) {
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
const importedBaseName = path.basename(importedName.trim());
|
|
1472
|
+
if (!importedBaseName)
|
|
1473
|
+
return undefined;
|
|
1474
|
+
const ext = path.extname(importedBaseName);
|
|
1475
|
+
if (ext)
|
|
1476
|
+
return importedBaseName;
|
|
1477
|
+
return `${slugify(importedBaseName) || "rule"}.md`;
|
|
1478
|
+
}
|
|
1479
|
+
return undefined;
|
|
1480
|
+
}
|
|
757
1481
|
function normalizeSkillRenameMap(renameMap) {
|
|
758
1482
|
if (!renameMap)
|
|
759
1483
|
return undefined;
|
|
@@ -771,20 +1495,65 @@ function normalizeSkillRenameMap(renameMap) {
|
|
|
771
1495
|
return undefined;
|
|
772
1496
|
return Object.fromEntries(normalizedEntries);
|
|
773
1497
|
}
|
|
774
|
-
function resolveMappedTargetSkillName(
|
|
1498
|
+
function resolveMappedTargetSkillName(sourceSkill, selectedSkills, renameMap) {
|
|
775
1499
|
if (!renameMap)
|
|
776
1500
|
return undefined;
|
|
777
|
-
const normalizedSourceName = normalizeSkillSelector(sourceSkillName);
|
|
778
|
-
if (!normalizedSourceName)
|
|
779
|
-
return undefined;
|
|
780
1501
|
for (const [sourceSelector, importedName] of Object.entries(renameMap)) {
|
|
781
|
-
|
|
1502
|
+
const matchedSkill = resolveSkillSelector(selectedSkills, sourceSelector);
|
|
1503
|
+
if (!matchedSkill || matchedSkill.sourcePath !== sourceSkill.sourcePath) {
|
|
782
1504
|
continue;
|
|
783
1505
|
}
|
|
784
1506
|
return slugify(path.basename(importedName.trim())) || "skill";
|
|
785
1507
|
}
|
|
786
1508
|
return undefined;
|
|
787
1509
|
}
|
|
1510
|
+
function moveLegacySkillDirectoryToCanonicalIfUnchanged(options) {
|
|
1511
|
+
if (options.legacySkillDirName === options.canonicalSkillDirName) {
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
const legacySkillDir = path.join(options.paths.skillsDir, options.legacySkillDirName);
|
|
1515
|
+
if (!fs.existsSync(legacySkillDir) ||
|
|
1516
|
+
!fs.statSync(legacySkillDir).isDirectory()) {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
const canonicalSkillDir = path.join(options.paths.skillsDir, options.canonicalSkillDirName);
|
|
1520
|
+
if (fs.existsSync(canonicalSkillDir)) {
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
if (!skillContentMatchesTarget(options.sourceSkill, legacySkillDir)) {
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
moveDirectory(legacySkillDir, canonicalSkillDir);
|
|
1527
|
+
}
|
|
1528
|
+
function removeLegacySkillDirectory(options) {
|
|
1529
|
+
if (options.legacySkillDirName === options.canonicalSkillDirName) {
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
const legacySkillDir = path.join(options.paths.skillsDir, options.legacySkillDirName);
|
|
1533
|
+
if (!fs.existsSync(legacySkillDir)) {
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
const stat = fs.lstatSync(legacySkillDir);
|
|
1537
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
fs.rmSync(legacySkillDir, { recursive: true, force: true });
|
|
1541
|
+
}
|
|
1542
|
+
function moveDirectory(sourceDir, targetDir) {
|
|
1543
|
+
ensureDir(path.dirname(targetDir));
|
|
1544
|
+
try {
|
|
1545
|
+
fs.renameSync(sourceDir, targetDir);
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
catch (error) {
|
|
1549
|
+
const code = error?.code;
|
|
1550
|
+
if (code !== "EXDEV") {
|
|
1551
|
+
throw error;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
fs.cpSync(sourceDir, targetDir, { recursive: true, force: true });
|
|
1555
|
+
fs.rmSync(sourceDir, { recursive: true, force: true });
|
|
1556
|
+
}
|
|
788
1557
|
function normalizeSkillsProviders(providers) {
|
|
789
1558
|
if (!providers || providers.length === 0)
|
|
790
1559
|
return undefined;
|
|
@@ -799,12 +1568,13 @@ function normalizeSkillsProviders(providers) {
|
|
|
799
1568
|
}
|
|
800
1569
|
async function resolveSkillConflict(options) {
|
|
801
1570
|
const targetPath = path.join(options.paths.skillsDir, options.targetSkillDirName);
|
|
802
|
-
|
|
1571
|
+
const conflictPath = resolveExistingSkillConflictPath(options, targetPath);
|
|
1572
|
+
if (!conflictPath)
|
|
803
1573
|
return options.targetSkillDirName;
|
|
804
|
-
if (!fs.statSync(
|
|
805
|
-
throw new Error(`Cannot import skill ${options.promptLabel}: ${
|
|
1574
|
+
if (!fs.statSync(conflictPath).isDirectory()) {
|
|
1575
|
+
throw new Error(`Cannot import skill ${options.promptLabel}: ${conflictPath} exists and is not a directory.`);
|
|
806
1576
|
}
|
|
807
|
-
if (skillContentMatchesTarget(options.sourceSkill,
|
|
1577
|
+
if (skillContentMatchesTarget(options.sourceSkill, conflictPath)) {
|
|
808
1578
|
return options.targetSkillDirName;
|
|
809
1579
|
}
|
|
810
1580
|
if (options.yes) {
|
|
@@ -851,6 +1621,17 @@ async function resolveSkillConflict(options) {
|
|
|
851
1621
|
}
|
|
852
1622
|
return options.targetSkillDirName;
|
|
853
1623
|
}
|
|
1624
|
+
function resolveExistingSkillConflictPath(options, targetPath) {
|
|
1625
|
+
if (fs.existsSync(targetPath)) {
|
|
1626
|
+
return targetPath;
|
|
1627
|
+
}
|
|
1628
|
+
if (!options.legacySkillDirName ||
|
|
1629
|
+
options.legacySkillDirName === options.canonicalSkillDirName) {
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
const legacyPath = path.join(options.paths.skillsDir, options.legacySkillDirName);
|
|
1633
|
+
return fs.existsSync(legacyPath) ? legacyPath : null;
|
|
1634
|
+
}
|
|
854
1635
|
async function resolveAgentConflict(options) {
|
|
855
1636
|
const targetPath = path.join(options.paths.agentsDir, options.targetFileName);
|
|
856
1637
|
if (!fs.existsSync(targetPath))
|
|
@@ -954,6 +1735,58 @@ async function resolveCommandConflict(options) {
|
|
|
954
1735
|
}
|
|
955
1736
|
return options.targetFileName;
|
|
956
1737
|
}
|
|
1738
|
+
async function resolveRuleConflict(options) {
|
|
1739
|
+
const targetPath = path.join(options.paths.rulesDir, options.targetFileName);
|
|
1740
|
+
if (!fs.existsSync(targetPath))
|
|
1741
|
+
return options.targetFileName;
|
|
1742
|
+
const existing = fs.readFileSync(targetPath, "utf8");
|
|
1743
|
+
if (existing === options.ruleContent)
|
|
1744
|
+
return options.targetFileName;
|
|
1745
|
+
if (options.yes) {
|
|
1746
|
+
return options.targetFileName;
|
|
1747
|
+
}
|
|
1748
|
+
if (options.nonInteractive) {
|
|
1749
|
+
throw new NonInteractiveConflictError(`Conflict for ${options.targetFileName}. Use --yes or run interactively.`);
|
|
1750
|
+
}
|
|
1751
|
+
const choice = await select({
|
|
1752
|
+
message: `Rule conflict for ${options.promptLabel}`,
|
|
1753
|
+
options: [
|
|
1754
|
+
{ value: "overwrite", label: `Overwrite ${options.targetFileName}` },
|
|
1755
|
+
{ value: "skip", label: "Skip this rule" },
|
|
1756
|
+
{ value: "rename", label: "Rename imported rule" },
|
|
1757
|
+
],
|
|
1758
|
+
});
|
|
1759
|
+
if (isCancel(choice)) {
|
|
1760
|
+
cancel("Operation cancelled.");
|
|
1761
|
+
process.exit(1);
|
|
1762
|
+
}
|
|
1763
|
+
if (choice === "skip")
|
|
1764
|
+
return null;
|
|
1765
|
+
if (choice === "rename") {
|
|
1766
|
+
const entered = await promptText({
|
|
1767
|
+
message: `New filename (without extension) for ${options.promptLabel}`,
|
|
1768
|
+
placeholder: options.targetFileName.replace(/\.(md|mdc)$/i, ""),
|
|
1769
|
+
validate(value) {
|
|
1770
|
+
if (!value.trim())
|
|
1771
|
+
return "Name is required.";
|
|
1772
|
+
if (/[\\/]/.test(value))
|
|
1773
|
+
return "Use a simple filename.";
|
|
1774
|
+
return undefined;
|
|
1775
|
+
},
|
|
1776
|
+
});
|
|
1777
|
+
if (isCancel(entered)) {
|
|
1778
|
+
cancel("Operation cancelled.");
|
|
1779
|
+
process.exit(1);
|
|
1780
|
+
}
|
|
1781
|
+
const extension = path.extname(options.targetFileName) || ".md";
|
|
1782
|
+
const renamedFileName = `${slugify(String(entered)) || "rule"}${extension}`;
|
|
1783
|
+
return resolveRuleConflict({
|
|
1784
|
+
...options,
|
|
1785
|
+
targetFileName: renamedFileName,
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
return options.targetFileName;
|
|
1789
|
+
}
|
|
957
1790
|
async function resolveCommandsToImport(options) {
|
|
958
1791
|
const selectors = options.selectors
|
|
959
1792
|
.map((selector) => selector.trim())
|
|
@@ -1073,6 +1906,62 @@ async function resolveMcpServersToImport(options) {
|
|
|
1073
1906
|
selectionMode,
|
|
1074
1907
|
};
|
|
1075
1908
|
}
|
|
1909
|
+
async function resolveRulesToImport(options) {
|
|
1910
|
+
const selectors = options.selectors
|
|
1911
|
+
.map((selector) => selector.trim())
|
|
1912
|
+
.filter(Boolean);
|
|
1913
|
+
if (selectors.length > 0) {
|
|
1914
|
+
const { selected, unmatched } = resolveRuleSelections(options.sourceRules, selectors);
|
|
1915
|
+
if (unmatched.length > 0) {
|
|
1916
|
+
throw new Error(`Rule(s) not found in source: ${unmatched.join(", ")}. Available: ${options.sourceRules.map((item) => item.fileName).join(", ")}`);
|
|
1917
|
+
}
|
|
1918
|
+
return {
|
|
1919
|
+
selectedRules: selected,
|
|
1920
|
+
selectionMode: "custom",
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
const selectionResolution = await resolveSelectionModeWithSkip({
|
|
1924
|
+
entityLabel: "rules",
|
|
1925
|
+
selectionMode: options.selectionMode,
|
|
1926
|
+
promptForSelection: options.promptForRules,
|
|
1927
|
+
nonInteractive: options.nonInteractive,
|
|
1928
|
+
});
|
|
1929
|
+
const selectionMode = selectionResolution.selectionMode;
|
|
1930
|
+
if (selectionResolution.skipImport) {
|
|
1931
|
+
return {
|
|
1932
|
+
selectedRules: [],
|
|
1933
|
+
selectionMode: "custom",
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
if (selectionMode === "all" ||
|
|
1937
|
+
!options.promptForRules ||
|
|
1938
|
+
options.nonInteractive) {
|
|
1939
|
+
return {
|
|
1940
|
+
selectedRules: options.sourceRules,
|
|
1941
|
+
selectionMode,
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
const selected = await multiselect({
|
|
1945
|
+
message: withMultiselectHelp("Select rules to import"),
|
|
1946
|
+
options: options.sourceRules.map((item) => ({
|
|
1947
|
+
value: item.fileName,
|
|
1948
|
+
label: item.fileName,
|
|
1949
|
+
hint: item.name,
|
|
1950
|
+
})),
|
|
1951
|
+
initialValues: options.sourceRules.map((item) => item.fileName),
|
|
1952
|
+
});
|
|
1953
|
+
if (isCancel(selected)) {
|
|
1954
|
+
cancel("Operation cancelled.");
|
|
1955
|
+
process.exit(1);
|
|
1956
|
+
}
|
|
1957
|
+
const selectedNames = Array.isArray(selected)
|
|
1958
|
+
? new Set(selected.map((value) => String(value)))
|
|
1959
|
+
: new Set();
|
|
1960
|
+
return {
|
|
1961
|
+
selectedRules: options.sourceRules.filter((item) => selectedNames.has(item.fileName)),
|
|
1962
|
+
selectionMode,
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1076
1965
|
async function resolveSkillsToImport(options) {
|
|
1077
1966
|
const selectors = options.selectors
|
|
1078
1967
|
.map((item) => item.trim())
|
|
@@ -1084,6 +1973,7 @@ async function resolveSkillsToImport(options) {
|
|
|
1084
1973
|
}
|
|
1085
1974
|
return {
|
|
1086
1975
|
selectedSkills: selected,
|
|
1976
|
+
selectedSourceSkills: selectors,
|
|
1087
1977
|
selectionMode: "custom",
|
|
1088
1978
|
};
|
|
1089
1979
|
}
|
|
@@ -1097,6 +1987,7 @@ async function resolveSkillsToImport(options) {
|
|
|
1097
1987
|
if (selectionResolution.skipImport) {
|
|
1098
1988
|
return {
|
|
1099
1989
|
selectedSkills: [],
|
|
1990
|
+
selectedSourceSkills: [],
|
|
1100
1991
|
selectionMode: "custom",
|
|
1101
1992
|
};
|
|
1102
1993
|
}
|
|
@@ -1105,6 +1996,7 @@ async function resolveSkillsToImport(options) {
|
|
|
1105
1996
|
options.nonInteractive) {
|
|
1106
1997
|
return {
|
|
1107
1998
|
selectedSkills: options.sourceSkills,
|
|
1999
|
+
selectedSourceSkills: options.sourceSkills.map((skill) => skill.name),
|
|
1108
2000
|
selectionMode,
|
|
1109
2001
|
};
|
|
1110
2002
|
}
|
|
@@ -1125,6 +2017,9 @@ async function resolveSkillsToImport(options) {
|
|
|
1125
2017
|
: new Set();
|
|
1126
2018
|
return {
|
|
1127
2019
|
selectedSkills: options.sourceSkills.filter((skill) => selectedNames.has(skill.name)),
|
|
2020
|
+
selectedSourceSkills: options.sourceSkills
|
|
2021
|
+
.filter((skill) => selectedNames.has(skill.name))
|
|
2022
|
+
.map((skill) => skill.name),
|
|
1128
2023
|
selectionMode,
|
|
1129
2024
|
};
|
|
1130
2025
|
}
|