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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// The interactive session: `skillshark` with no arguments (TTY only) drops
|
|
2
|
+
// into a guided menu; bare `share`/`install`/`inspect`/`revoke` run their
|
|
3
|
+
// wizard one-shot. Built on @clack/prompts — every flow ends in the same
|
|
4
|
+
// audited pipelines as the flag-driven CLI; the wizard only collects answers.
|
|
5
|
+
import { CliError } from './errors.js';
|
|
6
|
+
import { discoverAll } from './discover.js';
|
|
7
|
+
import { parseSource } from './source.js';
|
|
8
|
+
import { runShare, runRevoke } from './share.js';
|
|
9
|
+
import { runInstall, runInspect } from './install.js';
|
|
10
|
+
import { loadConfig } from './config.js';
|
|
11
|
+
import { displayPath } from './ui.js';
|
|
12
|
+
import { AGENTS } from './agents.js';
|
|
13
|
+
import { VERSION } from './version.js';
|
|
14
|
+
|
|
15
|
+
const FIN = [
|
|
16
|
+
' |\\',
|
|
17
|
+
' | \\',
|
|
18
|
+
' ~~~;~~\\~~~~',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
async function ux() {
|
|
22
|
+
const clack = await import('@clack/prompts');
|
|
23
|
+
const pc = (await import('picocolors')).default;
|
|
24
|
+
return { clack, pc };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function bail(clack, value) {
|
|
28
|
+
return value === null || value === undefined || clack.isCancel(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --- share wizard --------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
async function wizardShare(deps, { clack, pc }) {
|
|
34
|
+
const found = await deps.ui.spin('Scanning for shareable artifacts', () => discoverAll(deps));
|
|
35
|
+
if (!found.length) {
|
|
36
|
+
clack.log.warn('Nothing shareable here or in your home directory. Create a skill first (e.g. .claude/skills/<name>/SKILL.md).');
|
|
37
|
+
return { status: 'cancelled' };
|
|
38
|
+
}
|
|
39
|
+
const pick = await clack.select({
|
|
40
|
+
message: `Share which artifact? ${pc.dim(`(${found.length} found)`)}`,
|
|
41
|
+
maxItems: 12,
|
|
42
|
+
options: found.map((a) => ({
|
|
43
|
+
value: a.root,
|
|
44
|
+
label: a.name,
|
|
45
|
+
hint: `${AGENTS[a.agent]?.label ?? a.agent} ${a.type} · ${a.scope === 'global' ? '~' : displayPath(a.root, deps)}`,
|
|
46
|
+
})),
|
|
47
|
+
});
|
|
48
|
+
if (bail(clack, pick)) return { status: 'cancelled' };
|
|
49
|
+
|
|
50
|
+
const expires = await clack.select({
|
|
51
|
+
message: 'Advisory expiry (installers refuse after this; the gist lives until you revoke):',
|
|
52
|
+
initialValue: '7d',
|
|
53
|
+
options: [
|
|
54
|
+
{ value: '30m', label: '30 minutes' },
|
|
55
|
+
{ value: '6h', label: '6 hours' },
|
|
56
|
+
{ value: '24h', label: '24 hours' },
|
|
57
|
+
{ value: '7d', label: '7 days', hint: 'default' },
|
|
58
|
+
{ value: '30d', label: '30 days' },
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
if (bail(clack, expires)) return { status: 'cancelled' };
|
|
62
|
+
|
|
63
|
+
return runShare(pick, { expires }, deps);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- install / inspect wizards ----------------------------------------------------
|
|
67
|
+
|
|
68
|
+
async function askSource(deps, { clack }) {
|
|
69
|
+
for (;;) {
|
|
70
|
+
const source = await clack.text({
|
|
71
|
+
message: 'Paste the link (gist URL, bare id, or gh:owner/repo[/path][@ref]):',
|
|
72
|
+
placeholder: 'https://gist.github.com/…#fp=…',
|
|
73
|
+
});
|
|
74
|
+
if (bail(clack, source)) return null;
|
|
75
|
+
try {
|
|
76
|
+
parseSource(String(source).trim());
|
|
77
|
+
return String(source).trim();
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (err instanceof CliError) {
|
|
80
|
+
clack.log.error(err.message);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function wizardInstall(deps, u) {
|
|
89
|
+
const source = await askSource(deps, u);
|
|
90
|
+
if (source === null) return { status: 'cancelled' };
|
|
91
|
+
return runInstall(source, {}, deps);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function wizardInspect(deps, u) {
|
|
95
|
+
const { clack } = u;
|
|
96
|
+
const source = await askSource(deps, u);
|
|
97
|
+
if (source === null) return { status: 'cancelled' };
|
|
98
|
+
await runInspect(source, {}, deps);
|
|
99
|
+
const next = await clack.select({
|
|
100
|
+
message: 'And now?',
|
|
101
|
+
options: [
|
|
102
|
+
{ value: 'install', label: 'Install it' },
|
|
103
|
+
{ value: 'done', label: 'Done' },
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
if (!bail(clack, next) && next === 'install') {
|
|
107
|
+
return runInstall(source, {}, deps);
|
|
108
|
+
}
|
|
109
|
+
return { status: 'inspected' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- revoke wizard -----------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
async function wizardRevoke(deps, { clack, pc }) {
|
|
115
|
+
const cfg = await loadConfig(deps.configDir);
|
|
116
|
+
let idOrName = null;
|
|
117
|
+
if (cfg.shares.length) {
|
|
118
|
+
const pick = await clack.select({
|
|
119
|
+
message: 'Revoke which share?',
|
|
120
|
+
maxItems: 12,
|
|
121
|
+
options: [
|
|
122
|
+
...cfg.shares.map((s) => ({
|
|
123
|
+
value: s.id,
|
|
124
|
+
label: s.name,
|
|
125
|
+
hint: `${s.id.slice(0, 12)}… · expires ${s.expiresAt ? s.expiresAt.slice(0, 10) : 'never'}`,
|
|
126
|
+
})),
|
|
127
|
+
{ value: '__manual', label: 'Enter a gist id or name…' },
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
if (bail(clack, pick)) return { status: 'cancelled' };
|
|
131
|
+
idOrName = pick === '__manual' ? null : pick;
|
|
132
|
+
}
|
|
133
|
+
if (idOrName === null) {
|
|
134
|
+
const entered = await clack.text({ message: 'Gist id (or share name):' });
|
|
135
|
+
if (bail(clack, entered) || !String(entered).trim()) return { status: 'cancelled' };
|
|
136
|
+
idOrName = String(entered).trim();
|
|
137
|
+
}
|
|
138
|
+
clack.log.warn(pc.yellow('Revoking deletes the gist — anyone holding the link loses access immediately.'));
|
|
139
|
+
return runRevoke(idOrName, {}, deps);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- the session --------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
const WIZARDS = {
|
|
145
|
+
share: wizardShare,
|
|
146
|
+
install: wizardInstall,
|
|
147
|
+
inspect: wizardInspect,
|
|
148
|
+
revoke: wizardRevoke,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// action: run a single wizard (bare `skillshark share` etc.); null: full menu.
|
|
152
|
+
export async function runInteractive(deps, action = null) {
|
|
153
|
+
const u = await ux();
|
|
154
|
+
const { clack, pc } = u;
|
|
155
|
+
|
|
156
|
+
clack.intro(`${pc.cyan(FIN.join('\n '))}\n ${pc.bold('skillshark')} ${pc.dim(`v${VERSION} — share agent skills like files`)}`);
|
|
157
|
+
|
|
158
|
+
if (action) {
|
|
159
|
+
const result = await WIZARDS[action](deps, u);
|
|
160
|
+
clack.outro(result?.status === 'cancelled' ? 'Nothing happened. 🦈' : 'Done. 🦈');
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (;;) {
|
|
165
|
+
const choice = await clack.select({
|
|
166
|
+
message: 'What are we doing?',
|
|
167
|
+
options: [
|
|
168
|
+
{ value: 'share', label: 'Share', hint: 'package a local skill → unlisted link' },
|
|
169
|
+
{ value: 'install', label: 'Install', hint: 'from a link or repo path' },
|
|
170
|
+
{ value: 'inspect', label: 'Inspect', hint: 'look before you leap' },
|
|
171
|
+
{ value: 'revoke', label: 'Revoke', hint: 'kill a link you shared' },
|
|
172
|
+
{ value: 'quit', label: 'Quit' },
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
if (bail(clack, choice) || choice === 'quit') break;
|
|
176
|
+
try {
|
|
177
|
+
await WIZARDS[choice](deps, u);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (err instanceof CliError) {
|
|
180
|
+
clack.log.error(err.message);
|
|
181
|
+
} else {
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
clack.log.message('');
|
|
186
|
+
}
|
|
187
|
+
clack.outro('Swim safe. 🦈');
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
package/src/share.js
CHANGED
|
@@ -153,13 +153,14 @@ export async function runShare(arg, opts, deps) {
|
|
|
153
153
|
} catch { /* preview is optional */ }
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
const { id, revision } = await
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
156
|
+
const { id, revision } = await ui.spin('Uploading as a secret gist', () =>
|
|
157
|
+
createGist({
|
|
158
|
+
manifestJson,
|
|
159
|
+
primaryDoc,
|
|
160
|
+
tarballB64: b64,
|
|
161
|
+
description: gistDescription({ name: manifest.name, agent: manifest.agent, type: manifest.type, fp8: shortFp }),
|
|
162
|
+
ghApi: deps.ghApi,
|
|
163
|
+
}));
|
|
163
164
|
const url = `https://gist.github.com/${id}#fp=${shortFp}`;
|
|
164
165
|
|
|
165
166
|
await addShareRecord(deps.configDir, {
|
package/src/ui.js
CHANGED
|
@@ -37,9 +37,29 @@ export function makeUi({ stdout = process.stdout, stderr = process.stderr, color
|
|
|
37
37
|
warn: (s) => write(stdout, ` ${c.yellow('⚠')} ${s}`),
|
|
38
38
|
info: (s) => write(stdout, ` ${c.cyan('ⓘ')} ${s}`),
|
|
39
39
|
fail: (s) => write(stderr, ` ${c.red('✗')} ${s}`),
|
|
40
|
+
// network-op wrapper; the TTY entrypoint swaps in a real spinner
|
|
41
|
+
spin: async (_label, fn) => fn(),
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
// Replace ui.spin with an animated @clack spinner (TTY only).
|
|
46
|
+
export async function attachSpinner(ui) {
|
|
47
|
+
const clack = await import('@clack/prompts');
|
|
48
|
+
ui.spin = async (label, fn) => {
|
|
49
|
+
const s = clack.spinner();
|
|
50
|
+
s.start(label);
|
|
51
|
+
try {
|
|
52
|
+
const result = await fn();
|
|
53
|
+
s.stop(`${label} — done`);
|
|
54
|
+
return result;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
s.stop(`${label} — failed`, 1);
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
return ui;
|
|
61
|
+
}
|
|
62
|
+
|
|
43
63
|
// The install/inspect preview, rendered only from verified bytes (§4.2 step 5).
|
|
44
64
|
export function renderPreview(ui, { manifest, fingerprint, fpFromLink, externalRefs }) {
|
|
45
65
|
const c = ui.colors;
|
|
@@ -99,5 +119,10 @@ export async function realPrompts() {
|
|
|
99
119
|
if (clack.isCancel(value)) return null;
|
|
100
120
|
return value;
|
|
101
121
|
},
|
|
122
|
+
async text({ message, placeholder }) {
|
|
123
|
+
const value = await clack.text({ message, placeholder });
|
|
124
|
+
if (clack.isCancel(value)) return null;
|
|
125
|
+
return String(value ?? '').trim();
|
|
126
|
+
},
|
|
102
127
|
};
|
|
103
128
|
}
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.2.0';
|
|
2
2
|
export const USER_AGENT = `skillshark/${VERSION}`;
|