agentskillsdk 0.2.0 → 0.3.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentskillsdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Install agent skills from agentskills.dk",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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,87 @@ export async function addCommand(skillName, options) {
71
71
  spinner.succeed(`Found skill: ${chalk.bold(skill.name)}`);
72
72
  }
73
73
 
74
- // --- detect agent ---
75
- const agents = await detectAgents(cwd);
76
- const agent = agents[0];
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
- if (options.global) {
82
- scope = 'global';
83
- } else if (options.project) {
84
- scope = 'project';
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 choice = await selectPrompt('how would you like to install?', [
88
+ { label: `all detected agents (${detected.length})`, value: 'all' },
89
+ { label: 'choose specific agents', value: 'choose' },
90
+ ]);
91
+
92
+ if (choice === null) {
93
+ // Esc from top-level → cancel
94
+ process.exit(0);
95
+ }
96
+
97
+ if (choice === 'all') {
98
+ agents = detected;
99
+ } else {
100
+ // Checkbox from detected agents
101
+ const choices = detected.map(a => ({ label: a.name, value: a }));
102
+ const selected = await checkboxPrompt('select agents:', choices);
103
+ if (selected === null) continue agentSelection; // Esc → back to all/choose
104
+ agents = selected;
105
+ }
106
+ } else {
107
+ // No agents detected — show checkbox of all known agents
108
+ const choices = AGENTS.map(a => ({ label: a.name, value: a }));
109
+ const selected = await checkboxPrompt('select agents:', choices);
110
+ if (selected === null) {
111
+ // Esc with no detected agents → cancel
112
+ process.exit(0);
113
+ }
114
+ agents = selected;
115
+ }
116
+
117
+ // --- resolve scope ---
118
+ if (options.global) {
119
+ scope = 'global';
120
+ } else if (options.project) {
121
+ scope = 'project';
122
+ } else {
123
+ // Build hint using first agent (representative)
124
+ const a = agents[0];
125
+ const result = await selectPrompt('install scope:', [
126
+ { label: 'project', hint: `(local ${a.folder}/skills/)`, value: 'project' },
127
+ { label: 'global', hint: `(~/${a.globalFolder}/skills/)`, value: 'global' },
128
+ ]);
129
+
130
+ if (result === null) {
131
+ // Esc → back to agent selection (unless auto-selected single agent)
132
+ if (detected.length === 1) process.exit(0);
133
+ continue agentSelection;
134
+ }
135
+ scope = result;
136
+ }
137
+
138
+ break; // selection complete
139
+ }
140
+
141
+ // --- step message ---
142
+ if (agents.length === 1) {
143
+ step(`Agent: ${chalk.bold(agents[0].name)}`);
85
144
  } else {
86
- scope = await selectPrompt('Install scope:', [
87
- { label: 'Project', hint: `(local ${agent.folder}/skills/)`, value: 'project' },
88
- { label: 'Global', hint: `(~/${agent.globalFolder}/skills/)`, value: 'global' },
89
- ]);
145
+ step(`Agents: ${chalk.bold(agents.map(a => a.name).join(', '))}`);
90
146
  }
91
147
 
92
- // --- download ---
93
- const destDir = agent.path(installName, { cwd, scope });
148
+ // --- download to each agent ---
94
149
  const spinner = ora({ text: 'Downloading skill files...', indent: 2 }).start();
95
150
  try {
96
- await downloadSkill(skill, destDir);
151
+ for (const agent of agents) {
152
+ const destDir = agent.path(installName, { cwd, scope });
153
+ await downloadSkill(skill, destDir);
154
+ }
97
155
  } catch (err) {
98
156
  spinner.fail('Download failed');
99
157
  error(err.message);
@@ -105,8 +163,7 @@ export async function addCommand(skillName, options) {
105
163
  printCompletionSummary({
106
164
  skillName: installName,
107
165
  scope,
108
- installPath: agent.displayPath(installName, scope),
109
- agentName: agent.name,
166
+ agents,
110
167
  isGithub,
111
168
  namespace: skill.namespace,
112
169
  });
@@ -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 async function detectAgents(cwd) {
36
- const found = AGENTS.filter(agent => existsSync(join(cwd, agent.detectFolder)));
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
- return choices.map((c, i) => {
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, installPath, agentName, isGithub, namespace }) {
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
  }