skillshark 0.1.0 → 0.3.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 +65 -6
- package/bin/skillshark.js +53 -16
- package/package.json +10 -2
- package/src/agents.js +336 -0
- package/src/discover.js +93 -41
- package/src/gh.js +32 -9
- package/src/install.js +294 -120
- package/src/interactive.js +189 -0
- package/src/share.js +36 -17
- package/src/source.js +33 -9
- package/src/transports/gist.js +60 -18
- package/src/transports/repo.js +54 -21
- 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 = [];
|
package/src/gh.js
CHANGED
|
@@ -1,30 +1,46 @@
|
|
|
1
1
|
// The gh helper — the ONLY module (besides the clipboard) that touches
|
|
2
|
-
// child_process. Used
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// child_process. Used by sender operations (share, revoke) and, for GitHub
|
|
3
|
+
// Enterprise hosts, by the receive path too: enterprise links are private by
|
|
4
|
+
// nature, so they ride the receiver's own gh auth — never anonymous HTTPS.
|
|
5
|
+
// execFile with argument arrays only; user input is never interpolated into
|
|
6
|
+
// a shell string (hard rule 3).
|
|
5
7
|
import { execFile as execFileCb } from 'node:child_process';
|
|
6
8
|
import { promisify } from 'node:util';
|
|
7
9
|
import { CliError, MSG } from './errors.js';
|
|
10
|
+
import { DEFAULT_HOST } from './source.js';
|
|
8
11
|
|
|
9
12
|
const execFileP = promisify(execFileCb);
|
|
10
13
|
|
|
11
14
|
// Default runner; tests inject their own to capture or forbid calls.
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
// opts.binary returns a Buffer (repo tarballs); default is utf8 text.
|
|
16
|
+
export async function defaultGhRunner(args, opts = {}) {
|
|
17
|
+
const { stdout } = await execFileP('gh', args, {
|
|
18
|
+
maxBuffer: 96 * 1024 * 1024,
|
|
19
|
+
encoding: opts.binary ? 'buffer' : 'utf8',
|
|
20
|
+
});
|
|
14
21
|
return stdout;
|
|
15
22
|
}
|
|
16
23
|
|
|
24
|
+
// Extra args to aim gh at a GitHub Enterprise host.
|
|
25
|
+
export function hostArgs(host) {
|
|
26
|
+
return host && host !== DEFAULT_HOST ? ['--hostname', host] : [];
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
// Run `gh api ...` and map the usual failure modes onto exit-code-2 guidance.
|
|
18
30
|
export function makeGhApi(runner = defaultGhRunner) {
|
|
19
|
-
return async function ghApi(args) {
|
|
31
|
+
return async function ghApi(args, opts = {}) {
|
|
20
32
|
try {
|
|
21
|
-
return await runner(['api', ...args]);
|
|
33
|
+
return await runner(['api', ...args], opts);
|
|
22
34
|
} catch (err) {
|
|
23
35
|
if (err instanceof CliError) throw err;
|
|
24
|
-
|
|
36
|
+
const hostIdx = args.indexOf('--hostname');
|
|
37
|
+
const host = hostIdx !== -1 ? args[hostIdx + 1] : null;
|
|
38
|
+
if (err?.code === 'ENOENT') {
|
|
39
|
+
throw new CliError(host ? enterpriseGhMsg(host) : MSG.ghMissing, 2);
|
|
40
|
+
}
|
|
25
41
|
const stderr = String(err?.stderr ?? '');
|
|
26
42
|
if (/not logged in|authentication|HTTP 401|gh auth login/i.test(stderr)) {
|
|
27
|
-
throw new CliError(MSG.ghMissing, 2);
|
|
43
|
+
throw new CliError(host ? enterpriseGhMsg(host) : MSG.ghMissing, 2);
|
|
28
44
|
}
|
|
29
45
|
if (/HTTP 404/.test(stderr)) throw new CliError('GitHub returned 404 for that id.', 1);
|
|
30
46
|
const detail = stderr.trim().split('\n')[0] || err.message;
|
|
@@ -32,3 +48,10 @@ export function makeGhApi(runner = defaultGhRunner) {
|
|
|
32
48
|
}
|
|
33
49
|
};
|
|
34
50
|
}
|
|
51
|
+
|
|
52
|
+
export function enterpriseGhMsg(host) {
|
|
53
|
+
return (
|
|
54
|
+
`This needs the GitHub CLI authenticated against ${host} (GitHub Enterprise):\n` +
|
|
55
|
+
` https://cli.github.com, then "gh auth login --hostname ${host}"`
|
|
56
|
+
);
|
|
57
|
+
}
|