agentskillsdk 0.5.2 → 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 +49 -1
- package/package.json +5 -2
- package/src/commands/add.js +279 -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 +82 -24
- package/src/lib/errors.js +58 -0
- package/src/lib/messages/da.js +98 -0
- package/src/lib/messages/en.js +98 -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 +141 -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.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
|
}
|
package/src/commands/add.js
CHANGED
|
@@ -1,233 +1,339 @@
|
|
|
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 { 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 {
|
|
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
|
-
|
|
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);
|
|
15
|
+
function isNonEmptyDir(p) {
|
|
16
|
+
try {
|
|
17
|
+
return readdirSync(p).length > 0;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
24
20
|
}
|
|
21
|
+
}
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
{ label:
|
|
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('
|
|
106
|
-
if (selected === null) continue
|
|
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('
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
? `${skill.githubOwner}/${skill.githubRepo}`
|
|
145
|
-
: (skill.namespace || skill.name);
|
|
155
|
+
validateOptions(options);
|
|
146
156
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
{ label: 'nej, annullér', value: 'no' },
|
|
158
|
-
]);
|
|
164
|
+
let skill;
|
|
165
|
+
let installName;
|
|
159
166
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
253
|
+
let agents = chooseAgents(detected, options);
|
|
254
|
+
let scope = chooseScope(options);
|
|
178
255
|
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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('
|
|
219
|
-
|
|
220
|
-
process.exit(1);
|
|
319
|
+
spinner.fail(t('add.error.download_fail'));
|
|
320
|
+
throw err;
|
|
221
321
|
}
|
|
222
|
-
spinner.succeed('
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
337
|
+
source: sourcePayload,
|
|
232
338
|
});
|
|
233
339
|
}
|
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
|
}
|