@agentskill.sh/cli 1.0.9 → 2.0.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.
@@ -0,0 +1,145 @@
1
+ import { writeFileSync, mkdirSync, symlinkSync, copyFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { agents } from './agents.js';
4
+ /**
5
+ * Sanitize a skill name into a filesystem-safe slug.
6
+ * Strips @owner/ prefix, lowercases, replaces non-alphanumeric with hyphens.
7
+ */
8
+ export function sanitizeName(name) {
9
+ return name
10
+ .replace(/^@[^/]+\//, '')
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9-]/g, '-')
13
+ .replace(/-+/g, '-')
14
+ .replace(/^-|-$/g, '');
15
+ }
16
+ /**
17
+ * Resolve the target directory for a skill given an agent and scope.
18
+ */
19
+ function resolveSkillDir(agentType, slug, global) {
20
+ const config = agents[agentType];
21
+ if (!config)
22
+ throw new Error(`Unknown agent: ${agentType}`);
23
+ const baseDir = global ? config.globalSkillsDir : join(process.cwd(), config.skillsDir);
24
+ return join(baseDir, sanitizeName(slug));
25
+ }
26
+ /**
27
+ * Write SKILL.md and supporting files to a target directory.
28
+ */
29
+ function writeSkillFiles(targetDir, skillData) {
30
+ mkdirSync(targetDir, { recursive: true });
31
+ writeFileSync(join(targetDir, 'SKILL.md'), skillData.skillMd, 'utf-8');
32
+ const written = ['SKILL.md'];
33
+ if (skillData.skillFiles?.length) {
34
+ for (const file of skillData.skillFiles) {
35
+ if (file.path && file.content) {
36
+ const filePath = join(targetDir, file.path);
37
+ mkdirSync(dirname(filePath), { recursive: true });
38
+ writeFileSync(filePath, file.content, 'utf-8');
39
+ written.push(file.path);
40
+ }
41
+ }
42
+ }
43
+ return written;
44
+ }
45
+ /**
46
+ * Recursively copy all files from source to target directory.
47
+ */
48
+ function copyDir(sourceDir, targetDir) {
49
+ mkdirSync(targetDir, { recursive: true });
50
+ for (const entry of readdirSync(sourceDir)) {
51
+ const src = join(sourceDir, entry);
52
+ const dst = join(targetDir, entry);
53
+ if (statSync(src).isDirectory()) {
54
+ copyDir(src, dst);
55
+ }
56
+ else {
57
+ copyFileSync(src, dst);
58
+ }
59
+ }
60
+ }
61
+ /**
62
+ * Install a skill to a single agent directory.
63
+ * Returns the path where the skill was installed.
64
+ */
65
+ export function installSkillToAgent(skillData, agentType, options = {}) {
66
+ const dir = resolveSkillDir(agentType, skillData.slug, options.global ?? false);
67
+ try {
68
+ const files = writeSkillFiles(dir, skillData);
69
+ return { agent: agentType, dir, files, success: true };
70
+ }
71
+ catch (err) {
72
+ return {
73
+ agent: agentType,
74
+ dir,
75
+ files: [],
76
+ success: false,
77
+ error: err instanceof Error ? err.message : String(err),
78
+ };
79
+ }
80
+ }
81
+ /**
82
+ * Install a skill to multiple agents.
83
+ *
84
+ * Strategy:
85
+ * 1. First agent gets the canonical (full) copy of all files.
86
+ * 2. Remaining agents get a directory symlink pointing to the canonical copy.
87
+ * 3. If symlink creation fails (permissions, cross-device, Windows), falls back to a full copy.
88
+ *
89
+ * Set options.mode = 'copy' to skip symlinks entirely.
90
+ */
91
+ export function installToAgents(skillData, agentTypes, options = {}) {
92
+ if (agentTypes.length === 0) {
93
+ throw new Error('At least one agent must be specified');
94
+ }
95
+ const mode = options.mode ?? 'symlink';
96
+ const results = [];
97
+ // First agent: canonical full copy
98
+ const canonical = installSkillToAgent(skillData, agentTypes[0], options);
99
+ results.push(canonical);
100
+ if (!canonical.success)
101
+ return results;
102
+ // Remaining agents: symlink or copy
103
+ for (let i = 1; i < agentTypes.length; i++) {
104
+ const agentType = agentTypes[i];
105
+ const targetDir = resolveSkillDir(agentType, skillData.slug, options.global ?? false);
106
+ if (mode === 'symlink') {
107
+ try {
108
+ mkdirSync(dirname(targetDir), { recursive: true });
109
+ symlinkSync(canonical.dir, targetDir);
110
+ results.push({
111
+ agent: agentType,
112
+ dir: targetDir,
113
+ files: canonical.files,
114
+ success: true,
115
+ });
116
+ }
117
+ catch {
118
+ // Symlink failed, fall back to full copy
119
+ try {
120
+ copyDir(canonical.dir, targetDir);
121
+ results.push({
122
+ agent: agentType,
123
+ dir: targetDir,
124
+ files: canonical.files,
125
+ success: true,
126
+ });
127
+ }
128
+ catch (err) {
129
+ results.push({
130
+ agent: agentType,
131
+ dir: targetDir,
132
+ files: [],
133
+ success: false,
134
+ error: err instanceof Error ? err.message : String(err),
135
+ });
136
+ }
137
+ }
138
+ }
139
+ else {
140
+ // Copy mode: write full copy for each agent
141
+ results.push(installSkillToAgent(skillData, agentType, options));
142
+ }
143
+ }
144
+ return results;
145
+ }
@@ -0,0 +1,19 @@
1
+ import type { SkillLockFile } from './types.js';
2
+ export declare const LOCK_VERSION = 1;
3
+ /**
4
+ * Resolve the path to .skill-lock.json.
5
+ * Uses XDG_STATE_HOME if set, otherwise falls back to ~/.agents/.skill-lock.json.
6
+ */
7
+ export declare function getLockPath(): string;
8
+ /** Read and parse the lock file. Returns an empty lock if the file is missing or corrupt. */
9
+ export declare function readLock(): SkillLockFile;
10
+ /** Write the lock file to disk (creates parent directories if needed). */
11
+ export declare function writeLock(lock: SkillLockFile): void;
12
+ /** Add or update a skill entry in the lock file. */
13
+ export declare function addToLock(slug: string, contentSha: string, agents: string[]): void;
14
+ /** Remove a skill entry from the lock file. */
15
+ export declare function removeFromLock(slug: string): void;
16
+ /** Get the last saved agent selection (for remembering user choice across runs). */
17
+ export declare function getLastSelectedAgents(): string[];
18
+ /** Save the current agent selection to the lock file. */
19
+ export declare function saveSelectedAgents(agents: string[]): void;
@@ -0,0 +1,63 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { homedir } from 'os';
4
+ export const LOCK_VERSION = 1;
5
+ /**
6
+ * Resolve the path to .skill-lock.json.
7
+ * Uses XDG_STATE_HOME if set, otherwise falls back to ~/.agents/.skill-lock.json.
8
+ */
9
+ export function getLockPath() {
10
+ const stateHome = process.env.XDG_STATE_HOME;
11
+ if (stateHome) {
12
+ return join(stateHome, 'agentskill', '.skill-lock.json');
13
+ }
14
+ return join(homedir(), '.agents', '.skill-lock.json');
15
+ }
16
+ /** Read and parse the lock file. Returns an empty lock if the file is missing or corrupt. */
17
+ export function readLock() {
18
+ const lockPath = getLockPath();
19
+ if (!existsSync(lockPath)) {
20
+ return { version: LOCK_VERSION, skills: {} };
21
+ }
22
+ try {
23
+ const raw = readFileSync(lockPath, 'utf-8');
24
+ return JSON.parse(raw);
25
+ }
26
+ catch {
27
+ return { version: LOCK_VERSION, skills: {} };
28
+ }
29
+ }
30
+ /** Write the lock file to disk (creates parent directories if needed). */
31
+ export function writeLock(lock) {
32
+ const lockPath = getLockPath();
33
+ mkdirSync(dirname(lockPath), { recursive: true });
34
+ writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8');
35
+ }
36
+ /** Add or update a skill entry in the lock file. */
37
+ export function addToLock(slug, contentSha, agents) {
38
+ const lock = readLock();
39
+ lock.skills[slug] = {
40
+ slug,
41
+ contentSha,
42
+ installedAt: new Date().toISOString(),
43
+ agents,
44
+ };
45
+ writeLock(lock);
46
+ }
47
+ /** Remove a skill entry from the lock file. */
48
+ export function removeFromLock(slug) {
49
+ const lock = readLock();
50
+ delete lock.skills[slug];
51
+ writeLock(lock);
52
+ }
53
+ /** Get the last saved agent selection (for remembering user choice across runs). */
54
+ export function getLastSelectedAgents() {
55
+ const lock = readLock();
56
+ return lock.lastSelectedAgents ?? [];
57
+ }
58
+ /** Save the current agent selection to the lock file. */
59
+ export function saveSelectedAgents(agents) {
60
+ const lock = readLock();
61
+ lock.lastSelectedAgents = agents;
62
+ writeLock(lock);
63
+ }
@@ -0,0 +1,58 @@
1
+ export interface AgentConfig {
2
+ name: string;
3
+ displayName: string;
4
+ /** Project-level skill directory (relative to cwd) */
5
+ skillsDir: string;
6
+ /** Global skill directory (absolute path) */
7
+ globalSkillsDir: string;
8
+ /** Whether to show in the universal agents list */
9
+ showInUniversalList?: boolean;
10
+ /** Detect if this agent is installed on the machine */
11
+ detectInstalled: () => Promise<boolean>;
12
+ }
13
+ export type AgentType = string;
14
+ export interface SearchResult {
15
+ slug: string;
16
+ name: string;
17
+ owner: string;
18
+ description: string;
19
+ installCount: number;
20
+ securityScore: number | null;
21
+ contentQualityScore: number | null;
22
+ }
23
+ export interface SearchResponse {
24
+ results: SearchResult[];
25
+ total: number;
26
+ hasMore: boolean;
27
+ }
28
+ export interface InstallResponse {
29
+ slug: string;
30
+ name: string;
31
+ owner: string;
32
+ description: string;
33
+ skillMd: string;
34
+ skillFiles: {
35
+ path: string;
36
+ content: string;
37
+ }[];
38
+ skillFolder: string;
39
+ installPath: string;
40
+ contentSha: string | null;
41
+ securityScore: number | null;
42
+ contentQualityScore: number | null;
43
+ score: number;
44
+ ratingCount: number;
45
+ installCount: number;
46
+ }
47
+ export interface SkillLockEntry {
48
+ slug: string;
49
+ contentSha: string;
50
+ installedAt: string;
51
+ agents: string[];
52
+ }
53
+ export interface SkillLockFile {
54
+ version: number;
55
+ skills: Record<string, SkillLockEntry>;
56
+ lastSelectedAgents?: string[];
57
+ }
58
+ export type InstallMode = 'symlink' | 'copy';
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/ui.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ export declare const ORANGE: (str: string) => string;
2
+ export declare const DIM: import("picocolors/types").Formatter;
3
+ export declare const TEXT: (str: string) => string;
4
+ export declare const BOLD: import("picocolors/types").Formatter;
5
+ export declare const GREEN: import("picocolors/types").Formatter;
6
+ export declare const LOGO: string;
7
+ export declare function showLogo(): void;
8
+ export declare function showBanner(): void;
9
+ /**
10
+ * Truncate a string to a max length, appending ellipsis if needed.
11
+ * Ignores ANSI escape codes when measuring length.
12
+ */
13
+ export declare function truncate(str: string, max: number): string;
14
+ /**
15
+ * Pad a string to a target width, ignoring ANSI escape codes.
16
+ */
17
+ export declare function padEnd(str: string, width: number): string;
package/dist/ui.js ADDED
@@ -0,0 +1,82 @@
1
+ import pc from 'picocolors';
2
+ // Brand orange: #E8613C approximated as 256-color (208 is closest)
3
+ // picocolors doesn't support 256-color directly, so we use ANSI escape
4
+ const ORANGE_CODE = '\x1b[38;5;208m';
5
+ const RESET = '\x1b[0m';
6
+ export const ORANGE = (str) => `${ORANGE_CODE}${str}${RESET}`;
7
+ export const DIM = pc.dim;
8
+ export const TEXT = (str) => str;
9
+ export const BOLD = pc.bold;
10
+ export const GREEN = pc.green;
11
+ // Gradient grays from bright to dim for the ASCII logo
12
+ const g1 = (s) => `\x1b[38;5;255m${s}${RESET}`; // brightest white
13
+ const g2 = (s) => `\x1b[38;5;250m${s}${RESET}`;
14
+ const g3 = (s) => `\x1b[38;5;245m${s}${RESET}`;
15
+ const g4 = (s) => `\x1b[38;5;240m${s}${RESET}`;
16
+ const g5 = (s) => `\x1b[38;5;236m${s}${RESET}`; // dimmest
17
+ export const LOGO = [
18
+ g1(' ▄▀▄ ▄▀▀ ▄▀▀'),
19
+ g2(' █▀█ █ █ ▀▀█') + ' ' + DIM('agentskill.sh'),
20
+ g3(' ▀ ▀ ▀▀ ▀▀▀'),
21
+ ].join('\n');
22
+ export function showLogo() {
23
+ console.log();
24
+ console.log(LOGO);
25
+ console.log();
26
+ }
27
+ export function showBanner() {
28
+ showLogo();
29
+ console.log(` ${DIM('The package manager for AI agent skills')}`);
30
+ console.log();
31
+ console.log(` ${BOLD('Commands:')}`);
32
+ console.log(` ${ORANGE('search')} ${DIM('<query>')} Search for skills`);
33
+ console.log(` ${ORANGE('install')} ${DIM('<slug>')} Install a skill`);
34
+ console.log(` ${ORANGE('setup')} Install official skills`);
35
+ console.log(` ${ORANGE('find')} Browse and discover skills`);
36
+ console.log(` ${ORANGE('list')} Show installed skills`);
37
+ console.log(` ${ORANGE('remove')} ${DIM('<slug>')} Uninstall a skill`);
38
+ console.log(` ${ORANGE('feedback')} ${DIM('<slug> <1-5>')} Rate a skill`);
39
+ console.log(` ${ORANGE('update')} Update installed skills`);
40
+ console.log();
41
+ console.log(` ${DIM('Run')} npx @agentskill.sh/cli ${DIM('<command> for more info')}`);
42
+ console.log();
43
+ }
44
+ /**
45
+ * Truncate a string to a max length, appending ellipsis if needed.
46
+ * Ignores ANSI escape codes when measuring length.
47
+ */
48
+ export function truncate(str, max) {
49
+ const stripped = stripAnsi(str);
50
+ if (stripped.length <= max)
51
+ return str;
52
+ // Walk the original string, counting only visible characters
53
+ let visible = 0;
54
+ let i = 0;
55
+ while (i < str.length && visible < max - 1) {
56
+ if (str[i] === '\x1b') {
57
+ // Skip ANSI escape sequence
58
+ const end = str.indexOf('m', i);
59
+ if (end !== -1) {
60
+ i = end + 1;
61
+ continue;
62
+ }
63
+ }
64
+ visible++;
65
+ i++;
66
+ }
67
+ return str.slice(0, i) + '\u2026';
68
+ }
69
+ /**
70
+ * Pad a string to a target width, ignoring ANSI escape codes.
71
+ */
72
+ export function padEnd(str, width) {
73
+ const visibleLength = stripAnsi(str).length;
74
+ if (visibleLength >= width)
75
+ return str;
76
+ return str + ' '.repeat(width - visibleLength);
77
+ }
78
+ /** Strip ANSI escape codes from a string */
79
+ function stripAnsi(str) {
80
+ // eslint-disable-next-line no-control-regex
81
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentskill.sh/cli",
3
- "version": "1.0.9",
3
+ "version": "2.0.1",
4
4
  "description": "Agent Skill CLI. Search, install, review, and manage AI agent skills from agentskill.sh.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,5 +40,9 @@
40
40
  "devDependencies": {
41
41
  "@types/node": "^22.0.0",
42
42
  "typescript": "^5.7.0"
43
+ },
44
+ "dependencies": {
45
+ "@clack/prompts": "^1.2.0",
46
+ "picocolors": "^1.1.1"
43
47
  }
44
48
  }