@npeercy/skills 0.1.3 → 0.1.5

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.
Files changed (2) hide show
  1. package/lib/skills.js +182 -19
  2. package/package.json +1 -1
package/lib/skills.js CHANGED
@@ -104,13 +104,103 @@ async function promptYesNo(question, defaultYes = true) {
104
104
  return ['y', 'yes'].includes(ans);
105
105
  }
106
106
 
107
+ function resolveSymlinkTarget(linkPath) {
108
+ const target = readlinkSync(linkPath);
109
+ return target.startsWith('/') ? target : resolve(dirname(linkPath), target);
110
+ }
111
+
112
+ function skillMdPathFromEntry(fullPath, entry) {
113
+ try {
114
+ if (entry.isDirectory()) {
115
+ const md = join(fullPath, 'SKILL.md');
116
+ return existsSync(md) ? md : null;
117
+ }
118
+
119
+ const st = lstatSync(fullPath);
120
+ if (!st.isSymbolicLink()) return null;
121
+ const target = resolveSymlinkTarget(fullPath);
122
+ const md = join(target, 'SKILL.md');
123
+ return existsSync(md) ? md : null;
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ function readSkillMeta(skillMdPath) {
130
+ try {
131
+ return parseFrontmatter(readFileSync(skillMdPath, 'utf8'));
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+
137
+ function discoverSkillNameUsage(agents) {
138
+ const usage = new Map(); // skill-name -> [{agent, entryName, path, skillMdPath}]
139
+
140
+ for (const [agentName, agentPath] of Object.entries(agents)) {
141
+ if (!existsSync(agentPath)) continue;
142
+
143
+ for (const entry of readdirSync(agentPath, { withFileTypes: true })) {
144
+ if (entry.name.startsWith('.')) continue;
145
+ const full = join(agentPath, entry.name);
146
+ const skillMd = skillMdPathFromEntry(full, entry);
147
+ if (!skillMd) continue;
148
+
149
+ const meta = readSkillMeta(skillMd);
150
+ if (!meta?.name) continue;
151
+
152
+ if (!usage.has(meta.name)) usage.set(meta.name, []);
153
+ usage.get(meta.name).push({
154
+ agent: agentName,
155
+ entryName: entry.name,
156
+ path: full,
157
+ skillMdPath: skillMd,
158
+ });
159
+ }
160
+ }
161
+
162
+ return usage;
163
+ }
164
+
165
+ function discoverAgentNameConflicts(agents) {
166
+ const conflicts = []; // [{agent,name,entries}]
167
+
168
+ for (const [agentName, agentPath] of Object.entries(agents)) {
169
+ if (!existsSync(agentPath)) continue;
170
+ const byName = new Map();
171
+
172
+ for (const entry of readdirSync(agentPath, { withFileTypes: true })) {
173
+ if (entry.name.startsWith('.')) continue;
174
+ const full = join(agentPath, entry.name);
175
+ const skillMd = skillMdPathFromEntry(full, entry);
176
+ if (!skillMd) continue;
177
+
178
+ const meta = readSkillMeta(skillMd);
179
+ if (!meta?.name) continue;
180
+
181
+ if (!byName.has(meta.name)) byName.set(meta.name, []);
182
+ byName.get(meta.name).push(entry.name);
183
+ }
184
+
185
+ for (const [name, entries] of byName.entries()) {
186
+ if (entries.length > 1) {
187
+ conflicts.push({ agent: agentName, name, entries: entries.sort() });
188
+ }
189
+ }
190
+ }
191
+
192
+ return conflicts;
193
+ }
194
+
107
195
  function discoverUnmanaged(agents, st) {
108
- const unmanaged = new Map(); // name -> { sources: [{agent,path,skillMdPath}] }
196
+ const unmanaged = new Map(); // skill-name -> { sources: [{agent,path,skillMdPath}] }
109
197
  const managedLinks = new Set(Object.keys(st.installed).map(linkNameForSkill));
110
198
 
111
199
  for (const [agentName, agentPath] of Object.entries(agents)) {
112
200
  if (!existsSync(agentPath)) continue;
201
+
113
202
  for (const entry of readdirSync(agentPath, { withFileTypes: true })) {
203
+ if (entry.name.startsWith('.')) continue;
114
204
  const full = join(agentPath, entry.name);
115
205
 
116
206
  // Skip managed link names
@@ -119,47 +209,74 @@ function discoverUnmanaged(agents, st) {
119
209
  // Skip if symlink points to managed store
120
210
  try {
121
211
  if (lstatSync(full).isSymbolicLink()) {
122
- const target = readlinkSync(full);
212
+ const target = resolveSymlinkTarget(full);
123
213
  if (target.includes('skill-sharer')) continue;
124
214
  }
125
215
  } catch {
126
216
  // ignore stat errors
127
217
  }
128
218
 
129
- const skillMd = entry.isDirectory()
130
- ? join(full, 'SKILL.md')
131
- : (lstatSync(full).isSymbolicLink() ? join(resolve(readlinkSync(full)), 'SKILL.md') : null);
132
-
133
- if (!skillMd || !existsSync(skillMd)) continue;
219
+ const skillMd = skillMdPathFromEntry(full, entry);
220
+ if (!skillMd) continue;
134
221
 
135
- const key = entry.name;
222
+ const meta = readSkillMeta(skillMd);
223
+ const key = meta?.name || entry.name;
136
224
  if (!unmanaged.has(key)) unmanaged.set(key, { sources: [] });
137
- unmanaged.get(key).sources.push({ agent: agentName, path: full, skillMdPath: skillMd });
225
+ unmanaged.get(key).sources.push({
226
+ agent: agentName,
227
+ path: full,
228
+ skillMdPath: skillMd,
229
+ });
138
230
  }
139
231
  }
140
232
 
141
233
  return unmanaged;
142
234
  }
143
235
 
144
- async function installBuiltins(agents) {
145
- const st = loadState();
236
+ function bundledBuiltins() {
237
+ const out = [];
146
238
  const builtinsRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'builtin-skills');
147
- if (!existsSync(builtinsRoot)) return;
239
+ if (!existsSync(builtinsRoot)) return out;
148
240
 
149
- const installed = [];
150
241
  for (const entry of readdirSync(builtinsRoot, { withFileTypes: true })) {
151
242
  if (!entry.isDirectory()) continue;
152
-
153
243
  const srcDir = join(builtinsRoot, entry.name);
154
244
  const skillMd = join(srcDir, 'SKILL.md');
155
245
  if (!existsSync(skillMd)) continue;
156
-
157
246
  const meta = parseFrontmatter(readFileSync(skillMd, 'utf8'));
158
247
  if (!meta) continue;
248
+ out.push({
249
+ id: `__builtin__/local/${meta.name}`,
250
+ name: meta.name,
251
+ srcDir,
252
+ });
253
+ }
159
254
 
160
- const id = `__builtin__/local/${meta.name}`;
255
+ return out;
256
+ }
257
+
258
+ async function installBuiltins(agents) {
259
+ const st = loadState();
260
+
261
+ const installed = [];
262
+ const skipped = [];
263
+ const nameUsage = discoverSkillNameUsage(agents);
264
+
265
+ for (const b of bundledBuiltins()) {
266
+ const srcDir = b.srcDir;
267
+ const id = b.id;
161
268
  const version = 'v1';
162
269
  const dest = managedPath(id, version);
270
+ const linkName = linkNameForSkill(id);
271
+
272
+ const conflictingEntries = (nameUsage.get(b.name) || [])
273
+ .filter(s => s.entryName !== linkName);
274
+
275
+ if (conflictingEntries.length > 0) {
276
+ skipped.push({ name: b.name, conflicts: conflictingEntries });
277
+ continue;
278
+ }
279
+
163
280
  const files = collectFiles(srcDir);
164
281
 
165
282
  // Write to managed store
@@ -172,8 +289,7 @@ async function installBuiltins(agents) {
172
289
  }
173
290
 
174
291
  // Symlink into all detected agents
175
- const linkName = linkNameForSkill(id);
176
- for (const agentPath of Object.values(agents)) {
292
+ for (const [agentName, agentPath] of Object.entries(agents)) {
177
293
  mkdirSync(agentPath, { recursive: true });
178
294
  const link = join(agentPath, linkName);
179
295
  try {
@@ -184,6 +300,14 @@ async function installBuiltins(agents) {
184
300
  // doesn't exist
185
301
  }
186
302
  symlinkSync(dest, link);
303
+
304
+ if (!nameUsage.has(b.name)) nameUsage.set(b.name, []);
305
+ nameUsage.get(b.name).push({
306
+ agent: agentName,
307
+ entryName: linkName,
308
+ path: link,
309
+ skillMdPath: join(dest, 'SKILL.md'),
310
+ });
187
311
  }
188
312
 
189
313
  st.installed[id] = {
@@ -195,16 +319,26 @@ async function installBuiltins(agents) {
195
319
  builtin: true,
196
320
  };
197
321
 
198
- installed.push(meta.name);
322
+ installed.push(b.name);
199
323
  }
200
324
 
201
325
  saveState(st);
326
+
202
327
  if (installed.length > 0) {
203
328
  console.log('\nInstalled built-in skills:');
204
329
  for (const name of installed) {
205
330
  console.log(` ✓ ${name} → ${Object.keys(agents).join(', ') || '(no agents detected)'}`);
206
331
  }
207
332
  }
333
+
334
+ if (skipped.length > 0) {
335
+ console.log('\nSkipped built-ins to avoid skill-name conflicts in agents:');
336
+ for (const s of skipped) {
337
+ const where = s.conflicts.map(c => `${c.agent}/${c.entryName}`).join(', ');
338
+ console.log(` ! ${s.name} (already present at: ${where})`);
339
+ }
340
+ console.log(' Tip: remove/rename duplicates if you want bundled docs linked automatically.');
341
+ }
208
342
  }
209
343
 
210
344
  // --- Commands ---
@@ -449,6 +583,20 @@ export async function cmdList(args = {}) {
449
583
  });
450
584
  }
451
585
 
586
+ // Bundled built-ins (show even if not installed yet)
587
+ const installedIds = new Set(Object.keys(st.installed));
588
+ for (const b of bundledBuiltins()) {
589
+ if (installedIds.has(b.id)) continue;
590
+ rows.push({
591
+ id: b.id,
592
+ version: '-',
593
+ latest: 'v1',
594
+ agents: '-',
595
+ status: 'bundled',
596
+ managed: false,
597
+ });
598
+ }
599
+
452
600
  // Unmanaged installs (present in agent dirs but not in skill-sharer state)
453
601
  const unmanaged = discoverUnmanaged(agents, st);
454
602
  for (const [name, info] of unmanaged.entries()) {
@@ -484,6 +632,15 @@ export async function cmdList(args = {}) {
484
632
  if (unmanagedCount > 0) {
485
633
  console.log(`\nFound ${unmanagedCount} unmanaged skill(s). Run: skills import`);
486
634
  }
635
+
636
+ const conflicts = discoverAgentNameConflicts(agents);
637
+ if (conflicts.length > 0) {
638
+ console.log('\nPotential skill-name conflicts detected (can break skill selection in agents):');
639
+ for (const c of conflicts) {
640
+ console.log(` ${c.agent}: "${c.name}" appears in ${c.entries.join(', ')}`);
641
+ }
642
+ console.log('Resolve duplicates by removing/renaming one copy.');
643
+ }
487
644
  }
488
645
 
489
646
  function verifyInstall(id, rec, agents) {
@@ -762,6 +919,12 @@ export async function cmdDoctor(args) {
762
919
  }
763
920
  }
764
921
 
922
+ const conflicts = discoverAgentNameConflicts(agents);
923
+ for (const c of conflicts) {
924
+ console.log(`Conflict: ${c.agent} has duplicate skill name "${c.name}" in ${c.entries.join(', ')}`);
925
+ issues++;
926
+ }
927
+
765
928
  if (issues === 0) console.log('All good.');
766
929
  else console.log(`\n${issues} issue(s) found.`);
767
930
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npeercy/skills",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "CLI-first skill marketplace for coding agents",
5
5
  "type": "module",
6
6
  "bin": {