skillshark 0.1.0 → 0.2.0
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/README.md +44 -4
- package/bin/skillshark.js +44 -16
- package/package.json +10 -2
- package/src/agents.js +336 -0
- package/src/discover.js +93 -41
- package/src/install.js +287 -114
- package/src/interactive.js +189 -0
- package/src/share.js +8 -7
- package/src/ui.js +25 -0
- package/src/version.js +1 -1
package/src/discover.js
CHANGED
|
@@ -5,6 +5,7 @@ import { existsSync } from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { sha256hex } from './fingerprint.js';
|
|
7
7
|
import { CliError } from './errors.js';
|
|
8
|
+
import { AGENTS, AGENT_IDS, classifyByConvention, artifactBaseName, parseGeminiToml } from './agents.js';
|
|
8
9
|
|
|
9
10
|
// Never packaged, not even with --force.
|
|
10
11
|
const HARD_EXCLUDE_DIRS = new Set(['.git', 'node_modules']);
|
|
@@ -30,14 +31,24 @@ function secretMatch(name) {
|
|
|
30
31
|
|
|
31
32
|
// --- share-argument resolution (§4.1) ---------------------------------------
|
|
32
33
|
|
|
33
|
-
// Candidate locations for a bare name, in search order
|
|
34
|
+
// Candidate locations for a bare name, in search order: claude-code first
|
|
35
|
+
// (project before global), then every other adapter in registry order.
|
|
34
36
|
export function nameCandidates(name, { cwd, home }) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const id of AGENT_IDS) {
|
|
39
|
+
for (const loc of AGENTS[id].locations) {
|
|
40
|
+
const base = loc.scope === 'project' ? cwd : home;
|
|
41
|
+
const rel = loc.rel(name);
|
|
42
|
+
out.push({
|
|
43
|
+
root: path.join(base, ...rel),
|
|
44
|
+
isDir: loc.container === 'dir',
|
|
45
|
+
type: loc.kind,
|
|
46
|
+
agent: id,
|
|
47
|
+
where: `${loc.scope === 'project' ? '' : '~/'}${rel.slice(0, -1).join('/')} (${loc.scope}, ${AGENTS[id].label})`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
async function existsAs(p, wantDir) {
|
|
@@ -49,38 +60,74 @@ async function existsAs(p, wantDir) {
|
|
|
49
60
|
}
|
|
50
61
|
}
|
|
51
62
|
|
|
52
|
-
// Classify an explicit path by its on-disk convention (§4.1):
|
|
53
|
-
//
|
|
54
|
-
// prompt (file) or bundle (directory).
|
|
63
|
+
// Classify an explicit path by its on-disk convention (§4.1): any adapter
|
|
64
|
+
// convention wins; otherwise prompt (file) or bundle (directory).
|
|
55
65
|
export function classifyPath(absPath, isDir) {
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
58
|
-
if (!isDir && /\/\.claude\/commands\/[^/]+\.md$/.test(norm)) return { type: 'command', agent: 'claude-code' };
|
|
66
|
+
const hit = classifyByConvention(absPath, isDir);
|
|
67
|
+
if (hit) return hit;
|
|
59
68
|
return { type: isDir ? 'bundle' : 'prompt', agent: '' };
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
// Enumerate every artifact visible from here, across all adapters — powers
|
|
72
|
+
// the interactive picker and name suggestions.
|
|
73
|
+
export async function discoverAll({ cwd, home }) {
|
|
74
|
+
const out = [];
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
for (const id of AGENT_IDS) {
|
|
77
|
+
for (const loc of AGENTS[id].locations) {
|
|
78
|
+
const base = loc.scope === 'project' ? cwd : home;
|
|
79
|
+
const probe = loc.rel('@');
|
|
80
|
+
const dir = path.join(base, ...probe.slice(0, -1));
|
|
81
|
+
const suffix = probe[probe.length - 1].replace('@', '');
|
|
82
|
+
let dirents;
|
|
83
|
+
try {
|
|
84
|
+
dirents = await readdir(dir, { withFileTypes: true });
|
|
85
|
+
} catch {
|
|
86
|
+
continue;
|
|
72
87
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
88
|
+
for (const ent of dirents) {
|
|
89
|
+
let name = null;
|
|
90
|
+
if (loc.container === 'dir' && ent.isDirectory()) name = ent.name;
|
|
91
|
+
else if (loc.container === 'file' && ent.isFile() && suffix && ent.name.endsWith(suffix)) {
|
|
92
|
+
name = artifactBaseName(ent.name);
|
|
93
|
+
}
|
|
94
|
+
if (!name || name.startsWith('.')) continue;
|
|
95
|
+
const root = path.join(dir, ent.name);
|
|
96
|
+
if (seen.has(root)) continue;
|
|
97
|
+
seen.add(root);
|
|
98
|
+
out.push({
|
|
99
|
+
name,
|
|
100
|
+
root,
|
|
101
|
+
isDir: loc.container === 'dir',
|
|
102
|
+
type: loc.kind,
|
|
103
|
+
agent: id,
|
|
104
|
+
scope: loc.scope,
|
|
105
|
+
});
|
|
82
106
|
}
|
|
83
|
-
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// List every artifact name visible from here, across all adapters (suggestions).
|
|
113
|
+
export async function knownNames({ cwd, home }) {
|
|
114
|
+
const names = new Set();
|
|
115
|
+
for (const id of AGENT_IDS) {
|
|
116
|
+
for (const loc of AGENTS[id].locations) {
|
|
117
|
+
const base = loc.scope === 'project' ? cwd : home;
|
|
118
|
+
// rel('') gives us the parent dir + the filename pattern's extension
|
|
119
|
+
const probe = loc.rel('@');
|
|
120
|
+
const dir = path.join(base, ...probe.slice(0, -1));
|
|
121
|
+
const suffix = probe[probe.length - 1].replace('@', '');
|
|
122
|
+
try {
|
|
123
|
+
for (const ent of await readdir(dir, { withFileTypes: true })) {
|
|
124
|
+
if (loc.container === 'dir' && ent.isDirectory()) names.add(ent.name);
|
|
125
|
+
else if (loc.container === 'file' && ent.isFile() && suffix && ent.name.endsWith(suffix)) {
|
|
126
|
+
names.add(artifactBaseName(ent.name));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch { /* location absent */ }
|
|
130
|
+
}
|
|
84
131
|
}
|
|
85
132
|
return [...names].sort();
|
|
86
133
|
}
|
|
@@ -129,13 +176,13 @@ export async function resolveShareArg(arg, { cwd, home }) {
|
|
|
129
176
|
const matches = [];
|
|
130
177
|
for (const cand of nameCandidates(name, { cwd, home })) {
|
|
131
178
|
if (await existsAs(cand.root, cand.isDir)) {
|
|
132
|
-
matches.push(
|
|
179
|
+
matches.push(cand);
|
|
133
180
|
}
|
|
134
181
|
}
|
|
135
182
|
if (matches.length === 0) {
|
|
136
183
|
const known = await knownNames({ cwd, home });
|
|
137
184
|
const near = nearestNames(name, known);
|
|
138
|
-
let msg = `No skill named "${name}" found here or in
|
|
185
|
+
let msg = `No skill named "${name}" found here or in any known agent location.`;
|
|
139
186
|
if (near.length) msg += `\nDid you mean: ${near.join(', ')}?`;
|
|
140
187
|
else if (known.length) msg += `\nAvailable: ${known.slice(0, 8).join(', ')}`;
|
|
141
188
|
throw new CliError(msg, 2);
|
|
@@ -253,22 +300,27 @@ export function primaryDocPath(files, { isDir, type }) {
|
|
|
253
300
|
|
|
254
301
|
// name: frontmatter `name:` → basename (--name overrides, applied by caller).
|
|
255
302
|
// description: frontmatter → first heading → first paragraph → "".
|
|
303
|
+
// Dialect-aware: .md/.mdc/.prompt.md use YAML frontmatter; .toml is gemini.
|
|
256
304
|
export async function inferMetadata({ root, isDir, type, agent, files }) {
|
|
257
305
|
let fm = {};
|
|
258
306
|
let body = '';
|
|
259
307
|
const docRel = primaryDocPath(files, { isDir, type });
|
|
260
308
|
if (docRel) {
|
|
261
309
|
const docAbs = isDir ? path.join(root, ...docRel.split('/')) : root;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
310
|
+
try {
|
|
311
|
+
const text = await readFile(docAbs, 'utf8');
|
|
312
|
+
if (docRel.endsWith('.toml')) {
|
|
313
|
+
const parsed = parseGeminiToml(text);
|
|
314
|
+
if (parsed.description) fm.description = parsed.description;
|
|
315
|
+
body = parsed.body ?? '';
|
|
316
|
+
} else if (/\.(md|mdc)$/.test(docRel)) {
|
|
265
317
|
const parsed = parseFrontmatter(text);
|
|
266
318
|
fm = parsed.data;
|
|
267
319
|
body = parsed.body;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
320
|
+
}
|
|
321
|
+
} catch { /* unreadable doc → fall back to basenames */ }
|
|
270
322
|
}
|
|
271
|
-
const base = path.basename(root)
|
|
323
|
+
const base = artifactBaseName(path.basename(root));
|
|
272
324
|
const name = (fm.name && String(fm.name).trim()) || base;
|
|
273
325
|
const description = (fm.description && String(fm.description).trim()) || firstHeadingOrParagraph(body) || '';
|
|
274
326
|
const dependencies = [];
|