skiller 0.9.8 → 0.9.10
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
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..]', '
|
|
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 {
|
|
@@ -630,27 +631,88 @@ async function migrateRulesToSkillsHandler(argv) {
|
|
|
630
631
|
}
|
|
631
632
|
}
|
|
632
633
|
async function addHandler(argv) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
634
|
+
const projectRoot = argv['project-root'];
|
|
635
|
+
const args = argv.args ?? [];
|
|
636
|
+
if ((0, AgentSourceCompatibility_1.hasGlobalFlag)(args)) {
|
|
637
|
+
throw new Error('Agent-compatible installs are project-scoped only. Drop --global and retry.');
|
|
638
|
+
}
|
|
639
|
+
const source = (0, AgentSourceCompatibility_1.extractAddSource)(args);
|
|
640
|
+
let inspection;
|
|
641
|
+
let installedAgentSkills = false;
|
|
642
|
+
try {
|
|
643
|
+
if (source) {
|
|
644
|
+
inspection = await (0, AgentSourceCompatibility_1.inspectCompatibleSource)(source, args);
|
|
645
|
+
}
|
|
646
|
+
if (inspection && (0, AgentSourceCompatibility_1.hasListFlag)(args)) {
|
|
647
|
+
if (inspection.nativeSkillNames.length > 0) {
|
|
648
|
+
await executeSkillsWrapper(projectRoot, [
|
|
649
|
+
'add',
|
|
650
|
+
...(0, AgentSourceCompatibility_1.buildAdjustedSkillsAddArgs)(args, inspection.nativeSkillNames),
|
|
651
|
+
]);
|
|
652
|
+
}
|
|
653
|
+
if (inspection.agentSkills.length > 0) {
|
|
654
|
+
console.log('[skiller] Compatible agent-derived skills:');
|
|
655
|
+
for (const agentSkill of inspection.agentSkills) {
|
|
656
|
+
console.log(`- ${agentSkill.installName}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (inspection && inspection.nativeSkillNames.length > 0) {
|
|
662
|
+
await executeSkillsWrapper(projectRoot, [
|
|
663
|
+
'add',
|
|
664
|
+
...(0, AgentSourceCompatibility_1.buildAdjustedSkillsAddArgs)(args, inspection.nativeSkillNames),
|
|
665
|
+
]);
|
|
666
|
+
}
|
|
667
|
+
else if (!inspection || inspection.agentSkills.length === 0) {
|
|
668
|
+
await executeSkillsWrapper(projectRoot, ['add', ...args]);
|
|
669
|
+
}
|
|
670
|
+
if (inspection && inspection.agentSkills.length > 0) {
|
|
671
|
+
const installed = await (0, AgentSourceCompatibility_1.installAgentSkillsFromInspection)(projectRoot, inspection);
|
|
672
|
+
installedAgentSkills = true;
|
|
673
|
+
console.log(`[skiller] Installed ${installed.length} agent-derived skill(s): ${installed.join(', ')}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
finally {
|
|
677
|
+
if (inspection && !installedAgentSkills) {
|
|
678
|
+
await inspection.workspace.cleanup();
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
await applyAfterSkillsLifecycleStep(projectRoot, argv.verbose ?? false);
|
|
638
682
|
}
|
|
639
683
|
async function installHandler(argv) {
|
|
640
684
|
await executeSkillsWrapper(argv['project-root'], [
|
|
641
|
-
'
|
|
685
|
+
'experimental_install',
|
|
642
686
|
...(argv.args ?? []),
|
|
643
687
|
]);
|
|
688
|
+
const restored = await (0, AgentSourceCompatibility_1.restoreAgentSkillsFromLock)(argv['project-root']);
|
|
689
|
+
if (restored.restored.length > 0) {
|
|
690
|
+
console.log(`[skiller] Restored ${restored.restored.length} agent-derived skill(s): ${restored.restored.join(', ')}`);
|
|
691
|
+
}
|
|
692
|
+
if (restored.warnings.length > 0) {
|
|
693
|
+
console.log(restored.warnings.map((warning) => `[skiller] ${warning}`).join('\n'));
|
|
694
|
+
}
|
|
644
695
|
await applyAfterSkillsLifecycleStep(argv['project-root'], argv.verbose ?? false);
|
|
645
696
|
}
|
|
646
697
|
async function removeHandler(argv) {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
698
|
+
const projectRoot = argv['project-root'];
|
|
699
|
+
const args = argv.args ?? [];
|
|
700
|
+
const requestedNames = normalizeRequestedSkillNames(args);
|
|
701
|
+
const ownership = await (0, SkillOwnership_1.resolveSkillOwnership)(projectRoot);
|
|
702
|
+
const shouldRunSkillsRemove = requestedNames.length === 0 ||
|
|
703
|
+
args.some((arg) => arg === '--all' ||
|
|
704
|
+
arg === '--agent' ||
|
|
705
|
+
arg === '-a' ||
|
|
706
|
+
arg === '--skill' ||
|
|
707
|
+
arg === '-s') ||
|
|
708
|
+
requestedNames.some((name) => ownership.upstreamOwned.has(name));
|
|
709
|
+
if (shouldRunSkillsRemove) {
|
|
710
|
+
await executeSkillsWrapper(projectRoot, ['remove', ...args]);
|
|
711
|
+
}
|
|
651
712
|
await scrubRequestedSkillsLockEntries(argv['project-root'], argv.args ?? []);
|
|
652
|
-
await pruneRequestedUnmanagedSkillOutputs(
|
|
653
|
-
await
|
|
713
|
+
await pruneRequestedUnmanagedSkillOutputs(projectRoot, args);
|
|
714
|
+
await (0, AgentSourceCompatibility_1.removeAgentManagedSkills)(projectRoot, requestedNames);
|
|
715
|
+
await applyAfterSkillsLifecycleStep(projectRoot, argv.verbose ?? false);
|
|
654
716
|
}
|
|
655
717
|
async function listHandler(argv) {
|
|
656
718
|
await executeSkillsWrapper(argv['project-root'], [
|
|
@@ -675,6 +737,13 @@ async function updateHandler(argv) {
|
|
|
675
737
|
'update',
|
|
676
738
|
...(argv.args ?? []),
|
|
677
739
|
]);
|
|
740
|
+
const updated = await (0, AgentSourceCompatibility_1.updateAgentSkillsFromLock)(argv['project-root']);
|
|
741
|
+
if (updated.updated.length > 0) {
|
|
742
|
+
console.log(`[skiller] Updated ${updated.updated.length} agent-derived skill(s): ${updated.updated.join(', ')}`);
|
|
743
|
+
}
|
|
744
|
+
if (updated.warnings.length > 0) {
|
|
745
|
+
console.log(updated.warnings.map((warning) => `[skiller] ${warning}`).join('\n'));
|
|
746
|
+
}
|
|
678
747
|
await applyAfterSkillsLifecycleStep(argv['project-root'], argv.verbose ?? false);
|
|
679
748
|
}
|
|
680
749
|
async function outdatedHandler(argv) {
|
|
@@ -682,6 +751,13 @@ async function outdatedHandler(argv) {
|
|
|
682
751
|
'outdated',
|
|
683
752
|
...(argv.args ?? []),
|
|
684
753
|
]);
|
|
754
|
+
const outdated = await (0, AgentSourceCompatibility_1.getOutdatedAgentSkills)(argv['project-root']);
|
|
755
|
+
if (outdated.outdated.length > 0) {
|
|
756
|
+
console.log(`[skiller] Agent-derived updates available:\n${outdated.outdated.map((name) => `- ${name}`).join('\n')}`);
|
|
757
|
+
}
|
|
758
|
+
if (outdated.warnings.length > 0) {
|
|
759
|
+
console.log(outdated.warnings.map((warning) => `[skiller] ${warning}`).join('\n'));
|
|
760
|
+
}
|
|
685
761
|
}
|
|
686
762
|
async function skillsHandler(argv) {
|
|
687
763
|
await executeSkillsWrapper(argv['project-root'], [
|
|
@@ -0,0 +1,710 @@
|
|
|
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
|
+
const crypto = __importStar(require("crypto"));
|
|
48
|
+
const fs = __importStar(require("fs/promises"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const os = __importStar(require("os"));
|
|
51
|
+
const yaml = __importStar(require("js-yaml"));
|
|
52
|
+
const child_process_1 = require("child_process");
|
|
53
|
+
const FrontmatterParser_1 = require("./FrontmatterParser");
|
|
54
|
+
const SkillerLock_1 = require("./SkillerLock");
|
|
55
|
+
const CANONICAL_SKILLS_DIR = path.join('.agents', 'skills');
|
|
56
|
+
const LEGACY_CLAUDE_SKILLS_DIR = path.join('.claude', 'skills');
|
|
57
|
+
const SKIP_DIRS = new Set([
|
|
58
|
+
'.git',
|
|
59
|
+
'.next',
|
|
60
|
+
'build',
|
|
61
|
+
'coverage',
|
|
62
|
+
'dist',
|
|
63
|
+
'fixtures',
|
|
64
|
+
'node_modules',
|
|
65
|
+
'test',
|
|
66
|
+
'tests',
|
|
67
|
+
'tmp',
|
|
68
|
+
'tmp-fixtures',
|
|
69
|
+
]);
|
|
70
|
+
function hashContent(content) {
|
|
71
|
+
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
72
|
+
}
|
|
73
|
+
function normalizeSkillNameForFilesystem(name) {
|
|
74
|
+
return name.trim().replace(/:/g, '-');
|
|
75
|
+
}
|
|
76
|
+
function isLocalPath(input) {
|
|
77
|
+
return (path.isAbsolute(input) ||
|
|
78
|
+
input.startsWith('./') ||
|
|
79
|
+
input.startsWith('../') ||
|
|
80
|
+
input === '.' ||
|
|
81
|
+
input === '..' ||
|
|
82
|
+
/^[a-zA-Z]:[/\\]/.test(input));
|
|
83
|
+
}
|
|
84
|
+
function sanitizeSubpath(subpath) {
|
|
85
|
+
const normalized = subpath.replace(/\\/g, '/');
|
|
86
|
+
for (const segment of normalized.split('/')) {
|
|
87
|
+
if (segment === '..') {
|
|
88
|
+
throw new Error(`Unsafe subpath '${subpath}'`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return normalized;
|
|
92
|
+
}
|
|
93
|
+
function inferLocalSourceRootFromFile(filePath) {
|
|
94
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
95
|
+
const agentMarker = '/agents/';
|
|
96
|
+
const skillsMarker = '/skills/';
|
|
97
|
+
if (normalized.includes(agentMarker)) {
|
|
98
|
+
return filePath.slice(0, normalized.indexOf(agentMarker));
|
|
99
|
+
}
|
|
100
|
+
if (normalized.includes(skillsMarker)) {
|
|
101
|
+
return filePath.slice(0, normalized.indexOf(skillsMarker));
|
|
102
|
+
}
|
|
103
|
+
return path.dirname(filePath);
|
|
104
|
+
}
|
|
105
|
+
function parseCompatibleSource(input) {
|
|
106
|
+
const trimmed = input.trim();
|
|
107
|
+
if (isLocalPath(trimmed)) {
|
|
108
|
+
return {
|
|
109
|
+
source: path.resolve(trimmed),
|
|
110
|
+
type: 'local',
|
|
111
|
+
url: path.resolve(trimmed),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const githubPrefixMatch = trimmed.match(/^github:(.+)$/);
|
|
115
|
+
if (githubPrefixMatch) {
|
|
116
|
+
return parseCompatibleSource(githubPrefixMatch[1]);
|
|
117
|
+
}
|
|
118
|
+
const githubBlobMatch = trimmed.match(/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)/);
|
|
119
|
+
if (githubBlobMatch) {
|
|
120
|
+
const [, owner, repo, ref, subpath] = githubBlobMatch;
|
|
121
|
+
return {
|
|
122
|
+
ref,
|
|
123
|
+
source: `${owner}/${repo.replace(/\.git$/, '')}`,
|
|
124
|
+
subpath: sanitizeSubpath(subpath),
|
|
125
|
+
type: 'github',
|
|
126
|
+
url: `https://github.com/${owner}/${repo.replace(/\.git$/, '')}.git`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const githubTreeWithPathMatch = trimmed.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/);
|
|
130
|
+
if (githubTreeWithPathMatch) {
|
|
131
|
+
const [, owner, repo, ref, subpath] = githubTreeWithPathMatch;
|
|
132
|
+
return {
|
|
133
|
+
ref,
|
|
134
|
+
source: `${owner}/${repo.replace(/\.git$/, '')}`,
|
|
135
|
+
subpath: sanitizeSubpath(subpath),
|
|
136
|
+
type: 'github',
|
|
137
|
+
url: `https://github.com/${owner}/${repo.replace(/\.git$/, '')}.git`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const githubTreeMatch = trimmed.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)$/);
|
|
141
|
+
if (githubTreeMatch) {
|
|
142
|
+
const [, owner, repo, ref] = githubTreeMatch;
|
|
143
|
+
return {
|
|
144
|
+
ref,
|
|
145
|
+
source: `${owner}/${repo.replace(/\.git$/, '')}`,
|
|
146
|
+
type: 'github',
|
|
147
|
+
url: `https://github.com/${owner}/${repo.replace(/\.git$/, '')}.git`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const githubRepoMatch = trimmed.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
151
|
+
if (githubRepoMatch) {
|
|
152
|
+
const [, owner, repo] = githubRepoMatch;
|
|
153
|
+
return {
|
|
154
|
+
source: `${owner}/${repo.replace(/\.git$/, '')}`,
|
|
155
|
+
type: 'github',
|
|
156
|
+
url: `https://github.com/${owner}/${repo.replace(/\.git$/, '')}.git`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const shorthandMatch = trimmed.match(/^([^/]+)\/([^/]+)$/);
|
|
160
|
+
if (shorthandMatch) {
|
|
161
|
+
const [, owner, repo] = shorthandMatch;
|
|
162
|
+
return {
|
|
163
|
+
source: `${owner}/${repo}`,
|
|
164
|
+
type: 'github',
|
|
165
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (trimmed.startsWith('git@') ||
|
|
169
|
+
trimmed.startsWith('https://') ||
|
|
170
|
+
trimmed.startsWith('http://')) {
|
|
171
|
+
return {
|
|
172
|
+
source: trimmed,
|
|
173
|
+
type: 'git',
|
|
174
|
+
url: trimmed,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
throw new Error(`Unsupported source '${input}'`);
|
|
178
|
+
}
|
|
179
|
+
async function runGitClone(url, ref) {
|
|
180
|
+
const cloneDir = await fs.mkdtemp(path.join(os.tmpdir(), 'skiller-agents-'));
|
|
181
|
+
const args = ['clone', '--depth', '1'];
|
|
182
|
+
if (ref) {
|
|
183
|
+
args.push('--branch', ref);
|
|
184
|
+
}
|
|
185
|
+
args.push(url, cloneDir);
|
|
186
|
+
await new Promise((resolve, reject) => {
|
|
187
|
+
const child = (0, child_process_1.spawn)('git', args, {
|
|
188
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
189
|
+
stdio: 'pipe',
|
|
190
|
+
});
|
|
191
|
+
let stderr = '';
|
|
192
|
+
child.stderr.on('data', (chunk) => {
|
|
193
|
+
stderr += chunk.toString();
|
|
194
|
+
});
|
|
195
|
+
child.on('error', reject);
|
|
196
|
+
child.on('exit', (code) => {
|
|
197
|
+
if (code === 0) {
|
|
198
|
+
resolve();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
reject(new Error(stderr.trim() || `git clone failed with exit code ${code}`));
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
return cloneDir;
|
|
205
|
+
}
|
|
206
|
+
async function withSourceWorkspace(rawSource) {
|
|
207
|
+
const parsed = parseCompatibleSource(rawSource);
|
|
208
|
+
return withParsedSourceWorkspace(parsed);
|
|
209
|
+
}
|
|
210
|
+
async function withParsedSourceWorkspace(parsed) {
|
|
211
|
+
if (parsed.type === 'local') {
|
|
212
|
+
const targetPath = parsed.subpath
|
|
213
|
+
? path.join(parsed.url, parsed.subpath)
|
|
214
|
+
: parsed.url;
|
|
215
|
+
let rootPath = parsed.url;
|
|
216
|
+
let searchPath = targetPath;
|
|
217
|
+
try {
|
|
218
|
+
const stats = await fs.stat(targetPath);
|
|
219
|
+
if (stats.isFile()) {
|
|
220
|
+
rootPath = inferLocalSourceRootFromFile(targetPath);
|
|
221
|
+
searchPath = targetPath;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Let downstream callers surface missing paths consistently.
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
cleanup: async () => undefined,
|
|
229
|
+
parsed,
|
|
230
|
+
rootPath,
|
|
231
|
+
searchPath,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const cloneDir = await runGitClone(parsed.url, parsed.ref);
|
|
235
|
+
const searchPath = parsed.subpath
|
|
236
|
+
? path.join(cloneDir, parsed.subpath)
|
|
237
|
+
: cloneDir;
|
|
238
|
+
return {
|
|
239
|
+
cleanup: async () => {
|
|
240
|
+
await fs.rm(cloneDir, { force: true, recursive: true });
|
|
241
|
+
},
|
|
242
|
+
parsed,
|
|
243
|
+
rootPath: cloneDir,
|
|
244
|
+
searchPath,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function parseSourceFromLockEntry(entry) {
|
|
248
|
+
if (entry.sourceType === 'local') {
|
|
249
|
+
return {
|
|
250
|
+
ref: entry.ref,
|
|
251
|
+
source: entry.source,
|
|
252
|
+
subpath: entry.subpath,
|
|
253
|
+
type: 'local',
|
|
254
|
+
url: entry.source,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (entry.sourceType === 'github') {
|
|
258
|
+
return {
|
|
259
|
+
ref: entry.ref,
|
|
260
|
+
source: entry.source,
|
|
261
|
+
subpath: entry.subpath,
|
|
262
|
+
type: 'github',
|
|
263
|
+
url: `https://github.com/${entry.source}.git`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
ref: entry.ref,
|
|
268
|
+
source: entry.source,
|
|
269
|
+
subpath: entry.subpath,
|
|
270
|
+
type: 'git',
|
|
271
|
+
url: entry.source,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function cloneRecord(value) {
|
|
275
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
276
|
+
return {};
|
|
277
|
+
}
|
|
278
|
+
return { ...value };
|
|
279
|
+
}
|
|
280
|
+
function buildCompiledSkillContent(name, rawFrontmatter, body, sourceRelPath) {
|
|
281
|
+
const frontmatter = rawFrontmatter ? { ...rawFrontmatter } : {};
|
|
282
|
+
if (typeof frontmatter.name !== 'string' || frontmatter.name.length === 0) {
|
|
283
|
+
frontmatter.name = name;
|
|
284
|
+
}
|
|
285
|
+
if (typeof frontmatter.description !== 'string' ||
|
|
286
|
+
frontmatter.description.length === 0) {
|
|
287
|
+
frontmatter.description = `Skill: ${name}`;
|
|
288
|
+
}
|
|
289
|
+
const metadata = cloneRecord(frontmatter.metadata);
|
|
290
|
+
const skiller = cloneRecord(metadata.skiller);
|
|
291
|
+
skiller.source = sourceRelPath;
|
|
292
|
+
metadata.skiller = skiller;
|
|
293
|
+
frontmatter.metadata = metadata;
|
|
294
|
+
return `---
|
|
295
|
+
${yaml.dump(frontmatter, { lineWidth: -1, noRefs: true }).trim()}
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
${body.trim()}
|
|
299
|
+
`;
|
|
300
|
+
}
|
|
301
|
+
function shouldSkipDir(name) {
|
|
302
|
+
return SKIP_DIRS.has(name);
|
|
303
|
+
}
|
|
304
|
+
function isValidRelativePath(value) {
|
|
305
|
+
return value.startsWith('./');
|
|
306
|
+
}
|
|
307
|
+
async function collectDeclaredPluginBases(basePath) {
|
|
308
|
+
const pluginBases = new Set();
|
|
309
|
+
try {
|
|
310
|
+
const marketplace = JSON.parse(await fs.readFile(path.join(basePath, '.claude-plugin', 'marketplace.json'), 'utf8'));
|
|
311
|
+
const pluginRoot = marketplace.metadata?.pluginRoot;
|
|
312
|
+
const validPluginRoot = pluginRoot === undefined || isValidRelativePath(pluginRoot);
|
|
313
|
+
if (validPluginRoot) {
|
|
314
|
+
for (const plugin of marketplace.plugins ?? []) {
|
|
315
|
+
if (typeof plugin.source !== 'string')
|
|
316
|
+
continue;
|
|
317
|
+
if (!isValidRelativePath(plugin.source))
|
|
318
|
+
continue;
|
|
319
|
+
pluginBases.add(path.join(basePath, pluginRoot ?? '', plugin.source));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// Ignore invalid or missing marketplace manifests.
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
await fs.access(path.join(basePath, '.claude-plugin', 'plugin.json'));
|
|
328
|
+
pluginBases.add(basePath);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
// Ignore.
|
|
332
|
+
}
|
|
333
|
+
return [...pluginBases];
|
|
334
|
+
}
|
|
335
|
+
async function discoverAgentFiles(basePath, rootPath = basePath) {
|
|
336
|
+
try {
|
|
337
|
+
const stats = await fs.stat(basePath);
|
|
338
|
+
if (stats.isFile()) {
|
|
339
|
+
if (!basePath.endsWith('.md'))
|
|
340
|
+
return [];
|
|
341
|
+
return [
|
|
342
|
+
{
|
|
343
|
+
absolutePath: basePath,
|
|
344
|
+
relativePath: path.relative(rootPath, basePath).replace(/\\/g, '/'),
|
|
345
|
+
},
|
|
346
|
+
];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
const discovered = new Map();
|
|
353
|
+
const candidateDirs = new Set([path.join(basePath, 'agents')]);
|
|
354
|
+
for (const pluginBase of await collectDeclaredPluginBases(basePath)) {
|
|
355
|
+
candidateDirs.add(path.join(pluginBase, 'agents'));
|
|
356
|
+
}
|
|
357
|
+
const walk = async (currentDir, depth) => {
|
|
358
|
+
if (depth > 8)
|
|
359
|
+
return;
|
|
360
|
+
let entries;
|
|
361
|
+
try {
|
|
362
|
+
entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
for (const entry of entries) {
|
|
368
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
369
|
+
if (entry.isDirectory()) {
|
|
370
|
+
if (shouldSkipDir(entry.name))
|
|
371
|
+
continue;
|
|
372
|
+
if (entry.name === 'agents') {
|
|
373
|
+
candidateDirs.add(absolutePath);
|
|
374
|
+
}
|
|
375
|
+
await walk(absolutePath, depth + 1);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
await walk(basePath, 0);
|
|
381
|
+
const collectMarkdown = async (dir, depth) => {
|
|
382
|
+
if (depth > 8)
|
|
383
|
+
return;
|
|
384
|
+
let entries;
|
|
385
|
+
try {
|
|
386
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
for (const entry of entries) {
|
|
392
|
+
const absolutePath = path.join(dir, entry.name);
|
|
393
|
+
if (entry.isDirectory()) {
|
|
394
|
+
if (shouldSkipDir(entry.name))
|
|
395
|
+
continue;
|
|
396
|
+
await collectMarkdown(absolutePath, depth + 1);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
400
|
+
continue;
|
|
401
|
+
const relativePath = path
|
|
402
|
+
.relative(basePath, absolutePath)
|
|
403
|
+
.replace(/\\/g, '/');
|
|
404
|
+
discovered.set(relativePath, { absolutePath, relativePath });
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
for (const candidateDir of candidateDirs) {
|
|
408
|
+
await collectMarkdown(candidateDir, 0);
|
|
409
|
+
}
|
|
410
|
+
return [...discovered.values()].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
411
|
+
}
|
|
412
|
+
async function discoverSkillNames(basePath) {
|
|
413
|
+
try {
|
|
414
|
+
const stats = await fs.stat(basePath);
|
|
415
|
+
if (stats.isFile()) {
|
|
416
|
+
if (path.basename(basePath) !== 'SKILL.md')
|
|
417
|
+
return [];
|
|
418
|
+
const parsed = (0, FrontmatterParser_1.parseFrontmatter)(await fs.readFile(basePath, 'utf8'));
|
|
419
|
+
const declaredName = typeof parsed.frontmatter?.name === 'string'
|
|
420
|
+
? parsed.frontmatter.name
|
|
421
|
+
: path.basename(path.dirname(basePath));
|
|
422
|
+
return [normalizeSkillNameForFilesystem(declaredName)];
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
// Ignore missing paths.
|
|
427
|
+
}
|
|
428
|
+
const discovered = new Set();
|
|
429
|
+
const candidateDirs = new Set([path.join(basePath, 'skills')]);
|
|
430
|
+
try {
|
|
431
|
+
const parsed = (0, FrontmatterParser_1.parseFrontmatter)(await fs.readFile(path.join(basePath, 'SKILL.md'), 'utf8'));
|
|
432
|
+
const declaredName = typeof parsed.frontmatter?.name === 'string'
|
|
433
|
+
? parsed.frontmatter.name
|
|
434
|
+
: path.basename(basePath);
|
|
435
|
+
discovered.add(normalizeSkillNameForFilesystem(declaredName));
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
// Ignore missing root SKILL.md.
|
|
439
|
+
}
|
|
440
|
+
for (const pluginBase of await collectDeclaredPluginBases(basePath)) {
|
|
441
|
+
candidateDirs.add(path.join(pluginBase, 'skills'));
|
|
442
|
+
}
|
|
443
|
+
for (const candidateDir of candidateDirs) {
|
|
444
|
+
let entries;
|
|
445
|
+
try {
|
|
446
|
+
entries = await fs.readdir(candidateDir, { withFileTypes: true });
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
for (const entry of entries) {
|
|
452
|
+
if (!entry.isDirectory())
|
|
453
|
+
continue;
|
|
454
|
+
const skillMdPath = path.join(candidateDir, entry.name, 'SKILL.md');
|
|
455
|
+
try {
|
|
456
|
+
const parsed = (0, FrontmatterParser_1.parseFrontmatter)(await fs.readFile(skillMdPath, 'utf8'));
|
|
457
|
+
const declaredName = typeof parsed.frontmatter?.name === 'string'
|
|
458
|
+
? parsed.frontmatter.name
|
|
459
|
+
: entry.name;
|
|
460
|
+
discovered.add(normalizeSkillNameForFilesystem(declaredName));
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return [...discovered].sort((a, b) => a.localeCompare(b));
|
|
468
|
+
}
|
|
469
|
+
async function discoverAgentSkillCandidates(basePath, rootPath = basePath) {
|
|
470
|
+
const agentFiles = await discoverAgentFiles(basePath, rootPath);
|
|
471
|
+
const candidates = [];
|
|
472
|
+
for (const agentFile of agentFiles) {
|
|
473
|
+
const parsed = (0, FrontmatterParser_1.parseFrontmatter)(await fs.readFile(agentFile.absolutePath, 'utf8'));
|
|
474
|
+
const declaredName = parsed.frontmatter?.name;
|
|
475
|
+
const description = parsed.frontmatter?.description;
|
|
476
|
+
if (typeof declaredName !== 'string' || typeof description !== 'string') {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const compiledContent = buildCompiledSkillContent(declaredName, parsed.rawFrontmatter, parsed.body, agentFile.relativePath);
|
|
480
|
+
candidates.push({
|
|
481
|
+
compiledContent,
|
|
482
|
+
computedHash: hashContent(compiledContent),
|
|
483
|
+
description,
|
|
484
|
+
installName: normalizeSkillNameForFilesystem(declaredName),
|
|
485
|
+
name: declaredName,
|
|
486
|
+
sourceRelPath: agentFile.relativePath,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
return candidates.sort((a, b) => a.installName.localeCompare(b.installName));
|
|
490
|
+
}
|
|
491
|
+
function parseRequestedSkillFilters(args) {
|
|
492
|
+
const names = new Set();
|
|
493
|
+
let wildcard = false;
|
|
494
|
+
const argv = args ?? [];
|
|
495
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
496
|
+
const current = argv[index];
|
|
497
|
+
if (current !== '--skill' && current !== '-s')
|
|
498
|
+
continue;
|
|
499
|
+
for (let valueIndex = index + 1; valueIndex < argv.length; valueIndex += 1) {
|
|
500
|
+
const value = argv[valueIndex];
|
|
501
|
+
if (value.startsWith('-'))
|
|
502
|
+
break;
|
|
503
|
+
if (value === '*') {
|
|
504
|
+
wildcard = true;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
names.add(normalizeSkillNameForFilesystem(value));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return { names, wildcard };
|
|
511
|
+
}
|
|
512
|
+
function hasListFlag(args) {
|
|
513
|
+
return (args ?? []).some((arg) => arg === '--list' || arg === '-l');
|
|
514
|
+
}
|
|
515
|
+
function hasGlobalFlag(args) {
|
|
516
|
+
return (args ?? []).some((arg) => arg === '--global' || arg === '-g');
|
|
517
|
+
}
|
|
518
|
+
function extractAddSource(args) {
|
|
519
|
+
for (const arg of args ?? []) {
|
|
520
|
+
if (arg.startsWith('-'))
|
|
521
|
+
continue;
|
|
522
|
+
return arg;
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
function buildAdjustedSkillsAddArgs(originalArgs, nativeSkillNames) {
|
|
527
|
+
const args = [...(originalArgs ?? [])];
|
|
528
|
+
const next = [];
|
|
529
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
530
|
+
const current = args[index];
|
|
531
|
+
if (current === '--skill' || current === '-s') {
|
|
532
|
+
index += 1;
|
|
533
|
+
while (index < args.length && !args[index].startsWith('-')) {
|
|
534
|
+
index += 1;
|
|
535
|
+
}
|
|
536
|
+
index -= 1;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
next.push(current);
|
|
540
|
+
}
|
|
541
|
+
if (nativeSkillNames.length > 0) {
|
|
542
|
+
next.push('--skill', ...nativeSkillNames);
|
|
543
|
+
}
|
|
544
|
+
return next;
|
|
545
|
+
}
|
|
546
|
+
async function inspectCompatibleSource(rawSource, args) {
|
|
547
|
+
const workspace = await withSourceWorkspace(rawSource);
|
|
548
|
+
const [nativeSkillNames, discoveredAgentSkills] = await Promise.all([
|
|
549
|
+
discoverSkillNames(workspace.searchPath),
|
|
550
|
+
discoverAgentSkillCandidates(workspace.searchPath, workspace.rootPath),
|
|
551
|
+
]);
|
|
552
|
+
const requested = parseRequestedSkillFilters(args);
|
|
553
|
+
const agentSkills = discoveredAgentSkills.filter((agentSkill) => {
|
|
554
|
+
if (requested.wildcard || requested.names.size === 0)
|
|
555
|
+
return true;
|
|
556
|
+
return requested.names.has(agentSkill.installName);
|
|
557
|
+
});
|
|
558
|
+
return {
|
|
559
|
+
agentSkills: agentSkills.sort((a, b) => a.installName.localeCompare(b.installName)),
|
|
560
|
+
nativeSkillNames: requested.wildcard
|
|
561
|
+
? nativeSkillNames
|
|
562
|
+
: nativeSkillNames.filter((name) => requested.names.size === 0 || requested.names.has(name)),
|
|
563
|
+
workspace,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
async function installAgentSkillsFromInspection(projectRoot, inspection) {
|
|
567
|
+
const installed = [];
|
|
568
|
+
const lockEntries = {};
|
|
569
|
+
const canonicalSkillsDir = path.join(projectRoot, CANONICAL_SKILLS_DIR);
|
|
570
|
+
for (const agentSkill of inspection.agentSkills) {
|
|
571
|
+
const skillDir = path.join(canonicalSkillsDir, agentSkill.installName);
|
|
572
|
+
await fs.rm(skillDir, { force: true, recursive: true });
|
|
573
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
574
|
+
await fs.writeFile(path.join(skillDir, 'SKILL.md'), agentSkill.compiledContent, 'utf8');
|
|
575
|
+
lockEntries[agentSkill.installName] = {
|
|
576
|
+
computedHash: agentSkill.computedHash,
|
|
577
|
+
ref: inspection.workspace.parsed.ref,
|
|
578
|
+
source: inspection.workspace.parsed.source,
|
|
579
|
+
sourceRelPath: agentSkill.sourceRelPath,
|
|
580
|
+
sourceType: inspection.workspace.parsed.type,
|
|
581
|
+
subpath: inspection.workspace.parsed.subpath,
|
|
582
|
+
};
|
|
583
|
+
installed.push(agentSkill.installName);
|
|
584
|
+
}
|
|
585
|
+
await (0, SkillerLock_1.upsertSkillerLockEntries)(projectRoot, lockEntries);
|
|
586
|
+
await inspection.workspace.cleanup();
|
|
587
|
+
return installed.sort((a, b) => a.localeCompare(b));
|
|
588
|
+
}
|
|
589
|
+
function groupLockEntriesBySource(skills) {
|
|
590
|
+
const groups = new Map();
|
|
591
|
+
for (const [skillName, entry] of Object.entries(skills)) {
|
|
592
|
+
const key = JSON.stringify([
|
|
593
|
+
entry.source,
|
|
594
|
+
entry.sourceType,
|
|
595
|
+
entry.ref ?? '',
|
|
596
|
+
entry.subpath ?? '',
|
|
597
|
+
]);
|
|
598
|
+
const existing = groups.get(key) ?? [];
|
|
599
|
+
existing.push([skillName, entry]);
|
|
600
|
+
groups.set(key, existing);
|
|
601
|
+
}
|
|
602
|
+
return groups;
|
|
603
|
+
}
|
|
604
|
+
async function installSingleAgentSkill(projectRoot, skillName, candidate, entry) {
|
|
605
|
+
const skillDir = path.join(projectRoot, CANONICAL_SKILLS_DIR, skillName);
|
|
606
|
+
await fs.rm(skillDir, { force: true, recursive: true });
|
|
607
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
608
|
+
await fs.writeFile(path.join(skillDir, 'SKILL.md'), candidate.compiledContent, 'utf8');
|
|
609
|
+
await (0, SkillerLock_1.upsertSkillerLockEntries)(projectRoot, {
|
|
610
|
+
[skillName]: {
|
|
611
|
+
...entry,
|
|
612
|
+
computedHash: candidate.computedHash,
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
async function restoreAgentSkillsFromLock(projectRoot) {
|
|
617
|
+
const lock = await (0, SkillerLock_1.readSkillerLock)(projectRoot);
|
|
618
|
+
const restored = [];
|
|
619
|
+
const warnings = [];
|
|
620
|
+
for (const entries of groupLockEntriesBySource(lock.skills).values()) {
|
|
621
|
+
const [, entry] = entries[0];
|
|
622
|
+
const workspace = await withParsedSourceWorkspace(parseSourceFromLockEntry(entry));
|
|
623
|
+
const agentSkills = await discoverAgentSkillCandidates(workspace.searchPath, workspace.rootPath);
|
|
624
|
+
const candidates = new Map(agentSkills.map((candidate) => [candidate.sourceRelPath, candidate]));
|
|
625
|
+
for (const [skillName, lockEntry] of entries) {
|
|
626
|
+
const candidate = candidates.get(lockEntry.sourceRelPath);
|
|
627
|
+
if (!candidate) {
|
|
628
|
+
warnings.push(`Could not restore '${skillName}' from ${lockEntry.source}: missing ${lockEntry.sourceRelPath}`);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
await installSingleAgentSkill(projectRoot, skillName, candidate, lockEntry);
|
|
632
|
+
restored.push(skillName);
|
|
633
|
+
}
|
|
634
|
+
await workspace.cleanup();
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
restored: restored.sort((a, b) => a.localeCompare(b)),
|
|
638
|
+
warnings,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
async function getOutdatedAgentSkills(projectRoot) {
|
|
642
|
+
const lock = await (0, SkillerLock_1.readSkillerLock)(projectRoot);
|
|
643
|
+
const outdated = [];
|
|
644
|
+
const warnings = [];
|
|
645
|
+
for (const entries of groupLockEntriesBySource(lock.skills).values()) {
|
|
646
|
+
const [, entry] = entries[0];
|
|
647
|
+
const workspace = await withParsedSourceWorkspace(parseSourceFromLockEntry(entry));
|
|
648
|
+
const agentSkills = await discoverAgentSkillCandidates(workspace.searchPath, workspace.rootPath);
|
|
649
|
+
const candidates = new Map(agentSkills.map((candidate) => [candidate.sourceRelPath, candidate]));
|
|
650
|
+
for (const [skillName, lockEntry] of entries) {
|
|
651
|
+
const candidate = candidates.get(lockEntry.sourceRelPath);
|
|
652
|
+
if (!candidate) {
|
|
653
|
+
warnings.push(`Could not check '${skillName}' from ${lockEntry.source}: missing ${lockEntry.sourceRelPath}`);
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (candidate.computedHash !== lockEntry.computedHash) {
|
|
657
|
+
outdated.push(skillName);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
await workspace.cleanup();
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
outdated: outdated.sort((a, b) => a.localeCompare(b)),
|
|
664
|
+
warnings,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
async function updateAgentSkillsFromLock(projectRoot) {
|
|
668
|
+
const lock = await (0, SkillerLock_1.readSkillerLock)(projectRoot);
|
|
669
|
+
const updated = [];
|
|
670
|
+
const warnings = [];
|
|
671
|
+
for (const entries of groupLockEntriesBySource(lock.skills).values()) {
|
|
672
|
+
const [, entry] = entries[0];
|
|
673
|
+
const workspace = await withParsedSourceWorkspace(parseSourceFromLockEntry(entry));
|
|
674
|
+
const agentSkills = await discoverAgentSkillCandidates(workspace.searchPath, workspace.rootPath);
|
|
675
|
+
const candidates = new Map(agentSkills.map((candidate) => [candidate.sourceRelPath, candidate]));
|
|
676
|
+
for (const [skillName, lockEntry] of entries) {
|
|
677
|
+
const candidate = candidates.get(lockEntry.sourceRelPath);
|
|
678
|
+
if (!candidate) {
|
|
679
|
+
warnings.push(`Could not update '${skillName}' from ${lockEntry.source}: missing ${lockEntry.sourceRelPath}`);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (candidate.computedHash === lockEntry.computedHash) {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
await installSingleAgentSkill(projectRoot, skillName, candidate, lockEntry);
|
|
686
|
+
updated.push(skillName);
|
|
687
|
+
}
|
|
688
|
+
await workspace.cleanup();
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
updated: updated.sort((a, b) => a.localeCompare(b)),
|
|
692
|
+
warnings,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
async function removeAgentManagedSkills(projectRoot, skillNames) {
|
|
696
|
+
const removed = await (0, SkillerLock_1.removeSkillerLockEntries)(projectRoot, skillNames);
|
|
697
|
+
if (removed.length === 0)
|
|
698
|
+
return [];
|
|
699
|
+
for (const skillName of removed) {
|
|
700
|
+
await fs.rm(path.join(projectRoot, CANONICAL_SKILLS_DIR, skillName), {
|
|
701
|
+
force: true,
|
|
702
|
+
recursive: true,
|
|
703
|
+
});
|
|
704
|
+
await fs.rm(path.join(projectRoot, LEGACY_CLAUDE_SKILLS_DIR, skillName), {
|
|
705
|
+
force: true,
|
|
706
|
+
recursive: true,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
return removed;
|
|
710
|
+
}
|
|
@@ -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');
|