agentskillsdk 0.5.3 → 0.6.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/bin/cli.js CHANGED
@@ -1,2 +1,50 @@
1
1
  #!/usr/bin/env node
2
- import '../src/index.js';
2
+ import { run } from '../src/index.js';
3
+ import { exitCodeFor, EXIT, AmbiguousError, NoAgentError, CanceledError } from '../src/lib/errors.js';
4
+ import { setLocale, resolveLocale } from '../src/lib/messages/index.js';
5
+
6
+ // Pre-parse --lang so help text and error messages emit in the requested
7
+ // language even before commander runs.
8
+ function scanLangFlag(argv) {
9
+ for (let i = 2; i < argv.length; i++) {
10
+ if (argv[i] === '--lang' && argv[i + 1]) return argv[i + 1];
11
+ if (argv[i].startsWith('--lang=')) return argv[i].slice(7);
12
+ }
13
+ return undefined;
14
+ }
15
+ function scanJsonFlag(argv) {
16
+ for (let i = 2; i < argv.length; i++) {
17
+ if (argv[i] === '--json') return true;
18
+ }
19
+ return false;
20
+ }
21
+ setLocale(resolveLocale({ flag: scanLangFlag(process.argv), env: process.env }));
22
+
23
+ function emitJsonError(err) {
24
+ if (err instanceof CanceledError) {
25
+ return JSON.stringify({ error: 'canceled' });
26
+ }
27
+ if (err instanceof NoAgentError) {
28
+ return JSON.stringify({ error: 'no_agent_target', candidates: err.candidates || [] });
29
+ }
30
+ if (err instanceof AmbiguousError) {
31
+ return JSON.stringify({ error: 'ambiguous_agent', candidates: err.candidates || [] });
32
+ }
33
+ return JSON.stringify({ error: err && err.message ? err.message : String(err) });
34
+ }
35
+
36
+ try {
37
+ const code = await run();
38
+ process.exit(code ?? EXIT.OK);
39
+ } catch (err) {
40
+ const code = exitCodeFor(err);
41
+ if (!err?.silent) {
42
+ if (scanJsonFlag(process.argv)) {
43
+ process.stderr.write(emitJsonError(err) + '\n');
44
+ } else {
45
+ const message = err && err.message ? err.message : String(err);
46
+ process.stderr.write(`${message}\n`);
47
+ }
48
+ }
49
+ process.exit(code);
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentskillsdk",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Install agent skills from agentskills.dk",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,5 +31,8 @@
31
31
  "type": "git",
32
32
  "url": "https://github.com/agentskillsdk/cli"
33
33
  },
34
- "homepage": "https://agentskills.dk"
34
+ "homepage": "https://agentskills.dk",
35
+ "scripts": {
36
+ "test": "node --test 'test/**/*.test.js'"
37
+ }
35
38
  }
@@ -1,233 +1,339 @@
1
1
  import chalk from 'chalk';
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';
2
+ import { existsSync, readdirSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
6
4
  import { fetchSkill, fetchSkills } from '../lib/api.js';
7
- import { detectAgents, AGENTS, getUniversalAgents, getNonUniversalAgents } from '../lib/detect-agent.js';
5
+ import { detectAgents, AGENTS, AGENT_BY_SLUG } from '../lib/detect-agent.js';
8
6
  import { downloadSkill } from '../lib/download.js';
9
- import { parseGithubSource } from '../lib/parse-source.js';
7
+ import { resolveGithubSkillPath } from '../lib/resolve-github-path.js';
8
+ import { parseSource } from '../lib/parse-source.js';
10
9
  import { selectPrompt, checkboxPrompt } from '../lib/prompt.js';
11
- import { printBanner, printInstallPlan, printCompletionSummary, step, error } from '../lib/ui.js';
10
+ import { createOutput } from '../lib/output.js';
11
+ import { detectRuntime } from '../lib/detect-runtime.js';
12
+ import { t, setLocale } from '../lib/messages/index.js';
13
+ import { UsageError, AmbiguousError, NoMatchError, FsError, NoAgentError, CanceledError } from '../lib/errors.js';
12
14
 
13
- export async function addCommand(skillName, options) {
14
- printBanner();
15
-
16
- const cwd = process.cwd();
17
- const githubSource = parseGithubSource(skillName);
18
- const isGithub = !!githubSource;
19
-
20
- // --- resolve scope flags ---
21
- if (options.global && options.project) {
22
- error('kan ikke bruge både --global og --project. vælg én.');
23
- process.exit(1);
15
+ function isNonEmptyDir(p) {
16
+ try {
17
+ return readdirSync(p).length > 0;
18
+ } catch {
19
+ return false;
24
20
  }
21
+ }
25
22
 
26
- // --- resolve skill ---
27
- let skill;
28
- let installName;
23
+ // When running inside an agent, hard-coding non-interactive defaults removes
24
+ // every prompt before it can be reached. The only opt-out is an explicit
25
+ // flag the caller has set (truthy `??` skip).
26
+ function forceAgentDefaults(options, runtime) {
27
+ if (!runtime.isAgent) return options;
28
+ options.yes = true;
29
+ options.quiet ??= true;
30
+ options.lang ??= 'en';
31
+ if (runtime.agentSlug && (!options.agent || options.agent.length === 0)) {
32
+ options.agent = [runtime.agentSlug];
33
+ }
34
+ return options;
35
+ }
29
36
 
30
- if (isGithub) {
31
- const skillPath = options.skill || '';
32
- installName = skillPath ? skillPath.split('/').pop() : githubSource.repo;
33
- skill = {
34
- name: installName,
35
- githubOwner: githubSource.owner,
36
- githubRepo: githubSource.repo,
37
- githubPath: skillPath,
38
- defaultBranch: 'main',
39
- };
40
- step(`GitHub source: ${chalk.bold(`${githubSource.owner}/${githubSource.repo}`)}` +
41
- (options.skill ? ` (skill: ${chalk.bold(options.skill)})` : ''));
42
- } else {
43
- installName = skillName;
44
- const spinner = ora({ text: `søger efter ${chalk.bold(skillName)}...`, indent: 2 }).start();
45
- try {
46
- skill = await fetchSkill(skillName);
47
- } catch (err) {
48
- spinner.fail('opslag i registret fejlede');
49
- error(err.message);
50
- process.exit(1);
37
+ function expandAgentSlugs(slugs) {
38
+ if (!slugs || slugs.length === 0) return [];
39
+ const out = [];
40
+ for (const s of slugs) {
41
+ if (s === '*') {
42
+ for (const a of AGENTS) out.push(a.slug);
43
+ } else {
44
+ out.push(s);
51
45
  }
46
+ }
47
+ // Dedupe in case '*' is mixed with explicit slugs.
48
+ return [...new Set(out)];
49
+ }
52
50
 
53
- if (!skill) {
54
- spinner.fail(`skill "${skillName}" ikke fundet`);
55
-
56
- let suggestions = [];
57
- try {
58
- const allSkills = await fetchSkills();
59
- suggestions = allSkills
60
- .filter(s => s.name.includes(skillName) || skillName.includes(s.name))
61
- .slice(0, 3);
62
- } catch {
63
- // ignore
64
- }
65
-
66
- if (suggestions.length > 0) {
67
- console.error('\n mente du:');
68
- suggestions.forEach(s => console.error(` - ${chalk.bold(s.name)}`));
69
- }
70
- console.error(`\n se alle skills: ${chalk.underline('https://agentskills.dk/skills')}\n`);
71
- process.exit(1);
51
+ export function chooseAgents(detected, options) {
52
+ if (options.agent && options.agent.length > 0) {
53
+ const slugs = expandAgentSlugs(options.agent);
54
+ const records = [];
55
+ for (const slug of slugs) {
56
+ const a = AGENT_BY_SLUG[slug];
57
+ if (!a) throw new UsageError(t('error.unknown_agent_slug', { slug }));
58
+ records.push(a);
72
59
  }
73
-
74
- spinner.succeed(`fundet skill: ${chalk.bold(skill.name)}`);
60
+ return records;
75
61
  }
62
+ if (options.yes) {
63
+ if (detected.length === 1) return detected;
64
+ if (detected.length > 1) {
65
+ const cs = detected.map(a => a.slug).join(', ');
66
+ throw new AmbiguousError(t('error.ambiguous_agent', { candidates: cs }), {
67
+ candidates: detected.map(a => a.slug),
68
+ });
69
+ }
70
+ const cs = AGENTS.map(a => a.slug).join(', ');
71
+ throw new NoAgentError(t('error.no_agent_target', { candidates: cs }), {
72
+ candidates: AGENTS.map(a => a.slug),
73
+ });
74
+ }
75
+ return null; // signals interactive flow
76
+ }
76
77
 
77
- // --- agent selection + scope + confirmation (with back navigation) ---
78
- const detected = detectAgents(cwd);
79
- let agents;
80
- let scope;
81
-
82
- agentSelection: while (true) {
83
- agents = null;
78
+ export function chooseScope(options) {
79
+ if (options.global) return 'global';
80
+ if (options.project) return 'project';
81
+ if (options.yes) return 'project';
82
+ return null; // signals interactive flow
83
+ }
84
84
 
85
+ async function chooseAgentsInteractive(detected, out) {
86
+ while (true) {
87
+ let agents = null;
85
88
  if (detected.length === 1) {
86
- // Auto-select the single detected agent
87
89
  agents = detected;
88
90
  } else if (detected.length > 1) {
89
- // Prompt: all detected vs choose specific
90
- const choice = await selectPrompt(chalk.bold('hvordan vil du installere?'), [
91
- { label: `alle fundne agenter (${detected.length})`, value: 'all' },
92
- { label: 'vælg specifikke agenter', value: 'choose' },
91
+ const choice = await selectPrompt(chalk.bold(t('add.prompt.how_install')), [
92
+ { label: t('add.prompt.all_detected', { count: detected.length }), value: 'all' },
93
+ { label: t('add.prompt.choose_specific'), value: 'choose' },
93
94
  ]);
94
-
95
- if (choice === null) {
96
- // Esc from top-level → cancel
97
- process.exit(0);
98
- }
99
-
95
+ if (choice === null) throw new CanceledError(t('error.canceled'));
100
96
  if (choice === 'all') {
101
97
  agents = detected;
102
98
  } else {
103
- // Checkbox from detected agents
104
99
  const choices = detected.map(a => ({ label: a.name, value: a }));
105
- const selected = await checkboxPrompt(chalk.bold('vælg agenter:'), choices);
106
- if (selected === null) continue agentSelection; // Esc → back to all/choose
100
+ const selected = await checkboxPrompt(chalk.bold(t('add.prompt.select_agents')), choices);
101
+ if (selected === null) continue;
107
102
  agents = selected;
108
103
  }
109
104
  } else {
110
- // No agents detected — show checkbox of all known agents
111
105
  const choices = AGENTS.map(a => ({ label: a.name, value: a }));
112
- const selected = await checkboxPrompt(chalk.bold('vælg agenter:'), choices);
113
- if (selected === null) {
114
- // Esc with no detected agents → cancel
115
- process.exit(0);
116
- }
106
+ const selected = await checkboxPrompt(chalk.bold(t('add.prompt.select_agents')), choices);
107
+ if (selected === null) throw new CanceledError(t('error.canceled'));
117
108
  agents = selected;
118
109
  }
110
+ return agents;
111
+ }
112
+ }
119
113
 
120
- // --- resolve scope ---
121
- if (options.global) {
122
- scope = 'global';
123
- } else if (options.project) {
124
- scope = 'project';
125
- } else {
126
- const result = await selectPrompt(chalk.bold(`hvor skal "${installName}" installeres?`), [
127
- { label: 'projekt', hint: '(lokalt .agents/skills/)', value: 'project' },
128
- { label: 'globalt', hint: '(~/.agents/skills/)', value: 'global' },
129
- ]);
114
+ async function chooseScopeInteractive(installName, agents, detected) {
115
+ const a = agents[0];
116
+ const result = await selectPrompt(chalk.bold(t('add.prompt.scope', { name: installName })), [
117
+ { label: t('add.prompt.scope_project'), hint: t('add.prompt.scope_project_hint', { folder: a.folder }), value: 'project' },
118
+ { label: t('add.prompt.scope_global'), hint: t('add.prompt.scope_global_hint', { folder: a.globalFolder }), value: 'global' },
119
+ ]);
120
+ if (result === null) {
121
+ // Walk back to agent selection if it was a real choice.
122
+ if (detected.length === 1) throw new CanceledError(t('error.canceled'));
123
+ return null;
124
+ }
125
+ return result;
126
+ }
127
+
128
+ function announceAgentFolderCreation(agents, cwd, scope, out) {
129
+ if (scope !== 'project') return;
130
+ for (const a of agents) {
131
+ if (!existsSync(join(cwd, a.folder))) {
132
+ out.step(t('agent.folder_created', { folder: `${a.folder}/`, name: a.name }));
133
+ }
134
+ }
135
+ }
130
136
 
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;
137
+ function validateOptions(options) {
138
+ if (options.global && options.project) {
139
+ throw new UsageError(t('add.error.both_flags'));
140
+ }
141
+ if (options.agent && options.agent.length > 0) {
142
+ for (const slug of expandAgentSlugs(options.agent)) {
143
+ if (!AGENT_BY_SLUG[slug]) {
144
+ throw new UsageError(t('error.unknown_agent_slug', { slug }));
135
145
  }
136
- scope = result;
137
146
  }
147
+ }
148
+ }
138
149
 
139
- // --- confirmation screen ---
140
- const universalAgents = getUniversalAgents(agents);
141
- const nonUniversalAgents = getNonUniversalAgents(agents);
150
+ export async function addCommand(skillName, options = {}) {
151
+ const runtime = detectRuntime();
152
+ forceAgentDefaults(options, runtime);
153
+ if (options.lang) setLocale(options.lang);
142
154
 
143
- const source = isGithub
144
- ? `${skill.githubOwner}/${skill.githubRepo}`
145
- : (skill.namespace || skill.name);
155
+ validateOptions(options);
146
156
 
147
- printInstallPlan({
148
- skillName: installName,
149
- source,
150
- scope,
151
- universalAgents,
152
- symlinkAgents: nonUniversalAgents,
153
- });
157
+ const out = createOutput(options, runtime);
158
+ out.banner();
159
+
160
+ const cwd = process.cwd();
161
+ const parsed = parseSource(skillName);
162
+ const isGithub = parsed.kind === 'github';
154
163
 
155
- const confirm = await selectPrompt(chalk.bold('fortsæt med installation?'), [
156
- { label: 'ja, installer', value: 'yes' },
157
- { label: 'nej, annullér', value: 'no' },
158
- ]);
164
+ let skill;
165
+ let installName;
159
166
 
160
- if (confirm === null || confirm === 'no') {
161
- // Esc or "nej" back to scope selection (or exit if flags were set)
162
- if (options.global || options.project) process.exit(0);
163
- continue agentSelection;
167
+ if (isGithub) {
168
+ // `--skill` flag wins over any parsed skill/path token.
169
+ const parsedPath = parsed.path || parsed.skill || '';
170
+ const requestedPath = options.skill || parsedPath;
171
+
172
+ const hasRef = !!parsed.ref;
173
+ const hasSkill = !!requestedPath;
174
+ let msgKey;
175
+ if (hasRef && hasSkill) msgKey = 'add.source.github_with_ref_and_skill';
176
+ else if (hasRef) msgKey = 'add.source.github_with_ref';
177
+ else if (hasSkill) msgKey = 'add.source.github_with_skill';
178
+ else msgKey = 'add.source.github';
179
+ out.step(t(msgKey, { owner: parsed.owner, repo: parsed.repo, ref: parsed.ref, skill: requestedPath }));
180
+
181
+ // Resolve the requested path against the actual repo layout. The website's
182
+ // registry runs `matchSkillToPath` during indexing for the same reason —
183
+ // users type `--skill foo` but the file lives at `skills/foo/SKILL.md`.
184
+ //
185
+ // Only resolve when the caller asked for a specific skill — for bare
186
+ // `owner/repo` we preserve the existing "install whatever's in the tarball"
187
+ // behavior so registry-style installs and single-skill repos still work.
188
+ let skillPath = requestedPath;
189
+ if (requestedPath) {
190
+ const lookup = out.spinner(t('add.spinner.resolving_path'));
191
+ let resolved;
192
+ try {
193
+ resolved = await resolveGithubSkillPath({
194
+ owner: parsed.owner,
195
+ repo: parsed.repo,
196
+ ref: parsed.ref,
197
+ requestedPath,
198
+ });
199
+ } catch (err) {
200
+ lookup.fail(t('add.error.resolve_fail'));
201
+ throw err;
202
+ }
203
+ if (resolved.resolved) {
204
+ lookup.succeed(t('add.spinner.resolved_path', { from: requestedPath, to: resolved.path }));
205
+ } else {
206
+ lookup.succeed(t('add.spinner.path_ok'));
207
+ }
208
+ skillPath = resolved.path;
209
+ }
210
+ installName = skillPath ? skillPath.split('/').pop() : parsed.repo;
211
+ skill = {
212
+ name: installName,
213
+ githubOwner: parsed.owner,
214
+ githubRepo: parsed.repo,
215
+ githubPath: skillPath || undefined,
216
+ ref: parsed.ref,
217
+ };
218
+ } else {
219
+ installName = parsed.name;
220
+ const spinner = out.spinner(t('add.spinner.lookup', { name: chalk.bold(parsed.name) }));
221
+ try {
222
+ skill = await fetchSkill(parsed.name);
223
+ } catch (err) {
224
+ spinner.fail(t('add.error.registry_fail'));
225
+ throw err;
164
226
  }
165
227
 
166
- break; // selection + confirmation complete
228
+ if (!skill) {
229
+ spinner.fail(t('add.error.not_found', { name: parsed.name }));
230
+ if (!out.isJson && !out.isQuiet) {
231
+ let suggestions = [];
232
+ try {
233
+ const allSkills = await fetchSkills();
234
+ suggestions = allSkills
235
+ .filter(s => s.name.includes(parsed.name) || parsed.name.includes(s.name))
236
+ .slice(0, 3);
237
+ } catch { /* ignore */ }
238
+ if (suggestions.length > 0) {
239
+ process.stderr.write(`\n ${t('add.suggest.header')}\n`);
240
+ for (const s of suggestions) {
241
+ process.stderr.write(` - ${chalk.bold(s.name)}\n`);
242
+ }
243
+ }
244
+ process.stderr.write(`\n ${t('add.suggest.browse')} ${chalk.underline('https://agentskills.dk/skills')}\n\n`);
245
+ }
246
+ throw new NoMatchError(t('add.error.not_found', { name: parsed.name }));
247
+ }
248
+ spinner.succeed(t('add.spinner.found', { name: chalk.bold(skill.name) }));
167
249
  }
168
250
 
169
- // --- step message ---
170
- if (agents.length === 1) {
171
- step(`Agent: ${chalk.bold(agents[0].name)}`);
172
- } else {
173
- step(`Agents: ${chalk.bold(agents.map(a => a.name).join(', '))}`);
174
- }
251
+ const detected = detectAgents(cwd);
175
252
 
176
- // --- download to canonical .agents/skills/ then symlink ---
177
- const spinner = ora({ text: 'downloader skill-filer...', indent: 2 }).start();
253
+ let agents = chooseAgents(detected, options);
254
+ let scope = chooseScope(options);
178
255
 
179
- const symlinkResults = { canonical: [], symlinked: [], copied: [] };
256
+ // Interactive fallback when --yes / --agent aren't supplied.
257
+ if (agents === null || scope === null) {
258
+ while (true) {
259
+ if (agents === null) agents = await chooseAgentsInteractive(detected, out);
260
+ if (scope === null) {
261
+ scope = await chooseScopeInteractive(installName, agents, detected);
262
+ if (scope === null) { agents = null; continue; }
263
+ }
264
+ break;
265
+ }
266
+ }
180
267
 
181
- try {
182
- // Canonical location: always .agents/skills/<name>/
183
- const base = scope === 'global' ? homedir() : cwd;
184
- const canonicalDir = join(base, '.agents', 'skills', installName);
268
+ announceAgentFolderCreation(agents, cwd, scope, out);
185
269
 
186
- // Download once to canonical
187
- await downloadSkill(skill, canonicalDir);
270
+ if (agents.length === 1) {
271
+ out.step(t('add.step.agent_single', { name: chalk.bold(agents[0].name) }));
272
+ } else {
273
+ out.step(t('add.step.agent_multi', { names: chalk.bold(agents.map(a => a.name).join(', ')) }));
274
+ }
188
275
 
189
- // Track which agents got the canonical copy (universal agents using .agents/)
190
- const universalAgents = getUniversalAgents(agents);
191
- const nonUniversalAgents = getNonUniversalAgents(agents);
276
+ // Compute destination dirs once used by both dry-run preview and the
277
+ // pre-download dest-exists check.
278
+ const destPlan = agents.map(agent => ({
279
+ agent,
280
+ destDir: agent.path(installName, { cwd, scope }),
281
+ }));
192
282
 
193
- for (const a of universalAgents) {
194
- symlinkResults.canonical.push(a);
195
- }
283
+ const sourcePayload = isGithub
284
+ ? { type: 'github', owner: parsed.owner, repo: parsed.repo, ref: parsed.ref, path: skill.githubPath || undefined }
285
+ : { type: 'registry', namespace: skill.namespace };
196
286
 
197
- // For each non-universal agent, symlink from their path → canonical
198
- for (const agent of nonUniversalAgents) {
199
- const agentDir = agent.path(installName, { cwd, scope });
287
+ if (options.dryRun) {
288
+ out.dryRun({
289
+ skill: installName,
290
+ scope,
291
+ source: sourcePayload,
292
+ agents: destPlan.map(({ agent, destDir }) => ({
293
+ slug: agent.slug,
294
+ name: agent.name,
295
+ path: destDir,
296
+ })),
297
+ });
298
+ return;
299
+ }
200
300
 
201
- if (agentDir === canonicalDir) {
202
- symlinkResults.canonical.push(agent);
203
- continue;
301
+ // --force / dest-exists guard. Runs before any network I/O so we don't
302
+ // download bytes only to refuse to write them.
303
+ for (const { destDir } of destPlan) {
304
+ if (existsSync(destDir) && isNonEmptyDir(destDir)) {
305
+ if (!options.force) {
306
+ throw new FsError(t('add.error.dest_exists', { path: destDir }));
204
307
  }
308
+ out.step(t('add.step.removing_existing', { path: destDir }));
309
+ rmSync(destDir, { recursive: true, force: true });
310
+ }
311
+ }
205
312
 
206
- mkdirSync(dirname(agentDir), { recursive: true });
207
- try {
208
- const rel = relative(dirname(agentDir), canonicalDir);
209
- symlinkSync(rel, agentDir);
210
- symlinkResults.symlinked.push(agent);
211
- } catch {
212
- // Fallback: download again
213
- await downloadSkill(skill, agentDir);
214
- symlinkResults.copied.push(agent);
215
- }
313
+ const spinner = out.spinner(t('add.spinner.downloading'));
314
+ try {
315
+ for (const { destDir } of destPlan) {
316
+ await downloadSkill(skill, destDir);
216
317
  }
217
318
  } catch (err) {
218
- spinner.fail('download fejlede');
219
- error(err.message);
220
- process.exit(1);
319
+ spinner.fail(t('add.error.download_fail'));
320
+ throw err;
221
321
  }
222
- spinner.succeed('skill-filer downloadet');
322
+ spinner.succeed(t('add.spinner.downloaded'));
323
+
324
+ const agentsWithPath = agents.map(a => ({
325
+ slug: a.slug,
326
+ name: a.name,
327
+ path: a.displayPath(installName, scope),
328
+ displayPath: a.displayPath,
329
+ }));
223
330
 
224
- // --- completion summary ---
225
- printCompletionSummary({
226
- skillName: installName,
331
+ out.success({
332
+ skill: installName,
227
333
  scope,
228
- agents,
334
+ agents: agentsWithPath,
229
335
  isGithub,
230
336
  namespace: skill.namespace,
231
- symlinkResults,
337
+ source: sourcePayload,
232
338
  });
233
339
  }
@@ -1,30 +1,32 @@
1
1
  import chalk from 'chalk';
2
2
  import { fetchSkills } from '../lib/api.js';
3
- import { printBanner } from '../lib/ui.js';
3
+ import { createOutput } from '../lib/output.js';
4
+ import { detectRuntime } from '../lib/detect-runtime.js';
5
+ import { t } from '../lib/messages/index.js';
4
6
 
5
- export async function listCommand() {
6
- printBanner();
7
- let skills;
8
- try {
9
- skills = await fetchSkills();
10
- } catch (err) {
11
- console.error(chalk.red(`\n ${err.message}\n`));
12
- process.exit(1);
7
+ export async function listCommand(options = {}) {
8
+ const runtime = detectRuntime();
9
+ const out = createOutput(options, runtime);
10
+ out.banner();
11
+
12
+ const skills = await fetchSkills();
13
+
14
+ if (out.isJson) {
15
+ out.json({ skills });
16
+ return;
13
17
  }
14
18
 
15
19
  if (skills.length === 0) {
16
- console.log('\n ingen skills tilgængelige endnu.\n');
20
+ process.stdout.write(`\n ${t('list.empty')}\n\n`);
17
21
  return;
18
22
  }
19
23
 
20
- console.log(chalk.bold('\n tilgængelige skills:\n'));
21
-
24
+ process.stdout.write(chalk.bold(`\n ${t('list.header')}\n\n`));
22
25
  const maxName = Math.max(...skills.map(s => s.name.length));
23
-
24
26
  for (const skill of skills) {
25
- console.log(` ${chalk.cyan(skill.name.padEnd(maxName + 2))} ${skill.description}`);
27
+ process.stdout.write(` ${chalk.cyan(skill.name.padEnd(maxName + 2))} ${skill.description}\n`);
26
28
  }
27
29
 
28
- console.log(`\n installer: ${chalk.bold('npx agentskillsdk add <skill-name>')}`);
29
- console.log(` gennemse: ${chalk.underline('https://agentskills.dk/skills')}\n`);
30
+ process.stdout.write(`\n ${t('list.install')} ${chalk.bold('npx agentskillsdk add <skill-name>')}\n`);
31
+ process.stdout.write(` ${t('list.browse')} ${chalk.underline('https://agentskills.dk/skills')}\n\n`);
30
32
  }