@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/commands/update.ts
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
import { fetchSkill, fetchRegistryIndex } from '../core/downloader.js';
|
|
2
|
-
import { installSkill } from '../core/installer.js';
|
|
3
|
-
import { addSkillToLockfile, getInstalledSkills, getSkillEntry } from '../core/lockfile.js';
|
|
4
|
-
import { sha256 } from '../utils/hash.js';
|
|
5
|
-
import { error, success, info, warn } from '../utils/logger.js';
|
|
6
|
-
|
|
7
|
-
export async function update(options: { force?: boolean; skill?: string } = {}): Promise<boolean> {
|
|
8
|
-
try {
|
|
9
|
-
const installed = await getInstalledSkills();
|
|
10
|
-
const index = await fetchRegistryIndex();
|
|
11
|
-
|
|
12
|
-
if (options.skill) {
|
|
13
|
-
// Update specific skill
|
|
14
|
-
const skillName = options.skill;
|
|
15
|
-
const entry = await getSkillEntry(skillName);
|
|
16
|
-
|
|
17
|
-
if (!entry) {
|
|
18
|
-
warn(`${skillName} is not installed`);
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const skillContent = await fetchSkill(skillName);
|
|
23
|
-
const newHash = sha256(skillContent);
|
|
24
|
-
|
|
25
|
-
if (newHash === entry.hash && !options.force) {
|
|
26
|
-
info(`${skillName} is already up-to-date`);
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
info(`Updating ${skillName}...`);
|
|
31
|
-
const installPaths = await installSkill(skillName, skillContent);
|
|
32
|
-
const version = index.skills[skillName]?.version || 'unknown';
|
|
33
|
-
await addSkillToLockfile(skillName, version, newHash, installPaths);
|
|
34
|
-
success(`Updated ${skillName}`);
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Update all installed skills
|
|
39
|
-
let updateCount = 0;
|
|
40
|
-
for (const entry of installed) {
|
|
41
|
-
try {
|
|
42
|
-
const skillContent = await fetchSkill(entry.name);
|
|
43
|
-
const newHash = sha256(skillContent);
|
|
44
|
-
|
|
45
|
-
if (newHash === entry.hash && !options.force) {
|
|
46
|
-
info(`${entry.name} is already up-to-date`);
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
info(`Updating ${entry.name}...`);
|
|
51
|
-
const installPaths = await installSkill(entry.name, skillContent);
|
|
52
|
-
const version = index.skills[entry.name]?.version || 'unknown';
|
|
53
|
-
await addSkillToLockfile(entry.name, version, newHash, installPaths);
|
|
54
|
-
success(`Updated ${entry.name}`);
|
|
55
|
-
updateCount++;
|
|
56
|
-
} catch (err) {
|
|
57
|
-
error(
|
|
58
|
-
`Failed to update ${entry.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
success(`Updated ${updateCount} skill(s)`);
|
|
64
|
-
return true;
|
|
65
|
-
} catch (err) {
|
|
66
|
-
error(`Update failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
1
|
+
import { fetchSkill, fetchRegistryIndex } from '../core/downloader.js';
|
|
2
|
+
import { installSkill } from '../core/installer.js';
|
|
3
|
+
import { addSkillToLockfile, getInstalledSkills, getSkillEntry } from '../core/lockfile.js';
|
|
4
|
+
import { sha256 } from '../utils/hash.js';
|
|
5
|
+
import { error, success, info, warn } from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
export async function update(options: { force?: boolean; skill?: string } = {}): Promise<boolean> {
|
|
8
|
+
try {
|
|
9
|
+
const installed = await getInstalledSkills();
|
|
10
|
+
const index = await fetchRegistryIndex();
|
|
11
|
+
|
|
12
|
+
if (options.skill) {
|
|
13
|
+
// Update specific skill
|
|
14
|
+
const skillName = options.skill;
|
|
15
|
+
const entry = await getSkillEntry(skillName);
|
|
16
|
+
|
|
17
|
+
if (!entry) {
|
|
18
|
+
warn(`${skillName} is not installed`);
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const skillContent = await fetchSkill(skillName);
|
|
23
|
+
const newHash = sha256(skillContent);
|
|
24
|
+
|
|
25
|
+
if (newHash === entry.hash && !options.force) {
|
|
26
|
+
info(`${skillName} is already up-to-date`);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
info(`Updating ${skillName}...`);
|
|
31
|
+
const installPaths = await installSkill(skillName, skillContent);
|
|
32
|
+
const version = index.skills[skillName]?.version || 'unknown';
|
|
33
|
+
await addSkillToLockfile(skillName, version, newHash, installPaths);
|
|
34
|
+
success(`Updated ${skillName}`);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Update all installed skills
|
|
39
|
+
let updateCount = 0;
|
|
40
|
+
for (const entry of installed) {
|
|
41
|
+
try {
|
|
42
|
+
const skillContent = await fetchSkill(entry.name);
|
|
43
|
+
const newHash = sha256(skillContent);
|
|
44
|
+
|
|
45
|
+
if (newHash === entry.hash && !options.force) {
|
|
46
|
+
info(`${entry.name} is already up-to-date`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
info(`Updating ${entry.name}...`);
|
|
51
|
+
const installPaths = await installSkill(entry.name, skillContent);
|
|
52
|
+
const version = index.skills[entry.name]?.version || 'unknown';
|
|
53
|
+
await addSkillToLockfile(entry.name, version, newHash, installPaths);
|
|
54
|
+
success(`Updated ${entry.name}`);
|
|
55
|
+
updateCount++;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
error(
|
|
58
|
+
`Failed to update ${entry.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
success(`Updated ${updateCount} skill(s)`);
|
|
64
|
+
return true;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
error(`Update failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import * as downloader from '../../core/downloader';
|
|
3
|
-
|
|
4
|
-
vi.mock('../../utils/retry', () => ({
|
|
5
|
-
fetchWithRetry: vi.fn(),
|
|
6
|
-
}));
|
|
7
|
-
|
|
8
|
-
describe('downloader', () => {
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
vi.clearAllMocks();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('should fetch registry index', async () => {
|
|
14
|
-
const { fetchWithRetry } = await import('../../utils/retry');
|
|
15
|
-
const mockIndex = {
|
|
16
|
-
skills: {
|
|
17
|
-
react: {
|
|
18
|
-
version: '18.2.0',
|
|
19
|
-
domains: ['frontend', 'ui'],
|
|
20
|
-
lastGenerated: '2026-02-06T00:00:00Z',
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
vi.mocked(fetchWithRetry).mockResolvedValue(JSON.stringify(mockIndex));
|
|
26
|
-
|
|
27
|
-
const result = await downloader.fetchRegistryIndex();
|
|
28
|
-
expect(result.skills.react).toBeDefined();
|
|
29
|
-
expect(result.skills.react.version).toBe('18.2.0');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('should fetch skill markdown', async () => {
|
|
33
|
-
const { fetchWithRetry } = await import('../../utils/retry');
|
|
34
|
-
const mockContent = '# React Skill\n\nSome content';
|
|
35
|
-
|
|
36
|
-
vi.mocked(fetchWithRetry).mockResolvedValue(mockContent);
|
|
37
|
-
|
|
38
|
-
const result = await downloader.fetchSkill('react');
|
|
39
|
-
expect(result).toBe(mockContent);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should get available skills', async () => {
|
|
43
|
-
const { fetchWithRetry } = await import('../../utils/retry');
|
|
44
|
-
const mockIndex = {
|
|
45
|
-
skills: {
|
|
46
|
-
react: {
|
|
47
|
-
version: '18.2.0',
|
|
48
|
-
domains: ['frontend'],
|
|
49
|
-
lastGenerated: '2026-02-06T00:00:00Z',
|
|
50
|
-
},
|
|
51
|
-
vue: {
|
|
52
|
-
version: '3.4.0',
|
|
53
|
-
domains: ['frontend'],
|
|
54
|
-
lastGenerated: '2026-02-06T00:00:00Z',
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
vi.mocked(fetchWithRetry).mockResolvedValue(JSON.stringify(mockIndex));
|
|
60
|
-
|
|
61
|
-
const result = await downloader.getAvailableSkills();
|
|
62
|
-
expect(result).toContain('react');
|
|
63
|
-
expect(result).toContain('vue');
|
|
64
|
-
expect(result).toHaveLength(2);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import * as downloader from '../../core/downloader';
|
|
3
|
+
|
|
4
|
+
vi.mock('../../utils/retry', () => ({
|
|
5
|
+
fetchWithRetry: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
describe('downloader', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should fetch registry index', async () => {
|
|
14
|
+
const { fetchWithRetry } = await import('../../utils/retry');
|
|
15
|
+
const mockIndex = {
|
|
16
|
+
skills: {
|
|
17
|
+
react: {
|
|
18
|
+
version: '18.2.0',
|
|
19
|
+
domains: ['frontend', 'ui'],
|
|
20
|
+
lastGenerated: '2026-02-06T00:00:00Z',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
vi.mocked(fetchWithRetry).mockResolvedValue(JSON.stringify(mockIndex));
|
|
26
|
+
|
|
27
|
+
const result = await downloader.fetchRegistryIndex();
|
|
28
|
+
expect(result.skills.react).toBeDefined();
|
|
29
|
+
expect(result.skills.react.version).toBe('18.2.0');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should fetch skill markdown', async () => {
|
|
33
|
+
const { fetchWithRetry } = await import('../../utils/retry');
|
|
34
|
+
const mockContent = '# React Skill\n\nSome content';
|
|
35
|
+
|
|
36
|
+
vi.mocked(fetchWithRetry).mockResolvedValue(mockContent);
|
|
37
|
+
|
|
38
|
+
const result = await downloader.fetchSkill('react');
|
|
39
|
+
expect(result).toBe(mockContent);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should get available skills', async () => {
|
|
43
|
+
const { fetchWithRetry } = await import('../../utils/retry');
|
|
44
|
+
const mockIndex = {
|
|
45
|
+
skills: {
|
|
46
|
+
react: {
|
|
47
|
+
version: '18.2.0',
|
|
48
|
+
domains: ['frontend'],
|
|
49
|
+
lastGenerated: '2026-02-06T00:00:00Z',
|
|
50
|
+
},
|
|
51
|
+
vue: {
|
|
52
|
+
version: '3.4.0',
|
|
53
|
+
domains: ['frontend'],
|
|
54
|
+
lastGenerated: '2026-02-06T00:00:00Z',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
vi.mocked(fetchWithRetry).mockResolvedValue(JSON.stringify(mockIndex));
|
|
60
|
+
|
|
61
|
+
const result = await downloader.getAvailableSkills();
|
|
62
|
+
expect(result).toContain('react');
|
|
63
|
+
expect(result).toContain('vue');
|
|
64
|
+
expect(result).toHaveLength(2);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { rm } from 'fs/promises';
|
|
3
|
-
import { tmpdir } from 'os';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import * as lockfile from '../../core/lockfile';
|
|
6
|
-
|
|
7
|
-
const lockfilePath = join(tmpdir(), `test-skills-lock-${Date.now()}.json`);
|
|
8
|
-
|
|
9
|
-
// Mock the config to use a temp directory
|
|
10
|
-
vi.mock('../../core/config', () => ({
|
|
11
|
-
getLockfilePath: () => lockfilePath,
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
describe('lockfile', () => {
|
|
15
|
-
beforeEach(async () => {
|
|
16
|
-
// Clear the lockfile before each test
|
|
17
|
-
try {
|
|
18
|
-
await rm(lockfilePath, { force: true });
|
|
19
|
-
} catch {
|
|
20
|
-
// File doesn't exist, that's fine
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
afterEach(async () => {
|
|
25
|
-
// Clean up after tests
|
|
26
|
-
try {
|
|
27
|
-
await rm(lockfilePath, { force: true });
|
|
28
|
-
} catch {
|
|
29
|
-
// File doesn't exist, that's fine
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should read an empty lockfile as initial state', async () => {
|
|
34
|
-
const result = await lockfile.readLockfile();
|
|
35
|
-
expect(result.version).toBe(1);
|
|
36
|
-
expect(result.installedSkills).toEqual({});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should add a skill to lockfile', async () => {
|
|
40
|
-
const skillName = 'react';
|
|
41
|
-
const version = '18.2.0';
|
|
42
|
-
const hash = 'abc123';
|
|
43
|
-
const paths = ['/home/user/.agents/skills/react/SKILLS.md'];
|
|
44
|
-
|
|
45
|
-
await lockfile.addSkillToLockfile(skillName, version, hash, paths);
|
|
46
|
-
|
|
47
|
-
const result = await lockfile.readLockfile();
|
|
48
|
-
expect(result.installedSkills[skillName]).toBeDefined();
|
|
49
|
-
expect(result.installedSkills[skillName].version).toBe(version);
|
|
50
|
-
expect(result.installedSkills[skillName].hash).toBe(hash);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should remove a skill from lockfile', async () => {
|
|
54
|
-
const skillName = 'react';
|
|
55
|
-
await lockfile.addSkillToLockfile(skillName, '18.2.0', 'hash1', []);
|
|
56
|
-
|
|
57
|
-
let result = await lockfile.readLockfile();
|
|
58
|
-
expect(result.installedSkills[skillName]).toBeDefined();
|
|
59
|
-
|
|
60
|
-
await lockfile.removeSkillFromLockfile(skillName);
|
|
61
|
-
|
|
62
|
-
result = await lockfile.readLockfile();
|
|
63
|
-
expect(result.installedSkills[skillName]).toBeUndefined();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should get installed skills', async () => {
|
|
67
|
-
await lockfile.addSkillToLockfile('react', '18.2.0', 'hash1', []);
|
|
68
|
-
await lockfile.addSkillToLockfile('vue', '3.4.0', 'hash2', []);
|
|
69
|
-
|
|
70
|
-
const installed = await lockfile.getInstalledSkills();
|
|
71
|
-
expect(installed).toHaveLength(2);
|
|
72
|
-
expect(installed.map((s) => s.name)).toContain('react');
|
|
73
|
-
expect(installed.map((s) => s.name)).toContain('vue');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should get a specific skill entry', async () => {
|
|
77
|
-
const skillName = 'react';
|
|
78
|
-
const version = '18.2.0';
|
|
79
|
-
const hash = 'abc123';
|
|
80
|
-
const paths = ['/home/user/.agents/skills/react/SKILLS.md'];
|
|
81
|
-
|
|
82
|
-
await lockfile.addSkillToLockfile(skillName, version, hash, paths);
|
|
83
|
-
|
|
84
|
-
const entry = await lockfile.getSkillEntry(skillName);
|
|
85
|
-
expect(entry).not.toBeNull();
|
|
86
|
-
expect(entry?.name).toBe(skillName);
|
|
87
|
-
expect(entry?.version).toBe(version);
|
|
88
|
-
expect(entry?.hash).toBe(hash);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { rm } from 'fs/promises';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import * as lockfile from '../../core/lockfile';
|
|
6
|
+
|
|
7
|
+
const lockfilePath = join(tmpdir(), `test-skills-lock-${Date.now()}.json`);
|
|
8
|
+
|
|
9
|
+
// Mock the config to use a temp directory
|
|
10
|
+
vi.mock('../../core/config', () => ({
|
|
11
|
+
getLockfilePath: () => lockfilePath,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe('lockfile', () => {
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
// Clear the lockfile before each test
|
|
17
|
+
try {
|
|
18
|
+
await rm(lockfilePath, { force: true });
|
|
19
|
+
} catch {
|
|
20
|
+
// File doesn't exist, that's fine
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
// Clean up after tests
|
|
26
|
+
try {
|
|
27
|
+
await rm(lockfilePath, { force: true });
|
|
28
|
+
} catch {
|
|
29
|
+
// File doesn't exist, that's fine
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should read an empty lockfile as initial state', async () => {
|
|
34
|
+
const result = await lockfile.readLockfile();
|
|
35
|
+
expect(result.version).toBe(1);
|
|
36
|
+
expect(result.installedSkills).toEqual({});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should add a skill to lockfile', async () => {
|
|
40
|
+
const skillName = 'react';
|
|
41
|
+
const version = '18.2.0';
|
|
42
|
+
const hash = 'abc123';
|
|
43
|
+
const paths = ['/home/user/.agents/skills/react/SKILLS.md'];
|
|
44
|
+
|
|
45
|
+
await lockfile.addSkillToLockfile(skillName, version, hash, paths);
|
|
46
|
+
|
|
47
|
+
const result = await lockfile.readLockfile();
|
|
48
|
+
expect(result.installedSkills[skillName]).toBeDefined();
|
|
49
|
+
expect(result.installedSkills[skillName].version).toBe(version);
|
|
50
|
+
expect(result.installedSkills[skillName].hash).toBe(hash);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should remove a skill from lockfile', async () => {
|
|
54
|
+
const skillName = 'react';
|
|
55
|
+
await lockfile.addSkillToLockfile(skillName, '18.2.0', 'hash1', []);
|
|
56
|
+
|
|
57
|
+
let result = await lockfile.readLockfile();
|
|
58
|
+
expect(result.installedSkills[skillName]).toBeDefined();
|
|
59
|
+
|
|
60
|
+
await lockfile.removeSkillFromLockfile(skillName);
|
|
61
|
+
|
|
62
|
+
result = await lockfile.readLockfile();
|
|
63
|
+
expect(result.installedSkills[skillName]).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should get installed skills', async () => {
|
|
67
|
+
await lockfile.addSkillToLockfile('react', '18.2.0', 'hash1', []);
|
|
68
|
+
await lockfile.addSkillToLockfile('vue', '3.4.0', 'hash2', []);
|
|
69
|
+
|
|
70
|
+
const installed = await lockfile.getInstalledSkills();
|
|
71
|
+
expect(installed).toHaveLength(2);
|
|
72
|
+
expect(installed.map((s) => s.name)).toContain('react');
|
|
73
|
+
expect(installed.map((s) => s.name)).toContain('vue');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should get a specific skill entry', async () => {
|
|
77
|
+
const skillName = 'react';
|
|
78
|
+
const version = '18.2.0';
|
|
79
|
+
const hash = 'abc123';
|
|
80
|
+
const paths = ['/home/user/.agents/skills/react/SKILLS.md'];
|
|
81
|
+
|
|
82
|
+
await lockfile.addSkillToLockfile(skillName, version, hash, paths);
|
|
83
|
+
|
|
84
|
+
const entry = await lockfile.getSkillEntry(skillName);
|
|
85
|
+
expect(entry).not.toBeNull();
|
|
86
|
+
expect(entry?.name).toBe(skillName);
|
|
87
|
+
expect(entry?.version).toBe(version);
|
|
88
|
+
expect(entry?.hash).toBe(hash);
|
|
89
|
+
});
|
|
90
|
+
});
|
package/src/core/config.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import { homedir, platform } from 'os';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
|
|
4
|
-
export const REGISTRY_URL =
|
|
5
|
-
'https://raw.githubusercontent.com/Emmraan/ai-skills/main/packages/skills-registry';
|
|
6
|
-
|
|
7
|
-
export const REGISTRY_INDEX_URL = `${REGISTRY_URL}/skills/.index.json`;
|
|
8
|
-
|
|
9
|
-
export const AGENT_PLATFORMS = ['.claude', '.gemini', '.vscode', '.opencode', '.codex', '.agents'];
|
|
10
|
-
|
|
11
|
-
export function getAgentPlatformDirs(): string[] {
|
|
12
|
-
const home = homedir();
|
|
13
|
-
return AGENT_PLATFORMS.map((p) => join(home, p, 'skills'));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function getLockfilePath(): string {
|
|
17
|
-
const home = homedir();
|
|
18
|
-
return join(home, '.ai-skills', '.skill-lock.json');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function getSkillInstallPaths(skillName: string): string[] {
|
|
22
|
-
return getAgentPlatformDirs().map((dir) => join(dir, skillName, 'SKILLS.md'));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function getSkillMetadataPaths(skillName: string): string[] {
|
|
26
|
-
return getAgentPlatformDirs().map((dir) => join(dir, skillName, '.skill-metadata.json'));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const IS_WINDOWS = platform() === 'win32';
|
|
1
|
+
import { homedir, platform } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
export const REGISTRY_URL =
|
|
5
|
+
'https://raw.githubusercontent.com/Emmraan/ai-skills/main/packages/skills-registry';
|
|
6
|
+
|
|
7
|
+
export const REGISTRY_INDEX_URL = `${REGISTRY_URL}/skills/.index.json`;
|
|
8
|
+
|
|
9
|
+
export const AGENT_PLATFORMS = ['.claude', '.gemini', '.vscode', '.opencode', '.codex', '.agents'];
|
|
10
|
+
|
|
11
|
+
export function getAgentPlatformDirs(): string[] {
|
|
12
|
+
const home = homedir();
|
|
13
|
+
return AGENT_PLATFORMS.map((p) => join(home, p, 'skills'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getLockfilePath(): string {
|
|
17
|
+
const home = homedir();
|
|
18
|
+
return join(home, '.ai-skills', '.skill-lock.json');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getSkillInstallPaths(skillName: string): string[] {
|
|
22
|
+
return getAgentPlatformDirs().map((dir) => join(dir, skillName, 'SKILLS.md'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getSkillMetadataPaths(skillName: string): string[] {
|
|
26
|
+
return getAgentPlatformDirs().map((dir) => join(dir, skillName, '.skill-metadata.json'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const IS_WINDOWS = platform() === 'win32';
|
package/src/core/downloader.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import { REGISTRY_INDEX_URL, REGISTRY_URL } from './config.js';
|
|
2
|
-
import { fetchWithRetry } from '../utils/retry.js';
|
|
3
|
-
|
|
4
|
-
export interface SkillIndexEntry {
|
|
5
|
-
version: string;
|
|
6
|
-
domains: string[];
|
|
7
|
-
lastGenerated: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface SkillIndex {
|
|
11
|
-
skills: Record<string, SkillIndexEntry>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function fetchRegistryIndex(): Promise<SkillIndex> {
|
|
15
|
-
const content = await fetchWithRetry(REGISTRY_INDEX_URL);
|
|
16
|
-
return JSON.parse(content);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function fetchSkill(skillName: string): Promise<string> {
|
|
20
|
-
const url = `${REGISTRY_URL}/skills/${skillName}/SKILLS.md`;
|
|
21
|
-
return fetchWithRetry(url);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function getAvailableSkills(): Promise<string[]> {
|
|
25
|
-
const index = await fetchRegistryIndex();
|
|
26
|
-
return Object.keys(index.skills);
|
|
27
|
-
}
|
|
1
|
+
import { REGISTRY_INDEX_URL, REGISTRY_URL } from './config.js';
|
|
2
|
+
import { fetchWithRetry } from '../utils/retry.js';
|
|
3
|
+
|
|
4
|
+
export interface SkillIndexEntry {
|
|
5
|
+
version: string;
|
|
6
|
+
domains: string[];
|
|
7
|
+
lastGenerated: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SkillIndex {
|
|
11
|
+
skills: Record<string, SkillIndexEntry>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function fetchRegistryIndex(): Promise<SkillIndex> {
|
|
15
|
+
const content = await fetchWithRetry(REGISTRY_INDEX_URL);
|
|
16
|
+
return JSON.parse(content);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function fetchSkill(skillName: string): Promise<string> {
|
|
20
|
+
const url = `${REGISTRY_URL}/skills/${skillName}/SKILLS.md`;
|
|
21
|
+
return fetchWithRetry(url);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function getAvailableSkills(): Promise<string[]> {
|
|
25
|
+
const index = await fetchRegistryIndex();
|
|
26
|
+
return Object.keys(index.skills);
|
|
27
|
+
}
|