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 +49 -1
- package/package.json +5 -2
- package/src/commands/add.js +273 -173
- package/src/commands/list.js +18 -16
- package/src/index.js +91 -19
- package/src/lib/api.js +14 -11
- package/src/lib/detect-agent.js +45 -44
- package/src/lib/detect-runtime.js +63 -0
- package/src/lib/download.js +165 -48
- package/src/lib/errors.js +58 -0
- package/src/lib/messages/da.js +96 -0
- package/src/lib/messages/en.js +96 -0
- package/src/lib/messages/index.js +49 -0
- package/src/lib/output.js +139 -0
- package/src/lib/parse-source.js +115 -5
- package/src/lib/prompt.js +14 -20
- package/src/lib/resolve-github-path.js +55 -0
- package/src/lib/ui.js +27 -116
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.
|
|
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
|
}
|
package/src/commands/add.js
CHANGED
|
@@ -1,233 +1,333 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
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,
|
|
5
|
+
import { detectAgents, AGENTS, AGENT_BY_SLUG } from '../lib/detect-agent.js';
|
|
8
6
|
import { downloadSkill } from '../lib/download.js';
|
|
9
|
-
import {
|
|
7
|
+
import { parseSource } from '../lib/parse-source.js';
|
|
10
8
|
import { selectPrompt, checkboxPrompt } from '../lib/prompt.js';
|
|
11
|
-
import {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
{ label:
|
|
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('
|
|
106
|
-
if (selected === null) continue
|
|
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('
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
? `${skill.githubOwner}/${skill.githubRepo}`
|
|
145
|
-
: (skill.namespace || skill.name);
|
|
154
|
+
validateOptions(options);
|
|
146
155
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
source,
|
|
150
|
-
scope,
|
|
151
|
-
universalAgents,
|
|
152
|
-
symlinkAgents: nonUniversalAgents,
|
|
153
|
-
});
|
|
156
|
+
const out = createOutput(options, runtime);
|
|
157
|
+
out.banner();
|
|
154
158
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
229
|
+
let agents = chooseAgents(detected, options);
|
|
230
|
+
let scope = chooseScope(options);
|
|
178
231
|
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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('
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
331
|
+
source: sourcePayload,
|
|
232
332
|
});
|
|
233
333
|
}
|
package/src/commands/list.js
CHANGED
|
@@ -1,30 +1,32 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { fetchSkills } from '../lib/api.js';
|
|
3
|
-
import {
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
20
|
+
process.stdout.write(`\n ${t('list.empty')}\n\n`);
|
|
17
21
|
return;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
|
|
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
|
-
|
|
27
|
+
process.stdout.write(` ${chalk.cyan(skill.name.padEnd(maxName + 2))} ${skill.description}\n`);
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
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
|
}
|