agentskillsdk 0.5.3 → 0.6.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/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.1",
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,333 @@
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 { parseSource } from '../lib/parse-source.js';
10
8
  import { selectPrompt, checkboxPrompt } from '../lib/prompt.js';
11
- import { printBanner, printInstallPlan, printCompletionSummary, step, error } from '../lib/ui.js';
9
+ import { createOutput } from '../lib/output.js';
10
+ import { detectRuntime } from '../lib/detect-runtime.js';
11
+ import { t, setLocale } from '../lib/messages/index.js';
12
+ import { UsageError, AmbiguousError, NoMatchError, FsError, NoAgentError, CanceledError } from '../lib/errors.js';
12
13
 
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);
14
+ function isNonEmptyDir(p) {
15
+ try {
16
+ return readdirSync(p).length > 0;
17
+ } catch {
18
+ return false;
24
19
  }
20
+ }
25
21
 
26
- // --- resolve skill ---
27
- let skill;
28
- let installName;
22
+ // When running inside an agent, hard-coding non-interactive defaults removes
23
+ // every prompt before it can be reached. The only opt-out is an explicit
24
+ // flag the caller has set (truthy `??` skip).
25
+ function forceAgentDefaults(options, runtime) {
26
+ if (!runtime.isAgent) return options;
27
+ options.yes = true;
28
+ options.quiet ??= true;
29
+ options.lang ??= 'en';
30
+ if (runtime.agentSlug && (!options.agent || options.agent.length === 0)) {
31
+ options.agent = [runtime.agentSlug];
32
+ }
33
+ return options;
34
+ }
29
35
 
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);
36
+ function expandAgentSlugs(slugs) {
37
+ if (!slugs || slugs.length === 0) return [];
38
+ const out = [];
39
+ for (const s of slugs) {
40
+ if (s === '*') {
41
+ for (const a of AGENTS) out.push(a.slug);
42
+ } else {
43
+ out.push(s);
51
44
  }
45
+ }
46
+ // Dedupe in case '*' is mixed with explicit slugs.
47
+ return [...new Set(out)];
48
+ }
52
49
 
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);
50
+ export function chooseAgents(detected, options) {
51
+ if (options.agent && options.agent.length > 0) {
52
+ const slugs = expandAgentSlugs(options.agent);
53
+ const records = [];
54
+ for (const slug of slugs) {
55
+ const a = AGENT_BY_SLUG[slug];
56
+ if (!a) throw new UsageError(t('error.unknown_agent_slug', { slug }));
57
+ records.push(a);
72
58
  }
73
-
74
- spinner.succeed(`fundet skill: ${chalk.bold(skill.name)}`);
59
+ return records;
75
60
  }
61
+ if (options.yes) {
62
+ if (detected.length === 1) return detected;
63
+ if (detected.length > 1) {
64
+ const cs = detected.map(a => a.slug).join(', ');
65
+ throw new AmbiguousError(t('error.ambiguous_agent', { candidates: cs }), {
66
+ candidates: detected.map(a => a.slug),
67
+ });
68
+ }
69
+ const cs = AGENTS.map(a => a.slug).join(', ');
70
+ throw new NoAgentError(t('error.no_agent_target', { candidates: cs }), {
71
+ candidates: AGENTS.map(a => a.slug),
72
+ });
73
+ }
74
+ return null; // signals interactive flow
75
+ }
76
76
 
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;
77
+ export function chooseScope(options) {
78
+ if (options.global) return 'global';
79
+ if (options.project) return 'project';
80
+ if (options.yes) return 'project';
81
+ return null; // signals interactive flow
82
+ }
84
83
 
84
+ async function chooseAgentsInteractive(detected, out) {
85
+ while (true) {
86
+ let agents = null;
85
87
  if (detected.length === 1) {
86
- // Auto-select the single detected agent
87
88
  agents = detected;
88
89
  } 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' },
90
+ const choice = await selectPrompt(chalk.bold(t('add.prompt.how_install')), [
91
+ { label: t('add.prompt.all_detected', { count: detected.length }), value: 'all' },
92
+ { label: t('add.prompt.choose_specific'), value: 'choose' },
93
93
  ]);
94
-
95
- if (choice === null) {
96
- // Esc from top-level → cancel
97
- process.exit(0);
98
- }
99
-
94
+ if (choice === null) throw new CanceledError(t('error.canceled'));
100
95
  if (choice === 'all') {
101
96
  agents = detected;
102
97
  } else {
103
- // Checkbox from detected agents
104
98
  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
99
+ const selected = await checkboxPrompt(chalk.bold(t('add.prompt.select_agents')), choices);
100
+ if (selected === null) continue;
107
101
  agents = selected;
108
102
  }
109
103
  } else {
110
- // No agents detected — show checkbox of all known agents
111
104
  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
- }
105
+ const selected = await checkboxPrompt(chalk.bold(t('add.prompt.select_agents')), choices);
106
+ if (selected === null) throw new CanceledError(t('error.canceled'));
117
107
  agents = selected;
118
108
  }
109
+ return agents;
110
+ }
111
+ }
119
112
 
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
- ]);
113
+ async function chooseScopeInteractive(installName, agents, detected) {
114
+ const a = agents[0];
115
+ const result = await selectPrompt(chalk.bold(t('add.prompt.scope', { name: installName })), [
116
+ { label: t('add.prompt.scope_project'), hint: t('add.prompt.scope_project_hint', { folder: a.folder }), value: 'project' },
117
+ { label: t('add.prompt.scope_global'), hint: t('add.prompt.scope_global_hint', { folder: a.globalFolder }), value: 'global' },
118
+ ]);
119
+ if (result === null) {
120
+ // Walk back to agent selection if it was a real choice.
121
+ if (detected.length === 1) throw new CanceledError(t('error.canceled'));
122
+ return null;
123
+ }
124
+ return result;
125
+ }
126
+
127
+ function announceAgentFolderCreation(agents, cwd, scope, out) {
128
+ if (scope !== 'project') return;
129
+ for (const a of agents) {
130
+ if (!existsSync(join(cwd, a.folder))) {
131
+ out.step(t('agent.folder_created', { folder: `${a.folder}/`, name: a.name }));
132
+ }
133
+ }
134
+ }
130
135
 
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;
136
+ function validateOptions(options) {
137
+ if (options.global && options.project) {
138
+ throw new UsageError(t('add.error.both_flags'));
139
+ }
140
+ if (options.agent && options.agent.length > 0) {
141
+ for (const slug of expandAgentSlugs(options.agent)) {
142
+ if (!AGENT_BY_SLUG[slug]) {
143
+ throw new UsageError(t('error.unknown_agent_slug', { slug }));
135
144
  }
136
- scope = result;
137
145
  }
146
+ }
147
+ }
138
148
 
139
- // --- confirmation screen ---
140
- const universalAgents = getUniversalAgents(agents);
141
- const nonUniversalAgents = getNonUniversalAgents(agents);
149
+ export async function addCommand(skillName, options = {}) {
150
+ const runtime = detectRuntime();
151
+ forceAgentDefaults(options, runtime);
152
+ if (options.lang) setLocale(options.lang);
142
153
 
143
- const source = isGithub
144
- ? `${skill.githubOwner}/${skill.githubRepo}`
145
- : (skill.namespace || skill.name);
154
+ validateOptions(options);
146
155
 
147
- printInstallPlan({
148
- skillName: installName,
149
- source,
150
- scope,
151
- universalAgents,
152
- symlinkAgents: nonUniversalAgents,
153
- });
156
+ const out = createOutput(options, runtime);
157
+ out.banner();
154
158
 
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
- ]);
159
+ const cwd = process.cwd();
160
+ const parsed = parseSource(skillName);
161
+ const isGithub = parsed.kind === 'github';
162
+
163
+ let skill;
164
+ let installName;
159
165
 
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;
166
+ if (isGithub) {
167
+ // `--skill` flag wins over any parsed skill/path token.
168
+ const parsedPath = parsed.path || parsed.skill || '';
169
+ const requestedPath = options.skill || parsedPath;
170
+
171
+ const hasRef = !!parsed.ref;
172
+ const hasSkill = !!requestedPath;
173
+ let msgKey;
174
+ if (hasRef && hasSkill) msgKey = 'add.source.github_with_ref_and_skill';
175
+ else if (hasRef) msgKey = 'add.source.github_with_ref';
176
+ else if (hasSkill) msgKey = 'add.source.github_with_skill';
177
+ else msgKey = 'add.source.github';
178
+ out.step(t(msgKey, { owner: parsed.owner, repo: parsed.repo, ref: parsed.ref, skill: requestedPath }));
179
+
180
+ // installName is derived from the requested skill so the on-disk folder
181
+ // matches what the user typed (`--skill ai-seo` → `.claude/skills/ai-seo/`),
182
+ // even when the actual source path inside the repo is nested
183
+ // (`skills/ai-seo/`). The real path is resolved inside downloadSkill from
184
+ // the tarball itself — no GitHub API call needed.
185
+ installName = requestedPath ? requestedPath.split('/').pop() : parsed.repo;
186
+ skill = {
187
+ name: installName,
188
+ githubOwner: parsed.owner,
189
+ githubRepo: parsed.repo,
190
+ githubPath: requestedPath || undefined,
191
+ ref: parsed.ref,
192
+ requestedPath,
193
+ };
194
+ } else {
195
+ installName = parsed.name;
196
+ const spinner = out.spinner(t('add.spinner.lookup', { name: chalk.bold(parsed.name) }));
197
+ try {
198
+ skill = await fetchSkill(parsed.name);
199
+ } catch (err) {
200
+ spinner.fail(t('add.error.registry_fail'));
201
+ throw err;
164
202
  }
165
203
 
166
- break; // selection + confirmation complete
204
+ if (!skill) {
205
+ spinner.fail(t('add.error.not_found', { name: parsed.name }));
206
+ if (!out.isJson && !out.isQuiet) {
207
+ let suggestions = [];
208
+ try {
209
+ const allSkills = await fetchSkills();
210
+ suggestions = allSkills
211
+ .filter(s => s.name.includes(parsed.name) || parsed.name.includes(s.name))
212
+ .slice(0, 3);
213
+ } catch { /* ignore */ }
214
+ if (suggestions.length > 0) {
215
+ process.stderr.write(`\n ${t('add.suggest.header')}\n`);
216
+ for (const s of suggestions) {
217
+ process.stderr.write(` - ${chalk.bold(s.name)}\n`);
218
+ }
219
+ }
220
+ process.stderr.write(`\n ${t('add.suggest.browse')} ${chalk.underline('https://agentskills.dk/skills')}\n\n`);
221
+ }
222
+ throw new NoMatchError(t('add.error.not_found', { name: parsed.name }));
223
+ }
224
+ spinner.succeed(t('add.spinner.found', { name: chalk.bold(skill.name) }));
167
225
  }
168
226
 
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
- }
227
+ const detected = detectAgents(cwd);
175
228
 
176
- // --- download to canonical .agents/skills/ then symlink ---
177
- const spinner = ora({ text: 'downloader skill-filer...', indent: 2 }).start();
229
+ let agents = chooseAgents(detected, options);
230
+ let scope = chooseScope(options);
178
231
 
179
- const symlinkResults = { canonical: [], symlinked: [], copied: [] };
232
+ // Interactive fallback when --yes / --agent aren't supplied.
233
+ if (agents === null || scope === null) {
234
+ while (true) {
235
+ if (agents === null) agents = await chooseAgentsInteractive(detected, out);
236
+ if (scope === null) {
237
+ scope = await chooseScopeInteractive(installName, agents, detected);
238
+ if (scope === null) { agents = null; continue; }
239
+ }
240
+ break;
241
+ }
242
+ }
180
243
 
181
- try {
182
- // Canonical location: always .agents/skills/<name>/
183
- const base = scope === 'global' ? homedir() : cwd;
184
- const canonicalDir = join(base, '.agents', 'skills', installName);
244
+ announceAgentFolderCreation(agents, cwd, scope, out);
185
245
 
186
- // Download once to canonical
187
- await downloadSkill(skill, canonicalDir);
246
+ if (agents.length === 1) {
247
+ out.step(t('add.step.agent_single', { name: chalk.bold(agents[0].name) }));
248
+ } else {
249
+ out.step(t('add.step.agent_multi', { names: chalk.bold(agents.map(a => a.name).join(', ')) }));
250
+ }
188
251
 
189
- // Track which agents got the canonical copy (universal agents using .agents/)
190
- const universalAgents = getUniversalAgents(agents);
191
- const nonUniversalAgents = getNonUniversalAgents(agents);
252
+ // Compute destination dirs once used by both dry-run preview and the
253
+ // pre-download dest-exists check.
254
+ const destPlan = agents.map(agent => ({
255
+ agent,
256
+ destDir: agent.path(installName, { cwd, scope }),
257
+ }));
192
258
 
193
- for (const a of universalAgents) {
194
- symlinkResults.canonical.push(a);
195
- }
259
+ const sourcePayload = isGithub
260
+ ? { type: 'github', owner: parsed.owner, repo: parsed.repo, ref: parsed.ref, path: skill.githubPath || undefined }
261
+ : { type: 'registry', namespace: skill.namespace };
196
262
 
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 });
263
+ if (options.dryRun) {
264
+ out.dryRun({
265
+ skill: installName,
266
+ scope,
267
+ source: sourcePayload,
268
+ agents: destPlan.map(({ agent, destDir }) => ({
269
+ slug: agent.slug,
270
+ name: agent.name,
271
+ path: destDir,
272
+ })),
273
+ });
274
+ return;
275
+ }
200
276
 
201
- if (agentDir === canonicalDir) {
202
- symlinkResults.canonical.push(agent);
203
- continue;
277
+ // --force / dest-exists guard. Runs before any network I/O so we don't
278
+ // download bytes only to refuse to write them.
279
+ for (const { destDir } of destPlan) {
280
+ if (existsSync(destDir) && isNonEmptyDir(destDir)) {
281
+ if (!options.force) {
282
+ throw new FsError(t('add.error.dest_exists', { path: destDir }));
204
283
  }
284
+ out.step(t('add.step.removing_existing', { path: destDir }));
285
+ rmSync(destDir, { recursive: true, force: true });
286
+ }
287
+ }
205
288
 
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
- }
289
+ const spinner = out.spinner(t('add.spinner.downloading'));
290
+ let lastResolved;
291
+ try {
292
+ for (const { destDir } of destPlan) {
293
+ const result = await downloadSkill(skill, destDir, {
294
+ requestedPath: skill.requestedPath,
295
+ owner: skill.githubOwner,
296
+ });
297
+ lastResolved = result;
216
298
  }
217
299
  } catch (err) {
218
- spinner.fail('download fejlede');
219
- error(err.message);
220
- process.exit(1);
300
+ spinner.fail(t('add.error.download_fail'));
301
+ throw err;
302
+ }
303
+ spinner.succeed(t('add.spinner.downloaded'));
304
+
305
+ // If the resolver picked a different path than the user asked for, surface
306
+ // it so they understand where the files actually came from.
307
+ if (isGithub && lastResolved && skill.requestedPath && lastResolved.resolvedPath !== skill.requestedPath) {
308
+ out.step(t('add.spinner.resolved_path', {
309
+ from: skill.requestedPath,
310
+ to: lastResolved.resolvedPath,
311
+ }));
221
312
  }
222
- spinner.succeed('skill-filer downloadet');
313
+ if (isGithub && lastResolved) {
314
+ // Update the JSON `source.path` to reflect what was actually installed.
315
+ sourcePayload.path = lastResolved.resolvedPath || undefined;
316
+ }
317
+
318
+ const agentsWithPath = agents.map(a => ({
319
+ slug: a.slug,
320
+ name: a.name,
321
+ path: a.displayPath(installName, scope),
322
+ displayPath: a.displayPath,
323
+ }));
223
324
 
224
- // --- completion summary ---
225
- printCompletionSummary({
226
- skillName: installName,
325
+ out.success({
326
+ skill: installName,
227
327
  scope,
228
- agents,
328
+ agents: agentsWithPath,
229
329
  isGithub,
230
330
  namespace: skill.namespace,
231
- symlinkResults,
331
+ source: sourcePayload,
232
332
  });
233
333
  }
@@ -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
  }