skiller 0.9.9 → 0.9.11
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/cli/commands.js +5 -5
- package/dist/cli/handlers.js +129 -11
- package/dist/core/AgentSourceCompatibility.js +824 -0
- package/dist/core/SkillOwnership.js +12 -6
- package/dist/core/SkillerLock.js +112 -0
- package/dist/core/SkillsProcessor.js +1 -1
- package/package.json +1 -1
package/dist/cli/commands.js
CHANGED
|
@@ -126,14 +126,14 @@ async function run() {
|
|
|
126
126
|
description: 'Enable/disable skills support (experimental, default: enabled)',
|
|
127
127
|
});
|
|
128
128
|
}, handlers_1.applyHandler)
|
|
129
|
-
.command('add [args..]', 'Run the local skills CLI add command', (y) => skillsArgsBuilder(y)
|
|
129
|
+
.command('add [args..]', 'Run the local skills CLI add command and install compatible agent sources into .agents/skills', (y) => skillsArgsBuilder(y)
|
|
130
130
|
.option('verbose', {
|
|
131
131
|
type: 'boolean',
|
|
132
132
|
description: 'Enable verbose logging for the follow-up apply step',
|
|
133
133
|
default: false,
|
|
134
134
|
})
|
|
135
135
|
.alias('verbose', 'v'), handlers_1.addHandler)
|
|
136
|
-
.command('remove [args..]', 'Run the local skills CLI remove command', (y) => skillsArgsBuilder(y)
|
|
136
|
+
.command('remove [args..]', 'Run the local skills CLI remove command, prune compatible agent installs, then skiller apply', (y) => skillsArgsBuilder(y)
|
|
137
137
|
.option('verbose', {
|
|
138
138
|
type: 'boolean',
|
|
139
139
|
description: 'Enable verbose logging for the follow-up apply step',
|
|
@@ -143,21 +143,21 @@ async function run() {
|
|
|
143
143
|
.command('list [args..]', 'Run the local skills CLI list command', skillsArgsBuilder, handlers_1.listHandler)
|
|
144
144
|
.command('find [args..]', 'Run the local skills CLI find command', skillsArgsBuilder, handlers_1.findHandler)
|
|
145
145
|
.command('check [args..]', 'Run the local skills CLI check command', skillsArgsBuilder, handlers_1.checkHandler)
|
|
146
|
-
.command('install [args..]', 'Restore lock-backed skills
|
|
146
|
+
.command('install [args..]', 'Restore lock-backed skills plus skiller-managed agent installs from lockfiles, then skiller apply', (y) => skillsArgsBuilder(y)
|
|
147
147
|
.option('verbose', {
|
|
148
148
|
type: 'boolean',
|
|
149
149
|
description: 'Enable verbose logging for the follow-up apply step',
|
|
150
150
|
default: false,
|
|
151
151
|
})
|
|
152
152
|
.alias('verbose', 'v'), handlers_1.installHandler)
|
|
153
|
-
.command('update [args..]', '
|
|
153
|
+
.command('update [args..]', 'Update local skills CLI installs plus skiller-managed agent installs, then skiller apply', (y) => skillsArgsBuilder(y)
|
|
154
154
|
.option('verbose', {
|
|
155
155
|
type: 'boolean',
|
|
156
156
|
description: 'Enable verbose logging for the follow-up apply step',
|
|
157
157
|
default: false,
|
|
158
158
|
})
|
|
159
159
|
.alias('verbose', 'v'), handlers_1.updateHandler)
|
|
160
|
-
.command('outdated [args..]', 'Run the local skills CLI outdated command', skillsArgsBuilder, handlers_1.outdatedHandler)
|
|
160
|
+
.command('outdated [args..]', 'Run the local skills CLI outdated command and report outdated skiller-managed agent installs', skillsArgsBuilder, handlers_1.outdatedHandler)
|
|
161
161
|
.command('skills <subcommand> [args..]', 'Pass through an arbitrary command to the local skills CLI', (y) => skillsArgsBuilder(y).positional('subcommand', {
|
|
162
162
|
type: 'string',
|
|
163
163
|
description: 'The local skills CLI subcommand to run',
|
package/dist/cli/handlers.js
CHANGED
|
@@ -61,6 +61,7 @@ const agents_2 = require("../agents");
|
|
|
61
61
|
const skills_cli_1 = require("./skills-cli");
|
|
62
62
|
const project_paths_1 = require("../core/project-paths");
|
|
63
63
|
const SkillOwnership_1 = require("../core/SkillOwnership");
|
|
64
|
+
const AgentSourceCompatibility_1 = require("../core/AgentSourceCompatibility");
|
|
64
65
|
const readline = __importStar(require("readline/promises"));
|
|
65
66
|
async function executeSkillsWrapper(projectRoot, args) {
|
|
66
67
|
try {
|
|
@@ -78,6 +79,46 @@ async function applyAfterSkillsLifecycleStep(projectRoot, verbose) {
|
|
|
78
79
|
verbose,
|
|
79
80
|
});
|
|
80
81
|
}
|
|
82
|
+
async function pruneSkillOutputs(projectRoot, skillNames) {
|
|
83
|
+
const normalizedNames = [...new Set(skillNames.filter(Boolean))];
|
|
84
|
+
if (normalizedNames.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
const skillDirs = new Set([(0, SkillOwnership_1.getCanonicalSkillsDir)(projectRoot)]);
|
|
87
|
+
for (const agent of agents_2.allAgents) {
|
|
88
|
+
if (!agent.supportsNativeSkills?.() || !agent.getSkillsPath)
|
|
89
|
+
continue;
|
|
90
|
+
const skillsPath = agent.getSkillsPath(projectRoot);
|
|
91
|
+
if (skillsPath) {
|
|
92
|
+
skillDirs.add(skillsPath);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const skillName of normalizedNames) {
|
|
96
|
+
for (const skillsDir of skillDirs) {
|
|
97
|
+
await fs.rm(path.join(skillsDir, skillName), {
|
|
98
|
+
force: true,
|
|
99
|
+
recursive: true,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function pruneStaleLockBackedSkills(projectRoot) {
|
|
105
|
+
const [nativePrune, agentPrune] = await Promise.all([
|
|
106
|
+
(0, AgentSourceCompatibility_1.pruneMissingNativeSkillsFromLock)(projectRoot),
|
|
107
|
+
(0, AgentSourceCompatibility_1.pruneMissingAgentSkillsFromLock)(projectRoot),
|
|
108
|
+
]);
|
|
109
|
+
if (nativePrune.prunedOutputNames.length > 0) {
|
|
110
|
+
await pruneSkillOutputs(projectRoot, nativePrune.prunedOutputNames);
|
|
111
|
+
console.log(`[skiller] Pruned ${nativePrune.prunedKeys.length} stale upstream skill(s): ${nativePrune.prunedKeys.join(', ')}`);
|
|
112
|
+
}
|
|
113
|
+
if (agentPrune.prunedOutputNames.length > 0) {
|
|
114
|
+
await pruneSkillOutputs(projectRoot, agentPrune.prunedOutputNames);
|
|
115
|
+
console.log(`[skiller] Pruned ${agentPrune.prunedKeys.length} stale agent-derived skill(s): ${agentPrune.prunedKeys.join(', ')}`);
|
|
116
|
+
}
|
|
117
|
+
const warnings = [...nativePrune.warnings, ...agentPrune.warnings];
|
|
118
|
+
if (warnings.length > 0) {
|
|
119
|
+
console.log(warnings.map((warning) => `[skiller] ${warning}`).join('\n'));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
81
122
|
function normalizeRequestedSkillNames(args) {
|
|
82
123
|
if (!args || args.length === 0)
|
|
83
124
|
return [];
|
|
@@ -630,27 +671,89 @@ async function migrateRulesToSkillsHandler(argv) {
|
|
|
630
671
|
}
|
|
631
672
|
}
|
|
632
673
|
async function addHandler(argv) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
674
|
+
const projectRoot = argv['project-root'];
|
|
675
|
+
const args = argv.args ?? [];
|
|
676
|
+
if ((0, AgentSourceCompatibility_1.hasGlobalFlag)(args)) {
|
|
677
|
+
throw new Error('Agent-compatible installs are project-scoped only. Drop --global and retry.');
|
|
678
|
+
}
|
|
679
|
+
const source = (0, AgentSourceCompatibility_1.extractAddSource)(args);
|
|
680
|
+
let inspection;
|
|
681
|
+
let installedAgentSkills = false;
|
|
682
|
+
try {
|
|
683
|
+
if (source) {
|
|
684
|
+
inspection = await (0, AgentSourceCompatibility_1.inspectCompatibleSource)(source, args);
|
|
685
|
+
}
|
|
686
|
+
if (inspection && (0, AgentSourceCompatibility_1.hasListFlag)(args)) {
|
|
687
|
+
if (inspection.nativeSkillNames.length > 0) {
|
|
688
|
+
await executeSkillsWrapper(projectRoot, [
|
|
689
|
+
'add',
|
|
690
|
+
...(0, AgentSourceCompatibility_1.buildAdjustedSkillsAddArgs)(args, inspection.nativeSkillNames),
|
|
691
|
+
]);
|
|
692
|
+
}
|
|
693
|
+
if (inspection.agentSkills.length > 0) {
|
|
694
|
+
console.log('[skiller] Compatible agent-derived skills:');
|
|
695
|
+
for (const agentSkill of inspection.agentSkills) {
|
|
696
|
+
console.log(`- ${agentSkill.installName}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (inspection && inspection.nativeSkillNames.length > 0) {
|
|
702
|
+
await executeSkillsWrapper(projectRoot, [
|
|
703
|
+
'add',
|
|
704
|
+
...(0, AgentSourceCompatibility_1.buildAdjustedSkillsAddArgs)(args, inspection.nativeSkillNames),
|
|
705
|
+
]);
|
|
706
|
+
}
|
|
707
|
+
else if (!inspection || inspection.agentSkills.length === 0) {
|
|
708
|
+
await executeSkillsWrapper(projectRoot, ['add', ...args]);
|
|
709
|
+
}
|
|
710
|
+
if (inspection && inspection.agentSkills.length > 0) {
|
|
711
|
+
const installed = await (0, AgentSourceCompatibility_1.installAgentSkillsFromInspection)(projectRoot, inspection);
|
|
712
|
+
installedAgentSkills = true;
|
|
713
|
+
console.log(`[skiller] Installed ${installed.length} agent-derived skill(s): ${installed.join(', ')}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
finally {
|
|
717
|
+
if (inspection && !installedAgentSkills) {
|
|
718
|
+
await inspection.workspace.cleanup();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
await applyAfterSkillsLifecycleStep(projectRoot, argv.verbose ?? false);
|
|
638
722
|
}
|
|
639
723
|
async function installHandler(argv) {
|
|
724
|
+
await pruneStaleLockBackedSkills(argv['project-root']);
|
|
640
725
|
await executeSkillsWrapper(argv['project-root'], [
|
|
641
726
|
'experimental_install',
|
|
642
727
|
...(argv.args ?? []),
|
|
643
728
|
]);
|
|
729
|
+
const restored = await (0, AgentSourceCompatibility_1.restoreAgentSkillsFromLock)(argv['project-root']);
|
|
730
|
+
if (restored.restored.length > 0) {
|
|
731
|
+
console.log(`[skiller] Restored ${restored.restored.length} agent-derived skill(s): ${restored.restored.join(', ')}`);
|
|
732
|
+
}
|
|
733
|
+
if (restored.warnings.length > 0) {
|
|
734
|
+
console.log(restored.warnings.map((warning) => `[skiller] ${warning}`).join('\n'));
|
|
735
|
+
}
|
|
644
736
|
await applyAfterSkillsLifecycleStep(argv['project-root'], argv.verbose ?? false);
|
|
645
737
|
}
|
|
646
738
|
async function removeHandler(argv) {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
739
|
+
const projectRoot = argv['project-root'];
|
|
740
|
+
const args = argv.args ?? [];
|
|
741
|
+
const requestedNames = normalizeRequestedSkillNames(args);
|
|
742
|
+
const ownership = await (0, SkillOwnership_1.resolveSkillOwnership)(projectRoot);
|
|
743
|
+
const shouldRunSkillsRemove = requestedNames.length === 0 ||
|
|
744
|
+
args.some((arg) => arg === '--all' ||
|
|
745
|
+
arg === '--agent' ||
|
|
746
|
+
arg === '-a' ||
|
|
747
|
+
arg === '--skill' ||
|
|
748
|
+
arg === '-s') ||
|
|
749
|
+
requestedNames.some((name) => ownership.upstreamOwned.has(name));
|
|
750
|
+
if (shouldRunSkillsRemove) {
|
|
751
|
+
await executeSkillsWrapper(projectRoot, ['remove', ...args]);
|
|
752
|
+
}
|
|
651
753
|
await scrubRequestedSkillsLockEntries(argv['project-root'], argv.args ?? []);
|
|
652
|
-
await pruneRequestedUnmanagedSkillOutputs(
|
|
653
|
-
await
|
|
754
|
+
await pruneRequestedUnmanagedSkillOutputs(projectRoot, args);
|
|
755
|
+
await (0, AgentSourceCompatibility_1.removeAgentManagedSkills)(projectRoot, requestedNames);
|
|
756
|
+
await applyAfterSkillsLifecycleStep(projectRoot, argv.verbose ?? false);
|
|
654
757
|
}
|
|
655
758
|
async function listHandler(argv) {
|
|
656
759
|
await executeSkillsWrapper(argv['project-root'], [
|
|
@@ -671,10 +774,18 @@ async function checkHandler(argv) {
|
|
|
671
774
|
]);
|
|
672
775
|
}
|
|
673
776
|
async function updateHandler(argv) {
|
|
777
|
+
await pruneStaleLockBackedSkills(argv['project-root']);
|
|
674
778
|
await executeSkillsWrapper(argv['project-root'], [
|
|
675
779
|
'update',
|
|
676
780
|
...(argv.args ?? []),
|
|
677
781
|
]);
|
|
782
|
+
const updated = await (0, AgentSourceCompatibility_1.updateAgentSkillsFromLock)(argv['project-root']);
|
|
783
|
+
if (updated.updated.length > 0) {
|
|
784
|
+
console.log(`[skiller] Updated ${updated.updated.length} agent-derived skill(s): ${updated.updated.join(', ')}`);
|
|
785
|
+
}
|
|
786
|
+
if (updated.warnings.length > 0) {
|
|
787
|
+
console.log(updated.warnings.map((warning) => `[skiller] ${warning}`).join('\n'));
|
|
788
|
+
}
|
|
678
789
|
await applyAfterSkillsLifecycleStep(argv['project-root'], argv.verbose ?? false);
|
|
679
790
|
}
|
|
680
791
|
async function outdatedHandler(argv) {
|
|
@@ -682,6 +793,13 @@ async function outdatedHandler(argv) {
|
|
|
682
793
|
'outdated',
|
|
683
794
|
...(argv.args ?? []),
|
|
684
795
|
]);
|
|
796
|
+
const outdated = await (0, AgentSourceCompatibility_1.getOutdatedAgentSkills)(argv['project-root']);
|
|
797
|
+
if (outdated.outdated.length > 0) {
|
|
798
|
+
console.log(`[skiller] Agent-derived updates available:\n${outdated.outdated.map((name) => `- ${name}`).join('\n')}`);
|
|
799
|
+
}
|
|
800
|
+
if (outdated.warnings.length > 0) {
|
|
801
|
+
console.log(outdated.warnings.map((warning) => `[skiller] ${warning}`).join('\n'));
|
|
802
|
+
}
|
|
685
803
|
}
|
|
686
804
|
async function skillsHandler(argv) {
|
|
687
805
|
await executeSkillsWrapper(argv['project-root'], [
|
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parseCompatibleSource = parseCompatibleSource;
|
|
37
|
+
exports.hasListFlag = hasListFlag;
|
|
38
|
+
exports.hasGlobalFlag = hasGlobalFlag;
|
|
39
|
+
exports.extractAddSource = extractAddSource;
|
|
40
|
+
exports.buildAdjustedSkillsAddArgs = buildAdjustedSkillsAddArgs;
|
|
41
|
+
exports.inspectCompatibleSource = inspectCompatibleSource;
|
|
42
|
+
exports.installAgentSkillsFromInspection = installAgentSkillsFromInspection;
|
|
43
|
+
exports.restoreAgentSkillsFromLock = restoreAgentSkillsFromLock;
|
|
44
|
+
exports.getOutdatedAgentSkills = getOutdatedAgentSkills;
|
|
45
|
+
exports.updateAgentSkillsFromLock = updateAgentSkillsFromLock;
|
|
46
|
+
exports.removeAgentManagedSkills = removeAgentManagedSkills;
|
|
47
|
+
exports.pruneMissingNativeSkillsFromLock = pruneMissingNativeSkillsFromLock;
|
|
48
|
+
exports.pruneMissingAgentSkillsFromLock = pruneMissingAgentSkillsFromLock;
|
|
49
|
+
const crypto = __importStar(require("crypto"));
|
|
50
|
+
const fs = __importStar(require("fs/promises"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const os = __importStar(require("os"));
|
|
53
|
+
const yaml = __importStar(require("js-yaml"));
|
|
54
|
+
const child_process_1 = require("child_process");
|
|
55
|
+
const FrontmatterParser_1 = require("./FrontmatterParser");
|
|
56
|
+
const SkillerLock_1 = require("./SkillerLock");
|
|
57
|
+
const CANONICAL_SKILLS_DIR = path.join('.agents', 'skills');
|
|
58
|
+
const LEGACY_CLAUDE_SKILLS_DIR = path.join('.claude', 'skills');
|
|
59
|
+
const SKIP_DIRS = new Set([
|
|
60
|
+
'.git',
|
|
61
|
+
'.next',
|
|
62
|
+
'build',
|
|
63
|
+
'coverage',
|
|
64
|
+
'dist',
|
|
65
|
+
'fixtures',
|
|
66
|
+
'node_modules',
|
|
67
|
+
'test',
|
|
68
|
+
'tests',
|
|
69
|
+
'tmp',
|
|
70
|
+
'tmp-fixtures',
|
|
71
|
+
]);
|
|
72
|
+
const NATIVE_SKILLS_LOCK_VERSION = 1;
|
|
73
|
+
function hashContent(content) {
|
|
74
|
+
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
75
|
+
}
|
|
76
|
+
function normalizeSkillNameForFilesystem(name) {
|
|
77
|
+
return name.trim().replace(/:/g, '-');
|
|
78
|
+
}
|
|
79
|
+
function createEmptyNativeSkillsLock() {
|
|
80
|
+
return {
|
|
81
|
+
version: NATIVE_SKILLS_LOCK_VERSION,
|
|
82
|
+
skills: {},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function readNativeSkillsLock(projectRoot) {
|
|
86
|
+
try {
|
|
87
|
+
const raw = JSON.parse(await fs.readFile(path.join(projectRoot, 'skills-lock.json'), 'utf8'));
|
|
88
|
+
if (raw.version !== NATIVE_SKILLS_LOCK_VERSION ||
|
|
89
|
+
!raw.skills ||
|
|
90
|
+
typeof raw.skills !== 'object') {
|
|
91
|
+
return createEmptyNativeSkillsLock();
|
|
92
|
+
}
|
|
93
|
+
return raw;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return createEmptyNativeSkillsLock();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function writeNativeSkillsLock(projectRoot, lock) {
|
|
100
|
+
const sortedSkills = {};
|
|
101
|
+
for (const key of Object.keys(lock.skills).sort((a, b) => a.localeCompare(b))) {
|
|
102
|
+
sortedSkills[key] = lock.skills[key];
|
|
103
|
+
}
|
|
104
|
+
await fs.writeFile(path.join(projectRoot, 'skills-lock.json'), JSON.stringify({
|
|
105
|
+
version: NATIVE_SKILLS_LOCK_VERSION,
|
|
106
|
+
skills: sortedSkills,
|
|
107
|
+
}, null, 2) + '\n', 'utf8');
|
|
108
|
+
}
|
|
109
|
+
function isLocalPath(input) {
|
|
110
|
+
return (path.isAbsolute(input) ||
|
|
111
|
+
input.startsWith('./') ||
|
|
112
|
+
input.startsWith('../') ||
|
|
113
|
+
input === '.' ||
|
|
114
|
+
input === '..' ||
|
|
115
|
+
/^[a-zA-Z]:[/\\]/.test(input));
|
|
116
|
+
}
|
|
117
|
+
function sanitizeSubpath(subpath) {
|
|
118
|
+
const normalized = subpath.replace(/\\/g, '/');
|
|
119
|
+
for (const segment of normalized.split('/')) {
|
|
120
|
+
if (segment === '..') {
|
|
121
|
+
throw new Error(`Unsafe subpath '${subpath}'`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return normalized;
|
|
125
|
+
}
|
|
126
|
+
function inferLocalSourceRootFromFile(filePath) {
|
|
127
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
128
|
+
const agentMarker = '/agents/';
|
|
129
|
+
const skillsMarker = '/skills/';
|
|
130
|
+
if (normalized.includes(agentMarker)) {
|
|
131
|
+
return filePath.slice(0, normalized.indexOf(agentMarker));
|
|
132
|
+
}
|
|
133
|
+
if (normalized.includes(skillsMarker)) {
|
|
134
|
+
return filePath.slice(0, normalized.indexOf(skillsMarker));
|
|
135
|
+
}
|
|
136
|
+
return path.dirname(filePath);
|
|
137
|
+
}
|
|
138
|
+
function parseCompatibleSource(input) {
|
|
139
|
+
const trimmed = input.trim();
|
|
140
|
+
if (isLocalPath(trimmed)) {
|
|
141
|
+
return {
|
|
142
|
+
source: path.resolve(trimmed),
|
|
143
|
+
type: 'local',
|
|
144
|
+
url: path.resolve(trimmed),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const githubPrefixMatch = trimmed.match(/^github:(.+)$/);
|
|
148
|
+
if (githubPrefixMatch) {
|
|
149
|
+
return parseCompatibleSource(githubPrefixMatch[1]);
|
|
150
|
+
}
|
|
151
|
+
const githubBlobMatch = trimmed.match(/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)/);
|
|
152
|
+
if (githubBlobMatch) {
|
|
153
|
+
const [, owner, repo, ref, subpath] = githubBlobMatch;
|
|
154
|
+
return {
|
|
155
|
+
ref,
|
|
156
|
+
source: `${owner}/${repo.replace(/\.git$/, '')}`,
|
|
157
|
+
subpath: sanitizeSubpath(subpath),
|
|
158
|
+
type: 'github',
|
|
159
|
+
url: `https://github.com/${owner}/${repo.replace(/\.git$/, '')}.git`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const githubTreeWithPathMatch = trimmed.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/);
|
|
163
|
+
if (githubTreeWithPathMatch) {
|
|
164
|
+
const [, owner, repo, ref, subpath] = githubTreeWithPathMatch;
|
|
165
|
+
return {
|
|
166
|
+
ref,
|
|
167
|
+
source: `${owner}/${repo.replace(/\.git$/, '')}`,
|
|
168
|
+
subpath: sanitizeSubpath(subpath),
|
|
169
|
+
type: 'github',
|
|
170
|
+
url: `https://github.com/${owner}/${repo.replace(/\.git$/, '')}.git`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const githubTreeMatch = trimmed.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)$/);
|
|
174
|
+
if (githubTreeMatch) {
|
|
175
|
+
const [, owner, repo, ref] = githubTreeMatch;
|
|
176
|
+
return {
|
|
177
|
+
ref,
|
|
178
|
+
source: `${owner}/${repo.replace(/\.git$/, '')}`,
|
|
179
|
+
type: 'github',
|
|
180
|
+
url: `https://github.com/${owner}/${repo.replace(/\.git$/, '')}.git`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const githubRepoMatch = trimmed.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
184
|
+
if (githubRepoMatch) {
|
|
185
|
+
const [, owner, repo] = githubRepoMatch;
|
|
186
|
+
return {
|
|
187
|
+
source: `${owner}/${repo.replace(/\.git$/, '')}`,
|
|
188
|
+
type: 'github',
|
|
189
|
+
url: `https://github.com/${owner}/${repo.replace(/\.git$/, '')}.git`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const shorthandMatch = trimmed.match(/^([^/]+)\/([^/]+)$/);
|
|
193
|
+
if (shorthandMatch) {
|
|
194
|
+
const [, owner, repo] = shorthandMatch;
|
|
195
|
+
return {
|
|
196
|
+
source: `${owner}/${repo}`,
|
|
197
|
+
type: 'github',
|
|
198
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (trimmed.startsWith('git@') ||
|
|
202
|
+
trimmed.startsWith('https://') ||
|
|
203
|
+
trimmed.startsWith('http://')) {
|
|
204
|
+
return {
|
|
205
|
+
source: trimmed,
|
|
206
|
+
type: 'git',
|
|
207
|
+
url: trimmed,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
throw new Error(`Unsupported source '${input}'`);
|
|
211
|
+
}
|
|
212
|
+
async function runGitClone(url, ref) {
|
|
213
|
+
const cloneDir = await fs.mkdtemp(path.join(os.tmpdir(), 'skiller-agents-'));
|
|
214
|
+
const args = ['clone', '--depth', '1'];
|
|
215
|
+
if (ref) {
|
|
216
|
+
args.push('--branch', ref);
|
|
217
|
+
}
|
|
218
|
+
args.push(url, cloneDir);
|
|
219
|
+
await new Promise((resolve, reject) => {
|
|
220
|
+
const child = (0, child_process_1.spawn)('git', args, {
|
|
221
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
222
|
+
stdio: 'pipe',
|
|
223
|
+
});
|
|
224
|
+
let stderr = '';
|
|
225
|
+
child.stderr.on('data', (chunk) => {
|
|
226
|
+
stderr += chunk.toString();
|
|
227
|
+
});
|
|
228
|
+
child.on('error', reject);
|
|
229
|
+
child.on('exit', (code) => {
|
|
230
|
+
if (code === 0) {
|
|
231
|
+
resolve();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
reject(new Error(stderr.trim() || `git clone failed with exit code ${code}`));
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
return cloneDir;
|
|
238
|
+
}
|
|
239
|
+
async function withSourceWorkspace(rawSource) {
|
|
240
|
+
const parsed = parseCompatibleSource(rawSource);
|
|
241
|
+
return withParsedSourceWorkspace(parsed);
|
|
242
|
+
}
|
|
243
|
+
async function withParsedSourceWorkspace(parsed) {
|
|
244
|
+
if (parsed.type === 'local') {
|
|
245
|
+
const targetPath = parsed.subpath
|
|
246
|
+
? path.join(parsed.url, parsed.subpath)
|
|
247
|
+
: parsed.url;
|
|
248
|
+
let rootPath = parsed.url;
|
|
249
|
+
let searchPath = targetPath;
|
|
250
|
+
try {
|
|
251
|
+
const stats = await fs.stat(targetPath);
|
|
252
|
+
if (stats.isFile()) {
|
|
253
|
+
rootPath = inferLocalSourceRootFromFile(targetPath);
|
|
254
|
+
searchPath = targetPath;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Let downstream callers surface missing paths consistently.
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
cleanup: async () => undefined,
|
|
262
|
+
parsed,
|
|
263
|
+
rootPath,
|
|
264
|
+
searchPath,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const cloneDir = await runGitClone(parsed.url, parsed.ref);
|
|
268
|
+
const searchPath = parsed.subpath
|
|
269
|
+
? path.join(cloneDir, parsed.subpath)
|
|
270
|
+
: cloneDir;
|
|
271
|
+
return {
|
|
272
|
+
cleanup: async () => {
|
|
273
|
+
await fs.rm(cloneDir, { force: true, recursive: true });
|
|
274
|
+
},
|
|
275
|
+
parsed,
|
|
276
|
+
rootPath: cloneDir,
|
|
277
|
+
searchPath,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function parseSourceFromLockEntry(entry) {
|
|
281
|
+
if (entry.sourceType === 'local') {
|
|
282
|
+
return {
|
|
283
|
+
ref: entry.ref,
|
|
284
|
+
source: entry.source,
|
|
285
|
+
subpath: entry.subpath,
|
|
286
|
+
type: 'local',
|
|
287
|
+
url: entry.source,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (entry.sourceType === 'github') {
|
|
291
|
+
return {
|
|
292
|
+
ref: entry.ref,
|
|
293
|
+
source: entry.source,
|
|
294
|
+
subpath: entry.subpath,
|
|
295
|
+
type: 'github',
|
|
296
|
+
url: `https://github.com/${entry.source}.git`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
ref: entry.ref,
|
|
301
|
+
source: entry.source,
|
|
302
|
+
subpath: entry.subpath,
|
|
303
|
+
type: 'git',
|
|
304
|
+
url: entry.source,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function cloneRecord(value) {
|
|
308
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
309
|
+
return {};
|
|
310
|
+
}
|
|
311
|
+
return { ...value };
|
|
312
|
+
}
|
|
313
|
+
function buildCompiledSkillContent(name, rawFrontmatter, body, sourceRelPath) {
|
|
314
|
+
const frontmatter = rawFrontmatter ? { ...rawFrontmatter } : {};
|
|
315
|
+
if (typeof frontmatter.name !== 'string' || frontmatter.name.length === 0) {
|
|
316
|
+
frontmatter.name = name;
|
|
317
|
+
}
|
|
318
|
+
if (typeof frontmatter.description !== 'string' ||
|
|
319
|
+
frontmatter.description.length === 0) {
|
|
320
|
+
frontmatter.description = `Skill: ${name}`;
|
|
321
|
+
}
|
|
322
|
+
const metadata = cloneRecord(frontmatter.metadata);
|
|
323
|
+
const skiller = cloneRecord(metadata.skiller);
|
|
324
|
+
skiller.source = sourceRelPath;
|
|
325
|
+
metadata.skiller = skiller;
|
|
326
|
+
frontmatter.metadata = metadata;
|
|
327
|
+
return `---
|
|
328
|
+
${yaml.dump(frontmatter, { lineWidth: -1, noRefs: true }).trim()}
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
${body.trim()}
|
|
332
|
+
`;
|
|
333
|
+
}
|
|
334
|
+
function shouldSkipDir(name) {
|
|
335
|
+
return SKIP_DIRS.has(name);
|
|
336
|
+
}
|
|
337
|
+
function isValidRelativePath(value) {
|
|
338
|
+
return value.startsWith('./');
|
|
339
|
+
}
|
|
340
|
+
async function collectDeclaredPluginBases(basePath) {
|
|
341
|
+
const pluginBases = new Set();
|
|
342
|
+
try {
|
|
343
|
+
const marketplace = JSON.parse(await fs.readFile(path.join(basePath, '.claude-plugin', 'marketplace.json'), 'utf8'));
|
|
344
|
+
const pluginRoot = marketplace.metadata?.pluginRoot;
|
|
345
|
+
const validPluginRoot = pluginRoot === undefined || isValidRelativePath(pluginRoot);
|
|
346
|
+
if (validPluginRoot) {
|
|
347
|
+
for (const plugin of marketplace.plugins ?? []) {
|
|
348
|
+
if (typeof plugin.source !== 'string')
|
|
349
|
+
continue;
|
|
350
|
+
if (!isValidRelativePath(plugin.source))
|
|
351
|
+
continue;
|
|
352
|
+
pluginBases.add(path.join(basePath, pluginRoot ?? '', plugin.source));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Ignore invalid or missing marketplace manifests.
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
await fs.access(path.join(basePath, '.claude-plugin', 'plugin.json'));
|
|
361
|
+
pluginBases.add(basePath);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// Ignore.
|
|
365
|
+
}
|
|
366
|
+
return [...pluginBases];
|
|
367
|
+
}
|
|
368
|
+
async function discoverAgentFiles(basePath, rootPath = basePath) {
|
|
369
|
+
try {
|
|
370
|
+
const stats = await fs.stat(basePath);
|
|
371
|
+
if (stats.isFile()) {
|
|
372
|
+
if (!basePath.endsWith('.md'))
|
|
373
|
+
return [];
|
|
374
|
+
return [
|
|
375
|
+
{
|
|
376
|
+
absolutePath: basePath,
|
|
377
|
+
relativePath: path.relative(rootPath, basePath).replace(/\\/g, '/'),
|
|
378
|
+
},
|
|
379
|
+
];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return [];
|
|
384
|
+
}
|
|
385
|
+
const discovered = new Map();
|
|
386
|
+
const candidateDirs = new Set([path.join(basePath, 'agents')]);
|
|
387
|
+
for (const pluginBase of await collectDeclaredPluginBases(basePath)) {
|
|
388
|
+
candidateDirs.add(path.join(pluginBase, 'agents'));
|
|
389
|
+
}
|
|
390
|
+
const walk = async (currentDir, depth) => {
|
|
391
|
+
if (depth > 8)
|
|
392
|
+
return;
|
|
393
|
+
let entries;
|
|
394
|
+
try {
|
|
395
|
+
entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
for (const entry of entries) {
|
|
401
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
402
|
+
if (entry.isDirectory()) {
|
|
403
|
+
if (shouldSkipDir(entry.name))
|
|
404
|
+
continue;
|
|
405
|
+
if (entry.name === 'agents') {
|
|
406
|
+
candidateDirs.add(absolutePath);
|
|
407
|
+
}
|
|
408
|
+
await walk(absolutePath, depth + 1);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
await walk(basePath, 0);
|
|
414
|
+
const collectMarkdown = async (dir, depth) => {
|
|
415
|
+
if (depth > 8)
|
|
416
|
+
return;
|
|
417
|
+
let entries;
|
|
418
|
+
try {
|
|
419
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
for (const entry of entries) {
|
|
425
|
+
const absolutePath = path.join(dir, entry.name);
|
|
426
|
+
if (entry.isDirectory()) {
|
|
427
|
+
if (shouldSkipDir(entry.name))
|
|
428
|
+
continue;
|
|
429
|
+
await collectMarkdown(absolutePath, depth + 1);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
433
|
+
continue;
|
|
434
|
+
const relativePath = path
|
|
435
|
+
.relative(basePath, absolutePath)
|
|
436
|
+
.replace(/\\/g, '/');
|
|
437
|
+
discovered.set(relativePath, { absolutePath, relativePath });
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
for (const candidateDir of candidateDirs) {
|
|
441
|
+
await collectMarkdown(candidateDir, 0);
|
|
442
|
+
}
|
|
443
|
+
return [...discovered.values()].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
444
|
+
}
|
|
445
|
+
async function discoverSkillNames(basePath) {
|
|
446
|
+
try {
|
|
447
|
+
const stats = await fs.stat(basePath);
|
|
448
|
+
if (stats.isFile()) {
|
|
449
|
+
if (path.basename(basePath) !== 'SKILL.md')
|
|
450
|
+
return [];
|
|
451
|
+
const parsed = (0, FrontmatterParser_1.parseFrontmatter)(await fs.readFile(basePath, 'utf8'));
|
|
452
|
+
const declaredName = typeof parsed.frontmatter?.name === 'string'
|
|
453
|
+
? parsed.frontmatter.name
|
|
454
|
+
: path.basename(path.dirname(basePath));
|
|
455
|
+
return [normalizeSkillNameForFilesystem(declaredName)];
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
// Ignore missing paths.
|
|
460
|
+
}
|
|
461
|
+
const discovered = new Set();
|
|
462
|
+
const candidateDirs = new Set([path.join(basePath, 'skills')]);
|
|
463
|
+
try {
|
|
464
|
+
const parsed = (0, FrontmatterParser_1.parseFrontmatter)(await fs.readFile(path.join(basePath, 'SKILL.md'), 'utf8'));
|
|
465
|
+
const declaredName = typeof parsed.frontmatter?.name === 'string'
|
|
466
|
+
? parsed.frontmatter.name
|
|
467
|
+
: path.basename(basePath);
|
|
468
|
+
discovered.add(normalizeSkillNameForFilesystem(declaredName));
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// Ignore missing root SKILL.md.
|
|
472
|
+
}
|
|
473
|
+
for (const pluginBase of await collectDeclaredPluginBases(basePath)) {
|
|
474
|
+
candidateDirs.add(path.join(pluginBase, 'skills'));
|
|
475
|
+
}
|
|
476
|
+
for (const candidateDir of candidateDirs) {
|
|
477
|
+
let entries;
|
|
478
|
+
try {
|
|
479
|
+
entries = await fs.readdir(candidateDir, { withFileTypes: true });
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
for (const entry of entries) {
|
|
485
|
+
if (!entry.isDirectory())
|
|
486
|
+
continue;
|
|
487
|
+
const skillMdPath = path.join(candidateDir, entry.name, 'SKILL.md');
|
|
488
|
+
try {
|
|
489
|
+
const parsed = (0, FrontmatterParser_1.parseFrontmatter)(await fs.readFile(skillMdPath, 'utf8'));
|
|
490
|
+
const declaredName = typeof parsed.frontmatter?.name === 'string'
|
|
491
|
+
? parsed.frontmatter.name
|
|
492
|
+
: entry.name;
|
|
493
|
+
discovered.add(normalizeSkillNameForFilesystem(declaredName));
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return [...discovered].sort((a, b) => a.localeCompare(b));
|
|
501
|
+
}
|
|
502
|
+
async function discoverAgentSkillCandidates(basePath, rootPath = basePath) {
|
|
503
|
+
const agentFiles = await discoverAgentFiles(basePath, rootPath);
|
|
504
|
+
const candidates = [];
|
|
505
|
+
for (const agentFile of agentFiles) {
|
|
506
|
+
const parsed = (0, FrontmatterParser_1.parseFrontmatter)(await fs.readFile(agentFile.absolutePath, 'utf8'));
|
|
507
|
+
const declaredName = parsed.frontmatter?.name;
|
|
508
|
+
const description = parsed.frontmatter?.description;
|
|
509
|
+
if (typeof declaredName !== 'string' || typeof description !== 'string') {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const compiledContent = buildCompiledSkillContent(declaredName, parsed.rawFrontmatter, parsed.body, agentFile.relativePath);
|
|
513
|
+
candidates.push({
|
|
514
|
+
compiledContent,
|
|
515
|
+
computedHash: hashContent(compiledContent),
|
|
516
|
+
description,
|
|
517
|
+
installName: normalizeSkillNameForFilesystem(declaredName),
|
|
518
|
+
name: declaredName,
|
|
519
|
+
sourceRelPath: agentFile.relativePath,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
return candidates.sort((a, b) => a.installName.localeCompare(b.installName));
|
|
523
|
+
}
|
|
524
|
+
function parseRequestedSkillFilters(args) {
|
|
525
|
+
const names = new Set();
|
|
526
|
+
let wildcard = false;
|
|
527
|
+
const argv = args ?? [];
|
|
528
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
529
|
+
const current = argv[index];
|
|
530
|
+
if (current !== '--skill' && current !== '-s')
|
|
531
|
+
continue;
|
|
532
|
+
for (let valueIndex = index + 1; valueIndex < argv.length; valueIndex += 1) {
|
|
533
|
+
const value = argv[valueIndex];
|
|
534
|
+
if (value.startsWith('-'))
|
|
535
|
+
break;
|
|
536
|
+
if (value === '*') {
|
|
537
|
+
wildcard = true;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
names.add(normalizeSkillNameForFilesystem(value));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return { names, wildcard };
|
|
544
|
+
}
|
|
545
|
+
function hasListFlag(args) {
|
|
546
|
+
return (args ?? []).some((arg) => arg === '--list' || arg === '-l');
|
|
547
|
+
}
|
|
548
|
+
function hasGlobalFlag(args) {
|
|
549
|
+
return (args ?? []).some((arg) => arg === '--global' || arg === '-g');
|
|
550
|
+
}
|
|
551
|
+
function extractAddSource(args) {
|
|
552
|
+
for (const arg of args ?? []) {
|
|
553
|
+
if (arg.startsWith('-'))
|
|
554
|
+
continue;
|
|
555
|
+
return arg;
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
function buildAdjustedSkillsAddArgs(originalArgs, nativeSkillNames) {
|
|
560
|
+
const args = [...(originalArgs ?? [])];
|
|
561
|
+
const next = [];
|
|
562
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
563
|
+
const current = args[index];
|
|
564
|
+
if (current === '--skill' || current === '-s') {
|
|
565
|
+
index += 1;
|
|
566
|
+
while (index < args.length && !args[index].startsWith('-')) {
|
|
567
|
+
index += 1;
|
|
568
|
+
}
|
|
569
|
+
index -= 1;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
next.push(current);
|
|
573
|
+
}
|
|
574
|
+
if (nativeSkillNames.length > 0) {
|
|
575
|
+
next.push('--skill', ...nativeSkillNames);
|
|
576
|
+
}
|
|
577
|
+
return next;
|
|
578
|
+
}
|
|
579
|
+
async function inspectCompatibleSource(rawSource, args) {
|
|
580
|
+
const workspace = await withSourceWorkspace(rawSource);
|
|
581
|
+
const [nativeSkillNames, discoveredAgentSkills] = await Promise.all([
|
|
582
|
+
discoverSkillNames(workspace.searchPath),
|
|
583
|
+
discoverAgentSkillCandidates(workspace.searchPath, workspace.rootPath),
|
|
584
|
+
]);
|
|
585
|
+
const requested = parseRequestedSkillFilters(args);
|
|
586
|
+
const agentSkills = discoveredAgentSkills.filter((agentSkill) => {
|
|
587
|
+
if (requested.wildcard || requested.names.size === 0)
|
|
588
|
+
return true;
|
|
589
|
+
return requested.names.has(agentSkill.installName);
|
|
590
|
+
});
|
|
591
|
+
return {
|
|
592
|
+
agentSkills: agentSkills.sort((a, b) => a.installName.localeCompare(b.installName)),
|
|
593
|
+
nativeSkillNames: requested.wildcard
|
|
594
|
+
? nativeSkillNames
|
|
595
|
+
: nativeSkillNames.filter((name) => requested.names.size === 0 || requested.names.has(name)),
|
|
596
|
+
workspace,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
async function installAgentSkillsFromInspection(projectRoot, inspection) {
|
|
600
|
+
const installed = [];
|
|
601
|
+
const lockEntries = {};
|
|
602
|
+
const canonicalSkillsDir = path.join(projectRoot, CANONICAL_SKILLS_DIR);
|
|
603
|
+
for (const agentSkill of inspection.agentSkills) {
|
|
604
|
+
const skillDir = path.join(canonicalSkillsDir, agentSkill.installName);
|
|
605
|
+
await fs.rm(skillDir, { force: true, recursive: true });
|
|
606
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
607
|
+
await fs.writeFile(path.join(skillDir, 'SKILL.md'), agentSkill.compiledContent, 'utf8');
|
|
608
|
+
lockEntries[agentSkill.installName] = {
|
|
609
|
+
computedHash: agentSkill.computedHash,
|
|
610
|
+
ref: inspection.workspace.parsed.ref,
|
|
611
|
+
source: inspection.workspace.parsed.source,
|
|
612
|
+
sourceRelPath: agentSkill.sourceRelPath,
|
|
613
|
+
sourceType: inspection.workspace.parsed.type,
|
|
614
|
+
subpath: inspection.workspace.parsed.subpath,
|
|
615
|
+
};
|
|
616
|
+
installed.push(agentSkill.installName);
|
|
617
|
+
}
|
|
618
|
+
await (0, SkillerLock_1.upsertSkillerLockEntries)(projectRoot, lockEntries);
|
|
619
|
+
await inspection.workspace.cleanup();
|
|
620
|
+
return installed.sort((a, b) => a.localeCompare(b));
|
|
621
|
+
}
|
|
622
|
+
function groupLockEntriesBySource(skills) {
|
|
623
|
+
const groups = new Map();
|
|
624
|
+
for (const [skillName, entry] of Object.entries(skills)) {
|
|
625
|
+
const key = JSON.stringify([
|
|
626
|
+
entry.source,
|
|
627
|
+
entry.sourceType,
|
|
628
|
+
entry.ref ?? '',
|
|
629
|
+
entry.subpath ?? '',
|
|
630
|
+
]);
|
|
631
|
+
const existing = groups.get(key) ?? [];
|
|
632
|
+
existing.push([skillName, entry]);
|
|
633
|
+
groups.set(key, existing);
|
|
634
|
+
}
|
|
635
|
+
return groups;
|
|
636
|
+
}
|
|
637
|
+
async function installSingleAgentSkill(projectRoot, skillName, candidate, entry) {
|
|
638
|
+
const skillDir = path.join(projectRoot, CANONICAL_SKILLS_DIR, skillName);
|
|
639
|
+
await fs.rm(skillDir, { force: true, recursive: true });
|
|
640
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
641
|
+
await fs.writeFile(path.join(skillDir, 'SKILL.md'), candidate.compiledContent, 'utf8');
|
|
642
|
+
await (0, SkillerLock_1.upsertSkillerLockEntries)(projectRoot, {
|
|
643
|
+
[skillName]: {
|
|
644
|
+
...entry,
|
|
645
|
+
computedHash: candidate.computedHash,
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
async function restoreAgentSkillsFromLock(projectRoot) {
|
|
650
|
+
const lock = await (0, SkillerLock_1.readSkillerLock)(projectRoot);
|
|
651
|
+
const restored = [];
|
|
652
|
+
const warnings = [];
|
|
653
|
+
for (const entries of groupLockEntriesBySource(lock.skills).values()) {
|
|
654
|
+
const [, entry] = entries[0];
|
|
655
|
+
const workspace = await withParsedSourceWorkspace(parseSourceFromLockEntry(entry));
|
|
656
|
+
const agentSkills = await discoverAgentSkillCandidates(workspace.searchPath, workspace.rootPath);
|
|
657
|
+
const candidates = new Map(agentSkills.map((candidate) => [candidate.sourceRelPath, candidate]));
|
|
658
|
+
for (const [skillName, lockEntry] of entries) {
|
|
659
|
+
const candidate = candidates.get(lockEntry.sourceRelPath);
|
|
660
|
+
if (!candidate) {
|
|
661
|
+
warnings.push(`Could not restore '${skillName}' from ${lockEntry.source}: missing ${lockEntry.sourceRelPath}`);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
await installSingleAgentSkill(projectRoot, skillName, candidate, lockEntry);
|
|
665
|
+
restored.push(skillName);
|
|
666
|
+
}
|
|
667
|
+
await workspace.cleanup();
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
restored: restored.sort((a, b) => a.localeCompare(b)),
|
|
671
|
+
warnings,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
async function getOutdatedAgentSkills(projectRoot) {
|
|
675
|
+
const lock = await (0, SkillerLock_1.readSkillerLock)(projectRoot);
|
|
676
|
+
const outdated = [];
|
|
677
|
+
const warnings = [];
|
|
678
|
+
for (const entries of groupLockEntriesBySource(lock.skills).values()) {
|
|
679
|
+
const [, entry] = entries[0];
|
|
680
|
+
const workspace = await withParsedSourceWorkspace(parseSourceFromLockEntry(entry));
|
|
681
|
+
const agentSkills = await discoverAgentSkillCandidates(workspace.searchPath, workspace.rootPath);
|
|
682
|
+
const candidates = new Map(agentSkills.map((candidate) => [candidate.sourceRelPath, candidate]));
|
|
683
|
+
for (const [skillName, lockEntry] of entries) {
|
|
684
|
+
const candidate = candidates.get(lockEntry.sourceRelPath);
|
|
685
|
+
if (!candidate) {
|
|
686
|
+
warnings.push(`Could not check '${skillName}' from ${lockEntry.source}: missing ${lockEntry.sourceRelPath}`);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (candidate.computedHash !== lockEntry.computedHash) {
|
|
690
|
+
outdated.push(skillName);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
await workspace.cleanup();
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
outdated: outdated.sort((a, b) => a.localeCompare(b)),
|
|
697
|
+
warnings,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
async function updateAgentSkillsFromLock(projectRoot) {
|
|
701
|
+
const lock = await (0, SkillerLock_1.readSkillerLock)(projectRoot);
|
|
702
|
+
const updated = [];
|
|
703
|
+
const warnings = [];
|
|
704
|
+
for (const entries of groupLockEntriesBySource(lock.skills).values()) {
|
|
705
|
+
const [, entry] = entries[0];
|
|
706
|
+
const workspace = await withParsedSourceWorkspace(parseSourceFromLockEntry(entry));
|
|
707
|
+
const agentSkills = await discoverAgentSkillCandidates(workspace.searchPath, workspace.rootPath);
|
|
708
|
+
const candidates = new Map(agentSkills.map((candidate) => [candidate.sourceRelPath, candidate]));
|
|
709
|
+
for (const [skillName, lockEntry] of entries) {
|
|
710
|
+
const candidate = candidates.get(lockEntry.sourceRelPath);
|
|
711
|
+
if (!candidate) {
|
|
712
|
+
warnings.push(`Could not update '${skillName}' from ${lockEntry.source}: missing ${lockEntry.sourceRelPath}`);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
if (candidate.computedHash === lockEntry.computedHash) {
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
await installSingleAgentSkill(projectRoot, skillName, candidate, lockEntry);
|
|
719
|
+
updated.push(skillName);
|
|
720
|
+
}
|
|
721
|
+
await workspace.cleanup();
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
updated: updated.sort((a, b) => a.localeCompare(b)),
|
|
725
|
+
warnings,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
async function removeAgentManagedSkills(projectRoot, skillNames) {
|
|
729
|
+
const removed = await (0, SkillerLock_1.removeSkillerLockEntries)(projectRoot, skillNames);
|
|
730
|
+
if (removed.length === 0)
|
|
731
|
+
return [];
|
|
732
|
+
for (const skillName of removed) {
|
|
733
|
+
await fs.rm(path.join(projectRoot, CANONICAL_SKILLS_DIR, skillName), {
|
|
734
|
+
force: true,
|
|
735
|
+
recursive: true,
|
|
736
|
+
});
|
|
737
|
+
await fs.rm(path.join(projectRoot, LEGACY_CLAUDE_SKILLS_DIR, skillName), {
|
|
738
|
+
force: true,
|
|
739
|
+
recursive: true,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
return removed;
|
|
743
|
+
}
|
|
744
|
+
async function pruneMissingNativeSkillsFromLock(projectRoot) {
|
|
745
|
+
const lock = await readNativeSkillsLock(projectRoot);
|
|
746
|
+
const prunedKeys = [];
|
|
747
|
+
const prunedOutputNames = [];
|
|
748
|
+
const warnings = [];
|
|
749
|
+
for (const entries of groupLockEntriesBySource(lock.skills).values()) {
|
|
750
|
+
const [, entry] = entries[0];
|
|
751
|
+
if (entry.sourceType === 'node_modules' ||
|
|
752
|
+
entry.sourceType === 'well-known') {
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
let workspace = null;
|
|
756
|
+
try {
|
|
757
|
+
workspace = await withSourceWorkspace(entry.source);
|
|
758
|
+
const availableSkillNames = new Set(await discoverSkillNames(workspace.searchPath));
|
|
759
|
+
for (const [skillName] of entries) {
|
|
760
|
+
const installName = normalizeSkillNameForFilesystem(skillName);
|
|
761
|
+
if (availableSkillNames.has(installName))
|
|
762
|
+
continue;
|
|
763
|
+
prunedKeys.push(skillName);
|
|
764
|
+
prunedOutputNames.push(installName);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
warnings.push(`Could not inspect '${entry.source}' for stale skills: ${error instanceof Error ? error.message : String(error)}`);
|
|
769
|
+
}
|
|
770
|
+
finally {
|
|
771
|
+
if (workspace) {
|
|
772
|
+
await workspace.cleanup();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (prunedKeys.length > 0) {
|
|
777
|
+
for (const skillName of prunedKeys) {
|
|
778
|
+
delete lock.skills[skillName];
|
|
779
|
+
}
|
|
780
|
+
await writeNativeSkillsLock(projectRoot, lock);
|
|
781
|
+
}
|
|
782
|
+
return {
|
|
783
|
+
prunedKeys: prunedKeys.sort((a, b) => a.localeCompare(b)),
|
|
784
|
+
prunedOutputNames: prunedOutputNames.sort((a, b) => a.localeCompare(b)),
|
|
785
|
+
warnings,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
async function pruneMissingAgentSkillsFromLock(projectRoot) {
|
|
789
|
+
const lock = await (0, SkillerLock_1.readSkillerLock)(projectRoot);
|
|
790
|
+
const prunedKeys = [];
|
|
791
|
+
const prunedOutputNames = [];
|
|
792
|
+
const warnings = [];
|
|
793
|
+
for (const entries of groupLockEntriesBySource(lock.skills).values()) {
|
|
794
|
+
const [, entry] = entries[0];
|
|
795
|
+
let workspace = null;
|
|
796
|
+
try {
|
|
797
|
+
workspace = await withParsedSourceWorkspace(parseSourceFromLockEntry(entry));
|
|
798
|
+
const agentSkills = await discoverAgentSkillCandidates(workspace.searchPath, workspace.rootPath);
|
|
799
|
+
const candidates = new Set(agentSkills.map((candidate) => candidate.sourceRelPath));
|
|
800
|
+
for (const [skillName, lockEntry] of entries) {
|
|
801
|
+
if (candidates.has(lockEntry.sourceRelPath))
|
|
802
|
+
continue;
|
|
803
|
+
prunedKeys.push(skillName);
|
|
804
|
+
prunedOutputNames.push(normalizeSkillNameForFilesystem(skillName));
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
warnings.push(`Could not inspect '${entry.source}' for stale agent-derived skills: ${error instanceof Error ? error.message : String(error)}`);
|
|
809
|
+
}
|
|
810
|
+
finally {
|
|
811
|
+
if (workspace) {
|
|
812
|
+
await workspace.cleanup();
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (prunedKeys.length > 0) {
|
|
817
|
+
await (0, SkillerLock_1.removeSkillerLockEntries)(projectRoot, prunedKeys);
|
|
818
|
+
}
|
|
819
|
+
return {
|
|
820
|
+
prunedKeys: prunedKeys.sort((a, b) => a.localeCompare(b)),
|
|
821
|
+
prunedOutputNames: prunedOutputNames.sort((a, b) => a.localeCompare(b)),
|
|
822
|
+
warnings,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
@@ -43,6 +43,7 @@ const path = __importStar(require("path"));
|
|
|
43
43
|
const yaml = __importStar(require("js-yaml"));
|
|
44
44
|
const project_paths_1 = require("./project-paths");
|
|
45
45
|
const FrontmatterParser_1 = require("./FrontmatterParser");
|
|
46
|
+
const SkillerLock_1 = require("./SkillerLock");
|
|
46
47
|
function normalizeSkillNameForFilesystem(name) {
|
|
47
48
|
return name.replace(/:/g, '-');
|
|
48
49
|
}
|
|
@@ -306,15 +307,20 @@ async function readCanonicalSkillNames(projectRoot) {
|
|
|
306
307
|
}
|
|
307
308
|
}
|
|
308
309
|
async function readUpstreamOwnedSkillNames(projectRoot) {
|
|
310
|
+
const upstreamOwned = new Set();
|
|
309
311
|
try {
|
|
310
312
|
const raw = JSON.parse(await fs.readFile(getSkillsLockPath(projectRoot), 'utf8'));
|
|
311
|
-
|
|
312
|
-
.
|
|
313
|
-
|
|
313
|
+
for (const key of Object.keys(raw.skills ?? {})) {
|
|
314
|
+
upstreamOwned.add(normalizeSkillNameForFilesystem(key));
|
|
315
|
+
}
|
|
314
316
|
}
|
|
315
317
|
catch {
|
|
316
|
-
|
|
318
|
+
// Ignore missing or invalid skills-lock.json.
|
|
319
|
+
}
|
|
320
|
+
for (const key of await (0, SkillerLock_1.readSkillerLockNames)(projectRoot)) {
|
|
321
|
+
upstreamOwned.add(normalizeSkillNameForFilesystem(key));
|
|
317
322
|
}
|
|
323
|
+
return new Set([...upstreamOwned].sort((a, b) => a.localeCompare(b)));
|
|
318
324
|
}
|
|
319
325
|
async function resolveSkillOwnership(projectRoot) {
|
|
320
326
|
const upstreamOwned = await readUpstreamOwnedSkillNames(projectRoot);
|
|
@@ -338,13 +344,13 @@ async function resolveSkillOwnership(projectRoot) {
|
|
|
338
344
|
...conflicts.map((name) => {
|
|
339
345
|
const owners = [];
|
|
340
346
|
if (upstreamOwned.has(name))
|
|
341
|
-
owners.push('
|
|
347
|
+
owners.push('upstream lock');
|
|
342
348
|
if (localOwned.has(name))
|
|
343
349
|
owners.push('.agents/rules');
|
|
344
350
|
return `Skill '${name}' has mixed ownership: ${owners.join(', ')}`;
|
|
345
351
|
}),
|
|
346
352
|
...[...orphaned].map((name) => {
|
|
347
|
-
return `Canonical skill '${name}' is unmanaged; leaving it untouched because it is not in skills-lock.json or .agents/rules/${name}.mdc.`;
|
|
353
|
+
return `Canonical skill '${name}' is unmanaged; leaving it untouched because it is not in skills-lock.json, skiller-lock.json, or .agents/rules/${name}.mdc.`;
|
|
348
354
|
}),
|
|
349
355
|
];
|
|
350
356
|
return {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SKILLER_LOCK_FILENAME = void 0;
|
|
37
|
+
exports.getSkillerLockPath = getSkillerLockPath;
|
|
38
|
+
exports.readSkillerLock = readSkillerLock;
|
|
39
|
+
exports.writeSkillerLock = writeSkillerLock;
|
|
40
|
+
exports.upsertSkillerLockEntries = upsertSkillerLockEntries;
|
|
41
|
+
exports.removeSkillerLockEntries = removeSkillerLockEntries;
|
|
42
|
+
exports.readSkillerLockNames = readSkillerLockNames;
|
|
43
|
+
const fs = __importStar(require("fs/promises"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
exports.SKILLER_LOCK_FILENAME = 'skiller-lock.json';
|
|
46
|
+
const SKILLER_LOCK_VERSION = 1;
|
|
47
|
+
function createEmptyLock() {
|
|
48
|
+
return {
|
|
49
|
+
version: SKILLER_LOCK_VERSION,
|
|
50
|
+
skills: {},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function getSkillerLockPath(projectRoot) {
|
|
54
|
+
return path.join(projectRoot, exports.SKILLER_LOCK_FILENAME);
|
|
55
|
+
}
|
|
56
|
+
async function readSkillerLock(projectRoot) {
|
|
57
|
+
try {
|
|
58
|
+
const raw = JSON.parse(await fs.readFile(getSkillerLockPath(projectRoot), 'utf8'));
|
|
59
|
+
if (raw.version !== SKILLER_LOCK_VERSION || !raw.skills) {
|
|
60
|
+
return createEmptyLock();
|
|
61
|
+
}
|
|
62
|
+
return raw;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return createEmptyLock();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function writeSkillerLock(projectRoot, lock) {
|
|
69
|
+
const lockPath = getSkillerLockPath(projectRoot);
|
|
70
|
+
const sortedSkills = {};
|
|
71
|
+
for (const key of Object.keys(lock.skills).sort((a, b) => a.localeCompare(b))) {
|
|
72
|
+
sortedSkills[key] = lock.skills[key];
|
|
73
|
+
}
|
|
74
|
+
if (Object.keys(sortedSkills).length === 0) {
|
|
75
|
+
await fs.rm(lockPath, { force: true });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await fs.writeFile(lockPath, JSON.stringify({
|
|
79
|
+
version: SKILLER_LOCK_VERSION,
|
|
80
|
+
skills: sortedSkills,
|
|
81
|
+
}, null, 2) + '\n', 'utf8');
|
|
82
|
+
}
|
|
83
|
+
async function upsertSkillerLockEntries(projectRoot, entries) {
|
|
84
|
+
if (Object.keys(entries).length === 0)
|
|
85
|
+
return;
|
|
86
|
+
const lock = await readSkillerLock(projectRoot);
|
|
87
|
+
lock.skills = {
|
|
88
|
+
...lock.skills,
|
|
89
|
+
...entries,
|
|
90
|
+
};
|
|
91
|
+
await writeSkillerLock(projectRoot, lock);
|
|
92
|
+
}
|
|
93
|
+
async function removeSkillerLockEntries(projectRoot, skillNames) {
|
|
94
|
+
if (skillNames.length === 0)
|
|
95
|
+
return [];
|
|
96
|
+
const lock = await readSkillerLock(projectRoot);
|
|
97
|
+
const removed = [];
|
|
98
|
+
for (const skillName of skillNames) {
|
|
99
|
+
if (!(skillName in lock.skills))
|
|
100
|
+
continue;
|
|
101
|
+
delete lock.skills[skillName];
|
|
102
|
+
removed.push(skillName);
|
|
103
|
+
}
|
|
104
|
+
if (removed.length > 0) {
|
|
105
|
+
await writeSkillerLock(projectRoot, lock);
|
|
106
|
+
}
|
|
107
|
+
return removed.sort((a, b) => a.localeCompare(b));
|
|
108
|
+
}
|
|
109
|
+
async function readSkillerLockNames(projectRoot) {
|
|
110
|
+
const lock = await readSkillerLock(projectRoot);
|
|
111
|
+
return new Set(Object.keys(lock.skills).sort((a, b) => a.localeCompare(b)));
|
|
112
|
+
}
|
|
@@ -727,7 +727,7 @@ async function compileRulesToSkills(skillerDir, projectRoot, verbose, dryRun) {
|
|
|
727
727
|
for (const ruleFile of ruleFiles) {
|
|
728
728
|
const skillName = path.basename(ruleFile.name, '.mdc');
|
|
729
729
|
if (ownership.upstreamOwned.has(skillName)) {
|
|
730
|
-
throw new Error(`Local rule '${skillName}' conflicts with
|
|
730
|
+
throw new Error(`Local rule '${skillName}' conflicts with lock-managed skill '${skillName}' in skills-lock.json or skiller-lock.json`);
|
|
731
731
|
}
|
|
732
732
|
const sourcePath = path.join(rulesDir, ruleFile.name);
|
|
733
733
|
const sourceContent = await fs.readFile(sourcePath, 'utf8');
|