@agentskill.sh/cli 1.0.8 → 2.0.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.
@@ -1,41 +1,109 @@
1
- import { installCommand } from './install.js';
2
- // Official skills bundled with the ags CLI
3
- const OFFICIAL_SKILLS = [
4
- 'agentskill-sh/learn',
5
- 'agentskill-sh/review-skill',
6
- ];
7
- export async function setupCommand(args) {
8
- const jsonFlag = args.includes('--json');
9
- const results = [];
10
- if (!jsonFlag) {
11
- console.log(`\nInstalling ${OFFICIAL_SKILLS.length} official skills...\n`);
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { apiFetch } from '../api.js';
4
+ import { showLogo, ORANGE } from '../ui.js';
5
+ import { detectInstalledAgents, getUniversalAgents, getNonUniversalAgents, getAgentDisplayName, } from '../agents.js';
6
+ import { installToAgents } from '../installer.js';
7
+ import { addToLock, saveSelectedAgents } from '../skill-lock.js';
8
+ const OFFICIAL_SKILLS = ['agentskill-sh/learn', 'agentskill-sh/review-skill'];
9
+ export async function setupCommand(_args) {
10
+ showLogo();
11
+ // Detect installed agents
12
+ const s = p.spinner();
13
+ s.start('Detecting installed agents...');
14
+ const installedAgents = await detectInstalledAgents();
15
+ if (!installedAgents.length) {
16
+ s.stop('No agents detected');
17
+ p.log.warn('No AI agents detected on this machine.');
18
+ p.log.info(`We will install to ${pc.bold('Claude Code')} by default. You can change this later.`);
19
+ installedAgents.push('claude-code');
12
20
  }
13
- // Temporarily suppress console.log from installCommand --json output
14
- const origLog = console.log;
21
+ else {
22
+ s.stop(`Found ${installedAgents.length} agent${installedAgents.length !== 1 ? 's' : ''}`);
23
+ }
24
+ // Show agent selection
25
+ const universal = getUniversalAgents().filter((a) => installedAgents.includes(a));
26
+ const nonUniversal = getNonUniversalAgents().filter((a) => installedAgents.includes(a));
27
+ // Build options
28
+ const options = [
29
+ ...universal.map((a) => ({
30
+ value: a,
31
+ label: `${getAgentDisplayName(a)} ${pc.dim('(.agents/skills)')}`,
32
+ hint: 'universal',
33
+ })),
34
+ ...nonUniversal.map((a) => ({
35
+ value: a,
36
+ label: getAgentDisplayName(a),
37
+ })),
38
+ ];
39
+ let selectedAgents;
40
+ if (options.length > 1) {
41
+ const selected = await p.multiselect({
42
+ message: 'Select agents to install skills to:',
43
+ options,
44
+ initialValues: installedAgents,
45
+ required: true,
46
+ });
47
+ if (p.isCancel(selected)) {
48
+ p.cancel('Setup cancelled.');
49
+ process.exit(0);
50
+ }
51
+ selectedAgents = selected;
52
+ }
53
+ else {
54
+ selectedAgents = installedAgents;
55
+ p.log.step(`Installing to ${pc.bold(getAgentDisplayName(installedAgents[0]))}`);
56
+ }
57
+ // Save selected agents
58
+ saveSelectedAgents(selectedAgents);
59
+ // Install official skills
60
+ p.log.step(`Installing ${OFFICIAL_SKILLS.length} official skill${OFFICIAL_SKILLS.length !== 1 ? 's' : ''}...`);
15
61
  for (const slug of OFFICIAL_SKILLS) {
62
+ const spin = p.spinner();
63
+ spin.start(`Fetching ${slug}...`);
16
64
  try {
17
- console.log = () => { }; // suppress install JSON output
18
- await installCommand([slug, '--json']);
19
- console.log = origLog;
20
- results.push({ slug, status: 'installed' });
21
- if (!jsonFlag) {
22
- console.log(` Installed: ${slug}`);
65
+ const data = await apiFetch(`/agent/skills/${encodeURIComponent(slug)}/install`);
66
+ if (!data.skillMd) {
67
+ spin.error(`${slug} has no SKILL.md`);
68
+ continue;
69
+ }
70
+ spin.stop(`Fetched ${ORANGE(data.name)}`);
71
+ const results = installToAgents(data, selectedAgents);
72
+ const successful = results.filter((r) => r.success);
73
+ if (successful.length) {
74
+ addToLock(data.slug, data.contentSha || '', selectedAgents);
75
+ p.log.success(`${ORANGE(data.name)} installed to ${successful.length} agent${successful.length !== 1 ? 's' : ''}`);
76
+ }
77
+ const failed = results.filter((r) => !r.success);
78
+ for (const r of failed) {
79
+ p.log.warn(`Failed for ${getAgentDisplayName(r.agent)}: ${r.error}`);
23
80
  }
24
81
  }
25
82
  catch (err) {
26
- console.log = origLog;
27
- const msg = err instanceof Error ? err.message : String(err);
28
- results.push({ slug, status: `failed: ${msg}` });
29
- if (!jsonFlag) {
30
- console.error(` Failed: ${slug} - ${msg}`);
31
- }
83
+ spin.error(`Failed to install ${slug}`);
84
+ p.log.error(err instanceof Error ? err.message : String(err));
32
85
  }
33
86
  }
34
- if (jsonFlag) {
35
- console.log(JSON.stringify({ skills: results }, null, 2));
36
- }
37
- else {
38
- console.log(`\nDone. ${results.filter((r) => r.status === 'installed').length}/${OFFICIAL_SKILLS.length} skills installed.`);
39
- console.log('Restart your agent or open a new session to activate.');
87
+ // Offer to install globally
88
+ console.log();
89
+ const installGlobal = await p.confirm({
90
+ message: 'Install ags globally? (npm install -g @agentskill.sh/cli)',
91
+ initialValue: false,
92
+ });
93
+ if (!p.isCancel(installGlobal) && installGlobal) {
94
+ const gs = p.spinner();
95
+ gs.start('Installing globally...');
96
+ try {
97
+ const { execSync } = await import('child_process');
98
+ execSync('npm install -g @agentskill.sh/cli', { stdio: 'pipe' });
99
+ gs.stop('Installed globally');
100
+ p.log.success('You can now use `ags` from anywhere.');
101
+ }
102
+ catch {
103
+ gs.error('Global install failed');
104
+ p.log.info('You can install manually: npm install -g @agentskill.sh/cli');
105
+ }
40
106
  }
107
+ console.log();
108
+ p.outro(pc.green('Setup complete! Restart your agent to use the new skills.'));
41
109
  }
@@ -1,133 +1,103 @@
1
- import { readdir, readFile, rm } from 'fs/promises';
2
- import { join } from 'path';
3
- import { existsSync } from 'fs';
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
4
3
  import { apiFetch } from '../api.js';
5
- import { detectSkillDir } from '../platform.js';
6
- import { installCommand } from './install.js';
7
- function parseHeader(content) {
8
- const meta = {};
9
- const lines = content.split('\n');
10
- let inHeader = false;
11
- for (const line of lines) {
12
- if (line.trim() === '# --- agentskill.sh ---') {
13
- inHeader = true;
14
- continue;
15
- }
16
- if (line.trim() === '# ---')
17
- break;
18
- if (inHeader && line.startsWith('# ')) {
19
- const match = line.match(/^# (\w+): (.+)$/);
20
- if (match)
21
- meta[match[1]] = match[2];
22
- }
23
- }
24
- return meta;
25
- }
26
- async function scanInstalled(baseDir) {
27
- const skills = [];
28
- async function scan(dir, depth) {
29
- if (depth > 2)
30
- return;
31
- const entries = await readdir(dir, { withFileTypes: true });
32
- for (const entry of entries) {
33
- if (!entry.isDirectory())
34
- continue;
35
- const entryPath = join(dir, entry.name);
36
- const skillMdPath = join(entryPath, 'SKILL.md');
37
- if (existsSync(skillMdPath)) {
38
- try {
39
- const content = await readFile(skillMdPath, 'utf-8');
40
- const meta = parseHeader(content);
41
- if (meta.slug) {
42
- skills.push({
43
- slug: meta.slug,
44
- owner: meta.owner || '',
45
- contentSha: meta.contentSha || '',
46
- dir: entryPath,
47
- });
48
- }
49
- }
50
- catch {
51
- // Skip unreadable
52
- }
53
- }
54
- else {
55
- await scan(entryPath, depth + 1);
56
- }
57
- }
58
- }
59
- await scan(baseDir, 0);
60
- return skills;
61
- }
4
+ import { readLock, addToLock } from '../skill-lock.js';
5
+ import { installToAgents } from '../installer.js';
6
+ import { getAgentDisplayName } from '../agents.js';
7
+ import { ORANGE } from '../ui.js';
62
8
  export async function updateCommand(args) {
63
9
  const jsonFlag = args.includes('--json');
64
- const baseDir = detectSkillDir();
65
- if (!existsSync(baseDir)) {
66
- if (jsonFlag) {
67
- console.log(JSON.stringify({ updated: [], upToDate: 0 }));
68
- }
69
- else {
70
- console.log('No skills installed.');
71
- }
72
- return;
73
- }
74
- const installed = await scanInstalled(baseDir);
10
+ const lock = readLock();
11
+ const installed = Object.values(lock.skills).filter((sk) => sk.slug && sk.contentSha);
75
12
  if (!installed.length) {
76
13
  if (jsonFlag) {
77
14
  console.log(JSON.stringify({ updated: [], upToDate: 0 }));
78
15
  }
79
16
  else {
80
- console.log('No skills installed.');
17
+ p.log.warn('No skills installed.');
81
18
  }
82
19
  return;
83
20
  }
84
- // Batch version check
85
- const slugs = installed.map((s) => s.slug).join(',');
86
- const remote = await apiFetch(`/agent/skills/version?slugs=${encodeURIComponent(slugs)}`);
21
+ const s = p.spinner();
22
+ s.start('Checking for updates...');
23
+ let remote;
24
+ try {
25
+ const slugs = installed.map((sk) => sk.slug).join(',');
26
+ remote = await apiFetch(`/agent/skills/version?slugs=${encodeURIComponent(slugs)}`);
27
+ }
28
+ catch (err) {
29
+ s.error('Failed to check versions');
30
+ throw err;
31
+ }
87
32
  const remoteMap = new Map(remote.map((r) => [r.slug, r.contentSha]));
88
- const outdated = installed.filter((s) => remoteMap.has(s.slug) && remoteMap.get(s.slug) !== s.contentSha);
33
+ const outdated = installed.filter((sk) => remoteMap.has(sk.slug) && remoteMap.get(sk.slug) !== sk.contentSha);
89
34
  if (!outdated.length) {
35
+ s.stop(`All ${installed.length} skill${installed.length !== 1 ? 's' : ''} up to date`);
90
36
  if (jsonFlag) {
91
37
  console.log(JSON.stringify({ updated: [], upToDate: installed.length }));
92
38
  }
93
- else {
94
- console.log(`All ${installed.length} skill(s) are up to date.`);
95
- }
96
39
  return;
97
40
  }
98
- if (!jsonFlag) {
99
- console.log(`\n${outdated.length} update(s) available:\n`);
100
- for (const s of outdated) {
101
- console.log(` - ${s.slug}`);
102
- }
103
- console.log('');
104
- }
105
- const updated = [];
106
- for (const s of outdated) {
107
- try {
108
- // Remove old version
109
- await rm(s.dir, { recursive: true, force: true });
110
- // Re-install via the install command
111
- await installCommand([s.slug, '--json']);
112
- updated.push(s.slug);
113
- if (!jsonFlag) {
114
- console.log(` Updated: ${s.slug}`);
41
+ s.stop(`${outdated.length} update${outdated.length !== 1 ? 's' : ''} available`);
42
+ if (jsonFlag) {
43
+ // In JSON mode, proceed without confirmation
44
+ const updated = [];
45
+ for (const sk of outdated) {
46
+ try {
47
+ const data = await apiFetch(`/agent/skills/${encodeURIComponent(sk.slug)}/install`);
48
+ const results = installToAgents(data, sk.agents);
49
+ const successful = results.filter((r) => r.success);
50
+ if (successful.length) {
51
+ addToLock(data.slug, data.contentSha || '', sk.agents);
52
+ updated.push(sk.slug);
53
+ }
115
54
  }
116
- }
117
- catch (err) {
118
- const msg = err instanceof Error ? err.message : String(err);
119
- if (!jsonFlag) {
120
- console.error(` Failed to update ${s.slug}: ${msg}`);
55
+ catch {
56
+ // Skip failed
121
57
  }
122
58
  }
123
- }
124
- if (jsonFlag) {
125
59
  console.log(JSON.stringify({
126
60
  updated,
127
61
  upToDate: installed.length - outdated.length,
128
62
  }, null, 2));
63
+ return;
129
64
  }
130
- else {
131
- console.log(`\nDone. ${updated.length} updated, ${installed.length - outdated.length} already current.`);
65
+ // Show what will be updated
66
+ for (const sk of outdated) {
67
+ const agentNames = sk.agents.map((a) => getAgentDisplayName(a)).join(', ');
68
+ p.log.info(`${ORANGE(sk.slug)} ${pc.dim(`(${agentNames})`)}`);
69
+ }
70
+ const proceed = await p.confirm({
71
+ message: `Update ${outdated.length} skill${outdated.length !== 1 ? 's' : ''}?`,
72
+ });
73
+ if (p.isCancel(proceed) || !proceed) {
74
+ p.cancel('Update cancelled.');
75
+ return;
76
+ }
77
+ // Update each skill
78
+ const updated = [];
79
+ for (const sk of outdated) {
80
+ const spin = p.spinner();
81
+ spin.start(`Updating ${sk.slug}...`);
82
+ try {
83
+ const data = await apiFetch(`/agent/skills/${encodeURIComponent(sk.slug)}/install`);
84
+ // Reinstall to the same agents
85
+ const results = installToAgents(data, sk.agents);
86
+ const successful = results.filter((r) => r.success);
87
+ if (successful.length) {
88
+ addToLock(data.slug, data.contentSha || '', sk.agents);
89
+ updated.push(sk.slug);
90
+ spin.stop(`Updated ${ORANGE(sk.slug)}`);
91
+ }
92
+ else {
93
+ spin.error(`Failed to update ${sk.slug}`);
94
+ }
95
+ }
96
+ catch (err) {
97
+ spin.error(`Failed to update ${sk.slug}`);
98
+ p.log.error(err instanceof Error ? err.message : String(err));
99
+ }
132
100
  }
101
+ console.log();
102
+ p.log.success(`${updated.length} updated, ${installed.length - outdated.length} already current.`);
133
103
  }
package/dist/index.js CHANGED
@@ -1,98 +1,130 @@
1
1
  #!/usr/bin/env node
2
- import { searchCommand } from './commands/search.js';
3
- import { installCommand } from './commands/install.js';
4
- import { listCommand } from './commands/list.js';
5
- import { removeCommand } from './commands/remove.js';
6
- import { feedbackCommand } from './commands/feedback.js';
7
- import { updateCommand } from './commands/update.js';
8
- import { setupCommand } from './commands/setup.js';
9
- const VERSION = '1.0.8';
10
- const HELP = `ags v${VERSION} — search, install, and manage AI agent skills
11
-
12
- Usage:
13
- ags search <query> [--json] [--limit N] [--platform NAME]
14
- ags install <slug> [--json] [--platform NAME]
15
- ags setup # Install all official skills
16
- ags list [--json]
17
- ags remove <slug>
18
- ags feedback <slug> <1-5> [comment]
19
- ags update
20
- ags --version
21
- ags --help
22
-
23
- Commands:
24
- search Search for skills on agentskill.sh
25
- install Install a skill to your project
26
- setup Install all official agentskill.sh skills (/learn, /review-skill)
27
- list Show installed skills
28
- remove Uninstall a skill
29
- feedback Rate a skill (1-5) with optional comment
30
- update Check for and apply skill updates
31
-
32
- Examples:
33
- ags setup
34
- ags search react
35
- ags install seo-optimizer
36
- ags install @anthropics/react-best-practices
37
- ags list --json
38
- ags remove seo-optimizer
39
- ags feedback seo-optimizer 5 "Worked perfectly"
40
- ags update
41
-
42
- More info: https://agentskill.sh/install
43
- `;
2
+ import { readFileSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import pc from 'picocolors';
6
+ import { showLogo, ORANGE, DIM } from './ui.js';
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
9
+ const VERSION = pkg.version;
10
+ const CMD = 'npx @agentskill.sh/cli';
11
+ function printHelp() {
12
+ showLogo();
13
+ console.log(` ${DIM('The package manager for AI agent skills')} ${DIM('v' + VERSION)}`);
14
+ console.log();
15
+ console.log(` ${pc.bold('Usage:')}`);
16
+ console.log(` ${DIM('$')} ${CMD} ${ORANGE('<command>')} ${DIM('[options]')}`);
17
+ console.log();
18
+ console.log(` ${pc.bold('Commands:')}`);
19
+ console.log(` ${ORANGE('search')} ${DIM('s')} Search for skills on agentskill.sh`);
20
+ console.log(` ${ORANGE('install')} ${DIM('i')} Install a skill to your project`);
21
+ console.log(` ${ORANGE('setup')} Install all official agentskill.sh skills`);
22
+ console.log(` ${ORANGE('find')} ${DIM('f')} Browse and discover skills interactively`);
23
+ console.log(` ${ORANGE('init')} Scaffold a new SKILL.md`);
24
+ console.log(` ${ORANGE('list')} ${DIM('ls')} Show installed skills`);
25
+ console.log(` ${ORANGE('remove')} ${DIM('rm')} Uninstall a skill`);
26
+ console.log(` ${ORANGE('feedback')} ${DIM('rate')} Rate a skill (1-5) with optional comment`);
27
+ console.log(` ${ORANGE('update')} Update installed skills to latest versions`);
28
+ console.log();
29
+ console.log(` ${pc.bold('Examples:')}`);
30
+ console.log(` ${DIM('$')} ${CMD} setup`);
31
+ console.log(` ${DIM('$')} ${CMD} search react`);
32
+ console.log(` ${DIM('$')} ${CMD} install seo-optimizer`);
33
+ console.log(` ${DIM('$')} ${CMD} install @anthropics/react-best-practices`);
34
+ console.log(` ${DIM('$')} ${CMD} list`);
35
+ console.log(` ${DIM('$')} ${CMD} feedback seo-optimizer 5 "Worked perfectly"`);
36
+ console.log();
37
+ console.log(` ${DIM('More info:')} ${pc.underline('https://agentskill.sh/install')}`);
38
+ console.log();
39
+ }
44
40
  async function main() {
45
41
  const args = process.argv.slice(2);
46
- if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
47
- console.log(HELP);
42
+ // No args: show full banner with commands
43
+ if (args.length === 0) {
44
+ printHelp();
45
+ return;
46
+ }
47
+ // Flags that bypass command routing
48
+ if (args.includes('--help') || args.includes('-h')) {
49
+ printHelp();
48
50
  return;
49
51
  }
50
52
  if (args.includes('--version') || args.includes('-v')) {
51
53
  console.log(VERSION);
52
54
  return;
53
55
  }
56
+ // Show logo on interactive commands (skip for --json)
57
+ if (!args.includes('--json')) {
58
+ showLogo();
59
+ }
54
60
  const command = args[0];
55
61
  const commandArgs = args.slice(1);
56
62
  try {
57
63
  switch (command) {
58
64
  case 'search':
59
- case 's':
65
+ case 's': {
66
+ const { searchCommand } = await import('./commands/search.js');
60
67
  await searchCommand(commandArgs);
61
68
  break;
69
+ }
62
70
  case 'install':
63
- case 'i':
71
+ case 'i': {
72
+ const { installCommand } = await import('./commands/install.js');
64
73
  await installCommand(commandArgs);
65
74
  break;
75
+ }
76
+ case 'setup': {
77
+ const { setupCommand } = await import('./commands/setup.js');
78
+ await setupCommand(commandArgs);
79
+ break;
80
+ }
81
+ case 'init': {
82
+ const { initCommand } = await import('./commands/init.js');
83
+ await initCommand(commandArgs);
84
+ break;
85
+ }
86
+ case 'find':
87
+ case 'f': {
88
+ const { findCommand } = await import('./commands/find.js');
89
+ await findCommand(commandArgs);
90
+ break;
91
+ }
66
92
  case 'list':
67
- case 'ls':
93
+ case 'ls': {
94
+ const { listCommand } = await import('./commands/list.js');
68
95
  await listCommand(commandArgs);
69
96
  break;
97
+ }
70
98
  case 'remove':
71
- case 'rm':
72
- case 'uninstall':
99
+ case 'rm': {
100
+ const { removeCommand } = await import('./commands/remove.js');
73
101
  await removeCommand(commandArgs);
74
102
  break;
103
+ }
75
104
  case 'feedback':
76
- case 'rate':
105
+ case 'rate': {
106
+ const { feedbackCommand } = await import('./commands/feedback.js');
77
107
  await feedbackCommand(commandArgs);
78
108
  break;
79
- case 'update':
80
- case 'upgrade':
109
+ }
110
+ case 'update': {
111
+ const { updateCommand } = await import('./commands/update.js');
81
112
  await updateCommand(commandArgs);
82
113
  break;
83
- case 'setup':
84
- case 'init':
85
- await setupCommand(commandArgs);
86
- break;
114
+ }
87
115
  default:
88
- console.error(`Unknown command: ${command}`);
89
- console.error('Run "ags --help" for usage.');
116
+ console.log(` ${pc.red('Unknown command:')} ${command}`);
117
+ console.log();
118
+ console.log(` Run ${DIM(`${CMD} --help`)} for usage.`);
119
+ console.log();
90
120
  process.exit(1);
91
121
  }
92
122
  }
93
123
  catch (err) {
94
124
  const message = err instanceof Error ? err.message : String(err);
95
- console.error(`Error: ${message}`);
125
+ console.log();
126
+ console.log(` ${pc.red('Error:')} ${message}`);
127
+ console.log();
96
128
  process.exit(1);
97
129
  }
98
130
  }
@@ -0,0 +1,33 @@
1
+ import type { AgentType, InstallResponse, InstallMode } from './types.js';
2
+ export interface InstallOptions {
3
+ global?: boolean;
4
+ mode?: InstallMode;
5
+ }
6
+ export interface InstallResult {
7
+ agent: string;
8
+ dir: string;
9
+ files: string[];
10
+ success: boolean;
11
+ error?: string;
12
+ }
13
+ /**
14
+ * Sanitize a skill name into a filesystem-safe slug.
15
+ * Strips @owner/ prefix, lowercases, replaces non-alphanumeric with hyphens.
16
+ */
17
+ export declare function sanitizeName(name: string): string;
18
+ /**
19
+ * Install a skill to a single agent directory.
20
+ * Returns the path where the skill was installed.
21
+ */
22
+ export declare function installSkillToAgent(skillData: InstallResponse, agentType: AgentType, options?: InstallOptions): InstallResult;
23
+ /**
24
+ * Install a skill to multiple agents.
25
+ *
26
+ * Strategy:
27
+ * 1. First agent gets the canonical (full) copy of all files.
28
+ * 2. Remaining agents get a directory symlink pointing to the canonical copy.
29
+ * 3. If symlink creation fails (permissions, cross-device, Windows), falls back to a full copy.
30
+ *
31
+ * Set options.mode = 'copy' to skip symlinks entirely.
32
+ */
33
+ export declare function installToAgents(skillData: InstallResponse, agentTypes: AgentType[], options?: InstallOptions): InstallResult[];