tools-cc 1.0.6 → 1.0.8
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/CHANGELOG.md +31 -0
- package/CHANGELOG_en.md +31 -0
- package/README.md +10 -4
- package/README_en.md +56 -4
- package/dist/commands/help.js +31 -4
- package/dist/commands/template.d.ts +18 -0
- package/dist/commands/template.js +154 -0
- package/dist/commands/use.d.ts +2 -2
- package/dist/commands/use.js +40 -35
- package/dist/core/template.d.ts +17 -0
- package/dist/core/template.js +74 -0
- package/dist/index.js +32 -0
- package/dist/types/config.d.ts +10 -0
- package/dist/utils/path.d.ts +2 -0
- package/dist/utils/path.js +6 -1
- package/package.json +3 -3
- package/src/commands/help.ts +31 -4
- package/src/commands/template.ts +160 -0
- package/src/commands/use.ts +12 -6
- package/src/core/template.ts +91 -0
- package/src/index.ts +48 -11
- package/src/types/config.ts +112 -101
- package/src/utils/path.ts +23 -18
- package/tests/commands/export.test.ts +120 -0
- package/tests/commands/use.test.ts +326 -0
- package/tests/core/config.test.ts +37 -0
- package/tests/core/manifest.test.ts +37 -0
- package/tests/core/project.test.ts +317 -0
- package/tests/core/source.test.ts +75 -0
- package/tests/core/symlink.test.ts +39 -0
- package/tests/core/template.test.ts +103 -0
- package/tests/types/config.test.ts +232 -0
- package/tests/utils/parsePath.test.ts +235 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { handleExport } from '../../src/commands/export';
|
|
5
|
+
import { initProject, useSource } from '../../src/core/project';
|
|
6
|
+
import { GLOBAL_CONFIG_DIR } from '../../src/utils/path';
|
|
7
|
+
|
|
8
|
+
describe('handleExport', () => {
|
|
9
|
+
const testProjectDir = path.join(__dirname, '../fixtures/test-export-cmd-project');
|
|
10
|
+
const testSourceDir = path.join(__dirname, '../fixtures/test-export-cmd-source');
|
|
11
|
+
const exportFilePath = path.join(__dirname, '../fixtures/test-export-cmd.json');
|
|
12
|
+
const globalExportPath = path.join(__dirname, '../fixtures/test-global-export-cmd.json');
|
|
13
|
+
|
|
14
|
+
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
// Create test source
|
|
18
|
+
await fs.ensureDir(path.join(testSourceDir, 'skills', 'test-skill'));
|
|
19
|
+
await fs.writeFile(path.join(testSourceDir, 'skills', 'test-skill', 'test.md'), '# test skill');
|
|
20
|
+
|
|
21
|
+
// Create project directory
|
|
22
|
+
await fs.ensureDir(testProjectDir);
|
|
23
|
+
|
|
24
|
+
// Spy on console.log
|
|
25
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
await fs.remove(testProjectDir);
|
|
30
|
+
await fs.remove(testSourceDir);
|
|
31
|
+
await fs.remove(exportFilePath);
|
|
32
|
+
await fs.remove(globalExportPath);
|
|
33
|
+
consoleLogSpy.mockRestore();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should export project config to default path', async () => {
|
|
37
|
+
// Initialize project with a source
|
|
38
|
+
await initProject(testProjectDir);
|
|
39
|
+
await useSource('test-source', testSourceDir, testProjectDir);
|
|
40
|
+
|
|
41
|
+
// Change to project directory for the test
|
|
42
|
+
const originalCwd = process.cwd();
|
|
43
|
+
process.chdir(testProjectDir);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await handleExport({});
|
|
47
|
+
|
|
48
|
+
// Check default export file was created
|
|
49
|
+
const defaultExportPath = path.join(testProjectDir, '.toolscc-export.json');
|
|
50
|
+
expect(await fs.pathExists(defaultExportPath)).toBe(true);
|
|
51
|
+
|
|
52
|
+
// Verify file content
|
|
53
|
+
const exported = await fs.readJson(defaultExportPath);
|
|
54
|
+
expect(exported.version).toBe('1.0');
|
|
55
|
+
expect(exported.type).toBe('project');
|
|
56
|
+
|
|
57
|
+
// Verify console output
|
|
58
|
+
expect(console.log).toHaveBeenCalledWith(
|
|
59
|
+
expect.stringContaining('✓ Project config exported to:')
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Cleanup
|
|
63
|
+
await fs.remove(defaultExportPath);
|
|
64
|
+
} finally {
|
|
65
|
+
process.chdir(originalCwd);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should export project config to custom path', async () => {
|
|
70
|
+
await initProject(testProjectDir);
|
|
71
|
+
await useSource('test-source', testSourceDir, testProjectDir);
|
|
72
|
+
|
|
73
|
+
const originalCwd = process.cwd();
|
|
74
|
+
process.chdir(testProjectDir);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await handleExport({ output: exportFilePath });
|
|
78
|
+
|
|
79
|
+
expect(await fs.pathExists(exportFilePath)).toBe(true);
|
|
80
|
+
|
|
81
|
+
const exported = await fs.readJson(exportFilePath);
|
|
82
|
+
expect(exported.version).toBe('1.0');
|
|
83
|
+
expect(exported.type).toBe('project');
|
|
84
|
+
} finally {
|
|
85
|
+
process.chdir(originalCwd);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should export global config with --global flag', async () => {
|
|
90
|
+
const originalCwd = process.cwd();
|
|
91
|
+
process.chdir(testProjectDir);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await handleExport({ global: true, output: globalExportPath });
|
|
95
|
+
|
|
96
|
+
expect(await fs.pathExists(globalExportPath)).toBe(true);
|
|
97
|
+
|
|
98
|
+
const exported = await fs.readJson(globalExportPath);
|
|
99
|
+
expect(exported.version).toBe('1.0');
|
|
100
|
+
expect(exported.type).toBe('global');
|
|
101
|
+
expect(exported.config).toBeDefined();
|
|
102
|
+
expect(exported.config.sourcesDir).toBeDefined();
|
|
103
|
+
} finally {
|
|
104
|
+
process.chdir(originalCwd);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should throw error when exporting non-initialized project', async () => {
|
|
109
|
+
const originalCwd = process.cwd();
|
|
110
|
+
process.chdir(testProjectDir);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// Don't initialize project
|
|
114
|
+
await expect(handleExport({ output: exportFilePath }))
|
|
115
|
+
.rejects.toThrow();
|
|
116
|
+
} finally {
|
|
117
|
+
process.chdir(originalCwd);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { ExportConfig } from '../../src/types/config';
|
|
5
|
+
|
|
6
|
+
// Mock inquirer to avoid interactive prompts in tests
|
|
7
|
+
vi.mock('inquirer', () => ({
|
|
8
|
+
default: {
|
|
9
|
+
prompt: vi.fn()
|
|
10
|
+
}
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import inquirer from 'inquirer';
|
|
14
|
+
|
|
15
|
+
describe('handleUse Command - parseSourcePath', () => {
|
|
16
|
+
it('should parse full path with type and item', async () => {
|
|
17
|
+
const { parseSourcePath } = await import('../../src/utils/parsePath');
|
|
18
|
+
|
|
19
|
+
const result = parseSourcePath('my-skills/skills/a-skill');
|
|
20
|
+
expect(result).toEqual({
|
|
21
|
+
sourceName: 'my-skills',
|
|
22
|
+
type: 'skills',
|
|
23
|
+
itemName: 'a-skill'
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should parse source name only', async () => {
|
|
28
|
+
const { parseSourcePath } = await import('../../src/utils/parsePath');
|
|
29
|
+
|
|
30
|
+
const result = parseSourcePath('my-skills');
|
|
31
|
+
expect(result).toEqual({
|
|
32
|
+
sourceName: 'my-skills'
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should parse commands path', async () => {
|
|
37
|
+
const { parseSourcePath } = await import('../../src/utils/parsePath');
|
|
38
|
+
|
|
39
|
+
const result = parseSourcePath('my-source/commands/test-cmd');
|
|
40
|
+
expect(result).toEqual({
|
|
41
|
+
sourceName: 'my-source',
|
|
42
|
+
type: 'commands',
|
|
43
|
+
itemName: 'test-cmd'
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should parse agents path', async () => {
|
|
48
|
+
const { parseSourcePath } = await import('../../src/utils/parsePath');
|
|
49
|
+
|
|
50
|
+
const result = parseSourcePath('my-source/agents/reviewer');
|
|
51
|
+
expect(result).toEqual({
|
|
52
|
+
sourceName: 'my-source',
|
|
53
|
+
type: 'agents',
|
|
54
|
+
itemName: 'reviewer'
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should handle invalid type gracefully', async () => {
|
|
59
|
+
const { parseSourcePath } = await import('../../src/utils/parsePath');
|
|
60
|
+
|
|
61
|
+
const result = parseSourcePath('my-source/invalid/item');
|
|
62
|
+
expect(result).toEqual({
|
|
63
|
+
sourceName: 'my-source'
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should handle empty string', async () => {
|
|
68
|
+
const { parseSourcePath } = await import('../../src/utils/parsePath');
|
|
69
|
+
|
|
70
|
+
const result = parseSourcePath('');
|
|
71
|
+
expect(result).toEqual({
|
|
72
|
+
sourceName: ''
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should handle path without item name', async () => {
|
|
77
|
+
const { parseSourcePath } = await import('../../src/utils/parsePath');
|
|
78
|
+
|
|
79
|
+
const result = parseSourcePath('my-source/skills');
|
|
80
|
+
expect(result).toEqual({
|
|
81
|
+
sourceName: 'my-source'
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('handleUse Command - buildSelectionFromPaths', () => {
|
|
87
|
+
it('should build selection from multiple paths', async () => {
|
|
88
|
+
const { buildSelectionFromPaths } = await import('../../src/utils/parsePath');
|
|
89
|
+
|
|
90
|
+
const result = buildSelectionFromPaths([
|
|
91
|
+
'my-skills/skills/a-skill',
|
|
92
|
+
'my-skills/skills/b-skill',
|
|
93
|
+
'my-skills/commands/test-cmd'
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
expect(result).toEqual({
|
|
97
|
+
'my-skills': {
|
|
98
|
+
skills: ['a-skill', 'b-skill'],
|
|
99
|
+
commands: ['test-cmd'],
|
|
100
|
+
agents: []
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should use wildcard for whole source', async () => {
|
|
106
|
+
const { buildSelectionFromPaths } = await import('../../src/utils/parsePath');
|
|
107
|
+
|
|
108
|
+
const result = buildSelectionFromPaths(['my-skills']);
|
|
109
|
+
|
|
110
|
+
expect(result).toEqual({
|
|
111
|
+
'my-skills': {
|
|
112
|
+
skills: ['*'],
|
|
113
|
+
commands: ['*'],
|
|
114
|
+
agents: ['*']
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should handle multiple sources', async () => {
|
|
120
|
+
const { buildSelectionFromPaths } = await import('../../src/utils/parsePath');
|
|
121
|
+
|
|
122
|
+
const result = buildSelectionFromPaths([
|
|
123
|
+
'source-a/skills/skill1',
|
|
124
|
+
'source-b/commands/cmd1'
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
expect(result).toEqual({
|
|
128
|
+
'source-a': {
|
|
129
|
+
skills: ['skill1'],
|
|
130
|
+
commands: [],
|
|
131
|
+
agents: []
|
|
132
|
+
},
|
|
133
|
+
'source-b': {
|
|
134
|
+
skills: [],
|
|
135
|
+
commands: ['cmd1'],
|
|
136
|
+
agents: []
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should deduplicate items', async () => {
|
|
142
|
+
const { buildSelectionFromPaths } = await import('../../src/utils/parsePath');
|
|
143
|
+
|
|
144
|
+
const result = buildSelectionFromPaths([
|
|
145
|
+
'my-skills/skills/a-skill',
|
|
146
|
+
'my-skills/skills/a-skill', // duplicate
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
expect(result['my-skills'].skills).toEqual(['a-skill']);
|
|
150
|
+
expect(result['my-skills'].skills.length).toBe(1);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should handle empty array', async () => {
|
|
154
|
+
const { buildSelectionFromPaths } = await import('../../src/utils/parsePath');
|
|
155
|
+
|
|
156
|
+
const result = buildSelectionFromPaths([]);
|
|
157
|
+
expect(result).toEqual({});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle mixed whole-source and partial paths', async () => {
|
|
161
|
+
const { buildSelectionFromPaths } = await import('../../src/utils/parsePath');
|
|
162
|
+
|
|
163
|
+
// When whole source is specified, it should override partial selections
|
|
164
|
+
const result = buildSelectionFromPaths([
|
|
165
|
+
'my-skills/skills/a-skill',
|
|
166
|
+
'my-skills', // whole source should overwrite
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
expect(result['my-skills']).toEqual({
|
|
170
|
+
skills: ['*'],
|
|
171
|
+
commands: ['*'],
|
|
172
|
+
agents: ['*']
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('handleUse Command - integration tests', () => {
|
|
178
|
+
// Use unique directory for each test to avoid conflicts
|
|
179
|
+
let testId = 0;
|
|
180
|
+
const getTestDir = (name: string) => path.join(__dirname, '../fixtures', `use-test-${testId}-${name}`);
|
|
181
|
+
|
|
182
|
+
beforeEach(() => {
|
|
183
|
+
testId++;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
afterEach(async () => {
|
|
187
|
+
// Cleanup all test directories
|
|
188
|
+
for (let i = 1; i <= testId; i++) {
|
|
189
|
+
try {
|
|
190
|
+
await fs.remove(path.join(__dirname, '../fixtures', `use-test-${i}-project`));
|
|
191
|
+
await fs.remove(path.join(__dirname, '../fixtures', `use-test-${i}-source`));
|
|
192
|
+
} catch {
|
|
193
|
+
// ignore cleanup errors
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should handle dot mode on non-initialized project', async () => {
|
|
199
|
+
const projectDir = getTestDir('project');
|
|
200
|
+
await fs.ensureDir(projectDir);
|
|
201
|
+
|
|
202
|
+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
203
|
+
|
|
204
|
+
const originalCwd = process.cwd();
|
|
205
|
+
process.chdir(projectDir);
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const { handleUse } = await import('../../src/commands/use');
|
|
209
|
+
await handleUse(['.'], {});
|
|
210
|
+
|
|
211
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
212
|
+
expect.stringContaining('Project not initialized')
|
|
213
|
+
);
|
|
214
|
+
} finally {
|
|
215
|
+
process.chdir(originalCwd);
|
|
216
|
+
consoleLogSpy.mockRestore();
|
|
217
|
+
await fs.remove(projectDir);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should handle dot mode with initialized project', async () => {
|
|
222
|
+
const projectDir = getTestDir('project');
|
|
223
|
+
const sourceDir = getTestDir('source');
|
|
224
|
+
|
|
225
|
+
await fs.ensureDir(projectDir);
|
|
226
|
+
await fs.ensureDir(path.join(sourceDir, 'skills', 'test-skill'));
|
|
227
|
+
await fs.writeFile(path.join(sourceDir, 'skills', 'test-skill', 'test.md'), '# test');
|
|
228
|
+
|
|
229
|
+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
230
|
+
|
|
231
|
+
const originalCwd = process.cwd();
|
|
232
|
+
process.chdir(projectDir);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const { initProject, useSource } = await import('../../src/core/project');
|
|
236
|
+
const { handleUse } = await import('../../src/commands/use');
|
|
237
|
+
|
|
238
|
+
// Initialize and add a source
|
|
239
|
+
await initProject(projectDir);
|
|
240
|
+
await useSource('test-source', sourceDir, projectDir);
|
|
241
|
+
|
|
242
|
+
// Now use dot mode
|
|
243
|
+
await handleUse(['.'], {});
|
|
244
|
+
|
|
245
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
246
|
+
expect.stringContaining('Using existing sources')
|
|
247
|
+
);
|
|
248
|
+
} finally {
|
|
249
|
+
process.chdir(originalCwd);
|
|
250
|
+
consoleLogSpy.mockRestore();
|
|
251
|
+
await fs.remove(projectDir);
|
|
252
|
+
await fs.remove(sourceDir);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should handle config import mode', async () => {
|
|
257
|
+
const projectDir = getTestDir('project');
|
|
258
|
+
const sourceDir = getTestDir('source');
|
|
259
|
+
const configPath = getTestDir('import-config.json');
|
|
260
|
+
|
|
261
|
+
await fs.ensureDir(projectDir);
|
|
262
|
+
await fs.ensureDir(path.join(sourceDir, 'skills', 'test-skill'));
|
|
263
|
+
await fs.writeFile(path.join(sourceDir, 'skills', 'test-skill', 'test.md'), '# test');
|
|
264
|
+
|
|
265
|
+
// Create export config
|
|
266
|
+
const exportConfig: ExportConfig = {
|
|
267
|
+
version: '1.0',
|
|
268
|
+
type: 'project',
|
|
269
|
+
config: {
|
|
270
|
+
sources: {
|
|
271
|
+
'imported-source': {
|
|
272
|
+
skills: ['test-skill'],
|
|
273
|
+
commands: [],
|
|
274
|
+
agents: []
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
links: []
|
|
278
|
+
},
|
|
279
|
+
exportedAt: new Date().toISOString()
|
|
280
|
+
};
|
|
281
|
+
await fs.writeJson(configPath, exportConfig, { spaces: 2 });
|
|
282
|
+
|
|
283
|
+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
284
|
+
|
|
285
|
+
const originalCwd = process.cwd();
|
|
286
|
+
process.chdir(projectDir);
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const { initProject } = await import('../../src/core/project');
|
|
290
|
+
const { handleUse } = await import('../../src/commands/use');
|
|
291
|
+
|
|
292
|
+
await initProject(projectDir);
|
|
293
|
+
|
|
294
|
+
// Mock the getSourcePath to return our test source
|
|
295
|
+
const originalGetSourcePath = (await import('../../src/core/source')).getSourcePath;
|
|
296
|
+
vi.mock('../../src/core/source', async (importOriginal) => {
|
|
297
|
+
const mod = await importOriginal();
|
|
298
|
+
return {
|
|
299
|
+
...mod,
|
|
300
|
+
getSourcePath: async (name: string) => {
|
|
301
|
+
if (name === 'imported-source') return sourceDir;
|
|
302
|
+
return originalGetSourcePath(name, '');
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Direct test of importProjectConfig
|
|
308
|
+
const { importProjectConfig } = await import('../../src/core/project');
|
|
309
|
+
await importProjectConfig(configPath, projectDir, async (name) => {
|
|
310
|
+
if (name === 'imported-source') return sourceDir;
|
|
311
|
+
throw new Error(`Unknown source: ${name}`);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Verify config was imported
|
|
315
|
+
const config = await fs.readJson(path.join(projectDir, '.toolscc', 'config.json'));
|
|
316
|
+
expect(config.sources['imported-source']).toBeDefined();
|
|
317
|
+
|
|
318
|
+
} finally {
|
|
319
|
+
process.chdir(originalCwd);
|
|
320
|
+
consoleLogSpy.mockRestore();
|
|
321
|
+
await fs.remove(projectDir);
|
|
322
|
+
await fs.remove(sourceDir);
|
|
323
|
+
await fs.remove(configPath);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { loadGlobalConfig, saveGlobalConfig } from '../../src/core/config';
|
|
5
|
+
|
|
6
|
+
describe('Config Module', () => {
|
|
7
|
+
const testConfigDir = path.join(__dirname, '../fixtures/.tools-cc');
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await fs.ensureDir(testConfigDir);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await fs.remove(testConfigDir);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should create default config if not exists', async () => {
|
|
18
|
+
const config = await loadGlobalConfig(testConfigDir);
|
|
19
|
+
expect(config.sourcesDir).toBeDefined();
|
|
20
|
+
expect(config.sources).toEqual({});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should save and load config correctly', async () => {
|
|
24
|
+
const testConfig = {
|
|
25
|
+
sourcesDir: '/test/sources',
|
|
26
|
+
sources: {
|
|
27
|
+
'test-source': { type: 'git' as const, url: 'https://github.com/test/repo.git' }
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await saveGlobalConfig(testConfig, testConfigDir);
|
|
32
|
+
const loaded = await loadGlobalConfig(testConfigDir);
|
|
33
|
+
|
|
34
|
+
expect(loaded.sourcesDir).toBe('/test/sources');
|
|
35
|
+
expect(loaded.sources['test-source'].type).toBe('git');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { loadManifest, scanSource } from '../../src/core/manifest';
|
|
5
|
+
|
|
6
|
+
describe('Manifest Module', () => {
|
|
7
|
+
const testSourceDir = path.join(__dirname, '../fixtures/test-source');
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await fs.ensureDir(path.join(testSourceDir, 'skills', 'test-skill'));
|
|
11
|
+
await fs.ensureDir(path.join(testSourceDir, 'commands'));
|
|
12
|
+
await fs.ensureDir(path.join(testSourceDir, 'agents'));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await fs.remove(testSourceDir);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should scan source directory without manifest', async () => {
|
|
20
|
+
const manifest = await scanSource(testSourceDir);
|
|
21
|
+
expect(manifest.name).toBe(path.basename(testSourceDir));
|
|
22
|
+
expect(manifest.skills).toContain('test-skill');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should load existing manifest', async () => {
|
|
26
|
+
const manifestPath = path.join(testSourceDir, 'manifest.json');
|
|
27
|
+
await fs.writeJson(manifestPath, {
|
|
28
|
+
name: 'custom-name',
|
|
29
|
+
version: '2.0.0',
|
|
30
|
+
skills: ['skill1']
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const manifest = await loadManifest(testSourceDir);
|
|
34
|
+
expect(manifest.name).toBe('custom-name');
|
|
35
|
+
expect(manifest.version).toBe('2.0.0');
|
|
36
|
+
});
|
|
37
|
+
});
|