@npeercy/skills 0.1.4 → 0.1.6

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/bin/skills.js CHANGED
@@ -23,7 +23,7 @@ Setup:
23
23
  Discovery:
24
24
  search [query] Search skills (no query = browse all)
25
25
  info <skill> Skill details + versions
26
- list [--verify] [--outdated] [--json] Show all local skills (managed + unmanaged)
26
+ list [--verify] [--outdated] [--json] Show all local skills (managed + unmanaged + bundled)
27
27
 
28
28
  Install:
29
29
  install <skill>[@ver] [--agent <a>] [--dry-run]
@@ -99,7 +99,7 @@ async function maybeWarnOutdated() {
99
99
  async function main() {
100
100
  const args = process.argv.slice(2);
101
101
 
102
- // `skills` by itself -> list all local skills (managed + unmanaged)
102
+ // `skills` by itself -> list all local skills (managed + unmanaged + bundled)
103
103
  if (args.length === 0) {
104
104
  await maybeWarnOutdated();
105
105
  await cmdList({ verify: false, outdated: false, json: false });
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,22 +209,24 @@ 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);
219
+ const skillMd = skillMdPathFromEntry(full, entry);
220
+ if (!skillMd) continue;
132
221
 
133
- if (!skillMd || !existsSync(skillMd)) continue;
134
-
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
 
@@ -167,11 +259,24 @@ async function installBuiltins(agents) {
167
259
  const st = loadState();
168
260
 
169
261
  const installed = [];
262
+ const skipped = [];
263
+ const nameUsage = discoverSkillNameUsage(agents);
264
+
170
265
  for (const b of bundledBuiltins()) {
171
266
  const srcDir = b.srcDir;
172
267
  const id = b.id;
173
268
  const version = 'v1';
174
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
+
175
280
  const files = collectFiles(srcDir);
176
281
 
177
282
  // Write to managed store
@@ -184,8 +289,7 @@ async function installBuiltins(agents) {
184
289
  }
185
290
 
186
291
  // Symlink into all detected agents
187
- const linkName = linkNameForSkill(id);
188
- for (const agentPath of Object.values(agents)) {
292
+ for (const [agentName, agentPath] of Object.entries(agents)) {
189
293
  mkdirSync(agentPath, { recursive: true });
190
294
  const link = join(agentPath, linkName);
191
295
  try {
@@ -196,6 +300,14 @@ async function installBuiltins(agents) {
196
300
  // doesn't exist
197
301
  }
198
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
+ });
199
311
  }
200
312
 
201
313
  st.installed[id] = {
@@ -211,12 +323,22 @@ async function installBuiltins(agents) {
211
323
  }
212
324
 
213
325
  saveState(st);
326
+
214
327
  if (installed.length > 0) {
215
328
  console.log('\nInstalled built-in skills:');
216
329
  for (const name of installed) {
217
330
  console.log(` ✓ ${name} → ${Object.keys(agents).join(', ') || '(no agents detected)'}`);
218
331
  }
219
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
+ }
220
342
  }
221
343
 
222
344
  // --- Commands ---
@@ -510,6 +632,15 @@ export async function cmdList(args = {}) {
510
632
  if (unmanagedCount > 0) {
511
633
  console.log(`\nFound ${unmanagedCount} unmanaged skill(s). Run: skills import`);
512
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
+ }
513
644
  }
514
645
 
515
646
  function verifyInstall(id, rec, agents) {
@@ -788,6 +919,12 @@ export async function cmdDoctor(args) {
788
919
  }
789
920
  }
790
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
+
791
928
  if (issues === 0) console.log('All good.');
792
929
  else console.log(`\n${issues} issue(s) found.`);
793
930
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npeercy/skills",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "CLI-first skill marketplace for coding agents",
5
5
  "type": "module",
6
6
  "bin": {