agentskillsdk 0.2.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/package.json +1 -1
- package/src/commands/add.js +79 -21
- package/src/lib/detect-agent.js +3 -16
- package/src/lib/prompt.js +132 -1
- package/src/lib/ui.js +23 -4
package/package.json
CHANGED
package/src/commands/add.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { fetchSkill, fetchSkills } from '../lib/api.js';
|
|
4
|
-
import { detectAgents } from '../lib/detect-agent.js';
|
|
4
|
+
import { detectAgents, AGENTS } from '../lib/detect-agent.js';
|
|
5
5
|
import { downloadSkill } from '../lib/download.js';
|
|
6
6
|
import { parseGithubSource } from '../lib/parse-source.js';
|
|
7
|
-
import { selectPrompt } from '../lib/prompt.js';
|
|
7
|
+
import { selectPrompt, checkboxPrompt } from '../lib/prompt.js';
|
|
8
8
|
import { printBanner, printCompletionSummary, step, error } from '../lib/ui.js';
|
|
9
9
|
|
|
10
10
|
export async function addCommand(skillName, options) {
|
|
@@ -71,29 +71,88 @@ export async function addCommand(skillName, options) {
|
|
|
71
71
|
spinner.succeed(`Found skill: ${chalk.bold(skill.name)}`);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
// ---
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
step(`Detected agent: ${chalk.bold(agent.name)}`);
|
|
78
|
-
|
|
79
|
-
// --- resolve scope ---
|
|
74
|
+
// --- agent selection + scope prompt (with back navigation) ---
|
|
75
|
+
const detected = detectAgents(cwd);
|
|
76
|
+
let agents;
|
|
80
77
|
let scope;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
78
|
+
|
|
79
|
+
agentSelection: while (true) {
|
|
80
|
+
agents = null;
|
|
81
|
+
|
|
82
|
+
if (detected.length === 1) {
|
|
83
|
+
// Auto-select the single detected agent
|
|
84
|
+
agents = detected;
|
|
85
|
+
} else if (detected.length > 1) {
|
|
86
|
+
// Prompt: all detected vs choose specific
|
|
87
|
+
const names = detected.map(a => a.name).join(', ');
|
|
88
|
+
const choice = await selectPrompt('How would you like to install?', [
|
|
89
|
+
{ label: `All detected agents (${names})`, value: 'all' },
|
|
90
|
+
{ label: 'Choose specific agents...', value: 'choose' },
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
if (choice === null) {
|
|
94
|
+
// Esc from top-level → cancel
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (choice === 'all') {
|
|
99
|
+
agents = detected;
|
|
100
|
+
} else {
|
|
101
|
+
// Checkbox from detected agents
|
|
102
|
+
const choices = detected.map(a => ({ label: a.name, value: a }));
|
|
103
|
+
const selected = await checkboxPrompt('Select agents:', choices);
|
|
104
|
+
if (selected === null) continue agentSelection; // Esc → back to all/choose
|
|
105
|
+
agents = selected;
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
// No agents detected — show checkbox of all known agents
|
|
109
|
+
const choices = AGENTS.map(a => ({ label: a.name, value: a }));
|
|
110
|
+
const selected = await checkboxPrompt('Select agents:', choices);
|
|
111
|
+
if (selected === null) {
|
|
112
|
+
// Esc with no detected agents → cancel
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
agents = selected;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- resolve scope ---
|
|
119
|
+
if (options.global) {
|
|
120
|
+
scope = 'global';
|
|
121
|
+
} else if (options.project) {
|
|
122
|
+
scope = 'project';
|
|
123
|
+
} else {
|
|
124
|
+
// Build hint using first agent (representative)
|
|
125
|
+
const a = agents[0];
|
|
126
|
+
const result = await selectPrompt('Install scope:', [
|
|
127
|
+
{ label: 'Project', hint: `(local ${a.folder}/skills/)`, value: 'project' },
|
|
128
|
+
{ label: 'Global', hint: `(~/${a.globalFolder}/skills/)`, value: 'global' },
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
if (result === null) {
|
|
132
|
+
// Esc → back to agent selection (unless auto-selected single agent)
|
|
133
|
+
if (detected.length === 1) process.exit(0);
|
|
134
|
+
continue agentSelection;
|
|
135
|
+
}
|
|
136
|
+
scope = result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
break; // selection complete
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- step message ---
|
|
143
|
+
if (agents.length === 1) {
|
|
144
|
+
step(`Agent: ${chalk.bold(agents[0].name)}`);
|
|
85
145
|
} else {
|
|
86
|
-
|
|
87
|
-
{ label: 'Project', hint: `(local ${agent.folder}/skills/)`, value: 'project' },
|
|
88
|
-
{ label: 'Global', hint: `(~/${agent.globalFolder}/skills/)`, value: 'global' },
|
|
89
|
-
]);
|
|
146
|
+
step(`Agents: ${chalk.bold(agents.map(a => a.name).join(', '))}`);
|
|
90
147
|
}
|
|
91
148
|
|
|
92
|
-
// --- download ---
|
|
93
|
-
const destDir = agent.path(installName, { cwd, scope });
|
|
149
|
+
// --- download to each agent ---
|
|
94
150
|
const spinner = ora({ text: 'Downloading skill files...', indent: 2 }).start();
|
|
95
151
|
try {
|
|
96
|
-
|
|
152
|
+
for (const agent of agents) {
|
|
153
|
+
const destDir = agent.path(installName, { cwd, scope });
|
|
154
|
+
await downloadSkill(skill, destDir);
|
|
155
|
+
}
|
|
97
156
|
} catch (err) {
|
|
98
157
|
spinner.fail('Download failed');
|
|
99
158
|
error(err.message);
|
|
@@ -105,8 +164,7 @@ export async function addCommand(skillName, options) {
|
|
|
105
164
|
printCompletionSummary({
|
|
106
165
|
skillName: installName,
|
|
107
166
|
scope,
|
|
108
|
-
|
|
109
|
-
agentName: agent.name,
|
|
167
|
+
agents,
|
|
110
168
|
isGithub,
|
|
111
169
|
namespace: skill.namespace,
|
|
112
170
|
});
|
package/src/lib/detect-agent.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { selectPrompt } from './prompt.js';
|
|
5
4
|
|
|
6
5
|
function makeAgent(name, folder, { globalFolder, detectFolder } = {}) {
|
|
7
6
|
const gFolder = globalFolder || folder;
|
|
@@ -22,7 +21,7 @@ function makeAgent(name, folder, { globalFolder, detectFolder } = {}) {
|
|
|
22
21
|
};
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
const AGENTS = [
|
|
24
|
+
export const AGENTS = [
|
|
26
25
|
makeAgent('Claude Code', '.claude'),
|
|
27
26
|
makeAgent('Codex CLI', '.agents', { globalFolder: '.codex' }),
|
|
28
27
|
makeAgent('Cursor', '.cursor'),
|
|
@@ -32,18 +31,6 @@ const AGENTS = [
|
|
|
32
31
|
makeAgent('OpenCode', '.opencode'),
|
|
33
32
|
];
|
|
34
33
|
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (found.length === 1) return [found[0]];
|
|
39
|
-
if (found.length > 1) {
|
|
40
|
-
const choices = found.map(a => ({ label: a.name, value: a }));
|
|
41
|
-
const selected = await selectPrompt('Which agent do you use?', choices);
|
|
42
|
-
return [selected];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// None found — ask user to pick
|
|
46
|
-
const choices = AGENTS.map(a => ({ label: a.name, value: a }));
|
|
47
|
-
const selected = await selectPrompt('Which agent do you use?', choices);
|
|
48
|
-
return [selected];
|
|
34
|
+
export function detectAgents(cwd) {
|
|
35
|
+
return AGENTS.filter(agent => existsSync(join(cwd, agent.detectFolder)));
|
|
49
36
|
}
|
package/src/lib/prompt.js
CHANGED
|
@@ -34,12 +34,15 @@ export function selectPrompt(question, choices, { defaultIndex = 0 } = {}) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function render() {
|
|
37
|
-
|
|
37
|
+
const lines = choices.map((c, i) => {
|
|
38
38
|
const marker = i === selected ? chalk.cyan('\u276f') : ' ';
|
|
39
39
|
const label = i === selected ? chalk.cyan(c.label) : c.label;
|
|
40
40
|
const hint = c.hint ? chalk.dim(` ${c.hint}`) : '';
|
|
41
41
|
return ` ${marker} ${label}${hint}`;
|
|
42
42
|
});
|
|
43
|
+
lines.push('');
|
|
44
|
+
lines.push(chalk.dim(' ↑↓ navigate · enter select · esc back'));
|
|
45
|
+
return lines;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
// Hide cursor
|
|
@@ -88,6 +91,16 @@ export function selectPrompt(question, choices, { defaultIndex = 0 } = {}) {
|
|
|
88
91
|
return;
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
// Esc (bare escape, not part of an arrow sequence)
|
|
95
|
+
if (key === '\x1b' && buf.length === 1) {
|
|
96
|
+
const upCount = totalPhysicalLines(prevLines) - 1;
|
|
97
|
+
if (upCount > 0) stdout.write(`\x1b[${upCount}A`);
|
|
98
|
+
stdout.write('\r\x1b[J');
|
|
99
|
+
cleanup();
|
|
100
|
+
resolve(null);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
91
104
|
// Arrow keys come as escape sequences: \x1b[A (up), \x1b[B (down)
|
|
92
105
|
if (key === '\x1b[A' || key === '\x1b[D') {
|
|
93
106
|
// Up or Left
|
|
@@ -110,3 +123,121 @@ export function selectPrompt(question, choices, { defaultIndex = 0 } = {}) {
|
|
|
110
123
|
stdin.on('data', onData);
|
|
111
124
|
});
|
|
112
125
|
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Interactive checkbox prompt with arrow-key navigation and space to toggle.
|
|
129
|
+
* Up/Down to move, Space to toggle, Enter to confirm, Esc to cancel.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} question
|
|
132
|
+
* @param {{ label: string, value: any }[]} choices
|
|
133
|
+
* @returns {Promise<any[]|null>} array of selected values, or null if Esc
|
|
134
|
+
*/
|
|
135
|
+
export function checkboxPrompt(question, choices) {
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
let cursor = 0;
|
|
138
|
+
const checked = new Array(choices.length).fill(false);
|
|
139
|
+
const { stdin, stdout } = process;
|
|
140
|
+
const cols = stdout.columns || 80;
|
|
141
|
+
|
|
142
|
+
function physicalLines(str) {
|
|
143
|
+
const w = stripAnsi(str).length;
|
|
144
|
+
return Math.max(1, Math.ceil(w / cols));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function totalPhysicalLines(lines) {
|
|
148
|
+
return lines.reduce((sum, l) => sum + physicalLines(l), 0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function render() {
|
|
152
|
+
const lines = choices.map((c, i) => {
|
|
153
|
+
const box = checked[i] ? chalk.cyan('◼') : '◻';
|
|
154
|
+
const label = i === cursor ? chalk.cyan(c.label) : c.label;
|
|
155
|
+
return ` ${box} ${label}`;
|
|
156
|
+
});
|
|
157
|
+
lines.push('');
|
|
158
|
+
lines.push(chalk.dim(' ↑↓ navigate · space toggle · enter confirm · esc back'));
|
|
159
|
+
return lines;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Hide cursor
|
|
163
|
+
stdout.write('\x1b[?25l');
|
|
164
|
+
|
|
165
|
+
// Print question
|
|
166
|
+
stdout.write(`\n ${question}\n\n`);
|
|
167
|
+
|
|
168
|
+
// Initial render
|
|
169
|
+
let prevLines = render();
|
|
170
|
+
stdout.write(prevLines.join('\n'));
|
|
171
|
+
|
|
172
|
+
const wasRaw = stdin.isRaw;
|
|
173
|
+
stdin.setRawMode(true);
|
|
174
|
+
stdin.resume();
|
|
175
|
+
|
|
176
|
+
function cleanup() {
|
|
177
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
178
|
+
stdin.removeListener('data', onData);
|
|
179
|
+
stdin.pause();
|
|
180
|
+
stdout.write('\x1b[?25h');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function onData(buf) {
|
|
184
|
+
const key = buf.toString();
|
|
185
|
+
|
|
186
|
+
// Ctrl+C
|
|
187
|
+
if (key === '\x03') {
|
|
188
|
+
cleanup();
|
|
189
|
+
stdout.write('\n');
|
|
190
|
+
process.exit(0);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Enter — confirm selection (must have at least 1)
|
|
194
|
+
if (key === '\r' || key === '\n') {
|
|
195
|
+
const selected = choices.filter((_, i) => checked[i]);
|
|
196
|
+
if (selected.length === 0) return; // ignore — must select at least 1
|
|
197
|
+
|
|
198
|
+
const upCount = totalPhysicalLines(prevLines) - 1;
|
|
199
|
+
if (upCount > 0) stdout.write(`\x1b[${upCount}A`);
|
|
200
|
+
stdout.write('\r\x1b[J');
|
|
201
|
+
const summary = selected.map(c => chalk.cyan(c.label)).join(', ');
|
|
202
|
+
stdout.write(` ${chalk.cyan('❯')} ${summary}\n`);
|
|
203
|
+
cleanup();
|
|
204
|
+
resolve(selected.map(c => c.value));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Esc
|
|
209
|
+
if (key === '\x1b' && buf.length === 1) {
|
|
210
|
+
const upCount = totalPhysicalLines(prevLines) - 1;
|
|
211
|
+
if (upCount > 0) stdout.write(`\x1b[${upCount}A`);
|
|
212
|
+
stdout.write('\r\x1b[J');
|
|
213
|
+
cleanup();
|
|
214
|
+
resolve(null);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Space — toggle
|
|
219
|
+
if (key === ' ') {
|
|
220
|
+
checked[cursor] = !checked[cursor];
|
|
221
|
+
}
|
|
222
|
+
// Up / Left
|
|
223
|
+
else if (key === '\x1b[A' || key === '\x1b[D') {
|
|
224
|
+
cursor = (cursor - 1 + choices.length) % choices.length;
|
|
225
|
+
}
|
|
226
|
+
// Down / Right
|
|
227
|
+
else if (key === '\x1b[B' || key === '\x1b[C') {
|
|
228
|
+
cursor = (cursor + 1) % choices.length;
|
|
229
|
+
} else {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Redraw
|
|
234
|
+
const upCount = totalPhysicalLines(prevLines) - 1;
|
|
235
|
+
if (upCount > 0) stdout.write(`\x1b[${upCount}A`);
|
|
236
|
+
stdout.write('\r\x1b[J');
|
|
237
|
+
prevLines = render();
|
|
238
|
+
stdout.write(prevLines.join('\n'));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
stdin.on('data', onData);
|
|
242
|
+
});
|
|
243
|
+
}
|
package/src/lib/ui.js
CHANGED
|
@@ -45,21 +45,40 @@ export function printBanner() {
|
|
|
45
45
|
|
|
46
46
|
// --- completion summary ---
|
|
47
47
|
|
|
48
|
-
export function printCompletionSummary({ skillName, scope,
|
|
48
|
+
export function printCompletionSummary({ skillName, scope, agents, isGithub, namespace }) {
|
|
49
49
|
const scopeLabel = scope === 'global'
|
|
50
50
|
? 'Global (all projects)'
|
|
51
51
|
: 'Project (local)';
|
|
52
52
|
|
|
53
|
+
const isSingle = agents.length === 1;
|
|
54
|
+
const agentNames = agents.map(a => a.name).join(', ');
|
|
55
|
+
const paths = agents.map(a => a.displayPath(skillName, scope));
|
|
56
|
+
|
|
53
57
|
const lines = [
|
|
54
58
|
chalk.bold.green('Skill installed successfully!'),
|
|
55
59
|
'',
|
|
56
60
|
` ${chalk.dim('Skill:')} ${skillName}`,
|
|
57
61
|
` ${chalk.dim('Scope:')} ${scopeLabel}`,
|
|
58
|
-
` ${chalk.dim('Path:')} ${installPath}`,
|
|
59
|
-
'',
|
|
60
|
-
`Start a new ${agentName} session to use it.`,
|
|
61
62
|
];
|
|
62
63
|
|
|
64
|
+
if (isSingle) {
|
|
65
|
+
lines.push(` ${chalk.dim('Agent:')} ${agentNames}`);
|
|
66
|
+
lines.push(` ${chalk.dim('Path:')} ${paths[0]}`);
|
|
67
|
+
} else {
|
|
68
|
+
lines.push(` ${chalk.dim('Agents:')} ${agentNames}`);
|
|
69
|
+
lines.push(` ${chalk.dim('Paths:')} ${paths[0]}`);
|
|
70
|
+
for (let i = 1; i < paths.length; i++) {
|
|
71
|
+
lines.push(` ${paths[i]}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
lines.push('');
|
|
76
|
+
if (isSingle) {
|
|
77
|
+
lines.push(`Start a new ${agents[0].name} session to use it.`);
|
|
78
|
+
} else {
|
|
79
|
+
lines.push('Start a new session in any of these agents to use it.');
|
|
80
|
+
}
|
|
81
|
+
|
|
63
82
|
if (!isGithub && namespace) {
|
|
64
83
|
lines.push(`Learn more: ${chalk.underline(`https://agentskills.dk/skills/${namespace}`)}`);
|
|
65
84
|
}
|