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/src/index.js CHANGED
@@ -1,30 +1,102 @@
1
- import { Command } from 'commander';
1
+ import { Command, CommanderError, Option } from 'commander';
2
2
  import { createRequire } from 'node:module';
3
3
  import { addCommand } from './commands/add.js';
4
4
  import { listCommand } from './commands/list.js';
5
+ import { EXIT, UsageError } from './lib/errors.js';
6
+ import { t } from './lib/messages/index.js';
5
7
 
6
8
  const require = createRequire(import.meta.url);
7
9
  const pkg = require('../package.json');
8
10
 
9
- const program = new Command();
11
+ function normalizeOptions(options) {
12
+ if (options.nonInteractive) options.yes = true;
13
+ if (options.all) {
14
+ options.agent = options.agent && options.agent.length ? options.agent : ['*'];
15
+ }
16
+ return options;
17
+ }
10
18
 
11
- program
12
- .name('agentskillsdk')
13
- .description('installer agent-skills fra agentskills.dk')
14
- .version(pkg.version);
19
+ const ADD_EXAMPLES = `
20
+ Examples:
21
+ $ agentskillsdk add genkit-ai/skills --skill developing-genkit-js -y
22
+ $ agentskillsdk add obra/superpowers@brainstorming -a claude-code -y --json
23
+ $ CLAUDECODE=1 npx agentskillsdk add my-skill # agent mode auto-detected
15
24
 
16
- program
17
- .command('add')
18
- .description('installer en skill i dit projekt')
19
- .argument('<skill-name>', 'navn på skill (eller owner/repo for GitHub)')
20
- .option('--skill <path>', 'sti i repo, f.eks. skills/twitter')
21
- .option('-g, --global', 'installer globalt i ~/.claude/skills/')
22
- .option('-p, --project', 'installer i projekt .claude/skills/ (standard)')
23
- .action(addCommand);
25
+ Output:
26
+ stdout JSON payload (--json), or quiet status line (--quiet)
27
+ stderr banner, spinners, errors, human-readable output
24
28
 
25
- program
26
- .command('list')
27
- .description('vis alle tilgængelige skills')
28
- .action(listCommand);
29
+ Exit codes:
30
+ 0 success
31
+ 1 generic error
32
+ 2 invalid usage
33
+ 3 ambiguous target (multiple agents detected without --agent)
34
+ 4 no matching skills
35
+ 5 network / registry unreachable
36
+ 6 filesystem error
37
+ 7 no agent detected
38
+ 130 user cancelled
29
39
 
30
- program.parse();
40
+ Environment:
41
+ AI_AGENT, CLAUDECODE, CURSOR_AGENT, CODEX_*, COPILOT_*, OPENCODE_CLIENT, ...
42
+ Any of these flip the CLI into agent mode (non-interactive, English, JSON-safe).
43
+ AGENTSKILLSDK_LANG=en|da
44
+ UI language. Same as --lang.
45
+ `;
46
+
47
+ export function buildProgram() {
48
+ const program = new Command();
49
+
50
+ program
51
+ .name('agentskillsdk')
52
+ .description(t('cli.description'))
53
+ .version(pkg.version)
54
+ .exitOverride((err) => {
55
+ if (err.exitCode === 0) throw err;
56
+ const wrapped = new UsageError(err.message);
57
+ wrapped.silent = true;
58
+ throw wrapped;
59
+ });
60
+
61
+ program
62
+ .command('add')
63
+ .description(t('cli.add.description'))
64
+ .argument('<skill-name>', t('cli.add.arg'))
65
+ .option('--skill <path>', t('cli.add.skill_option'))
66
+ .option('-g, --global', t('cli.add.global_option'))
67
+ .option('-p, --project', t('cli.add.project_option'))
68
+ .option('-y, --yes', t('cli.add.yes_option'))
69
+ .option('--non-interactive', t('cli.add.non_interactive_option'))
70
+ .option('-a, --agent <slugs...>', t('cli.add.agent_option'))
71
+ .option('--all', t('cli.add.all_option'))
72
+ .option('--json', t('cli.add.json_option'))
73
+ .option('--quiet', t('cli.add.quiet_option'))
74
+ .option('--lang <code>', t('cli.add.lang_option'))
75
+ .option('--dry-run', t('cli.add.dry_run_option'))
76
+ .option('--force', t('cli.add.force_option'))
77
+ .addHelpText('after', ADD_EXAMPLES)
78
+ .action((skillName, options) => addCommand(skillName, normalizeOptions(options)));
79
+
80
+ program
81
+ .command('list')
82
+ .description(t('cli.list.description'))
83
+ .option('--json', t('cli.add.json_option'))
84
+ .option('--quiet', t('cli.add.quiet_option'))
85
+ .option('--lang <code>', t('cli.add.lang_option'))
86
+ .action((options) => listCommand(normalizeOptions(options)));
87
+
88
+ return program;
89
+ }
90
+
91
+ export async function run(argv = process.argv) {
92
+ const program = buildProgram();
93
+ try {
94
+ await program.parseAsync(argv);
95
+ } catch (err) {
96
+ if (err instanceof CommanderError && err.exitCode === 0) {
97
+ return EXIT.OK;
98
+ }
99
+ throw err;
100
+ }
101
+ return EXIT.OK;
102
+ }
package/src/lib/api.js CHANGED
@@ -1,24 +1,27 @@
1
+ import { NetworkError } from './errors.js';
2
+ import { t } from './messages/index.js';
3
+
1
4
  const API_BASE = 'https://agentskills.dk/api/cli';
2
5
 
3
- export async function fetchSkills() {
6
+ async function fetchJson(url) {
4
7
  let res;
5
8
  try {
6
- res = await fetch(`${API_BASE}/skills`);
9
+ res = await fetch(url);
7
10
  } catch {
8
- throw new Error('Could not reach agentskills.dk. Check your internet connection.');
11
+ throw new NetworkError(t('api.error.unreachable'));
9
12
  }
10
- if (!res.ok) throw new Error(`Failed to fetch skills: ${res.status}`);
13
+ return res;
14
+ }
15
+
16
+ export async function fetchSkills() {
17
+ const res = await fetchJson(`${API_BASE}/skills`);
18
+ if (!res.ok) throw new NetworkError(t('api.error.fetch_skills', { status: res.status }));
11
19
  return res.json();
12
20
  }
13
21
 
14
22
  export async function fetchSkill(name) {
15
- let res;
16
- try {
17
- res = await fetch(`${API_BASE}/skills/${encodeURIComponent(name)}`);
18
- } catch {
19
- throw new Error('Could not reach agentskills.dk. Check your internet connection.');
20
- }
23
+ const res = await fetchJson(`${API_BASE}/skills/${encodeURIComponent(name)}`);
21
24
  if (res.status === 404) return null;
22
- if (!res.ok) throw new Error(`Failed to fetch skill: ${res.status}`);
25
+ if (!res.ok) throw new NetworkError(t('api.error.fetch_skill', { status: res.status }));
23
26
  return res.json();
24
27
  }
@@ -2,19 +2,26 @@ 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, universal } = {}) {
5
+ // `slug` is the canonical kebab-case identifier used by:
6
+ // - the `-a/--agent <slug>` CLI flag
7
+ // - env-var → agent mapping in detect-runtime.js
8
+ // - JSON output payloads
9
+ // Slugs are frozen once shipped. Renames go through `legacySlugAliases` in
10
+ // the resolver, not by editing this list.
11
+ function makeAgent(name, folder, slug, { globalFolder, detectFolder, path } = {}) {
6
12
  const gFolder = globalFolder || folder;
7
13
  const dFolder = detectFolder || folder;
14
+ const defaultPath = (skill, { cwd, scope }) => {
15
+ if (scope === 'global') return join(homedir(), gFolder, 'skills', skill);
16
+ return join(cwd, folder, 'skills', skill);
17
+ };
8
18
  return {
9
19
  name,
20
+ slug,
10
21
  folder,
11
22
  globalFolder: gFolder,
12
23
  detectFolder: dFolder,
13
- universal: !!universal,
14
- path: (skill, { cwd, scope }) => {
15
- if (scope === 'global') return join(homedir(), gFolder, 'skills', skill);
16
- return join(cwd, folder, 'skills', skill);
17
- },
24
+ path: path || defaultPath,
18
25
  displayPath: (skill, scope) => {
19
26
  if (scope === 'global') return `~/${gFolder}/skills/${skill}/`;
20
27
  return `${folder}/skills/${skill}/`;
@@ -22,47 +29,41 @@ function makeAgent(name, folder, { globalFolder, detectFolder, universal } = {})
22
29
  };
23
30
  }
24
31
 
32
+ // GitHub Copilot note: `folder` is `.github` and the default `path()` then
33
+ // resolves to `.github/skills/<skill>/`, which is the convention the
34
+ // community has settled on. `detectFolder` of `.github/skills` matches that
35
+ // install location for auto-detect.
25
36
  export const AGENTS = [
26
- makeAgent('Amp', '.agents', { detectFolder: '.config/amp', universal: true }),
27
- makeAgent('Cline', '.cline'),
28
- makeAgent('Claude Code', '.claude'),
29
- makeAgent('CodeBuddy', '.codebuddy'),
30
- makeAgent('Codex CLI', '.agents', { globalFolder: '.codex', detectFolder: '.codex', universal: true }),
31
- makeAgent('Command Code', '.commandcode'),
32
- makeAgent('Continue', '.continue'),
33
- makeAgent('Crush', '.crush'),
34
- makeAgent('Cursor', '.agents', { globalFolder: '.cursor', detectFolder: '.cursor', universal: true }),
35
- makeAgent('Droid', '.factory'),
36
- makeAgent('Gemini CLI', '.agents', { detectFolder: '.gemini', universal: true }),
37
- makeAgent('GitHub Copilot', '.agents', { detectFolder: '.github/skills', globalFolder: '.github', universal: true }),
38
- makeAgent('Goose', '.goose'),
39
- makeAgent('Kilo Code', '.kilocode'),
40
- makeAgent('Kimi CLI', '.agents', { detectFolder: '.kimi', universal: true }),
41
- makeAgent('Kiro CLI', '.kiro'),
42
- makeAgent('MCPJam', '.mcpjam'),
43
- makeAgent('Mux', '.mux'),
44
- makeAgent('Neovate', '.neovate'),
45
- makeAgent('OpenClaw', '.openclaw'),
46
- makeAgent('OpenCode', '.agents', { globalFolder: '.opencode', detectFolder: '.opencode', universal: true }),
47
- makeAgent('OpenHands', '.openhands'),
48
- makeAgent('Pi', '.pi'),
49
- makeAgent('Qoder', '.qoder'),
50
- makeAgent('Qwen Code', '.qwen'),
51
- makeAgent('Replit', '.agents', { detectFolder: '.replit', universal: true }),
52
- makeAgent('Roo Code', '.roo'),
53
- makeAgent('Trae', '.trae'),
54
- makeAgent('Windsurf', '.windsurf'),
55
- makeAgent('Zencoder', '.zencoder'),
37
+ makeAgent('Cline', '.cline', 'cline'),
38
+ makeAgent('Claude Code', '.claude', 'claude-code'),
39
+ makeAgent('CodeBuddy', '.codebuddy', 'codebuddy'),
40
+ makeAgent('Codex CLI', '.agents', 'codex', { globalFolder: '.codex' }),
41
+ makeAgent('Command Code', '.commandcode', 'commandcode'),
42
+ makeAgent('Continue', '.continue', 'continue'),
43
+ makeAgent('Crush', '.crush', 'crush'),
44
+ makeAgent('Cursor', '.cursor', 'cursor'),
45
+ makeAgent('Droid', '.factory', 'droid'),
46
+ makeAgent('GitHub Copilot', '.github', 'copilot', { detectFolder: '.github/skills' }),
47
+ makeAgent('Goose', '.goose', 'goose'),
48
+ makeAgent('Kilo Code', '.kilocode', 'kilo-code'),
49
+ makeAgent('Kiro CLI', '.kiro', 'kiro'),
50
+ makeAgent('MCPJam', '.mcpjam', 'mcpjam'),
51
+ makeAgent('Mux', '.mux', 'mux'),
52
+ makeAgent('Neovate', '.neovate', 'neovate'),
53
+ makeAgent('OpenClaw', '.openclaw', 'openclaw'),
54
+ makeAgent('OpenCode', '.opencode', 'opencode'),
55
+ makeAgent('OpenHands', '.openhands', 'openhands'),
56
+ makeAgent('Pi', '.pi', 'pi'),
57
+ makeAgent('Qoder', '.qoder', 'qoder'),
58
+ makeAgent('Qwen Code', '.qwen', 'qwen'),
59
+ makeAgent('Roo Code', '.roo', 'roo'),
60
+ makeAgent('Trae', '.trae', 'trae'),
61
+ makeAgent('Windsurf', '.windsurf', 'windsurf'),
62
+ makeAgent('Zencoder', '.zencoder', 'zencoder'),
56
63
  ];
57
64
 
65
+ export const AGENT_BY_SLUG = Object.fromEntries(AGENTS.map(a => [a.slug, a]));
66
+
58
67
  export function detectAgents(cwd) {
59
68
  return AGENTS.filter(agent => existsSync(join(cwd, agent.detectFolder)));
60
69
  }
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
- }
@@ -0,0 +1,63 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { AGENT_BY_SLUG } from './detect-agent.js';
3
+
4
+ // Env-var matrix for agent detection. Order matters: the first truthy match wins.
5
+ // Mirrors @vercel/detect-agent; re-sync quarterly. `slug` is null when the
6
+ // agent is known to be running but we don't have an install target for it
7
+ // (e.g. Gemini, Replit, Devin); the caller still sees isAgent=true and the
8
+ // install will fail noisily on the missing -a flag.
9
+ const ENV_MATRIX = [
10
+ { env: 'CLAUDECODE', slug: 'claude-code' },
11
+ { env: 'CLAUDE_CODE', slug: 'claude-code' },
12
+ { env: 'CURSOR_TRACE_ID', slug: 'cursor' },
13
+ { env: 'CURSOR_AGENT', slug: 'cursor' },
14
+ { env: 'GEMINI_CLI', slug: null },
15
+ { env: 'CODEX_SANDBOX', slug: 'codex' },
16
+ { env: 'CODEX_CI', slug: 'codex' },
17
+ { env: 'CODEX_THREAD_ID', slug: 'codex' },
18
+ { env: 'ANTIGRAVITY_AGENT', slug: null },
19
+ { env: 'AUGMENT_AGENT', slug: null },
20
+ { env: 'OPENCODE_CLIENT', slug: 'opencode' },
21
+ { env: 'REPL_ID', slug: null },
22
+ { env: 'COPILOT_MODEL', slug: 'copilot' },
23
+ { env: 'COPILOT_ALLOW_ALL', slug: 'copilot' },
24
+ { env: 'COPILOT_GITHUB_TOKEN', slug: 'copilot' },
25
+ ];
26
+
27
+ const DEVIN_MARKER = '/opt/.devin';
28
+
29
+ export function detectRuntime(env = process.env) {
30
+ // AI_AGENT is the explicit "I'm an agent" signal. If its value happens to
31
+ // be a known slug, use it as the agent target. If not (e.g. Conductor sets
32
+ // it to a versioned string), still mark isAgent=true but fall through to
33
+ // the implicit env matrix so we can recover a slug from CLAUDECODE etc.
34
+ let aiAgentSlug = null;
35
+ if (env.AI_AGENT) {
36
+ aiAgentSlug = AGENT_BY_SLUG[env.AI_AGENT] ? env.AI_AGENT : null;
37
+ if (aiAgentSlug) {
38
+ return { isAgent: true, agentSlug: aiAgentSlug, source: 'AI_AGENT' };
39
+ }
40
+ }
41
+
42
+ for (const { env: name, slug } of ENV_MATRIX) {
43
+ if (env[name]) {
44
+ return {
45
+ isAgent: true,
46
+ agentSlug: slug && AGENT_BY_SLUG[slug] ? slug : null,
47
+ source: name,
48
+ };
49
+ }
50
+ }
51
+
52
+ if (env.AI_AGENT) {
53
+ // AI_AGENT was set but didn't match a known slug and no implicit env
54
+ // var picked it up. Still flag as agent — the caller has to pass -a.
55
+ return { isAgent: true, agentSlug: null, source: 'AI_AGENT' };
56
+ }
57
+
58
+ if (existsSync(DEVIN_MARKER)) {
59
+ return { isAgent: true, agentSlug: null, source: DEVIN_MARKER };
60
+ }
61
+
62
+ return { isAgent: false, agentSlug: null, source: null };
63
+ }
@@ -1,11 +1,14 @@
1
1
  import { pipeline } from 'node:stream/promises';
2
2
  import { createGunzip } from 'node:zlib';
3
3
  import { createWriteStream, mkdirSync } from 'node:fs';
4
- import { join, dirname } from 'node:path';
4
+ import { join, dirname, resolve, sep } from 'node:path';
5
5
  import tar from 'tar-stream';
6
+ import { NetworkError, FsError, NoMatchError } from './errors.js';
7
+ import { t } from './messages/index.js';
6
8
 
7
9
  export async function downloadSkill(skill, destDir) {
8
- const url = `https://api.github.com/repos/${skill.githubOwner}/${skill.githubRepo}/tarball/${skill.defaultBranch}`;
10
+ const base = `https://api.github.com/repos/${skill.githubOwner}/${skill.githubRepo}/tarball`;
11
+ const url = skill.ref ? `${base}/${skill.ref}` : base;
9
12
 
10
13
  let res;
11
14
  try {
@@ -17,18 +20,38 @@ export async function downloadSkill(skill, destDir) {
17
20
  redirect: 'follow',
18
21
  });
19
22
  } catch {
20
- throw new Error('kunne ikke downloade skill-filer. tjek din internetforbindelse.');
23
+ throw new NetworkError(t('download.error.network'));
21
24
  }
22
25
 
23
- if (!res.ok) throw new Error(`GitHub download fejlede: ${res.status}`);
26
+ if (!res.ok) throw new NetworkError(t('download.error.github', { status: res.status }));
24
27
 
25
28
  const extract = tar.extract();
26
- const skillPath = skill.githubPath;
27
- const filePromises = [];
29
+ const skillPath = skill.githubPath || '';
30
+ const destDirResolved = resolve(destDir);
31
+ let aborted = null;
32
+ let filesWritten = 0;
33
+
34
+ // Swallow late errors after we've already captured `aborted`; pipeline()
35
+ // will surface the original failure.
36
+ extract.on('error', () => {});
28
37
 
29
38
  extract.on('entry', (header, stream, next) => {
39
+ if (aborted) {
40
+ stream.on('end', next);
41
+ stream.resume();
42
+ return;
43
+ }
44
+
45
+ // Reject symlink/link entries outright.
46
+ if (header.type === 'symlink' || header.type === 'link') {
47
+ aborted = new FsError(t('download.error.unsupported_entry', { type: header.type, name: header.name }));
48
+ stream.on('end', () => next(aborted));
49
+ stream.resume();
50
+ return;
51
+ }
52
+
30
53
  // Tarball entries: "{owner}-{repo}-{sha}/skills/real-estate-crm/SKILL.md"
31
- // Strip the first path segment (the repo prefix)
54
+ // Strip the first path segment (the repo prefix).
32
55
  const parts = header.name.split('/');
33
56
  parts.shift();
34
57
  const relativePath = parts.join('/');
@@ -39,43 +62,68 @@ export async function downloadSkill(skill, destDir) {
39
62
  fileRelative = relativePath;
40
63
  }
41
64
  } else {
42
- const marker = skillPath + '/';
43
- const idx = relativePath.indexOf(marker);
44
- if (header.type === 'file' && idx !== -1) {
45
- fileRelative = relativePath.slice(idx + marker.length);
65
+ if (header.type === 'file' && relativePath.startsWith(skillPath + '/')) {
66
+ fileRelative = relativePath.slice(skillPath.length + 1);
46
67
  }
47
68
  }
48
69
 
49
- if (fileRelative) {
50
- const destPath = join(destDir, fileRelative);
70
+ if (!fileRelative) {
71
+ stream.on('end', next);
72
+ stream.resume();
73
+ return;
74
+ }
51
75
 
52
- mkdirSync(dirname(destPath), { recursive: true });
76
+ const destPath = join(destDir, fileRelative);
77
+ const destPathResolved = resolve(destPath);
53
78
 
54
- const writePromise = new Promise((resolve, reject) => {
55
- stream.pipe(createWriteStream(destPath))
56
- .on('finish', resolve)
57
- .on('error', reject);
58
- });
59
- filePromises.push(writePromise);
60
- } else {
79
+ // Zip-slip guard: destination must stay within destDir.
80
+ if (destPathResolved !== destDirResolved && !destPathResolved.startsWith(destDirResolved + sep)) {
81
+ aborted = new FsError(t('download.error.traversal', { name: header.name }));
82
+ stream.on('end', () => next(aborted));
61
83
  stream.resume();
84
+ return;
62
85
  }
63
- next();
64
- });
65
86
 
66
- await pipeline(
67
- res.body,
68
- createGunzip(),
69
- extract,
70
- );
87
+ mkdirSync(dirname(destPathResolved), { recursive: true });
71
88
 
72
- await Promise.all(filePromises);
89
+ const ws = createWriteStream(destPathResolved);
90
+ let finished = false;
91
+ ws.on('finish', () => {
92
+ finished = true;
93
+ filesWritten++;
94
+ next();
95
+ });
96
+ ws.on('error', (err) => {
97
+ if (!aborted) aborted = err instanceof FsError ? err : new FsError(err.message);
98
+ if (!finished) next();
99
+ });
100
+ stream.on('error', (err) => {
101
+ if (!aborted) aborted = err instanceof FsError ? err : new FsError(err.message);
102
+ });
103
+ stream.pipe(ws);
104
+ });
73
105
 
74
- if (filePromises.length === 0) {
75
- throw new Error(
76
- skillPath
77
- ? `ingen filer fundet for "${skillPath}" i repository.`
78
- : 'ingen filer fundet i repository.'
106
+ try {
107
+ await pipeline(
108
+ res.body,
109
+ createGunzip(),
110
+ extract,
79
111
  );
112
+ } catch (err) {
113
+ if (aborted) throw aborted;
114
+ throw err;
115
+ }
116
+
117
+ if (aborted) throw aborted;
118
+
119
+ // Defense in depth: even with upfront path resolution, refuse to report a
120
+ // successful install when zero files were written. Previously the CLI would
121
+ // print a success banner with a path that didn't exist on disk.
122
+ if (filesWritten === 0) {
123
+ throw new NoMatchError(t('download.error.no_files_matched', {
124
+ path: skillPath || '<root>',
125
+ owner: skill.githubOwner,
126
+ repo: skill.githubRepo,
127
+ }));
80
128
  }
81
129
  }
@@ -0,0 +1,58 @@
1
+ export const EXIT = {
2
+ OK: 0,
3
+ GENERIC: 1,
4
+ USAGE: 2,
5
+ AMBIGUOUS: 3,
6
+ NO_MATCH: 4,
7
+ NETWORK: 5,
8
+ FS: 6,
9
+ NO_AGENT: 7,
10
+ CANCELED: 130,
11
+ };
12
+
13
+ class CliError extends Error {
14
+ constructor(message, code) {
15
+ super(message);
16
+ this.name = this.constructor.name;
17
+ this.code = code;
18
+ }
19
+ }
20
+
21
+ export class UsageError extends CliError {
22
+ constructor(message) { super(message, EXIT.USAGE); }
23
+ }
24
+
25
+ export class AmbiguousError extends CliError {
26
+ constructor(message, { candidates } = {}) {
27
+ super(message, EXIT.AMBIGUOUS);
28
+ this.candidates = candidates;
29
+ }
30
+ }
31
+
32
+ export class NoMatchError extends CliError {
33
+ constructor(message) { super(message, EXIT.NO_MATCH); }
34
+ }
35
+
36
+ export class NetworkError extends CliError {
37
+ constructor(message) { super(message, EXIT.NETWORK); }
38
+ }
39
+
40
+ export class FsError extends CliError {
41
+ constructor(message) { super(message, EXIT.FS); }
42
+ }
43
+
44
+ export class NoAgentError extends CliError {
45
+ constructor(message, { candidates } = {}) {
46
+ super(message, EXIT.NO_AGENT);
47
+ this.candidates = candidates;
48
+ }
49
+ }
50
+
51
+ export class CanceledError extends CliError {
52
+ constructor(message) { super(message, EXIT.CANCELED); }
53
+ }
54
+
55
+ export function exitCodeFor(err) {
56
+ if (err && typeof err.code === 'number') return err.code;
57
+ return EXIT.GENERIC;
58
+ }
@@ -0,0 +1,98 @@
1
+ export const da = {
2
+ 'banner.subtitle': 'installer skills til AI-agenter',
3
+
4
+ 'add.error.both_flags': 'kan ikke bruge både --global og --project. vælg én.',
5
+ 'add.error.dest_exists': 'destinationen findes allerede: {path} (brug --force for at overskrive)',
6
+ 'add.dry_run.header': 'dry-run — ingen filer bliver skrevet',
7
+ 'add.dry_run.would_write': 'ville skrive til {path}',
8
+ 'add.dry_run.scope': 'omfang: {scope}',
9
+ 'add.dry_run.source': 'kilde: {source}',
10
+ 'add.step.removing_existing': 'fjerner eksisterende {path}...',
11
+ 'add.spinner.lookup': 'søger efter {name}...',
12
+ 'add.error.registry_fail': 'opslag i registret fejlede',
13
+ 'add.error.not_found': 'skill "{name}" ikke fundet',
14
+ 'add.suggest.header': 'mente du:',
15
+ 'add.suggest.browse': 'se alle skills:',
16
+ 'add.spinner.found': 'fundet skill: {name}',
17
+ 'add.prompt.how_install': 'hvordan vil du installere?',
18
+ 'add.prompt.all_detected': 'alle fundne agenter ({count})',
19
+ 'add.prompt.choose_specific': 'vælg specifikke agenter',
20
+ 'add.prompt.select_agents': 'vælg agenter:',
21
+ 'add.prompt.scope': 'hvor skal "{name}" installeres?',
22
+ 'add.prompt.scope_project': 'projekt',
23
+ 'add.prompt.scope_project_hint': '(lokalt {folder}/skills/)',
24
+ 'add.prompt.scope_global': 'globalt',
25
+ 'add.prompt.scope_global_hint': '(~/{folder}/skills/)',
26
+ 'add.spinner.downloading': 'downloader skill-filer...',
27
+ 'add.error.download_fail': 'download fejlede',
28
+ 'add.spinner.downloaded': 'skill-filer downloadet',
29
+ 'add.spinner.resolving_path': 'finder skill-sti i repo...',
30
+ 'add.spinner.resolved_path': 'matchede "{from}" → "{to}"',
31
+ 'add.spinner.path_ok': 'skill-sti bekræftet',
32
+ 'add.error.resolve_fail': 'kunne ikke finde skill-stien',
33
+ 'add.source.github': 'GitHub-kilde: {owner}/{repo}',
34
+ 'add.source.github_with_skill': 'GitHub-kilde: {owner}/{repo} (skill: {skill})',
35
+ 'add.source.github_with_ref': 'GitHub-kilde: {owner}/{repo}@{ref}',
36
+ 'add.source.github_with_ref_and_skill': 'GitHub-kilde: {owner}/{repo}@{ref} (skill: {skill})',
37
+ 'add.step.agent_single': 'agent: {name}',
38
+ 'add.step.agent_multi': 'agenter: {names}',
39
+
40
+ 'summary.title': 'skill installeret!',
41
+ 'summary.skill': 'skill:',
42
+ 'summary.scope': 'omfang:',
43
+ 'summary.scope_global': 'globalt (alle projekter)',
44
+ 'summary.scope_project': 'projekt (lokalt)',
45
+ 'summary.agent': 'agent:',
46
+ 'summary.agents': 'agenter:',
47
+ 'summary.path': 'sti:',
48
+ 'summary.paths': 'stier:',
49
+ 'summary.session_single': 'start en ny {agent}-session for at bruge den.',
50
+ 'summary.session_multi': 'start en ny session i en af disse agenter for at bruge den.',
51
+ 'summary.learn_more': 'læs mere:',
52
+
53
+ 'hints.select': '↑↓ naviger · enter vælg · esc tilbage',
54
+ 'hints.checkbox': '↑↓ naviger · space skift · enter bekræft · esc tilbage',
55
+
56
+ 'list.empty': 'ingen skills tilgængelige endnu.',
57
+ 'list.header': 'tilgængelige skills:',
58
+ 'list.install': 'installer:',
59
+ 'list.browse': 'gennemse:',
60
+
61
+ 'api.error.unreachable': 'kunne ikke kontakte agentskills.dk. tjek din internetforbindelse.',
62
+ 'api.error.fetch_skills': 'kunne ikke hente skills: {status}',
63
+ 'api.error.fetch_skill': 'kunne ikke hente skill: {status}',
64
+
65
+ 'download.error.network': 'kunne ikke downloade skill-filer. tjek din internetforbindelse.',
66
+ 'download.error.github': 'GitHub-download fejlede: {status}',
67
+ 'download.error.traversal': 'afviste usikker tarball-fil (sti-traversal): {name}',
68
+ 'download.error.unsupported_entry': 'afviste ikke-understøttet tarball-fil ({type}): {name}',
69
+ 'download.error.no_files_matched': 'ingen filer fundet på stien "{path}" i {owner}/{repo}',
70
+
71
+ 'resolve.error.no_skills': 'ingen SKILL.md-filer fundet i {owner}/{repo}',
72
+ 'resolve.error.multiple_skills': '{owner}/{repo} indeholder flere skills — angiv --skill <navn>:\n{candidates}',
73
+ 'resolve.error.skill_not_in_repo': 'skill "{skill}" findes ikke i {owner}/{repo}. tilgængelige:\n{candidates}',
74
+
75
+ 'cli.description': 'installer agent-skills fra agentskills.dk',
76
+ 'cli.add.description': 'installer en skill i dit projekt',
77
+ 'cli.add.arg': 'navn på skill (eller owner/repo for GitHub)',
78
+ 'cli.add.skill_option': 'sti i repo, f.eks. skills/twitter',
79
+ 'cli.add.global_option': 'installer globalt i ~/.claude/skills/',
80
+ 'cli.add.project_option': 'installer i projekt .claude/skills/ (standard)',
81
+ 'cli.add.yes_option': 'spring prompts over (påkrævet i agent-kontekster)',
82
+ 'cli.add.non_interactive_option': 'alias for --yes',
83
+ 'cli.add.agent_option': "agent-slug(s), f.eks. -a claude-code -a cursor (brug '*' for alle)",
84
+ 'cli.add.all_option': "alle kendte agenter (alias for -a '*')",
85
+ 'cli.add.json_option': 'maskinlæsbar JSON på stdout',
86
+ 'cli.add.quiet_option': 'undertryk banner og spinners',
87
+ 'cli.add.lang_option': 'UI-sprog (en|da)',
88
+ 'cli.add.dry_run_option': 'vis planlagte skrivninger uden at røre disken',
89
+ 'cli.add.force_option': 'overskriv eksisterende skill-mappe',
90
+ 'cli.list.description': 'vis alle tilgængelige skills',
91
+
92
+ 'error.no_tty_no_yes': 'ikke-interaktiv kontekst: brug --yes (-y) og påkrævede flag',
93
+ 'error.unknown_agent_slug': 'ukendt agent-slug: {slug}',
94
+ 'error.ambiguous_agent': 'flere agenter opdaget — brug --agent for at vælge ({candidates})',
95
+ 'error.no_agent_target': 'ingen agent-target — brug --agent eller kør i et projekt med en af: {candidates}',
96
+ 'error.canceled': 'annulleret',
97
+ 'agent.folder_created': 'oprettede {folder} til {name}',
98
+ };