agentskillsdk 0.4.5 → 0.5.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.4.5",
3
+ "version": "0.5.1",
4
4
  "description": "Install agent skills from agentskills.dk",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,11 +1,14 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
+ import { symlinkSync, mkdirSync } from 'node:fs';
4
+ import { join, relative, dirname } from 'node:path';
5
+ import { homedir } from 'node:os';
3
6
  import { fetchSkill, fetchSkills } from '../lib/api.js';
4
- import { detectAgents, AGENTS } from '../lib/detect-agent.js';
7
+ import { detectAgents, AGENTS, getUniversalAgents, getNonUniversalAgents } from '../lib/detect-agent.js';
5
8
  import { downloadSkill } from '../lib/download.js';
6
9
  import { parseGithubSource } from '../lib/parse-source.js';
7
10
  import { selectPrompt, checkboxPrompt } from '../lib/prompt.js';
8
- import { printBanner, printCompletionSummary, step, error } from '../lib/ui.js';
11
+ import { printBanner, printInstallPlan, printCompletionSummary, step, error } from '../lib/ui.js';
9
12
 
10
13
  export async function addCommand(skillName, options) {
11
14
  printBanner();
@@ -71,7 +74,7 @@ export async function addCommand(skillName, options) {
71
74
  spinner.succeed(`fundet skill: ${chalk.bold(skill.name)}`);
72
75
  }
73
76
 
74
- // --- agent selection + scope prompt (with back navigation) ---
77
+ // --- agent selection + scope + confirmation (with back navigation) ---
75
78
  const detected = detectAgents(cwd);
76
79
  let agents;
77
80
  let scope;
@@ -135,7 +138,34 @@ export async function addCommand(skillName, options) {
135
138
  scope = result;
136
139
  }
137
140
 
138
- break; // selection complete
141
+ // --- confirmation screen ---
142
+ const universalAgents = getUniversalAgents(agents);
143
+ const nonUniversalAgents = getNonUniversalAgents(agents);
144
+
145
+ const source = isGithub
146
+ ? `${skill.githubOwner}/${skill.githubRepo}`
147
+ : (skill.namespace || skill.name);
148
+
149
+ printInstallPlan({
150
+ skillName: installName,
151
+ source,
152
+ scope,
153
+ universalAgents,
154
+ symlinkAgents: nonUniversalAgents,
155
+ });
156
+
157
+ const confirm = await selectPrompt(chalk.bold('fortsæt med installation?'), [
158
+ { label: 'ja, installer', value: 'yes' },
159
+ { label: 'nej, annullér', value: 'no' },
160
+ ]);
161
+
162
+ if (confirm === null || confirm === 'no') {
163
+ // Esc or "nej" → back to scope selection (or exit if flags were set)
164
+ if (options.global || options.project) process.exit(0);
165
+ continue agentSelection;
166
+ }
167
+
168
+ break; // selection + confirmation complete
139
169
  }
140
170
 
141
171
  // --- step message ---
@@ -145,12 +175,46 @@ export async function addCommand(skillName, options) {
145
175
  step(`Agents: ${chalk.bold(agents.map(a => a.name).join(', '))}`);
146
176
  }
147
177
 
148
- // --- download to each agent ---
178
+ // --- download to canonical .agents/skills/ then symlink ---
149
179
  const spinner = ora({ text: 'downloader skill-filer...', indent: 2 }).start();
180
+
181
+ const symlinkResults = { canonical: [], symlinked: [], copied: [] };
182
+
150
183
  try {
151
- for (const agent of agents) {
152
- const destDir = agent.path(installName, { cwd, scope });
153
- await downloadSkill(skill, destDir);
184
+ // Canonical location: always .agents/skills/<name>/
185
+ const base = scope === 'global' ? homedir() : cwd;
186
+ const canonicalDir = join(base, '.agents', 'skills', installName);
187
+
188
+ // Download once to canonical
189
+ await downloadSkill(skill, canonicalDir);
190
+
191
+ // Track which agents got the canonical copy (universal agents using .agents/)
192
+ const universalAgents = getUniversalAgents(agents);
193
+ const nonUniversalAgents = getNonUniversalAgents(agents);
194
+
195
+ for (const a of universalAgents) {
196
+ symlinkResults.canonical.push(a);
197
+ }
198
+
199
+ // For each non-universal agent, symlink from their path → canonical
200
+ for (const agent of nonUniversalAgents) {
201
+ const agentDir = agent.path(installName, { cwd, scope });
202
+
203
+ if (agentDir === canonicalDir) {
204
+ symlinkResults.canonical.push(agent);
205
+ continue;
206
+ }
207
+
208
+ mkdirSync(dirname(agentDir), { recursive: true });
209
+ try {
210
+ const rel = relative(dirname(agentDir), canonicalDir);
211
+ symlinkSync(rel, agentDir);
212
+ symlinkResults.symlinked.push(agent);
213
+ } catch {
214
+ // Fallback: download again
215
+ await downloadSkill(skill, agentDir);
216
+ symlinkResults.copied.push(agent);
217
+ }
154
218
  }
155
219
  } catch (err) {
156
220
  spinner.fail('download fejlede');
@@ -166,5 +230,6 @@ export async function addCommand(skillName, options) {
166
230
  agents,
167
231
  isGithub,
168
232
  namespace: skill.namespace,
233
+ symlinkResults,
169
234
  });
170
235
  }
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
 
5
- function makeAgent(name, folder, { globalFolder, detectFolder } = {}) {
5
+ function makeAgent(name, folder, { globalFolder, detectFolder, universal } = {}) {
6
6
  const gFolder = globalFolder || folder;
7
7
  const dFolder = detectFolder || folder;
8
8
  return {
@@ -10,6 +10,7 @@ function makeAgent(name, folder, { globalFolder, detectFolder } = {}) {
10
10
  folder,
11
11
  globalFolder: gFolder,
12
12
  detectFolder: dFolder,
13
+ universal: !!universal,
13
14
  path: (skill, { cwd, scope }) => {
14
15
  if (scope === 'global') return join(homedir(), gFolder, 'skills', skill);
15
16
  return join(cwd, folder, 'skills', skill);
@@ -22,28 +23,32 @@ function makeAgent(name, folder, { globalFolder, detectFolder } = {}) {
22
23
  }
23
24
 
24
25
  export const AGENTS = [
26
+ makeAgent('Amp', '.agents', { detectFolder: '.config/amp', universal: true }),
25
27
  makeAgent('Cline', '.cline'),
26
28
  makeAgent('Claude Code', '.claude'),
27
29
  makeAgent('CodeBuddy', '.codebuddy'),
28
- makeAgent('Codex CLI', '.agents', { globalFolder: '.codex' }),
30
+ makeAgent('Codex CLI', '.agents', { globalFolder: '.codex', detectFolder: '.codex', universal: true }),
29
31
  makeAgent('Command Code', '.commandcode'),
30
32
  makeAgent('Continue', '.continue'),
31
33
  makeAgent('Crush', '.crush'),
32
- makeAgent('Cursor', '.cursor'),
34
+ makeAgent('Cursor', '.agents', { globalFolder: '.cursor', detectFolder: '.cursor', universal: true }),
33
35
  makeAgent('Droid', '.factory'),
34
- makeAgent('GitHub Copilot', '.github', { detectFolder: '.github/skills' }),
36
+ makeAgent('Gemini CLI', '.agents', { detectFolder: '.gemini', universal: true }),
37
+ makeAgent('GitHub Copilot', '.agents', { detectFolder: '.github/skills', globalFolder: '.github', universal: true }),
35
38
  makeAgent('Goose', '.goose'),
36
39
  makeAgent('Kilo Code', '.kilocode'),
40
+ makeAgent('Kimi CLI', '.agents', { detectFolder: '.kimi', universal: true }),
37
41
  makeAgent('Kiro CLI', '.kiro'),
38
42
  makeAgent('MCPJam', '.mcpjam'),
39
43
  makeAgent('Mux', '.mux'),
40
44
  makeAgent('Neovate', '.neovate'),
41
45
  makeAgent('OpenClaw', '.openclaw'),
42
- makeAgent('OpenCode', '.opencode'),
46
+ makeAgent('OpenCode', '.agents', { globalFolder: '.opencode', detectFolder: '.opencode', universal: true }),
43
47
  makeAgent('OpenHands', '.openhands'),
44
48
  makeAgent('Pi', '.pi'),
45
49
  makeAgent('Qoder', '.qoder'),
46
50
  makeAgent('Qwen Code', '.qwen'),
51
+ makeAgent('Replit', '.agents', { detectFolder: '.replit', universal: true }),
47
52
  makeAgent('Roo Code', '.roo'),
48
53
  makeAgent('Trae', '.trae'),
49
54
  makeAgent('Windsurf', '.windsurf'),
@@ -53,3 +58,11 @@ export const AGENTS = [
53
58
  export function detectAgents(cwd) {
54
59
  return AGENTS.filter(agent => existsSync(join(cwd, agent.detectFolder)));
55
60
  }
61
+
62
+ export function getUniversalAgents(agentList) {
63
+ return agentList.filter(a => a.universal);
64
+ }
65
+
66
+ export function getNonUniversalAgents(agentList) {
67
+ return agentList.filter(a => !a.universal);
68
+ }
package/src/lib/prompt.js CHANGED
@@ -134,9 +134,10 @@ export function selectPrompt(question, choices, { defaultIndex = 0 } = {}) {
134
134
  * @param {{ label: string, value: any }[]} choices
135
135
  * @returns {Promise<any[]|null>} array of selected values, or null if Esc
136
136
  */
137
- export function checkboxPrompt(question, choices) {
137
+ export function checkboxPrompt(question, choices, { pageSize = 10 } = {}) {
138
138
  return new Promise((resolve) => {
139
139
  let cursor = 0;
140
+ let scrollOffset = 0;
140
141
  const checked = new Array(choices.length).fill(false);
141
142
  const { stdin, stdout } = process;
142
143
  const cols = stdout.columns || 80;
@@ -151,11 +152,20 @@ export function checkboxPrompt(question, choices) {
151
152
  }
152
153
 
153
154
  function render() {
154
- const lines = choices.map((c, i) => {
155
+ const visible = choices.slice(scrollOffset, scrollOffset + pageSize);
156
+ const lines = visible.map((c, vi) => {
157
+ const i = vi + scrollOffset;
155
158
  const box = checked[i] ? orange('◼') : '◻';
156
159
  const label = i === cursor ? orange(c.label) : c.label;
157
160
  return ` ${box} ${label}`;
158
161
  });
162
+ if (scrollOffset > 0) {
163
+ lines.unshift(chalk.dim(` ↑ ${scrollOffset} mere`));
164
+ }
165
+ const remaining = choices.length - (scrollOffset + pageSize);
166
+ if (remaining > 0) {
167
+ lines.push(chalk.dim(` ↓ ${remaining} mere`));
168
+ }
159
169
  lines.push('');
160
170
  lines.push(chalk.dim(' ↑↓ naviger · space skift · enter bekræft · esc tilbage'));
161
171
  return lines;
@@ -224,14 +234,20 @@ export function checkboxPrompt(question, choices) {
224
234
  // Up / Left
225
235
  else if (key === '\x1b[A' || key === '\x1b[D') {
226
236
  cursor = (cursor - 1 + choices.length) % choices.length;
237
+ if (cursor === choices.length - 1) scrollOffset = Math.max(0, choices.length - pageSize);
227
238
  }
228
239
  // Down / Right
229
240
  else if (key === '\x1b[B' || key === '\x1b[C') {
230
241
  cursor = (cursor + 1) % choices.length;
242
+ if (cursor === 0) scrollOffset = 0;
231
243
  } else {
232
244
  return;
233
245
  }
234
246
 
247
+ // Keep cursor within the visible scroll window
248
+ if (cursor < scrollOffset) scrollOffset = cursor;
249
+ if (cursor >= scrollOffset + pageSize) scrollOffset = cursor - pageSize + 1;
250
+
235
251
  // Redraw
236
252
  const upCount = totalPhysicalLines(prevLines) - 1;
237
253
  if (upCount > 0) stdout.write(`\x1b[${upCount}A`);
package/src/lib/ui.js CHANGED
@@ -43,16 +43,48 @@ export function printBanner() {
43
43
  console.log('');
44
44
  }
45
45
 
46
- // --- completion summary ---
46
+ // --- install plan ---
47
47
 
48
- export function printCompletionSummary({ skillName, scope, agents, isGithub, namespace }) {
48
+ export function printInstallPlan({ skillName, source, scope, universalAgents, symlinkAgents }) {
49
49
  const scopeLabel = scope === 'global'
50
50
  ? 'globalt (alle projekter)'
51
51
  : 'projekt (lokalt)';
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));
53
+ const lines = [
54
+ chalk.bold('installationsplan:'),
55
+ '',
56
+ ` ${chalk.dim('skill:')} ${skillName}`,
57
+ ` ${chalk.dim('kilde:')} ${source}`,
58
+ ` ${chalk.dim('omfang:')} ${scopeLabel}`,
59
+ ];
60
+
61
+ if (universalAgents.length > 0) {
62
+ lines.push('');
63
+ lines.push(chalk.dim(' ── Universal (.agents/skills/) ──'));
64
+ for (const a of universalAgents) {
65
+ lines.push(` ${chalk.green('\u2713')} ${a.name}`);
66
+ }
67
+ }
68
+
69
+ if (symlinkAgents.length > 0) {
70
+ lines.push('');
71
+ lines.push(chalk.dim(' ── Symlink \u2192 .agents/skills/ ──'));
72
+ for (const a of symlinkAgents) {
73
+ lines.push(` ${chalk.green('\u2713')} ${a.name} ${chalk.dim(a.displayPath(skillName, scope))}`);
74
+ }
75
+ }
76
+
77
+ console.log('');
78
+ console.log(box(lines));
79
+ console.log('');
80
+ }
81
+
82
+ // --- completion summary ---
83
+
84
+ export function printCompletionSummary({ skillName, scope, agents, isGithub, namespace, symlinkResults }) {
85
+ const scopeLabel = scope === 'global'
86
+ ? 'globalt (alle projekter)'
87
+ : 'projekt (lokalt)';
56
88
 
57
89
  const lines = [
58
90
  chalk.bold.green('skill installeret!'),
@@ -61,19 +93,52 @@ export function printCompletionSummary({ skillName, scope, agents, isGithub, nam
61
93
  ` ${chalk.dim('omfang:')} ${scopeLabel}`,
62
94
  ];
63
95
 
64
- if (isSingle) {
65
- lines.push(` ${chalk.dim('agent:')} ${agentNames}`);
66
- lines.push(` ${chalk.dim('sti:')} ${paths[0]}`);
96
+ if (symlinkResults) {
97
+ // Group by installation method
98
+ const { canonical, symlinked, copied } = symlinkResults;
99
+
100
+ if (canonical.length > 0) {
101
+ lines.push('');
102
+ lines.push(chalk.dim(' canonical (.agents/skills/):'));
103
+ for (const a of canonical) {
104
+ lines.push(` ${chalk.green('\u2713')} ${a.name}`);
105
+ }
106
+ }
107
+
108
+ if (symlinked.length > 0) {
109
+ lines.push('');
110
+ lines.push(chalk.dim(' symlink:'));
111
+ for (const a of symlinked) {
112
+ lines.push(` ${chalk.green('\u2713')} ${a.name} ${chalk.dim(a.displayPath(skillName, scope) + ' \u2192 .agents/skills/')}`);
113
+ }
114
+ }
115
+
116
+ if (copied.length > 0) {
117
+ lines.push('');
118
+ lines.push(chalk.dim(' kopi (symlink fejlede):'));
119
+ for (const a of copied) {
120
+ lines.push(` ${chalk.green('\u2713')} ${a.name} ${chalk.dim(a.displayPath(skillName, scope))}`);
121
+ }
122
+ }
67
123
  } else {
68
- lines.push(` ${chalk.dim('agenter:')} ${agentNames}`);
69
- lines.push(` ${chalk.dim('stier:')} ${paths[0]}`);
70
- for (let i = 1; i < paths.length; i++) {
71
- lines.push(` ${paths[i]}`);
124
+ // Fallback: old-style summary
125
+ const agentNames = agents.map(a => a.name).join(', ');
126
+ const paths = agents.map(a => a.displayPath(skillName, scope));
127
+
128
+ if (agents.length === 1) {
129
+ lines.push(` ${chalk.dim('agent:')} ${agentNames}`);
130
+ lines.push(` ${chalk.dim('sti:')} ${paths[0]}`);
131
+ } else {
132
+ lines.push(` ${chalk.dim('agenter:')} ${agentNames}`);
133
+ lines.push(` ${chalk.dim('stier:')} ${paths[0]}`);
134
+ for (let i = 1; i < paths.length; i++) {
135
+ lines.push(` ${paths[i]}`);
136
+ }
72
137
  }
73
138
  }
74
139
 
75
140
  lines.push('');
76
- if (isSingle) {
141
+ if (agents.length === 1) {
77
142
  lines.push(`start en ny ${agents[0].name}-session for at bruge den.`);
78
143
  } else {
79
144
  lines.push('start en ny session i en af disse agenter for at bruge den.');