Skip to content

Commit 3161729

Browse files
committed
Add custom skill registries
1 parent 7e7216e commit 3161729

File tree

10 files changed

+361
-23
lines changed

10 files changed

+361
-23
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
- **Custom Skill Registries** - Support `skills.registries` in global `~/.ai-devkit/.ai-devkit.json` for adding multiple registries that merge with defaults and override on conflicts.
12+
- **Global Registry Reader** - New global config reader for resolving custom registries in skill commands.
13+
14+
### Changed
15+
- **Skill Registry Resolution** - Skill commands now merge default and custom registries, with offline cache fallback when a registry URL is not configured.
16+
817
## [0.8.0] - 2026-01-26
918

1019
### Added

docs/ai/implementation/feature-custom-skill-registries.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ description: Technical implementation notes, patterns, and code guidelines
2626

2727
### Core Features
2828
- Load default registry from remote `registry.json` (current behavior).
29-
- Load custom registries from global config using the same `registries` map format.
29+
- Load custom registries from global config using the same `skills.registries` map format.
3030
- Merge registries with custom entries overriding default on registry ID collision.
3131
- Use the merged registry map for all skill commands.
3232
- When remote registry fetch fails, fall back to cached registry data if available.
@@ -42,7 +42,7 @@ description: Technical implementation notes, patterns, and code guidelines
4242
- `SkillManager` calls into registry resolver for merged registry map.
4343
- Registry resolver reads:
4444
- Remote default registry JSON
45-
- Global config `skillRegistries.registries`
45+
- Global config `skills.registries`
4646
- Repository cloning remains in `cloneRepositoryToCache`.
4747

4848
## Error Handling

docs/ai/planning/feature-custom-skill-registries.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,19 @@ description: Break down work into actionable tasks and estimate timeline
1717
**What specific work needs to be done?**
1818

1919
### Phase 1: Foundation
20-
- [ ] Task 1.1: Define global config schema for `skillRegistries`
21-
- [ ] Task 1.2: Add global config manager for `~/.ai-devkit/.ai-devkit.json`
22-
- [ ] Task 1.3: Update types to include optional `skillRegistries`
20+
- [x] Task 1.1: Define global config schema for `skills.registries` (Notes: aligned requirements/design docs)
21+
- [x] Task 1.2: Add global config manager for `~/.ai-devkit/.ai-devkit.json` (Notes: new `GlobalConfigManager` added)
22+
- [x] Task 1.3: Update types to include optional `skills.registries` (Notes: added `GlobalDevKitConfig` with `skills.registries`)
2323

2424
### Phase 2: Core Features
25-
- [ ] Task 2.1: Implement registry merge logic (custom overrides default)
26-
- [ ] Task 2.2: Update `SkillManager` to use merged registries for all commands
27-
- [ ] Task 2.3: Ensure offline behavior uses cached registries when remote fetch fails
25+
- [x] Task 2.1: Implement registry merge logic (custom overrides default) (Notes: merge in `SkillManager` with custom override)
26+
- [x] Task 2.2: Update `SkillManager` to use merged registries for all commands (Notes: addSkill now uses merged registries)
27+
- [x] Task 2.3: Ensure offline behavior uses cached registries when remote fetch fails (Notes: cached repo used if URL missing)
2828

2929
### Phase 3: Integration & Polish
30-
- [ ] Task 3.1: Add unit tests for registry merging and config parsing
31-
- [ ] Task 3.2: Add integration tests for skill commands with custom registries
32-
- [ ] Task 3.3: Update docs and finalize testing notes
30+
- [x] Task 3.1: Add unit tests for registry merging and config parsing (Notes: added GlobalConfig + SkillManager tests)
31+
- [x] Task 3.2: Add integration tests for skill commands with custom registries (Notes: added SkillManager test using real global config)
32+
- [x] Task 3.3: Update docs and finalize testing notes (Notes: updated requirements, implementation, testing docs)
3333

3434
## Dependencies
3535
**What needs to happen in what order?**

docs/ai/requirements/feature-custom-skill-registries.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ description: Clarify the problem space, gather requirements, and define success
1818

1919
**What do we want to achieve?**
2020

21-
- Allow multiple custom registries configured in a global `.ai-devkit.json`.
21+
- Allow multiple custom registries configured in a global `.ai-devkit.json` at `skills.registries`.
2222
- Use the same registry format as `skills/registry.json` (a `registries` map).
2323
- Merge default and custom registries for all skill commands.
2424
- Prioritize local (custom) registry entries over default when registry IDs conflict.
@@ -58,7 +58,7 @@ description: Clarify the problem space, gather requirements, and define success
5858

5959
**What limitations do we need to work within?**
6060

61-
- Custom registries use the same structure as `skills/registry.json`.
61+
- Custom registries use the same structure as `skills/registry.json` under `skills.registries`.
6262
- Registries are Git repositories that contain `skills/<skillName>/SKILL.md`.
6363
- User manages any authentication externally; CLI only clones locally.
6464
- Git must be installed for cloning.

docs/ai/testing/feature-custom-skill-registries.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ description: Define testing approach, test cases, and quality assurance
3535
## Integration Tests
3636
**How do we test component interactions?**
3737

38-
- [ ] Install a skill from a custom registry repo
38+
- [ ] Install a skill from a custom registry repo (via global config registries)
3939
- [ ] Install a skill when default registry is unreachable but cached
4040
- [ ] List skills after installing from custom registry
4141
- [ ] Remove a skill installed from custom registry
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import * as fs from 'fs-extra';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
import { GlobalConfigManager } from '../../lib/GlobalConfig';
5+
6+
jest.mock('fs-extra');
7+
jest.mock('os');
8+
jest.mock('path');
9+
10+
describe('GlobalConfigManager', () => {
11+
let configManager: GlobalConfigManager;
12+
let mockFs: jest.Mocked<typeof fs>;
13+
let mockOs: jest.Mocked<typeof os>;
14+
let mockPath: jest.Mocked<typeof path>;
15+
16+
beforeEach(() => {
17+
configManager = new GlobalConfigManager();
18+
mockFs = fs as jest.Mocked<typeof fs>;
19+
mockOs = os as jest.Mocked<typeof os>;
20+
mockPath = path as jest.Mocked<typeof path>;
21+
22+
mockOs.homedir.mockReturnValue('/home/test');
23+
mockPath.join.mockImplementation((...args) => args.join('/'));
24+
jest.spyOn(console, 'warn').mockImplementation(() => {});
25+
});
26+
27+
afterEach(() => {
28+
jest.clearAllMocks();
29+
});
30+
31+
describe('read', () => {
32+
it('should return null when global config does not exist', async () => {
33+
(mockFs.pathExists as any).mockResolvedValue(false);
34+
35+
const result = await configManager.read();
36+
37+
expect(result).toBeNull();
38+
expect(mockFs.readJson).not.toHaveBeenCalled();
39+
});
40+
41+
it('should return parsed config when file exists', async () => {
42+
const config = {
43+
skills: {
44+
registries: {
45+
'my-org/skills': 'https://github.com/my-org/skills.git'
46+
}
47+
}
48+
};
49+
50+
(mockFs.pathExists as any).mockResolvedValue(true);
51+
(mockFs.readJson as any).mockResolvedValue(config);
52+
53+
const result = await configManager.read();
54+
55+
expect(result).toEqual(config);
56+
expect(mockFs.readJson).toHaveBeenCalledWith('/home/test/.ai-devkit/.ai-devkit.json');
57+
});
58+
59+
it('should warn and return null when JSON is invalid', async () => {
60+
(mockFs.pathExists as any).mockResolvedValue(true);
61+
(mockFs.readJson as any).mockRejectedValue(new Error('Invalid JSON'));
62+
63+
const result = await configManager.read();
64+
65+
expect(result).toBeNull();
66+
expect(console.warn).toHaveBeenCalled();
67+
});
68+
});
69+
70+
describe('getSkillRegistries', () => {
71+
it('should return empty map when no config', async () => {
72+
(mockFs.pathExists as any).mockResolvedValue(false);
73+
74+
const result = await configManager.getSkillRegistries();
75+
76+
expect(result).toEqual({});
77+
});
78+
79+
it('should return only string registry entries', async () => {
80+
const config = {
81+
skills: {
82+
registries: {
83+
'my-org/skills': 'https://github.com/my-org/skills.git',
84+
'bad/entry': 123
85+
}
86+
}
87+
};
88+
89+
(mockFs.pathExists as any).mockResolvedValue(true);
90+
(mockFs.readJson as any).mockResolvedValue(config);
91+
92+
const result = await configManager.getSkillRegistries();
93+
94+
expect(result).toEqual({
95+
'my-org/skills': 'https://github.com/my-org/skills.git'
96+
});
97+
});
98+
});
99+
});

0 commit comments

Comments
 (0)