tools-cc 1.0.7 → 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 +13 -0
- package/CHANGELOG_en.md +13 -0
- package/dist/commands/template.d.ts +18 -0
- package/dist/commands/template.js +154 -0
- 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/template.ts +160 -0
- 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,232 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
isSourceSelection,
|
|
4
|
+
normalizeProjectConfig,
|
|
5
|
+
SourceSelection,
|
|
6
|
+
ProjectConfig,
|
|
7
|
+
LegacyProjectConfig,
|
|
8
|
+
ExportConfig,
|
|
9
|
+
GlobalExportConfig
|
|
10
|
+
} from '../../src/types/config';
|
|
11
|
+
|
|
12
|
+
describe('SourceSelection', () => {
|
|
13
|
+
describe('isSourceSelection', () => {
|
|
14
|
+
it('should return true for valid SourceSelection object', () => {
|
|
15
|
+
const valid: SourceSelection = {
|
|
16
|
+
skills: ['skill1'],
|
|
17
|
+
commands: ['cmd1'],
|
|
18
|
+
agents: ['agent1']
|
|
19
|
+
};
|
|
20
|
+
expect(isSourceSelection(valid)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should return true for empty arrays', () => {
|
|
24
|
+
const empty: SourceSelection = {
|
|
25
|
+
skills: [],
|
|
26
|
+
commands: [],
|
|
27
|
+
agents: []
|
|
28
|
+
};
|
|
29
|
+
expect(isSourceSelection(empty)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should return true for wildcard selection', () => {
|
|
33
|
+
const wildcard: SourceSelection = {
|
|
34
|
+
skills: ['*'],
|
|
35
|
+
commands: ['*'],
|
|
36
|
+
agents: ['*']
|
|
37
|
+
};
|
|
38
|
+
expect(isSourceSelection(wildcard)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return false for null', () => {
|
|
42
|
+
expect(isSourceSelection(null)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return false for undefined', () => {
|
|
46
|
+
expect(isSourceSelection(undefined)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return false for non-object values', () => {
|
|
50
|
+
expect(isSourceSelection('string')).toBe(false);
|
|
51
|
+
expect(isSourceSelection(123)).toBe(false);
|
|
52
|
+
expect(isSourceSelection(true)).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should return false for object missing skills', () => {
|
|
56
|
+
const invalid = {
|
|
57
|
+
commands: ['cmd1'],
|
|
58
|
+
agents: ['agent1']
|
|
59
|
+
};
|
|
60
|
+
expect(isSourceSelection(invalid)).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return false for object missing commands', () => {
|
|
64
|
+
const invalid = {
|
|
65
|
+
skills: ['skill1'],
|
|
66
|
+
agents: ['agent1']
|
|
67
|
+
};
|
|
68
|
+
expect(isSourceSelection(invalid)).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should return false for object missing agents', () => {
|
|
72
|
+
const invalid = {
|
|
73
|
+
skills: ['skill1'],
|
|
74
|
+
commands: ['cmd1']
|
|
75
|
+
};
|
|
76
|
+
expect(isSourceSelection(invalid)).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should return false for object with non-array skills', () => {
|
|
80
|
+
const invalid = {
|
|
81
|
+
skills: 'not-array',
|
|
82
|
+
commands: ['cmd1'],
|
|
83
|
+
agents: ['agent1']
|
|
84
|
+
};
|
|
85
|
+
expect(isSourceSelection(invalid)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should return false for object with non-array commands', () => {
|
|
89
|
+
const invalid = {
|
|
90
|
+
skills: ['skill1'],
|
|
91
|
+
commands: 123,
|
|
92
|
+
agents: ['agent1']
|
|
93
|
+
};
|
|
94
|
+
expect(isSourceSelection(invalid)).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should return false for object with non-array agents', () => {
|
|
98
|
+
const invalid = {
|
|
99
|
+
skills: ['skill1'],
|
|
100
|
+
commands: ['cmd1'],
|
|
101
|
+
agents: null
|
|
102
|
+
};
|
|
103
|
+
expect(isSourceSelection(invalid)).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('normalizeProjectConfig', () => {
|
|
109
|
+
describe('LegacyProjectConfig conversion', () => {
|
|
110
|
+
it('should convert legacy string array sources to Record format', () => {
|
|
111
|
+
const legacy: LegacyProjectConfig = {
|
|
112
|
+
sources: ['source1', 'source2'],
|
|
113
|
+
links: ['iflow', 'claude']
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const result = normalizeProjectConfig(legacy);
|
|
117
|
+
|
|
118
|
+
expect(result.sources).toEqual({
|
|
119
|
+
source1: { skills: ['*'], commands: ['*'], agents: ['*'] },
|
|
120
|
+
source2: { skills: ['*'], commands: ['*'], agents: ['*'] }
|
|
121
|
+
});
|
|
122
|
+
expect(result.links).toEqual(['iflow', 'claude']);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should handle empty legacy sources array', () => {
|
|
126
|
+
const legacy: LegacyProjectConfig = {
|
|
127
|
+
sources: [],
|
|
128
|
+
links: []
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const result = normalizeProjectConfig(legacy);
|
|
132
|
+
|
|
133
|
+
expect(result.sources).toEqual({});
|
|
134
|
+
expect(result.links).toEqual([]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should preserve links from legacy config', () => {
|
|
138
|
+
const legacy: LegacyProjectConfig = {
|
|
139
|
+
sources: ['source1'],
|
|
140
|
+
links: ['iflow', 'claude', 'codebuddy']
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const result = normalizeProjectConfig(legacy);
|
|
144
|
+
|
|
145
|
+
expect(result.links).toEqual(['iflow', 'claude', 'codebuddy']);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('ProjectConfig passthrough', () => {
|
|
150
|
+
it('should return ProjectConfig as-is when already in new format', () => {
|
|
151
|
+
const newConfig: ProjectConfig = {
|
|
152
|
+
sources: {
|
|
153
|
+
source1: { skills: ['skill1'], commands: ['cmd1'], agents: ['agent1'] }
|
|
154
|
+
},
|
|
155
|
+
links: ['iflow']
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const result = normalizeProjectConfig(newConfig);
|
|
159
|
+
|
|
160
|
+
expect(result).toEqual(newConfig);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should handle partial selection in new format', () => {
|
|
164
|
+
const newConfig: ProjectConfig = {
|
|
165
|
+
sources: {
|
|
166
|
+
source1: { skills: ['skill1', 'skill2'], commands: [], agents: ['*'] }
|
|
167
|
+
},
|
|
168
|
+
links: ['claude']
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const result = normalizeProjectConfig(newConfig);
|
|
172
|
+
|
|
173
|
+
expect(result).toEqual(newConfig);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should handle multiple sources with different selections', () => {
|
|
177
|
+
const newConfig: ProjectConfig = {
|
|
178
|
+
sources: {
|
|
179
|
+
source1: { skills: ['*'], commands: ['*'], agents: ['*'] },
|
|
180
|
+
source2: { skills: ['skill-a'], commands: [], agents: ['agent-x'] }
|
|
181
|
+
},
|
|
182
|
+
links: ['iflow', 'opencode']
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const result = normalizeProjectConfig(newConfig);
|
|
186
|
+
|
|
187
|
+
expect(result).toEqual(newConfig);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('ExportConfig', () => {
|
|
193
|
+
describe('type structure', () => {
|
|
194
|
+
it('should allow creating ExportConfig with project config', () => {
|
|
195
|
+
const exportConfig: ExportConfig = {
|
|
196
|
+
version: '1.0.0',
|
|
197
|
+
type: 'project',
|
|
198
|
+
config: {
|
|
199
|
+
sources: {
|
|
200
|
+
source1: { skills: ['*'], commands: ['*'], agents: ['*'] }
|
|
201
|
+
},
|
|
202
|
+
links: ['iflow']
|
|
203
|
+
},
|
|
204
|
+
exportedAt: '2026-02-28T00:00:00Z'
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
expect(exportConfig.type).toBe('project');
|
|
208
|
+
expect(exportConfig.version).toBe('1.0.0');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('GlobalExportConfig', () => {
|
|
214
|
+
describe('type structure', () => {
|
|
215
|
+
it('should allow creating GlobalExportConfig with global config', () => {
|
|
216
|
+
const globalExportConfig: GlobalExportConfig = {
|
|
217
|
+
version: '1.0.0',
|
|
218
|
+
type: 'global',
|
|
219
|
+
config: {
|
|
220
|
+
sourcesDir: '~/.tools-cc/sources',
|
|
221
|
+
sources: {
|
|
222
|
+
source1: { type: 'git', url: 'https://github.com/example/skills' }
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
exportedAt: '2026-02-28T00:00:00Z'
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
expect(globalExportConfig.type).toBe('global');
|
|
229
|
+
expect(globalExportConfig.config.sourcesDir).toBe('~/.tools-cc/sources');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseSourcePath, buildSelectionFromPaths, ParsedSourcePath } from '../../src/utils/parsePath';
|
|
3
|
+
import { SourceSelection } from '../../src/types/config';
|
|
4
|
+
|
|
5
|
+
describe('parseSourcePath', () => {
|
|
6
|
+
describe('whole source', () => {
|
|
7
|
+
it('should parse source name only', () => {
|
|
8
|
+
const result = parseSourcePath('my-skills');
|
|
9
|
+
expect(result).toEqual({
|
|
10
|
+
sourceName: 'my-skills'
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should parse source name with hyphens', () => {
|
|
15
|
+
const result = parseSourcePath('my-awesome-skills');
|
|
16
|
+
expect(result).toEqual({
|
|
17
|
+
sourceName: 'my-awesome-skills'
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should parse source name with underscores', () => {
|
|
22
|
+
const result = parseSourcePath('my_skills_repo');
|
|
23
|
+
expect(result).toEqual({
|
|
24
|
+
sourceName: 'my_skills_repo'
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('specific skill', () => {
|
|
30
|
+
it('should parse skill path', () => {
|
|
31
|
+
const result = parseSourcePath('my-skills/skills/a-skill');
|
|
32
|
+
expect(result).toEqual({
|
|
33
|
+
sourceName: 'my-skills',
|
|
34
|
+
type: 'skills',
|
|
35
|
+
itemName: 'a-skill'
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should parse skill path with underscores', () => {
|
|
40
|
+
const result = parseSourcePath('my-skills/skills/my_awesome_skill');
|
|
41
|
+
expect(result).toEqual({
|
|
42
|
+
sourceName: 'my-skills',
|
|
43
|
+
type: 'skills',
|
|
44
|
+
itemName: 'my_awesome_skill'
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('specific command', () => {
|
|
50
|
+
it('should parse command path', () => {
|
|
51
|
+
const result = parseSourcePath('my-skills/commands/test');
|
|
52
|
+
expect(result).toEqual({
|
|
53
|
+
sourceName: 'my-skills',
|
|
54
|
+
type: 'commands',
|
|
55
|
+
itemName: 'test'
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should parse command path with hyphens', () => {
|
|
60
|
+
const result = parseSourcePath('tools/commands/my-command');
|
|
61
|
+
expect(result).toEqual({
|
|
62
|
+
sourceName: 'tools',
|
|
63
|
+
type: 'commands',
|
|
64
|
+
itemName: 'my-command'
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('specific agent', () => {
|
|
70
|
+
it('should parse agent path', () => {
|
|
71
|
+
const result = parseSourcePath('other/agents/reviewer');
|
|
72
|
+
expect(result).toEqual({
|
|
73
|
+
sourceName: 'other',
|
|
74
|
+
type: 'agents',
|
|
75
|
+
itemName: 'reviewer'
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('invalid paths', () => {
|
|
81
|
+
it('should return sourceName only for invalid type', () => {
|
|
82
|
+
const result = parseSourcePath('my-skills/invalid/item');
|
|
83
|
+
expect(result).toEqual({
|
|
84
|
+
sourceName: 'my-skills'
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should return sourceName only for incomplete path', () => {
|
|
89
|
+
const result = parseSourcePath('my-skills/skills');
|
|
90
|
+
expect(result).toEqual({
|
|
91
|
+
sourceName: 'my-skills'
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle empty string', () => {
|
|
96
|
+
const result = parseSourcePath('');
|
|
97
|
+
expect(result).toEqual({
|
|
98
|
+
sourceName: ''
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should handle path with too many segments', () => {
|
|
103
|
+
const result = parseSourcePath('my-skills/skills/a/b/c');
|
|
104
|
+
expect(result).toEqual({
|
|
105
|
+
sourceName: 'my-skills',
|
|
106
|
+
type: 'skills',
|
|
107
|
+
itemName: 'a'
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('buildSelectionFromPaths', () => {
|
|
114
|
+
it('should build selection from single skill path', () => {
|
|
115
|
+
const paths = ['my-skills/skills/a-skill'];
|
|
116
|
+
const result = buildSelectionFromPaths(paths);
|
|
117
|
+
|
|
118
|
+
expect(result).toEqual({
|
|
119
|
+
'my-skills': {
|
|
120
|
+
skills: ['a-skill'],
|
|
121
|
+
commands: [],
|
|
122
|
+
agents: []
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should build selection from multiple skills in same source', () => {
|
|
128
|
+
const paths = ['my-skills/skills/a', 'my-skills/skills/b'];
|
|
129
|
+
const result = buildSelectionFromPaths(paths);
|
|
130
|
+
|
|
131
|
+
expect(result).toEqual({
|
|
132
|
+
'my-skills': {
|
|
133
|
+
skills: ['a', 'b'],
|
|
134
|
+
commands: [],
|
|
135
|
+
agents: []
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should build selection from mixed types', () => {
|
|
141
|
+
const paths = [
|
|
142
|
+
'my-skills/skills/a',
|
|
143
|
+
'my-skills/skills/b',
|
|
144
|
+
'my-skills/commands/test'
|
|
145
|
+
];
|
|
146
|
+
const result = buildSelectionFromPaths(paths);
|
|
147
|
+
|
|
148
|
+
expect(result).toEqual({
|
|
149
|
+
'my-skills': {
|
|
150
|
+
skills: ['a', 'b'],
|
|
151
|
+
commands: ['test'],
|
|
152
|
+
agents: []
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should build selection from multiple sources', () => {
|
|
158
|
+
const paths = [
|
|
159
|
+
'source-a/skills/skill1',
|
|
160
|
+
'source-b/commands/cmd1',
|
|
161
|
+
'source-b/agents/agent1'
|
|
162
|
+
];
|
|
163
|
+
const result = buildSelectionFromPaths(paths);
|
|
164
|
+
|
|
165
|
+
expect(result).toEqual({
|
|
166
|
+
'source-a': {
|
|
167
|
+
skills: ['skill1'],
|
|
168
|
+
commands: [],
|
|
169
|
+
agents: []
|
|
170
|
+
},
|
|
171
|
+
'source-b': {
|
|
172
|
+
skills: [],
|
|
173
|
+
commands: ['cmd1'],
|
|
174
|
+
agents: ['agent1']
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should handle whole source path (no type/item)', () => {
|
|
180
|
+
const paths = ['my-skills'];
|
|
181
|
+
const result = buildSelectionFromPaths(paths);
|
|
182
|
+
|
|
183
|
+
expect(result).toEqual({
|
|
184
|
+
'my-skills': {
|
|
185
|
+
skills: ['*'],
|
|
186
|
+
commands: ['*'],
|
|
187
|
+
agents: ['*']
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should handle mixed whole and partial source paths', () => {
|
|
193
|
+
const paths = [
|
|
194
|
+
'source-a',
|
|
195
|
+
'source-b/skills/skill1',
|
|
196
|
+
'source-b/commands/cmd1'
|
|
197
|
+
];
|
|
198
|
+
const result = buildSelectionFromPaths(paths);
|
|
199
|
+
|
|
200
|
+
expect(result).toEqual({
|
|
201
|
+
'source-a': {
|
|
202
|
+
skills: ['*'],
|
|
203
|
+
commands: ['*'],
|
|
204
|
+
agents: ['*']
|
|
205
|
+
},
|
|
206
|
+
'source-b': {
|
|
207
|
+
skills: ['skill1'],
|
|
208
|
+
commands: ['cmd1'],
|
|
209
|
+
agents: []
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should handle empty array', () => {
|
|
215
|
+
const result = buildSelectionFromPaths([]);
|
|
216
|
+
expect(result).toEqual({});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should deduplicate items', () => {
|
|
220
|
+
const paths = [
|
|
221
|
+
'my-skills/skills/a',
|
|
222
|
+
'my-skills/skills/a',
|
|
223
|
+
'my-skills/skills/b'
|
|
224
|
+
];
|
|
225
|
+
const result = buildSelectionFromPaths(paths);
|
|
226
|
+
|
|
227
|
+
expect(result).toEqual({
|
|
228
|
+
'my-skills': {
|
|
229
|
+
skills: ['a', 'b'],
|
|
230
|
+
commands: [],
|
|
231
|
+
agents: []
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|