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.
@@ -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
+ });