agentskillsdk 0.4.4 → 0.5.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.4.4",
3
+ "version": "0.5.0",
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', 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', universal: true }),
33
35
  makeAgent('Droid', '.factory'),
34
- makeAgent('GitHub Copilot', '.github', { detectFolder: '.github/skills' }),
36
+ makeAgent('Gemini CLI', '.agents', { 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', { 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', 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', { 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
@@ -1,5 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
 
3
+ const orange = chalk.hex('#FF8C00');
4
+
3
5
  // --- strip ANSI for width calculation ---
4
6
 
5
7
  const ANSI_RE = /\x1b\[[0-9;]*m/g;
@@ -35,8 +37,8 @@ export function selectPrompt(question, choices, { defaultIndex = 0 } = {}) {
35
37
 
36
38
  function render() {
37
39
  const lines = choices.map((c, i) => {
38
- const marker = i === selected ? chalk.cyan('\u276f') : ' ';
39
- const label = i === selected ? chalk.cyan(c.label) : c.label;
40
+ const marker = i === selected ? orange('\u276f') : ' ';
41
+ const label = i === selected ? orange(c.label) : c.label;
40
42
  const hint = c.hint ? chalk.dim(` ${c.hint}`) : '';
41
43
  return ` ${marker} ${label}${hint}`;
42
44
  });
@@ -84,7 +86,7 @@ export function selectPrompt(question, choices, { defaultIndex = 0 } = {}) {
84
86
  const upCount = totalPhysicalLines(prevLines) - 1;
85
87
  if (upCount > 0) stdout.write(`\x1b[${upCount}A`);
86
88
  stdout.write('\r\x1b[J'); // col 0, clear to end
87
- const final = ` ${chalk.cyan('\u276f')} ${chalk.cyan(choices[selected].label)}`;
89
+ const final = ` ${orange('\u276f')} ${orange(choices[selected].label)}`;
88
90
  stdout.write(final + '\n');
89
91
  cleanup();
90
92
  resolve(choices[selected].value);
@@ -150,8 +152,8 @@ export function checkboxPrompt(question, choices) {
150
152
 
151
153
  function render() {
152
154
  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
+ const box = checked[i] ? orange('◼') : '◻';
156
+ const label = i === cursor ? orange(c.label) : c.label;
155
157
  return ` ${box} ${label}`;
156
158
  });
157
159
  lines.push('');
@@ -198,8 +200,8 @@ export function checkboxPrompt(question, choices) {
198
200
  const upCount = totalPhysicalLines(prevLines) - 1;
199
201
  if (upCount > 0) stdout.write(`\x1b[${upCount}A`);
200
202
  stdout.write('\r\x1b[J');
201
- const summary = selected.map(c => chalk.cyan(c.label)).join(', ');
202
- stdout.write(` ${chalk.cyan('❯')} ${summary}\n`);
203
+ const summary = selected.map(c => orange(c.label)).join(', ');
204
+ stdout.write(` ${orange('❯')} ${summary}\n`);
203
205
  cleanup();
204
206
  resolve(selected.map(c => c.value));
205
207
  return;
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.');