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.
@@ -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 with the local skills CLI experimental_install command, then skiller apply', (y) => skillsArgsBuilder(y)
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..]', 'Run the local skills CLI update command, then skiller apply', (y) => skillsArgsBuilder(y)
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',
@@ -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
- await executeSkillsWrapper(argv['project-root'], [
634
- 'add',
635
- ...(argv.args ?? []),
636
- ]);
637
- await applyAfterSkillsLifecycleStep(argv['project-root'], argv.verbose ?? false);
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
- await executeSkillsWrapper(argv['project-root'], [
648
- 'remove',
649
- ...(argv.args ?? []),
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(argv['project-root'], argv.args ?? []);
653
- await applyAfterSkillsLifecycleStep(argv['project-root'], argv.verbose ?? false);
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
- return new Set(Object.keys(raw.skills ?? {})
312
- .map(normalizeSkillNameForFilesystem)
313
- .sort((a, b) => a.localeCompare(b)));
313
+ for (const key of Object.keys(raw.skills ?? {})) {
314
+ upstreamOwned.add(normalizeSkillNameForFilesystem(key));
315
+ }
314
316
  }
315
317
  catch {
316
- return new Set();
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('skills-lock.json');
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 upstream-managed skill '${skillName}' in skills-lock.json`);
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');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skiller",
3
- "version": "0.9.9",
3
+ "version": "0.9.11",
4
4
  "description": "Skiller — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "publishConfig": {