agentloom 0.1.6 → 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/dist/core/copy.js +0 -7
- package/dist/core/importer.js +338 -59
- package/dist/core/router.js +10 -2
- package/dist/core/skills.d.ts +3 -0
- package/dist/core/skills.js +26 -10
- package/dist/core/sources.d.ts +5 -0
- package/dist/core/sources.js +120 -25
- package/dist/sync/index.js +32 -4
- package/package.json +1 -1
package/dist/core/copy.js
CHANGED
|
@@ -249,13 +249,6 @@ export function formatUsageError(input) {
|
|
|
249
249
|
return lines.join("\n");
|
|
250
250
|
}
|
|
251
251
|
export function formatUnknownCommandError(command) {
|
|
252
|
-
if (command === "skills") {
|
|
253
|
-
return formatUsageError({
|
|
254
|
-
issue: 'Command "skills" was removed.',
|
|
255
|
-
usage: "agentloom skill <add|list|delete|find|update|sync> [options]",
|
|
256
|
-
example: "agentloom skill find typescript",
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
252
|
return formatUsageError({
|
|
260
253
|
issue: `Unknown command "${command}".`,
|
|
261
254
|
usage: "agentloom --help",
|
package/dist/core/importer.js
CHANGED
|
@@ -7,12 +7,12 @@ import YAML from "yaml";
|
|
|
7
7
|
import { buildAgentMarkdown, parseAgentsDir, targetFileNameForAgent, } from "./agents.js";
|
|
8
8
|
import { normalizeCommandArgumentsForCanonical, normalizeCommandSelector, parseCommandsDir, resolveCommandSelections, } from "./commands.js";
|
|
9
9
|
import { normalizeRuleSelector, parseRulesDir, resolveRuleSelections, stripRuleFileExtension, } from "./rules.js";
|
|
10
|
-
import { applySkillProviderSideEffects, copySkillArtifacts, normalizeSkillSelector, parseSkillsDir, resolveSkillSelections, skillContentMatchesTarget, } from "./skills.js";
|
|
10
|
+
import { applySkillProviderSideEffects, copySkillArtifacts, normalizeSkillSelector, parseSkillsDir, resolveSkillSelector, resolveSkillSelections, skillContentMatchesTarget, } from "./skills.js";
|
|
11
11
|
import { ensureDir, hashContent, isObject, readJsonIfExists, relativePosix, slugify, writeTextAtomic, } from "./fs.js";
|
|
12
12
|
import { ALL_PROVIDERS } from "../types.js";
|
|
13
13
|
import { readLockfile, upsertLockEntry, writeLockfile } from "./lockfile.js";
|
|
14
14
|
import { readCanonicalMcp, writeCanonicalMcp } from "./mcp.js";
|
|
15
|
-
import {
|
|
15
|
+
import { discoverPluginSourceRoots, discoverSourceAgentsDirs, discoverSourceCommandsDirs, discoverSourceMcpPaths, discoverSourceRulesDirs, discoverSourceSkillsDirs, prepareSource, } from "./sources.js";
|
|
16
16
|
import { isProviderEntityFileName } from "./provider-entity-validation.js";
|
|
17
17
|
export class NonInteractiveConflictError extends Error {
|
|
18
18
|
constructor(message) {
|
|
@@ -51,67 +51,77 @@ export async function importSource(options) {
|
|
|
51
51
|
? `${options.source} (subdir: ${options.subdir})`
|
|
52
52
|
: options.source;
|
|
53
53
|
try {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
const pluginSourceRoots = discoverPluginSourceRoots(prepared.importRoot);
|
|
55
|
+
const sourceAgentsDirs = shouldImportAgents
|
|
56
|
+
? discoverSourceAgentsDirs(prepared.importRoot)
|
|
57
|
+
: [];
|
|
57
58
|
const sourceCommandsDirs = shouldImportCommands
|
|
58
59
|
? discoverSourceCommandsDirs(prepared.importRoot)
|
|
59
60
|
: [];
|
|
60
|
-
const
|
|
61
|
-
(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
? discoverSourceSkillsDir(prepared.importRoot)
|
|
72
|
-
: null;
|
|
73
|
-
const sourceAgents = sourceAgentsDir
|
|
74
|
-
? parseSourceAgentsForImport(sourceAgentsDir)
|
|
61
|
+
const sourceMcpPaths = shouldImportMcp
|
|
62
|
+
? discoverSourceMcpPaths(prepared.importRoot)
|
|
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)
|
|
75
72
|
: [];
|
|
76
73
|
const sourceCommands = sourceCommandsDirs.length > 0
|
|
77
|
-
? parseSourceCommandsForImport(sourceCommandsDirs)
|
|
74
|
+
? parseSourceCommandsForImport(sourceCommandsDirs, pluginSourceRoots)
|
|
78
75
|
: [];
|
|
79
|
-
const sourceMcp =
|
|
80
|
-
?
|
|
76
|
+
const sourceMcp = sourceMcpPaths.length > 0
|
|
77
|
+
? parseSourceMcpForImport(sourceMcpPaths, pluginSourceRoots)
|
|
81
78
|
: null;
|
|
82
|
-
const sourceRules =
|
|
83
|
-
|
|
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;
|
|
84
90
|
const hasExplicitCommandSelection = (options.commandSelectors?.length ?? 0) > 0;
|
|
85
91
|
const isAggregateImport = shouldImportAgents &&
|
|
86
92
|
shouldImportCommands &&
|
|
87
93
|
shouldImportMcp &&
|
|
88
94
|
(options.importRules === undefined || shouldImportRules) &&
|
|
89
95
|
shouldImportSkills;
|
|
90
|
-
if (shouldImportAgents && requireAgents &&
|
|
91
|
-
throw new Error(`No source agents directory found under ${prepared.importRoot} (expected agents/, .agents/agents/, or .github/agents/).`);
|
|
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).`);
|
|
92
98
|
}
|
|
93
99
|
if (shouldImportAgents && requireAgents && sourceAgents.length === 0) {
|
|
94
100
|
throw new Error(`No agent files found in ${sourceAgentsDir}.`);
|
|
95
101
|
}
|
|
96
|
-
if (shouldImportCommands &&
|
|
97
|
-
|
|
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).`);
|
|
98
106
|
}
|
|
99
107
|
if (shouldImportCommands &&
|
|
100
108
|
options.requireCommands &&
|
|
101
109
|
sourceCommands.length === 0) {
|
|
102
110
|
throw new Error(`No command files found in ${sourceCommandsDir}.`);
|
|
103
111
|
}
|
|
104
|
-
if (shouldImportMcp && options.requireMcp &&
|
|
105
|
-
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).`);
|
|
106
114
|
}
|
|
107
|
-
if (shouldImportRules && requireRules &&
|
|
108
|
-
throw new Error(`No source rules directory found under ${prepared.importRoot} (expected .agents/rules/ or rules/).`);
|
|
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).`);
|
|
109
117
|
}
|
|
110
118
|
if (shouldImportRules && requireRules && sourceRules.length === 0) {
|
|
111
119
|
throw new Error(`No rule files found in ${sourceRulesDir}.`);
|
|
112
120
|
}
|
|
113
|
-
if (shouldImportSkills &&
|
|
114
|
-
|
|
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).`);
|
|
115
125
|
}
|
|
116
126
|
if (shouldImportSkills &&
|
|
117
127
|
options.requireSkills &&
|
|
@@ -124,7 +134,7 @@ export async function importSource(options) {
|
|
|
124
134
|
sourceRules.length === 0 &&
|
|
125
135
|
sourceSkills.length === 0 &&
|
|
126
136
|
Object.keys(sourceMcp?.mcpServers ?? {}).length === 0) {
|
|
127
|
-
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/,
|
|
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.`);
|
|
128
138
|
}
|
|
129
139
|
const shouldResolveAgents = shouldImportAgents &&
|
|
130
140
|
(sourceAgents.length > 0 ||
|
|
@@ -198,8 +208,8 @@ export async function importSource(options) {
|
|
|
198
208
|
selectionMode: options.selectionMode,
|
|
199
209
|
});
|
|
200
210
|
selectedSkills = skillSelection.selectedSkills;
|
|
211
|
+
selectedSourceSkills = skillSelection.selectedSourceSkills;
|
|
201
212
|
skillSelectionMode = skillSelection.selectionMode;
|
|
202
|
-
selectedSourceSkills = selectedSkills.map((skill) => skill.name);
|
|
203
213
|
}
|
|
204
214
|
const importedAgents = [];
|
|
205
215
|
if (shouldImportAgents && selection.selectedAgents.length > 0) {
|
|
@@ -331,10 +341,16 @@ export async function importSource(options) {
|
|
|
331
341
|
ensureDir(options.paths.skillsDir);
|
|
332
342
|
}
|
|
333
343
|
for (const [index, sourceSkill] of selectedSkills.entries()) {
|
|
334
|
-
|
|
335
|
-
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);
|
|
336
348
|
if (mappedTargetSkillDirName) {
|
|
337
|
-
targetSkillDirName =
|
|
349
|
+
targetSkillDirName =
|
|
350
|
+
mappedTargetSkillDirName === legacySkillDirName &&
|
|
351
|
+
legacySkillDirName !== canonicalSkillDirName
|
|
352
|
+
? canonicalSkillDirName
|
|
353
|
+
: mappedTargetSkillDirName;
|
|
338
354
|
}
|
|
339
355
|
else if (options.rename &&
|
|
340
356
|
selectedSkills.length === 1 &&
|
|
@@ -347,6 +363,10 @@ export async function importSource(options) {
|
|
|
347
363
|
const resolvedSkillDirName = await resolveSkillConflict({
|
|
348
364
|
sourceSkill,
|
|
349
365
|
targetSkillDirName,
|
|
366
|
+
legacySkillDirName: targetSkillDirName === canonicalSkillDirName
|
|
367
|
+
? legacySkillDirName
|
|
368
|
+
: undefined,
|
|
369
|
+
canonicalSkillDirName,
|
|
350
370
|
paths: options.paths,
|
|
351
371
|
yes: !!options.yes,
|
|
352
372
|
nonInteractive: !!options.nonInteractive,
|
|
@@ -355,10 +375,25 @@ export async function importSource(options) {
|
|
|
355
375
|
if (!resolvedSkillDirName)
|
|
356
376
|
continue;
|
|
357
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
|
+
}
|
|
358
386
|
if (!skillContentMatchesTarget(sourceSkill, targetSkillDir)) {
|
|
359
387
|
fs.rmSync(targetSkillDir, { recursive: true, force: true });
|
|
360
388
|
copySkillArtifacts(sourceSkill, targetSkillDir);
|
|
361
389
|
}
|
|
390
|
+
if (resolvedSkillDirName === canonicalSkillDirName) {
|
|
391
|
+
removeLegacySkillDirectory({
|
|
392
|
+
legacySkillDirName,
|
|
393
|
+
canonicalSkillDirName,
|
|
394
|
+
paths: options.paths,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
362
397
|
importedSkills.push(resolvedSkillDirName);
|
|
363
398
|
telemetrySkills.push({
|
|
364
399
|
name: sourceSkill.name,
|
|
@@ -429,6 +464,7 @@ export async function importSource(options) {
|
|
|
429
464
|
selectedSourceMcpServers: selectedSourceMcpServersForLock,
|
|
430
465
|
selectedSourceRules: selectedSourceRulesForLock,
|
|
431
466
|
selectedSourceSkills: selectedSourceSkillsForLock,
|
|
467
|
+
selectedSkills,
|
|
432
468
|
skillsProviders: skillsProvidersForLock,
|
|
433
469
|
});
|
|
434
470
|
const shouldMergeCommandOnlyEntry = isCommandOnlyImport &&
|
|
@@ -605,17 +641,164 @@ export async function importSource(options) {
|
|
|
605
641
|
prepared.cleanup();
|
|
606
642
|
}
|
|
607
643
|
}
|
|
608
|
-
function parseSourceAgentsForImport(
|
|
609
|
-
|
|
610
|
-
|
|
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
|
+
});
|
|
611
784
|
}
|
|
612
|
-
return parseAgentsDir(sourceAgentsDir);
|
|
613
785
|
}
|
|
614
|
-
function
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
return
|
|
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;
|
|
619
802
|
}
|
|
620
803
|
function parseSourceCommandsFromDir(sourceCommandsDir) {
|
|
621
804
|
if (isGeminiCommandsDir(sourceCommandsDir)) {
|
|
@@ -1117,7 +1300,7 @@ function findMatchingLockEntry(entries, key) {
|
|
|
1117
1300
|
sameStringSelectionForMatch(entry.selectedSourceCommands, key.selectedSourceCommands) &&
|
|
1118
1301
|
sameStringSelectionForMatch(entry.selectedSourceMcpServers, key.selectedSourceMcpServers) &&
|
|
1119
1302
|
sameStringSelectionForMatch(entry.selectedSourceRules, key.selectedSourceRules, { wildcardWhenRightIsUndefined: true }) &&
|
|
1120
|
-
|
|
1303
|
+
sameSkillSelectionForMatch(entry.selectedSourceSkills, key.selectedSourceSkills, key.selectedSkills, { wildcardWhenRightIsUndefined: true }) &&
|
|
1121
1304
|
sameStringSelectionForMatch(entry.skillsProviders, key.skillsProviders, {
|
|
1122
1305
|
wildcardWhenRightIsUndefined: true,
|
|
1123
1306
|
}));
|
|
@@ -1161,6 +1344,39 @@ function sameStringSelectionForMatch(left, right, options = {}) {
|
|
|
1161
1344
|
}
|
|
1162
1345
|
return normalizedLeft.every((value, index) => value === normalizedRight[index]);
|
|
1163
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
|
+
}
|
|
1164
1380
|
function uniqueStrings(values) {
|
|
1165
1381
|
return [...new Set(values)];
|
|
1166
1382
|
}
|
|
@@ -1279,20 +1495,65 @@ function normalizeSkillRenameMap(renameMap) {
|
|
|
1279
1495
|
return undefined;
|
|
1280
1496
|
return Object.fromEntries(normalizedEntries);
|
|
1281
1497
|
}
|
|
1282
|
-
function resolveMappedTargetSkillName(
|
|
1498
|
+
function resolveMappedTargetSkillName(sourceSkill, selectedSkills, renameMap) {
|
|
1283
1499
|
if (!renameMap)
|
|
1284
1500
|
return undefined;
|
|
1285
|
-
const normalizedSourceName = normalizeSkillSelector(sourceSkillName);
|
|
1286
|
-
if (!normalizedSourceName)
|
|
1287
|
-
return undefined;
|
|
1288
1501
|
for (const [sourceSelector, importedName] of Object.entries(renameMap)) {
|
|
1289
|
-
|
|
1502
|
+
const matchedSkill = resolveSkillSelector(selectedSkills, sourceSelector);
|
|
1503
|
+
if (!matchedSkill || matchedSkill.sourcePath !== sourceSkill.sourcePath) {
|
|
1290
1504
|
continue;
|
|
1291
1505
|
}
|
|
1292
1506
|
return slugify(path.basename(importedName.trim())) || "skill";
|
|
1293
1507
|
}
|
|
1294
1508
|
return undefined;
|
|
1295
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
|
+
}
|
|
1296
1557
|
function normalizeSkillsProviders(providers) {
|
|
1297
1558
|
if (!providers || providers.length === 0)
|
|
1298
1559
|
return undefined;
|
|
@@ -1307,12 +1568,13 @@ function normalizeSkillsProviders(providers) {
|
|
|
1307
1568
|
}
|
|
1308
1569
|
async function resolveSkillConflict(options) {
|
|
1309
1570
|
const targetPath = path.join(options.paths.skillsDir, options.targetSkillDirName);
|
|
1310
|
-
|
|
1571
|
+
const conflictPath = resolveExistingSkillConflictPath(options, targetPath);
|
|
1572
|
+
if (!conflictPath)
|
|
1311
1573
|
return options.targetSkillDirName;
|
|
1312
|
-
if (!fs.statSync(
|
|
1313
|
-
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.`);
|
|
1314
1576
|
}
|
|
1315
|
-
if (skillContentMatchesTarget(options.sourceSkill,
|
|
1577
|
+
if (skillContentMatchesTarget(options.sourceSkill, conflictPath)) {
|
|
1316
1578
|
return options.targetSkillDirName;
|
|
1317
1579
|
}
|
|
1318
1580
|
if (options.yes) {
|
|
@@ -1359,6 +1621,17 @@ async function resolveSkillConflict(options) {
|
|
|
1359
1621
|
}
|
|
1360
1622
|
return options.targetSkillDirName;
|
|
1361
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
|
+
}
|
|
1362
1635
|
async function resolveAgentConflict(options) {
|
|
1363
1636
|
const targetPath = path.join(options.paths.agentsDir, options.targetFileName);
|
|
1364
1637
|
if (!fs.existsSync(targetPath))
|
|
@@ -1700,6 +1973,7 @@ async function resolveSkillsToImport(options) {
|
|
|
1700
1973
|
}
|
|
1701
1974
|
return {
|
|
1702
1975
|
selectedSkills: selected,
|
|
1976
|
+
selectedSourceSkills: selectors,
|
|
1703
1977
|
selectionMode: "custom",
|
|
1704
1978
|
};
|
|
1705
1979
|
}
|
|
@@ -1713,6 +1987,7 @@ async function resolveSkillsToImport(options) {
|
|
|
1713
1987
|
if (selectionResolution.skipImport) {
|
|
1714
1988
|
return {
|
|
1715
1989
|
selectedSkills: [],
|
|
1990
|
+
selectedSourceSkills: [],
|
|
1716
1991
|
selectionMode: "custom",
|
|
1717
1992
|
};
|
|
1718
1993
|
}
|
|
@@ -1721,6 +1996,7 @@ async function resolveSkillsToImport(options) {
|
|
|
1721
1996
|
options.nonInteractive) {
|
|
1722
1997
|
return {
|
|
1723
1998
|
selectedSkills: options.sourceSkills,
|
|
1999
|
+
selectedSourceSkills: options.sourceSkills.map((skill) => skill.name),
|
|
1724
2000
|
selectionMode,
|
|
1725
2001
|
};
|
|
1726
2002
|
}
|
|
@@ -1741,6 +2017,9 @@ async function resolveSkillsToImport(options) {
|
|
|
1741
2017
|
: new Set();
|
|
1742
2018
|
return {
|
|
1743
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),
|
|
1744
2023
|
selectionMode,
|
|
1745
2024
|
};
|
|
1746
2025
|
}
|
package/dist/core/router.js
CHANGED
|
@@ -14,6 +14,13 @@ const ENTITY_NOUNS = new Set([
|
|
|
14
14
|
"rule",
|
|
15
15
|
"skill",
|
|
16
16
|
]);
|
|
17
|
+
const ENTITY_NOUN_ALIASES = {
|
|
18
|
+
agents: "agent",
|
|
19
|
+
commands: "command",
|
|
20
|
+
mcps: "mcp",
|
|
21
|
+
rules: "rule",
|
|
22
|
+
skills: "skill",
|
|
23
|
+
};
|
|
17
24
|
const ENTITY_VERBS = new Set([
|
|
18
25
|
"add",
|
|
19
26
|
"list",
|
|
@@ -24,9 +31,10 @@ const ENTITY_VERBS = new Set([
|
|
|
24
31
|
]);
|
|
25
32
|
const MCP_SERVER_VERBS = new Set(["add", "list", "delete"]);
|
|
26
33
|
export function parseCommandRoute(argv) {
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
34
|
+
const rawRoot = argv[0]?.trim().toLowerCase();
|
|
35
|
+
if (!rawRoot)
|
|
29
36
|
return null;
|
|
37
|
+
const root = ENTITY_NOUN_ALIASES[rawRoot] ?? rawRoot;
|
|
30
38
|
if (AGGREGATE_VERBS.has(root)) {
|
|
31
39
|
return {
|
|
32
40
|
mode: "aggregate",
|
package/dist/core/skills.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Provider, ScopePaths } from "../types.js";
|
|
2
2
|
export interface CanonicalSkill {
|
|
3
3
|
name: string;
|
|
4
|
+
aliases: string[];
|
|
5
|
+
sourceDirName: string;
|
|
4
6
|
sourcePath: string;
|
|
5
7
|
skillPath: string;
|
|
6
8
|
layout: "nested" | "root";
|
|
@@ -8,6 +10,7 @@ export interface CanonicalSkill {
|
|
|
8
10
|
export declare const ROOT_SKILL_ARTIFACT_DIRS: readonly ["references", "assets", "scripts", "templates", "examples"];
|
|
9
11
|
export declare function parseSkillsDir(skillsDir: string): CanonicalSkill[];
|
|
10
12
|
export declare function normalizeSkillSelector(value: string): string;
|
|
13
|
+
export declare function resolveSkillSelector(skills: CanonicalSkill[], selector: string): CanonicalSkill | null;
|
|
11
14
|
export declare function resolveSkillSelections(skills: CanonicalSkill[], selectors: string[]): {
|
|
12
15
|
selected: CanonicalSkill[];
|
|
13
16
|
unmatched: string[];
|
package/dist/core/skills.js
CHANGED
|
@@ -22,8 +22,12 @@ export function parseSkillsDir(skillsDir) {
|
|
|
22
22
|
const skillFile = path.join(skillDir, "SKILL.md");
|
|
23
23
|
if (!fs.existsSync(skillFile))
|
|
24
24
|
continue;
|
|
25
|
+
const raw = fs.readFileSync(skillFile, "utf8");
|
|
26
|
+
const canonicalName = extractSkillName(raw) || entry.name;
|
|
25
27
|
skills.push({
|
|
26
|
-
name:
|
|
28
|
+
name: canonicalName,
|
|
29
|
+
aliases: buildSkillAliases(canonicalName, entry.name),
|
|
30
|
+
sourceDirName: entry.name,
|
|
27
31
|
sourcePath: skillDir,
|
|
28
32
|
skillPath: skillFile,
|
|
29
33
|
layout: "nested",
|
|
@@ -37,9 +41,12 @@ export function parseSkillsDir(skillsDir) {
|
|
|
37
41
|
return [];
|
|
38
42
|
}
|
|
39
43
|
const raw = fs.readFileSync(rootSkillFile, "utf8");
|
|
44
|
+
const canonicalName = extractSkillName(raw) || path.basename(skillsDir);
|
|
40
45
|
return [
|
|
41
46
|
{
|
|
42
|
-
name:
|
|
47
|
+
name: canonicalName,
|
|
48
|
+
aliases: buildSkillAliases(canonicalName, path.basename(skillsDir)),
|
|
49
|
+
sourceDirName: path.basename(skillsDir),
|
|
43
50
|
sourcePath: skillsDir,
|
|
44
51
|
skillPath: rootSkillFile,
|
|
45
52
|
layout: "root",
|
|
@@ -61,6 +68,16 @@ function extractSkillName(raw) {
|
|
|
61
68
|
export function normalizeSkillSelector(value) {
|
|
62
69
|
return slugify(value.trim().replace(/\/+$/, "")).toLowerCase();
|
|
63
70
|
}
|
|
71
|
+
export function resolveSkillSelector(skills, selector) {
|
|
72
|
+
const normalizedSelector = normalizeSkillSelector(selector);
|
|
73
|
+
if (!normalizedSelector)
|
|
74
|
+
return null;
|
|
75
|
+
const canonicalMatch = skills.find((skill) => normalizeSkillSelector(skill.name) === normalizedSelector) ?? null;
|
|
76
|
+
if (canonicalMatch) {
|
|
77
|
+
return canonicalMatch;
|
|
78
|
+
}
|
|
79
|
+
return (skills.find((skill) => normalizeSkillSelector(skill.sourceDirName) === normalizedSelector) ?? null);
|
|
80
|
+
}
|
|
64
81
|
export function resolveSkillSelections(skills, selectors) {
|
|
65
82
|
const normalizedSelectors = selectors
|
|
66
83
|
.map((item) => item.trim())
|
|
@@ -70,14 +87,12 @@ export function resolveSkillSelections(skills, selectors) {
|
|
|
70
87
|
const selectedMap = new Map();
|
|
71
88
|
const unmatched = [];
|
|
72
89
|
for (const selector of normalizedSelectors) {
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
90
|
+
const match = resolveSkillSelector(skills, selector);
|
|
91
|
+
if (!match) {
|
|
75
92
|
unmatched.push(selector);
|
|
76
93
|
continue;
|
|
77
94
|
}
|
|
78
|
-
|
|
79
|
-
selectedMap.set(match.name, match);
|
|
80
|
-
}
|
|
95
|
+
selectedMap.set(match.name, match);
|
|
81
96
|
}
|
|
82
97
|
return {
|
|
83
98
|
selected: [...selectedMap.values()],
|
|
@@ -195,9 +210,7 @@ function enforceProviderSkillsSymlink(options) {
|
|
|
195
210
|
function migrateProviderSkillsIntoCanonical(options) {
|
|
196
211
|
const providerSkills = parseSkillsDir(options.providerSkillsDir);
|
|
197
212
|
for (const skill of providerSkills) {
|
|
198
|
-
const targetSkillDirName = skill.
|
|
199
|
-
? path.basename(skill.sourcePath)
|
|
200
|
-
: slugify(skill.name) || "skill";
|
|
213
|
+
const targetSkillDirName = slugify(skill.name) || "skill";
|
|
201
214
|
const targetSkillDir = path.join(options.canonicalSkillsDir, targetSkillDirName);
|
|
202
215
|
if (fs.existsSync(targetSkillDir)) {
|
|
203
216
|
const sameContent = skill.layout === "nested"
|
|
@@ -218,6 +231,9 @@ function migrateProviderSkillsIntoCanonical(options) {
|
|
|
218
231
|
copyRootSkillArtifacts(skill.sourcePath, targetSkillDir);
|
|
219
232
|
}
|
|
220
233
|
}
|
|
234
|
+
function buildSkillAliases(name, sourceDirName) {
|
|
235
|
+
return [...new Set([name, sourceDirName].filter(Boolean))];
|
|
236
|
+
}
|
|
221
237
|
function moveDirectory(sourceDir, targetDir) {
|
|
222
238
|
ensureDir(path.dirname(targetDir));
|
|
223
239
|
try {
|
package/dist/core/sources.d.ts
CHANGED
|
@@ -16,9 +16,14 @@ export declare function prepareSource(options: {
|
|
|
16
16
|
ref?: string;
|
|
17
17
|
subdir?: string;
|
|
18
18
|
}): PreparedSource;
|
|
19
|
+
export declare function discoverPluginSourceRoots(importRoot: string): string[];
|
|
19
20
|
export declare function discoverSourceAgentsDir(importRoot: string): string | null;
|
|
21
|
+
export declare function discoverSourceAgentsDirs(importRoot: string): string[];
|
|
20
22
|
export declare function discoverSourceMcpPath(importRoot: string): string | null;
|
|
23
|
+
export declare function discoverSourceMcpPaths(importRoot: string): string[];
|
|
21
24
|
export declare function discoverSourceCommandsDirs(importRoot: string): string[];
|
|
22
25
|
export declare function discoverSourceCommandsDir(importRoot: string): string | null;
|
|
23
26
|
export declare function discoverSourceSkillsDir(importRoot: string): string | null;
|
|
27
|
+
export declare function discoverSourceSkillsDirs(importRoot: string): string[];
|
|
24
28
|
export declare function discoverSourceRulesDir(importRoot: string): string | null;
|
|
29
|
+
export declare function discoverSourceRulesDirs(importRoot: string): string[];
|
package/dist/core/sources.js
CHANGED
|
@@ -56,31 +56,72 @@ export function prepareSource(options) {
|
|
|
56
56
|
},
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
|
-
export function
|
|
60
|
-
const
|
|
61
|
-
if (fs.existsSync(
|
|
62
|
-
|
|
59
|
+
export function discoverPluginSourceRoots(importRoot) {
|
|
60
|
+
const marketplacePath = path.join(importRoot, ".claude-plugin", "marketplace.json");
|
|
61
|
+
if (!fs.existsSync(marketplacePath) ||
|
|
62
|
+
!fs.statSync(marketplacePath).isFile()) {
|
|
63
|
+
return [];
|
|
63
64
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
let parsed;
|
|
66
|
+
try {
|
|
67
|
+
parsed = JSON.parse(fs.readFileSync(marketplacePath, "utf8"));
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const plugins = Array.isArray(parsed?.plugins)
|
|
73
|
+
? parsed.plugins
|
|
74
|
+
: [];
|
|
75
|
+
const discoveredRoots = [];
|
|
76
|
+
for (const plugin of plugins) {
|
|
77
|
+
if (!plugin ||
|
|
78
|
+
typeof plugin !== "object" ||
|
|
79
|
+
Array.isArray(plugin) ||
|
|
80
|
+
typeof plugin.source !== "string") {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const source = plugin.source.trim();
|
|
84
|
+
if (!source)
|
|
85
|
+
continue;
|
|
86
|
+
const pluginRoot = path.resolve(importRoot, source);
|
|
87
|
+
if (!isPathWithinRoot(importRoot, pluginRoot)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!fs.existsSync(pluginRoot) || !fs.statSync(pluginRoot).isDirectory()) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
discoveredRoots.push(pluginRoot);
|
|
71
94
|
}
|
|
72
|
-
return
|
|
95
|
+
return dedupePaths(discoveredRoots);
|
|
96
|
+
}
|
|
97
|
+
export function discoverSourceAgentsDir(importRoot) {
|
|
98
|
+
return discoverSourceAgentsDirs(importRoot)[0] ?? null;
|
|
99
|
+
}
|
|
100
|
+
export function discoverSourceAgentsDirs(importRoot) {
|
|
101
|
+
const direct = discoverSourceAgentsDirsForRoot(importRoot);
|
|
102
|
+
if (direct.length > 0) {
|
|
103
|
+
return direct;
|
|
104
|
+
}
|
|
105
|
+
return dedupePaths(discoverPluginSourceRoots(importRoot).flatMap((pluginRoot) => discoverSourceAgentsDirsForRoot(pluginRoot)));
|
|
73
106
|
}
|
|
74
107
|
export function discoverSourceMcpPath(importRoot) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const direct =
|
|
79
|
-
if (
|
|
108
|
+
return discoverSourceMcpPaths(importRoot)[0] ?? null;
|
|
109
|
+
}
|
|
110
|
+
export function discoverSourceMcpPaths(importRoot) {
|
|
111
|
+
const direct = discoverSourceMcpPathsForRoot(importRoot);
|
|
112
|
+
if (direct.length > 0) {
|
|
80
113
|
return direct;
|
|
81
|
-
|
|
114
|
+
}
|
|
115
|
+
return dedupePaths(discoverPluginSourceRoots(importRoot).flatMap((pluginRoot) => discoverSourceMcpPathsForRoot(pluginRoot)));
|
|
82
116
|
}
|
|
83
117
|
export function discoverSourceCommandsDirs(importRoot) {
|
|
118
|
+
const direct = discoverSourceCommandsDirsForRoot(importRoot);
|
|
119
|
+
if (direct.length > 0) {
|
|
120
|
+
return direct;
|
|
121
|
+
}
|
|
122
|
+
return dedupePaths(discoverPluginSourceRoots(importRoot).flatMap((pluginRoot) => discoverSourceCommandsDirsForRoot(pluginRoot)));
|
|
123
|
+
}
|
|
124
|
+
function discoverSourceCommandsDirsForRoot(importRoot) {
|
|
84
125
|
const nested = path.join(importRoot, ".agents", "commands");
|
|
85
126
|
if (fs.existsSync(nested) && fs.statSync(nested).isDirectory()) {
|
|
86
127
|
return [nested];
|
|
@@ -110,30 +151,84 @@ export function discoverSourceCommandsDir(importRoot) {
|
|
|
110
151
|
return discoverSourceCommandsDirs(importRoot)[0] ?? null;
|
|
111
152
|
}
|
|
112
153
|
export function discoverSourceSkillsDir(importRoot) {
|
|
154
|
+
return discoverSourceSkillsDirs(importRoot)[0] ?? null;
|
|
155
|
+
}
|
|
156
|
+
export function discoverSourceSkillsDirs(importRoot) {
|
|
157
|
+
const direct = discoverSourceSkillsDirsForRoot(importRoot);
|
|
158
|
+
if (direct.length > 0) {
|
|
159
|
+
return direct;
|
|
160
|
+
}
|
|
161
|
+
return dedupePaths(discoverPluginSourceRoots(importRoot).flatMap((pluginRoot) => discoverSourceSkillsDirsForRoot(pluginRoot)));
|
|
162
|
+
}
|
|
163
|
+
export function discoverSourceRulesDir(importRoot) {
|
|
164
|
+
return discoverSourceRulesDirs(importRoot)[0] ?? null;
|
|
165
|
+
}
|
|
166
|
+
export function discoverSourceRulesDirs(importRoot) {
|
|
167
|
+
const direct = discoverSourceRulesDirsForRoot(importRoot);
|
|
168
|
+
if (direct.length > 0) {
|
|
169
|
+
return direct;
|
|
170
|
+
}
|
|
171
|
+
return dedupePaths(discoverPluginSourceRoots(importRoot).flatMap((pluginRoot) => discoverSourceRulesDirsForRoot(pluginRoot)));
|
|
172
|
+
}
|
|
173
|
+
function discoverSourceAgentsDirsForRoot(importRoot) {
|
|
174
|
+
const direct = path.join(importRoot, "agents");
|
|
175
|
+
if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) {
|
|
176
|
+
return [direct];
|
|
177
|
+
}
|
|
178
|
+
const nested = path.join(importRoot, ".agents", "agents");
|
|
179
|
+
if (fs.existsSync(nested) && fs.statSync(nested).isDirectory()) {
|
|
180
|
+
return [nested];
|
|
181
|
+
}
|
|
182
|
+
const githubAgents = path.join(importRoot, ".github", "agents");
|
|
183
|
+
if (fs.existsSync(githubAgents) && fs.statSync(githubAgents).isDirectory()) {
|
|
184
|
+
return [githubAgents];
|
|
185
|
+
}
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
function discoverSourceMcpPathsForRoot(importRoot) {
|
|
189
|
+
const nested = path.join(importRoot, ".agents", "mcp.json");
|
|
190
|
+
if (fs.existsSync(nested) && fs.statSync(nested).isFile()) {
|
|
191
|
+
return [nested];
|
|
192
|
+
}
|
|
193
|
+
const direct = path.join(importRoot, "mcp.json");
|
|
194
|
+
if (fs.existsSync(direct) && fs.statSync(direct).isFile()) {
|
|
195
|
+
return [direct];
|
|
196
|
+
}
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
function discoverSourceSkillsDirsForRoot(importRoot) {
|
|
113
200
|
const nested = path.join(importRoot, ".agents", "skills");
|
|
114
201
|
if (fs.existsSync(nested) && fs.statSync(nested).isDirectory()) {
|
|
115
|
-
return nested;
|
|
202
|
+
return [nested];
|
|
116
203
|
}
|
|
117
204
|
const direct = path.join(importRoot, "skills");
|
|
118
205
|
if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) {
|
|
119
|
-
return direct;
|
|
206
|
+
return [direct];
|
|
120
207
|
}
|
|
121
208
|
const rootSkill = path.join(importRoot, "SKILL.md");
|
|
122
209
|
if (fs.existsSync(rootSkill) && fs.statSync(rootSkill).isFile()) {
|
|
123
|
-
return importRoot;
|
|
210
|
+
return [importRoot];
|
|
124
211
|
}
|
|
125
|
-
return
|
|
212
|
+
return [];
|
|
126
213
|
}
|
|
127
|
-
|
|
214
|
+
function discoverSourceRulesDirsForRoot(importRoot) {
|
|
128
215
|
const nested = path.join(importRoot, ".agents", "rules");
|
|
129
216
|
if (fs.existsSync(nested) && fs.statSync(nested).isDirectory()) {
|
|
130
|
-
return nested;
|
|
217
|
+
return [nested];
|
|
131
218
|
}
|
|
132
219
|
const direct = path.join(importRoot, "rules");
|
|
133
220
|
if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) {
|
|
134
|
-
return direct;
|
|
221
|
+
return [direct];
|
|
135
222
|
}
|
|
136
|
-
return
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
function dedupePaths(paths) {
|
|
226
|
+
return [...new Set(paths)];
|
|
227
|
+
}
|
|
228
|
+
function isPathWithinRoot(rootPath, targetPath) {
|
|
229
|
+
const relative = path.relative(rootPath, targetPath);
|
|
230
|
+
return (relative === "" ||
|
|
231
|
+
(!relative.startsWith("..") && !path.isAbsolute(relative)));
|
|
137
232
|
}
|
|
138
233
|
function resolveImportRoot(rootPath, subdir) {
|
|
139
234
|
if (!subdir)
|
package/dist/sync/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export async function syncFromCanonical(options) {
|
|
|
24
24
|
const agents = parseAgentsDir(options.paths.agentsDir);
|
|
25
25
|
const commands = parseCommandsDir(options.paths.commandsDir);
|
|
26
26
|
const rules = parseRulesDir(options.paths.rulesDir);
|
|
27
|
+
const managedInstructionRules = rules.filter((rule) => rule.frontmatter.alwaysApply !== false);
|
|
27
28
|
const mcp = readCanonicalMcp(options.paths);
|
|
28
29
|
const manifest = readManifest(options.paths);
|
|
29
30
|
const effectiveManifest = {
|
|
@@ -100,7 +101,7 @@ export async function syncFromCanonical(options) {
|
|
|
100
101
|
syncManagedRuleInstructions({
|
|
101
102
|
paths: options.paths,
|
|
102
103
|
providers,
|
|
103
|
-
rules,
|
|
104
|
+
rules: managedInstructionRules,
|
|
104
105
|
generated: generatedRules,
|
|
105
106
|
updated: updatedRuleInstructionFiles,
|
|
106
107
|
retained: retainedRuleInstructionFiles,
|
|
@@ -507,7 +508,18 @@ function syncManagedRuleInstructions(options) {
|
|
|
507
508
|
}
|
|
508
509
|
const cleanupTargets = new Set(getRuleInstructionPaths(options.paths, cleanupProviders));
|
|
509
510
|
for (const targetPath of cleanupTargets) {
|
|
510
|
-
|
|
511
|
+
if (!fs.existsSync(targetPath)) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const stat = fs.lstatSync(targetPath);
|
|
515
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
516
|
+
options.retained.add(targetPath);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const existing = readTextIfExists(targetPath);
|
|
520
|
+
if (existing === null) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
511
523
|
const next = upsertManagedRuleBlocks(existing, activeTargets.has(targetPath) ? options.rules : []);
|
|
512
524
|
const shouldTrackGenerated = activeTargets.has(targetPath) &&
|
|
513
525
|
options.rules.length > 0 &&
|
|
@@ -531,7 +543,6 @@ function syncManagedRuleInstructions(options) {
|
|
|
531
543
|
}
|
|
532
544
|
}
|
|
533
545
|
else {
|
|
534
|
-
ensureDir(path.dirname(targetPath));
|
|
535
546
|
writeTextAtomic(targetPath, next);
|
|
536
547
|
}
|
|
537
548
|
}
|
|
@@ -550,7 +561,8 @@ function syncCopilotDiscoverySettings(options) {
|
|
|
550
561
|
appendPathSetting(settings, "chat.agentFilesLocations", path.join(options.paths.homeDir, ".copilot", "agents"));
|
|
551
562
|
}
|
|
552
563
|
if (options.includeInstructionLocations) {
|
|
553
|
-
|
|
564
|
+
const instructionPath = path.join(options.paths.homeDir, ".copilot", "copilot-instructions.md");
|
|
565
|
+
setPathSettingEnabled(settings, "chat.instructionsFilesLocations", instructionPath, fs.existsSync(instructionPath));
|
|
554
566
|
}
|
|
555
567
|
maybeWriteJson(settingsPath, settings, options.dryRun);
|
|
556
568
|
}
|
|
@@ -724,6 +736,22 @@ function appendPathSetting(settings, key, settingPath) {
|
|
|
724
736
|
}
|
|
725
737
|
settings[key] = existing;
|
|
726
738
|
}
|
|
739
|
+
function setPathSettingEnabled(settings, key, settingPath, enabled) {
|
|
740
|
+
if (enabled) {
|
|
741
|
+
appendPathSetting(settings, key, settingPath);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const existing = Array.isArray(settings[key])
|
|
745
|
+
? settings[key].filter((value) => typeof value === "string" &&
|
|
746
|
+
value.trim() !== "" &&
|
|
747
|
+
value !== settingPath)
|
|
748
|
+
: [];
|
|
749
|
+
if (existing.length === 0) {
|
|
750
|
+
delete settings[key];
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
settings[key] = existing;
|
|
754
|
+
}
|
|
727
755
|
function syncCodex(options) {
|
|
728
756
|
const codexDir = getCodexRootDir(options.paths);
|
|
729
757
|
const codexConfigPath = getCodexConfigPath(options.paths);
|