@emmraan/ai-skills 0.0.1 → 0.0.3
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/README.md +24 -24
- package/bin/skills.js +3 -3
- package/dist/index.js +21 -21
- package/package.json +32 -34
- package/src/bin/skills.ts +8 -8
- package/src/commands/__tests__/install.test.ts +49 -49
- package/src/commands/generate-local.ts +38 -38
- package/src/commands/install.ts +31 -31
- package/src/commands/list.ts +41 -41
- package/src/commands/remove.ts +22 -22
- package/src/commands/update.ts +69 -69
- package/src/core/__tests__/downloader.test.ts +66 -66
- package/src/core/__tests__/lockfile.test.ts +90 -90
- package/src/core/config.ts +29 -29
- package/src/core/downloader.ts +27 -27
- package/src/core/installer.ts +101 -101
- package/src/core/lockfile.ts +70 -70
- package/src/index.ts +80 -80
- package/src/utils/__tests__/hash.test.ts +22 -22
- package/src/utils/hash.ts +5 -5
- package/src/utils/logger.ts +21 -21
- package/src/utils/retry.ts +20 -20
- package/tsconfig.json +11 -11
- package/vitest.config.ts +13 -13
package/src/core/installer.ts
CHANGED
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
import { writeFile, mkdir, readFile, rm } from 'fs/promises';
|
|
2
|
-
import { dirname } from 'path';
|
|
3
|
-
import { getSkillInstallPaths, getSkillMetadataPaths } from './config.js';
|
|
4
|
-
import { sha256 } from '../utils/hash.js';
|
|
5
|
-
import { info, warn } from '../utils/logger.js';
|
|
6
|
-
|
|
7
|
-
export async function installSkill(skillName: string, skillContent: string): Promise<string[]> {
|
|
8
|
-
const installPaths = getSkillInstallPaths(skillName);
|
|
9
|
-
const metadataPaths = getSkillMetadataPaths(skillName);
|
|
10
|
-
const installedPaths: string[] = [];
|
|
11
|
-
const installedMetadataPaths: string[] = [];
|
|
12
|
-
const hash = sha256(skillContent);
|
|
13
|
-
const installedAt = new Date().toISOString();
|
|
14
|
-
|
|
15
|
-
for (let i = 0; i < installPaths.length; i++) {
|
|
16
|
-
const skillPath = installPaths[i];
|
|
17
|
-
const metadataPath = metadataPaths[i];
|
|
18
|
-
try {
|
|
19
|
-
const dir = dirname(skillPath);
|
|
20
|
-
await mkdir(dir, { recursive: true });
|
|
21
|
-
await writeFile(skillPath, skillContent, 'utf-8');
|
|
22
|
-
await writeFile(
|
|
23
|
-
metadataPath,
|
|
24
|
-
JSON.stringify(
|
|
25
|
-
{
|
|
26
|
-
skill: skillName,
|
|
27
|
-
hash,
|
|
28
|
-
installedAt,
|
|
29
|
-
file: 'SKILLS.md',
|
|
30
|
-
},
|
|
31
|
-
null,
|
|
32
|
-
2
|
|
33
|
-
),
|
|
34
|
-
'utf-8'
|
|
35
|
-
);
|
|
36
|
-
installedPaths.push(skillPath);
|
|
37
|
-
installedMetadataPaths.push(metadataPath);
|
|
38
|
-
info(`Installed to ${skillPath}`);
|
|
39
|
-
} catch (err) {
|
|
40
|
-
warn(
|
|
41
|
-
`Failed to install to ${skillPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (installedMetadataPaths.length > 0) {
|
|
47
|
-
info(`Wrote metadata to ${installedMetadataPaths.length} location(s)`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return installedPaths;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function removeSkill(skillName: string): Promise<string[]> {
|
|
54
|
-
const installPaths = getSkillInstallPaths(skillName);
|
|
55
|
-
const metadataPaths = getSkillMetadataPaths(skillName);
|
|
56
|
-
const removedPaths: string[] = [];
|
|
57
|
-
|
|
58
|
-
for (let i = 0; i < installPaths.length; i++) {
|
|
59
|
-
const skillPath = installPaths[i];
|
|
60
|
-
const metadataPath = metadataPaths[i];
|
|
61
|
-
try {
|
|
62
|
-
await rm(skillPath, { force: true });
|
|
63
|
-
await rm(metadataPath, { force: true });
|
|
64
|
-
removedPaths.push(skillPath);
|
|
65
|
-
info(`Removed ${skillPath}`);
|
|
66
|
-
} catch (err) {
|
|
67
|
-
warn(`Failed to remove ${skillPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return removedPaths;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export async function skillExists(skillName: string): Promise<boolean> {
|
|
75
|
-
const installPaths = getSkillInstallPaths(skillName);
|
|
76
|
-
|
|
77
|
-
for (const skillPath of installPaths) {
|
|
78
|
-
try {
|
|
79
|
-
await readFile(skillPath, 'utf-8');
|
|
80
|
-
return true;
|
|
81
|
-
} catch {
|
|
82
|
-
// Continue checking other paths
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export async function readSkill(skillName: string): Promise<string | null> {
|
|
90
|
-
const installPaths = getSkillInstallPaths(skillName);
|
|
91
|
-
|
|
92
|
-
for (const skillPath of installPaths) {
|
|
93
|
-
try {
|
|
94
|
-
return await readFile(skillPath, 'utf-8');
|
|
95
|
-
} catch {
|
|
96
|
-
// Continue checking other paths
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
1
|
+
import { writeFile, mkdir, readFile, rm } from 'fs/promises';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
import { getSkillInstallPaths, getSkillMetadataPaths } from './config.js';
|
|
4
|
+
import { sha256 } from '../utils/hash.js';
|
|
5
|
+
import { info, warn } from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
export async function installSkill(skillName: string, skillContent: string): Promise<string[]> {
|
|
8
|
+
const installPaths = getSkillInstallPaths(skillName);
|
|
9
|
+
const metadataPaths = getSkillMetadataPaths(skillName);
|
|
10
|
+
const installedPaths: string[] = [];
|
|
11
|
+
const installedMetadataPaths: string[] = [];
|
|
12
|
+
const hash = sha256(skillContent);
|
|
13
|
+
const installedAt = new Date().toISOString();
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < installPaths.length; i++) {
|
|
16
|
+
const skillPath = installPaths[i];
|
|
17
|
+
const metadataPath = metadataPaths[i];
|
|
18
|
+
try {
|
|
19
|
+
const dir = dirname(skillPath);
|
|
20
|
+
await mkdir(dir, { recursive: true });
|
|
21
|
+
await writeFile(skillPath, skillContent, 'utf-8');
|
|
22
|
+
await writeFile(
|
|
23
|
+
metadataPath,
|
|
24
|
+
JSON.stringify(
|
|
25
|
+
{
|
|
26
|
+
skill: skillName,
|
|
27
|
+
hash,
|
|
28
|
+
installedAt,
|
|
29
|
+
file: 'SKILLS.md',
|
|
30
|
+
},
|
|
31
|
+
null,
|
|
32
|
+
2
|
|
33
|
+
),
|
|
34
|
+
'utf-8'
|
|
35
|
+
);
|
|
36
|
+
installedPaths.push(skillPath);
|
|
37
|
+
installedMetadataPaths.push(metadataPath);
|
|
38
|
+
info(`Installed to ${skillPath}`);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
warn(
|
|
41
|
+
`Failed to install to ${skillPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (installedMetadataPaths.length > 0) {
|
|
47
|
+
info(`Wrote metadata to ${installedMetadataPaths.length} location(s)`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return installedPaths;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function removeSkill(skillName: string): Promise<string[]> {
|
|
54
|
+
const installPaths = getSkillInstallPaths(skillName);
|
|
55
|
+
const metadataPaths = getSkillMetadataPaths(skillName);
|
|
56
|
+
const removedPaths: string[] = [];
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < installPaths.length; i++) {
|
|
59
|
+
const skillPath = installPaths[i];
|
|
60
|
+
const metadataPath = metadataPaths[i];
|
|
61
|
+
try {
|
|
62
|
+
await rm(skillPath, { force: true });
|
|
63
|
+
await rm(metadataPath, { force: true });
|
|
64
|
+
removedPaths.push(skillPath);
|
|
65
|
+
info(`Removed ${skillPath}`);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
warn(`Failed to remove ${skillPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return removedPaths;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function skillExists(skillName: string): Promise<boolean> {
|
|
75
|
+
const installPaths = getSkillInstallPaths(skillName);
|
|
76
|
+
|
|
77
|
+
for (const skillPath of installPaths) {
|
|
78
|
+
try {
|
|
79
|
+
await readFile(skillPath, 'utf-8');
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
// Continue checking other paths
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function readSkill(skillName: string): Promise<string | null> {
|
|
90
|
+
const installPaths = getSkillInstallPaths(skillName);
|
|
91
|
+
|
|
92
|
+
for (const skillPath of installPaths) {
|
|
93
|
+
try {
|
|
94
|
+
return await readFile(skillPath, 'utf-8');
|
|
95
|
+
} catch {
|
|
96
|
+
// Continue checking other paths
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
}
|
package/src/core/lockfile.ts
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
|
-
import { dirname } from 'path';
|
|
3
|
-
import { getLockfilePath } from './config.js';
|
|
4
|
-
|
|
5
|
-
export interface LockfileEntry {
|
|
6
|
-
name: string;
|
|
7
|
-
version: string;
|
|
8
|
-
hash: string;
|
|
9
|
-
timestamp: string;
|
|
10
|
-
installPaths: string[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface Lockfile {
|
|
14
|
-
version: 1;
|
|
15
|
-
installedSkills: Record<string, LockfileEntry>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const INITIAL_LOCKFILE: Lockfile = {
|
|
19
|
-
version: 1,
|
|
20
|
-
installedSkills: {},
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export async function readLockfile(): Promise<Lockfile> {
|
|
24
|
-
const path = getLockfilePath();
|
|
25
|
-
try {
|
|
26
|
-
const content = await readFile(path, 'utf-8');
|
|
27
|
-
return JSON.parse(content);
|
|
28
|
-
} catch {
|
|
29
|
-
return INITIAL_LOCKFILE;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export async function writeLockfile(lockfile: Lockfile): Promise<void> {
|
|
34
|
-
const path = getLockfilePath();
|
|
35
|
-
await mkdir(dirname(path), { recursive: true });
|
|
36
|
-
await writeFile(path, JSON.stringify(lockfile, null, 2), 'utf-8');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function addSkillToLockfile(
|
|
40
|
-
skillName: string,
|
|
41
|
-
version: string,
|
|
42
|
-
hash: string,
|
|
43
|
-
installPaths: string[]
|
|
44
|
-
): Promise<void> {
|
|
45
|
-
const lockfile = await readLockfile();
|
|
46
|
-
lockfile.installedSkills[skillName] = {
|
|
47
|
-
name: skillName,
|
|
48
|
-
version,
|
|
49
|
-
hash,
|
|
50
|
-
timestamp: new Date().toISOString(),
|
|
51
|
-
installPaths,
|
|
52
|
-
};
|
|
53
|
-
await writeLockfile(lockfile);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function removeSkillFromLockfile(skillName: string): Promise<void> {
|
|
57
|
-
const lockfile = await readLockfile();
|
|
58
|
-
delete lockfile.installedSkills[skillName];
|
|
59
|
-
await writeLockfile(lockfile);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function getInstalledSkills(): Promise<LockfileEntry[]> {
|
|
63
|
-
const lockfile = await readLockfile();
|
|
64
|
-
return Object.values(lockfile.installedSkills);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function getSkillEntry(skillName: string): Promise<LockfileEntry | null> {
|
|
68
|
-
const lockfile = await readLockfile();
|
|
69
|
-
return lockfile.installedSkills[skillName] || null;
|
|
70
|
-
}
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
import { getLockfilePath } from './config.js';
|
|
4
|
+
|
|
5
|
+
export interface LockfileEntry {
|
|
6
|
+
name: string;
|
|
7
|
+
version: string;
|
|
8
|
+
hash: string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
installPaths: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Lockfile {
|
|
14
|
+
version: 1;
|
|
15
|
+
installedSkills: Record<string, LockfileEntry>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const INITIAL_LOCKFILE: Lockfile = {
|
|
19
|
+
version: 1,
|
|
20
|
+
installedSkills: {},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export async function readLockfile(): Promise<Lockfile> {
|
|
24
|
+
const path = getLockfilePath();
|
|
25
|
+
try {
|
|
26
|
+
const content = await readFile(path, 'utf-8');
|
|
27
|
+
return JSON.parse(content);
|
|
28
|
+
} catch {
|
|
29
|
+
return INITIAL_LOCKFILE;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function writeLockfile(lockfile: Lockfile): Promise<void> {
|
|
34
|
+
const path = getLockfilePath();
|
|
35
|
+
await mkdir(dirname(path), { recursive: true });
|
|
36
|
+
await writeFile(path, JSON.stringify(lockfile, null, 2), 'utf-8');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function addSkillToLockfile(
|
|
40
|
+
skillName: string,
|
|
41
|
+
version: string,
|
|
42
|
+
hash: string,
|
|
43
|
+
installPaths: string[]
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
const lockfile = await readLockfile();
|
|
46
|
+
lockfile.installedSkills[skillName] = {
|
|
47
|
+
name: skillName,
|
|
48
|
+
version,
|
|
49
|
+
hash,
|
|
50
|
+
timestamp: new Date().toISOString(),
|
|
51
|
+
installPaths,
|
|
52
|
+
};
|
|
53
|
+
await writeLockfile(lockfile);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function removeSkillFromLockfile(skillName: string): Promise<void> {
|
|
57
|
+
const lockfile = await readLockfile();
|
|
58
|
+
delete lockfile.installedSkills[skillName];
|
|
59
|
+
await writeLockfile(lockfile);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function getInstalledSkills(): Promise<LockfileEntry[]> {
|
|
63
|
+
const lockfile = await readLockfile();
|
|
64
|
+
return Object.values(lockfile.installedSkills);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function getSkillEntry(skillName: string): Promise<LockfileEntry | null> {
|
|
68
|
+
const lockfile = await readLockfile();
|
|
69
|
+
return lockfile.installedSkills[skillName] || null;
|
|
70
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
import { install } from './commands/install.js';
|
|
2
|
-
import { remove } from './commands/remove.js';
|
|
3
|
-
import { list } from './commands/list.js';
|
|
4
|
-
import { update } from './commands/update.js';
|
|
5
|
-
import { generateLocal } from './commands/generate-local.js';
|
|
6
|
-
import { error, info } from './utils/logger.js';
|
|
7
|
-
|
|
8
|
-
const args = process.argv.slice(2);
|
|
9
|
-
|
|
10
|
-
export async function main(): Promise<void> {
|
|
11
|
-
if (args.length === 0) {
|
|
12
|
-
showHelp();
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const command = args[0];
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
if (command === 'install' && args.length >= 2) {
|
|
20
|
-
const skill = args[1];
|
|
21
|
-
const success = await install(skill);
|
|
22
|
-
process.exit(success ? 0 : 1);
|
|
23
|
-
} else if (command === 'remove' && args.length >= 2) {
|
|
24
|
-
const skill = args[1];
|
|
25
|
-
const success = await remove(skill);
|
|
26
|
-
process.exit(success ? 0 : 1);
|
|
27
|
-
} else if (command === 'list') {
|
|
28
|
-
const jsonFlag = args.includes('--json');
|
|
29
|
-
await list(jsonFlag);
|
|
30
|
-
process.exit(0);
|
|
31
|
-
} else if (command === 'update') {
|
|
32
|
-
const forceFlag = args.includes('--force');
|
|
33
|
-
let skillArg: string | undefined;
|
|
34
|
-
const skillIndex = args.indexOf('--skill');
|
|
35
|
-
if (skillIndex !== -1 && args.length > skillIndex + 1) {
|
|
36
|
-
skillArg = args[skillIndex + 1];
|
|
37
|
-
}
|
|
38
|
-
const success = await update({ force: forceFlag, skill: skillArg });
|
|
39
|
-
process.exit(success ? 0 : 1);
|
|
40
|
-
} else if (command === 'generate-local') {
|
|
41
|
-
const skillArg = args[1];
|
|
42
|
-
const success = await generateLocal(skillArg);
|
|
43
|
-
process.exit(success ? 0 : 1);
|
|
44
|
-
} else if (!command.startsWith('-')) {
|
|
45
|
-
// Default behavior: treat first arg as skill name to install
|
|
46
|
-
const success = await install(command);
|
|
47
|
-
process.exit(success ? 0 : 1);
|
|
48
|
-
} else {
|
|
49
|
-
showHelp();
|
|
50
|
-
}
|
|
51
|
-
} catch (err) {
|
|
52
|
-
error(`CLI error: ${err instanceof Error ? err.message : String(err)}`);
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function showHelp() {
|
|
58
|
-
info(`
|
|
59
|
-
AI Skills CLI - Install framework-agnostic SKILLS.md files
|
|
60
|
-
|
|
61
|
-
Usage:
|
|
62
|
-
ai-skills <skill> Install a skill
|
|
63
|
-
ai-skills install <skill> Install a skill
|
|
64
|
-
ai-skills list [--json] List installed and available skills
|
|
65
|
-
ai-skills remove <skill> Remove an installed skill
|
|
66
|
-
ai-skills update [--force] Update all installed skills
|
|
67
|
-
[--skill <name>] Update a specific skill
|
|
68
|
-
ai-skills generate-local [skill] Run local backend generator
|
|
69
|
-
|
|
70
|
-
Examples:
|
|
71
|
-
ai-skills react
|
|
72
|
-
ai-skills install typescript
|
|
73
|
-
ai-skills list
|
|
74
|
-
ai-skills remove python
|
|
75
|
-
ai-skills update --force
|
|
76
|
-
ai-skills update --skill react
|
|
77
|
-
ai-skills generate-local
|
|
78
|
-
ai-skills generate-local react
|
|
79
|
-
`);
|
|
80
|
-
}
|
|
1
|
+
import { install } from './commands/install.js';
|
|
2
|
+
import { remove } from './commands/remove.js';
|
|
3
|
+
import { list } from './commands/list.js';
|
|
4
|
+
import { update } from './commands/update.js';
|
|
5
|
+
import { generateLocal } from './commands/generate-local.js';
|
|
6
|
+
import { error, info } from './utils/logger.js';
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
|
|
10
|
+
export async function main(): Promise<void> {
|
|
11
|
+
if (args.length === 0) {
|
|
12
|
+
showHelp();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const command = args[0];
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
if (command === 'install' && args.length >= 2) {
|
|
20
|
+
const skill = args[1];
|
|
21
|
+
const success = await install(skill);
|
|
22
|
+
process.exit(success ? 0 : 1);
|
|
23
|
+
} else if (command === 'remove' && args.length >= 2) {
|
|
24
|
+
const skill = args[1];
|
|
25
|
+
const success = await remove(skill);
|
|
26
|
+
process.exit(success ? 0 : 1);
|
|
27
|
+
} else if (command === 'list') {
|
|
28
|
+
const jsonFlag = args.includes('--json');
|
|
29
|
+
await list(jsonFlag);
|
|
30
|
+
process.exit(0);
|
|
31
|
+
} else if (command === 'update') {
|
|
32
|
+
const forceFlag = args.includes('--force');
|
|
33
|
+
let skillArg: string | undefined;
|
|
34
|
+
const skillIndex = args.indexOf('--skill');
|
|
35
|
+
if (skillIndex !== -1 && args.length > skillIndex + 1) {
|
|
36
|
+
skillArg = args[skillIndex + 1];
|
|
37
|
+
}
|
|
38
|
+
const success = await update({ force: forceFlag, skill: skillArg });
|
|
39
|
+
process.exit(success ? 0 : 1);
|
|
40
|
+
} else if (command === 'generate-local') {
|
|
41
|
+
const skillArg = args[1];
|
|
42
|
+
const success = await generateLocal(skillArg);
|
|
43
|
+
process.exit(success ? 0 : 1);
|
|
44
|
+
} else if (!command.startsWith('-')) {
|
|
45
|
+
// Default behavior: treat first arg as skill name to install
|
|
46
|
+
const success = await install(command);
|
|
47
|
+
process.exit(success ? 0 : 1);
|
|
48
|
+
} else {
|
|
49
|
+
showHelp();
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
error(`CLI error: ${err instanceof Error ? err.message : String(err)}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function showHelp() {
|
|
58
|
+
info(`
|
|
59
|
+
AI Skills CLI - Install framework-agnostic SKILLS.md files
|
|
60
|
+
|
|
61
|
+
Usage:
|
|
62
|
+
ai-skills <skill> Install a skill
|
|
63
|
+
ai-skills install <skill> Install a skill
|
|
64
|
+
ai-skills list [--json] List installed and available skills
|
|
65
|
+
ai-skills remove <skill> Remove an installed skill
|
|
66
|
+
ai-skills update [--force] Update all installed skills
|
|
67
|
+
[--skill <name>] Update a specific skill
|
|
68
|
+
ai-skills generate-local [skill] Run local backend generator
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
ai-skills react
|
|
72
|
+
ai-skills install typescript
|
|
73
|
+
ai-skills list
|
|
74
|
+
ai-skills remove python
|
|
75
|
+
ai-skills update --force
|
|
76
|
+
ai-skills update --skill react
|
|
77
|
+
ai-skills generate-local
|
|
78
|
+
ai-skills generate-local react
|
|
79
|
+
`);
|
|
80
|
+
}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { sha256 } from '../../utils/hash';
|
|
3
|
-
|
|
4
|
-
describe('hash', () => {
|
|
5
|
-
it('should hash a string consistently', () => {
|
|
6
|
-
const content = 'test content';
|
|
7
|
-
const hash1 = sha256(content);
|
|
8
|
-
const hash2 = sha256(content);
|
|
9
|
-
expect(hash1).toBe(hash2);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('should produce different hashes for different content', () => {
|
|
13
|
-
const hash1 = sha256('content1');
|
|
14
|
-
const hash2 = sha256('content2');
|
|
15
|
-
expect(hash1).not.toBe(hash2);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should produce valid sha256 hash (64 hex characters)', () => {
|
|
19
|
-
const hash = sha256('test');
|
|
20
|
-
expect(hash).toMatch(/^[a-f0-9]{64}$/);
|
|
21
|
-
});
|
|
22
|
-
});
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { sha256 } from '../../utils/hash';
|
|
3
|
+
|
|
4
|
+
describe('hash', () => {
|
|
5
|
+
it('should hash a string consistently', () => {
|
|
6
|
+
const content = 'test content';
|
|
7
|
+
const hash1 = sha256(content);
|
|
8
|
+
const hash2 = sha256(content);
|
|
9
|
+
expect(hash1).toBe(hash2);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should produce different hashes for different content', () => {
|
|
13
|
+
const hash1 = sha256('content1');
|
|
14
|
+
const hash2 = sha256('content2');
|
|
15
|
+
expect(hash1).not.toBe(hash2);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should produce valid sha256 hash (64 hex characters)', () => {
|
|
19
|
+
const hash = sha256('test');
|
|
20
|
+
expect(hash).toMatch(/^[a-f0-9]{64}$/);
|
|
21
|
+
});
|
|
22
|
+
});
|
package/src/utils/hash.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createHash } from 'crypto';
|
|
2
|
-
|
|
3
|
-
export function sha256(content: string): string {
|
|
4
|
-
return createHash('sha256').update(content).digest('hex');
|
|
5
|
-
}
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
|
|
3
|
+
export function sha256(content: string): string {
|
|
4
|
+
return createHash('sha256').update(content).digest('hex');
|
|
5
|
+
}
|
package/src/utils/logger.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
|
|
3
|
-
export function log(message: string) {
|
|
4
|
-
console.log(message);
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function info(message: string) {
|
|
8
|
-
console.log(`ℹ ${message}`);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function success(message: string) {
|
|
12
|
-
console.log(`✓ ${message}`);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function error(message: string) {
|
|
16
|
-
console.error(`✗ ${message}`);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function warn(message: string) {
|
|
20
|
-
console.warn(`⚠ ${message}`);
|
|
21
|
-
}
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
|
|
3
|
+
export function log(message: string) {
|
|
4
|
+
console.log(message);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function info(message: string) {
|
|
8
|
+
console.log(`ℹ ${message}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function success(message: string) {
|
|
12
|
+
console.log(`✓ ${message}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function error(message: string) {
|
|
16
|
+
console.error(`✗ ${message}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function warn(message: string) {
|
|
20
|
+
console.warn(`⚠ ${message}`);
|
|
21
|
+
}
|
package/src/utils/retry.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
export async function fetchWithRetry(url: string, maxRetries: number = 3): Promise<string> {
|
|
2
|
-
let lastError: Error | null = null;
|
|
3
|
-
|
|
4
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
5
|
-
try {
|
|
6
|
-
const response = await fetch(url);
|
|
7
|
-
if (!response.ok) {
|
|
8
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
9
|
-
}
|
|
10
|
-
return await response.text();
|
|
11
|
-
} catch (err) {
|
|
12
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
13
|
-
if (i < maxRetries - 1) {
|
|
14
|
-
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 1000));
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
throw lastError || new Error('Failed to fetch after retries');
|
|
20
|
-
}
|
|
1
|
+
export async function fetchWithRetry(url: string, maxRetries: number = 3): Promise<string> {
|
|
2
|
+
let lastError: Error | null = null;
|
|
3
|
+
|
|
4
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
5
|
+
try {
|
|
6
|
+
const response = await fetch(url);
|
|
7
|
+
if (!response.ok) {
|
|
8
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
9
|
+
}
|
|
10
|
+
return await response.text();
|
|
11
|
+
} catch (err) {
|
|
12
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
13
|
+
if (i < maxRetries - 1) {
|
|
14
|
+
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 1000));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
throw lastError || new Error('Failed to fetch after retries');
|
|
20
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"outDir": "./dist",
|
|
5
|
-
"rootDir": "./src",
|
|
6
|
-
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
-
"types": ["node"]
|
|
8
|
-
},
|
|
9
|
-
"include": ["src"],
|
|
10
|
-
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
11
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"types": ["node"]
|
|
8
|
+
},
|
|
9
|
+
"include": ["src"],
|
|
10
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
11
|
+
}
|