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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentskillsdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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,88 @@ 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 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
- 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
- ]);
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
- await downloadSkill(skill, destDir);
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
- installPath: agent.displayPath(installName, scope),
109
- agentName: agent.name,
167
+ agents,
110
168
  isGithub,
111
169
  namespace: skill.namespace,
112
170
  });
@@ -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
  }