tools-cc 1.0.0

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.
Files changed (49) hide show
  1. package/dist/commands/config.d.ts +3 -0
  2. package/dist/commands/config.js +56 -0
  3. package/dist/commands/help.d.ts +1 -0
  4. package/dist/commands/help.js +84 -0
  5. package/dist/commands/source.d.ts +4 -0
  6. package/dist/commands/source.js +72 -0
  7. package/dist/commands/use.d.ts +6 -0
  8. package/dist/commands/use.js +133 -0
  9. package/dist/core/config.d.ts +5 -0
  10. package/dist/core/config.js +37 -0
  11. package/dist/core/manifest.d.ts +3 -0
  12. package/dist/core/manifest.js +56 -0
  13. package/dist/core/project.d.ts +4 -0
  14. package/dist/core/project.js +118 -0
  15. package/dist/core/source.d.ts +6 -0
  16. package/dist/core/source.js +86 -0
  17. package/dist/core/symlink.d.ts +3 -0
  18. package/dist/core/symlink.js +56 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +165 -0
  21. package/dist/types/config.d.ts +23 -0
  22. package/dist/types/config.js +2 -0
  23. package/dist/types/index.d.ts +1 -0
  24. package/dist/types/index.js +17 -0
  25. package/dist/utils/path.d.ts +8 -0
  26. package/dist/utils/path.js +22 -0
  27. package/docs/plans/2026-02-25-tools-cc-design.md +195 -0
  28. package/docs/plans/2026-02-25-tools-cc-impl.md +1600 -0
  29. package/package.json +44 -0
  30. package/readme.md +182 -0
  31. package/src/commands/config.ts +50 -0
  32. package/src/commands/help.ts +79 -0
  33. package/src/commands/source.ts +63 -0
  34. package/src/commands/use.ts +147 -0
  35. package/src/core/config.ts +37 -0
  36. package/src/core/manifest.ts +57 -0
  37. package/src/core/project.ts +136 -0
  38. package/src/core/source.ts +100 -0
  39. package/src/core/symlink.ts +56 -0
  40. package/src/index.ts +186 -0
  41. package/src/types/config.ts +27 -0
  42. package/src/types/index.ts +1 -0
  43. package/src/utils/path.ts +18 -0
  44. package/tests/core/config.test.ts +37 -0
  45. package/tests/core/manifest.test.ts +37 -0
  46. package/tests/core/project.test.ts +50 -0
  47. package/tests/core/source.test.ts +75 -0
  48. package/tests/core/symlink.test.ts +39 -0
  49. package/tsconfig.json +17 -0
@@ -0,0 +1,136 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { ProjectConfig } from '../types';
4
+ import { loadManifest } from './manifest';
5
+ import { getToolsccDir, getProjectConfigPath } from '../utils/path';
6
+
7
+ export async function initProject(projectDir: string): Promise<void> {
8
+ const toolsccDir = getToolsccDir(projectDir);
9
+ const configFile = getProjectConfigPath(projectDir);
10
+
11
+ // Create .toolscc directory structure
12
+ await fs.ensureDir(path.join(toolsccDir, 'skills'));
13
+ await fs.ensureDir(path.join(toolsccDir, 'commands'));
14
+ await fs.ensureDir(path.join(toolsccDir, 'agents'));
15
+
16
+ // Create project config if not exists
17
+ if (!(await fs.pathExists(configFile))) {
18
+ const config: ProjectConfig = {
19
+ sources: [],
20
+ links: []
21
+ };
22
+ await fs.writeJson(configFile, config, { spaces: 2 });
23
+ }
24
+ }
25
+
26
+ export async function useSource(
27
+ sourceName: string,
28
+ sourceDir: string,
29
+ projectDir: string
30
+ ): Promise<void> {
31
+ // Input validation
32
+ if (!sourceName || !sourceName.trim()) {
33
+ throw new Error('Source name is required');
34
+ }
35
+
36
+ // Check source directory existence
37
+ if (!(await fs.pathExists(sourceDir))) {
38
+ throw new Error(`Source directory does not exist: ${sourceDir}`);
39
+ }
40
+
41
+ const toolsccDir = getToolsccDir(projectDir);
42
+ const manifest = await loadManifest(sourceDir);
43
+
44
+ // Ensure project is initialized
45
+ await initProject(projectDir);
46
+
47
+ // Copy/link skills (flattened with prefix)
48
+ const sourceSkillsDir = path.join(sourceDir, 'skills');
49
+ if (await fs.pathExists(sourceSkillsDir)) {
50
+ const skills = await fs.readdir(sourceSkillsDir);
51
+ for (const skill of skills) {
52
+ const srcPath = path.join(sourceSkillsDir, skill);
53
+ const destPath = path.join(toolsccDir, 'skills', `${sourceName}-${skill}`);
54
+
55
+ // Remove existing if exists
56
+ await fs.remove(destPath);
57
+
58
+ // Copy directory
59
+ await fs.copy(srcPath, destPath);
60
+ }
61
+ }
62
+
63
+ // Copy commands (in subdirectory by source name)
64
+ const sourceCommandsDir = path.join(sourceDir, 'commands');
65
+ if (await fs.pathExists(sourceCommandsDir)) {
66
+ const destDir = path.join(toolsccDir, 'commands', sourceName);
67
+ await fs.remove(destDir);
68
+ await fs.copy(sourceCommandsDir, destDir);
69
+ }
70
+
71
+ // Copy agents (in subdirectory by source name)
72
+ const sourceAgentsDir = path.join(sourceDir, 'agents');
73
+ if (await fs.pathExists(sourceAgentsDir)) {
74
+ const destDir = path.join(toolsccDir, 'agents', sourceName);
75
+ await fs.remove(destDir);
76
+ await fs.copy(sourceAgentsDir, destDir);
77
+ }
78
+
79
+ // Update project config
80
+ const configFile = getProjectConfigPath(projectDir);
81
+ const config: ProjectConfig = await fs.readJson(configFile);
82
+ if (!config.sources.includes(sourceName)) {
83
+ config.sources.push(sourceName);
84
+ }
85
+ await fs.writeJson(configFile, config, { spaces: 2 });
86
+ }
87
+
88
+ export async function unuseSource(sourceName: string, projectDir: string): Promise<void> {
89
+ // Input validation
90
+ if (!sourceName || !sourceName.trim()) {
91
+ throw new Error('Source name is required');
92
+ }
93
+
94
+ const toolsccDir = getToolsccDir(projectDir);
95
+ const configFile = getProjectConfigPath(projectDir);
96
+
97
+ // Remove skills with prefix
98
+ const skillsDir = path.join(toolsccDir, 'skills');
99
+ if (await fs.pathExists(skillsDir)) {
100
+ const skills = await fs.readdir(skillsDir);
101
+ for (const skill of skills) {
102
+ if (skill.startsWith(`${sourceName}-`)) {
103
+ await fs.remove(path.join(skillsDir, skill));
104
+ }
105
+ }
106
+ }
107
+
108
+ // Remove commands subdirectory
109
+ await fs.remove(path.join(toolsccDir, 'commands', sourceName));
110
+
111
+ // Remove agents subdirectory
112
+ await fs.remove(path.join(toolsccDir, 'agents', sourceName));
113
+
114
+ // Update project config with error handling
115
+ let config: ProjectConfig;
116
+ try {
117
+ config = await fs.readJson(configFile);
118
+ } catch (error) {
119
+ // If config file doesn't exist or is invalid, nothing to update
120
+ return;
121
+ }
122
+
123
+ config.sources = config.sources.filter(s => s !== sourceName);
124
+ await fs.writeJson(configFile, config, { spaces: 2 });
125
+ }
126
+
127
+ export async function listUsedSources(projectDir: string): Promise<string[]> {
128
+ const configFile = getProjectConfigPath(projectDir);
129
+
130
+ if (!(await fs.pathExists(configFile))) {
131
+ return [];
132
+ }
133
+
134
+ const config: ProjectConfig = await fs.readJson(configFile);
135
+ return config.sources;
136
+ }
@@ -0,0 +1,100 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { execSync } from 'child_process';
4
+ import { loadGlobalConfig, saveGlobalConfig } from './config';
5
+ import { SourceConfig } from '../types';
6
+
7
+ export async function addSource(
8
+ name: string,
9
+ sourcePath: string,
10
+ configDir: string
11
+ ): Promise<SourceConfig> {
12
+ const config = await loadGlobalConfig(configDir);
13
+
14
+ // 判断是 git url 还是本地路径
15
+ const isGit = sourcePath.startsWith('http') || sourcePath.startsWith('git@');
16
+
17
+ let sourceConfig: SourceConfig;
18
+
19
+ if (isGit) {
20
+ // Clone git repo
21
+ const cloneDir = path.join(config.sourcesDir, name);
22
+ console.log(`Cloning ${sourcePath} to ${cloneDir}...`);
23
+
24
+ await fs.ensureDir(config.sourcesDir);
25
+ execSync(`git clone ${sourcePath} "${cloneDir}"`, { stdio: 'inherit' });
26
+
27
+ sourceConfig = { type: 'git', url: sourcePath };
28
+ } else {
29
+ // 本地路径
30
+ const absolutePath = path.resolve(sourcePath);
31
+ if (!(await fs.pathExists(absolutePath))) {
32
+ throw new Error(`Path does not exist: ${absolutePath}`);
33
+ }
34
+ sourceConfig = { type: 'local', path: absolutePath };
35
+ }
36
+
37
+ config.sources[name] = sourceConfig;
38
+ await saveGlobalConfig(config, configDir);
39
+
40
+ return sourceConfig;
41
+ }
42
+
43
+ export async function listSources(configDir: string): Promise<Record<string, SourceConfig>> {
44
+ const config = await loadGlobalConfig(configDir);
45
+ return config.sources;
46
+ }
47
+
48
+ export async function removeSource(name: string, configDir: string): Promise<void> {
49
+ const config = await loadGlobalConfig(configDir);
50
+
51
+ if (!config.sources[name]) {
52
+ throw new Error(`Source not found: ${name}`);
53
+ }
54
+
55
+ const source = config.sources[name];
56
+
57
+ // 如果是 git 类型,清理克隆目录
58
+ if (source.type === 'git') {
59
+ const cloneDir = path.join(config.sourcesDir, name);
60
+ if (await fs.pathExists(cloneDir)) {
61
+ console.log(`Removing cloned directory: ${cloneDir}`);
62
+ await fs.remove(cloneDir);
63
+ }
64
+ }
65
+
66
+ delete config.sources[name];
67
+ await saveGlobalConfig(config, configDir);
68
+ }
69
+
70
+ export async function updateSource(name: string, configDir: string): Promise<void> {
71
+ const config = await loadGlobalConfig(configDir);
72
+ const source = config.sources[name];
73
+
74
+ if (!source) {
75
+ throw new Error(`Source not found: ${name}`);
76
+ }
77
+
78
+ if (source.type === 'git') {
79
+ const cloneDir = path.join(config.sourcesDir, name);
80
+ console.log(`Updating ${name}...`);
81
+ execSync(`git -C "${cloneDir}" pull`, { stdio: 'inherit' });
82
+ } else {
83
+ console.log(`Source ${name} is local, no update needed.`);
84
+ }
85
+ }
86
+
87
+ export async function getSourcePath(name: string, configDir: string): Promise<string> {
88
+ const config = await loadGlobalConfig(configDir);
89
+ const source = config.sources[name];
90
+
91
+ if (!source) {
92
+ throw new Error(`Source not found: ${name}`);
93
+ }
94
+
95
+ if (source.type === 'local') {
96
+ return source.path!;
97
+ }
98
+
99
+ return path.join(config.sourcesDir, name);
100
+ }
@@ -0,0 +1,56 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ export async function createSymlink(
5
+ target: string,
6
+ linkPath: string,
7
+ force: boolean = false
8
+ ): Promise<void> {
9
+ // 如果目标已存在
10
+ if (await fs.pathExists(linkPath)) {
11
+ if (!force) {
12
+ throw new Error(`Path already exists: ${linkPath}. Use force=true to overwrite.`);
13
+ }
14
+
15
+ // 检查是否已经是符号链接
16
+ if (await isSymlink(linkPath)) {
17
+ await fs.remove(linkPath);
18
+ } else {
19
+ // 是真实目录,删除
20
+ await fs.remove(linkPath);
21
+ }
22
+ }
23
+
24
+ // 确保目标存在
25
+ if (!(await fs.pathExists(target))) {
26
+ throw new Error(`Target does not exist: ${target}`);
27
+ }
28
+
29
+ // 创建符号链接
30
+ // Windows: 使用 junction (不需要管理员权限)
31
+ // Linux/macOS: 使用 symlink
32
+ const targetPath = path.resolve(target);
33
+
34
+ if (process.platform === 'win32') {
35
+ // Windows: 使用 junction
36
+ await fs.ensureSymlink(targetPath, linkPath, 'junction');
37
+ } else {
38
+ // Linux/macOS: 使用 dir symlink
39
+ await fs.ensureSymlink(targetPath, linkPath, 'dir');
40
+ }
41
+ }
42
+
43
+ export async function removeSymlink(linkPath: string): Promise<void> {
44
+ if (await isSymlink(linkPath)) {
45
+ await fs.remove(linkPath);
46
+ }
47
+ }
48
+
49
+ export async function isSymlink(path: string): Promise<boolean> {
50
+ try {
51
+ const stats = await fs.lstat(path);
52
+ return stats.isSymbolicLink();
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
package/src/index.ts ADDED
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { handleConfigSet, handleConfigGet, handleConfigList } from './commands/config';
5
+ import { handleSourceAdd, handleSourceList, handleSourceRemove, handleSourceUpdate } from './commands/source';
6
+ import { handleUse, handleList, handleRemove, handleStatus } from './commands/use';
7
+ import { showHelp } from './commands/help';
8
+ import { GLOBAL_CONFIG_DIR } from './utils/path';
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name('tools-cc')
14
+ .description('CLI tool for managing skills/commands/agents across multiple AI coding tools')
15
+ .version('0.0.1');
16
+
17
+ // Source management (shortcut options)
18
+ program
19
+ .option('-s, --source <command> [args...]', 'Source management (shortcut)')
20
+ .option('-c, --config <command> [args...]', 'Config management (shortcut)');
21
+
22
+ // Source subcommands (full command version)
23
+ const sourceCmd = program
24
+ .command('sources')
25
+ .description('Source management');
26
+
27
+ sourceCmd
28
+ .command('add <name> <path-or-url>')
29
+ .description('Add a source')
30
+ .action(async (name: string, pathOrUrl: string) => {
31
+ await handleSourceAdd(name, pathOrUrl);
32
+ });
33
+
34
+ sourceCmd
35
+ .command('list')
36
+ .alias('ls')
37
+ .description('List all sources')
38
+ .action(async () => {
39
+ await handleSourceList();
40
+ });
41
+
42
+ sourceCmd
43
+ .command('remove <name>')
44
+ .alias('rm')
45
+ .description('Remove a source')
46
+ .action(async (name: string) => {
47
+ await handleSourceRemove(name);
48
+ });
49
+
50
+ sourceCmd
51
+ .command('update [name]')
52
+ .alias('up')
53
+ .description('Update source(s)')
54
+ .action(async (name?: string) => {
55
+ await handleSourceUpdate(name);
56
+ });
57
+
58
+ // Config subcommands (full command version)
59
+ const configCmd = program
60
+ .command('config')
61
+ .description('Config management');
62
+
63
+ configCmd
64
+ .command('set <key> <value>')
65
+ .description('Set a config value')
66
+ .action(async (key: string, value: string) => {
67
+ await handleConfigSet(key, value);
68
+ });
69
+
70
+ configCmd
71
+ .command('get <key>')
72
+ .description('Get a config value')
73
+ .action(async (key: string) => {
74
+ await handleConfigGet(key);
75
+ });
76
+
77
+ configCmd
78
+ .command('list')
79
+ .description('Show full configuration')
80
+ .action(async () => {
81
+ await handleConfigList();
82
+ });
83
+
84
+ // Project commands
85
+ program
86
+ .command('use [sources...]')
87
+ .description('Use sources in current project')
88
+ .option('-p, --projects <tools...>', 'Tools to link (iflow, claude, codebuddy, opencode)')
89
+ .action(async (sources: string[], options) => {
90
+ await handleUse(sources, options);
91
+ });
92
+
93
+ program
94
+ .command('list')
95
+ .description('List sources in use')
96
+ .action(async () => {
97
+ await handleList();
98
+ });
99
+
100
+ program
101
+ .command('rm <source>')
102
+ .description('Remove a source from project')
103
+ .action(async (source: string) => {
104
+ await handleRemove(source);
105
+ });
106
+
107
+ program
108
+ .command('status')
109
+ .description('Show project status')
110
+ .action(async () => {
111
+ await handleStatus();
112
+ });
113
+
114
+ // Help command
115
+ program
116
+ .command('help')
117
+ .description('Show bilingual help information')
118
+ .action(() => {
119
+ showHelp();
120
+ });
121
+
122
+ // Main action handler for -s and -c options
123
+ program
124
+ .action(async (options) => {
125
+ // Handle -s/--source
126
+ if (options.source) {
127
+ const [cmd, ...args] = options.source;
128
+ switch (cmd) {
129
+ case 'add':
130
+ if (args.length < 2) {
131
+ console.log('Usage: tools-cc -s add <name> <path-or-url>');
132
+ return;
133
+ }
134
+ await handleSourceAdd(args[0], args[1]);
135
+ break;
136
+ case 'list':
137
+ case 'ls':
138
+ await handleSourceList();
139
+ break;
140
+ case 'remove':
141
+ case 'rm':
142
+ if (args.length < 1) {
143
+ console.log('Usage: tools-cc -s remove <name>');
144
+ return;
145
+ }
146
+ await handleSourceRemove(args[0]);
147
+ break;
148
+ case 'update':
149
+ case 'up':
150
+ await handleSourceUpdate(args[0]);
151
+ break;
152
+ default:
153
+ console.log(`Unknown source command: ${cmd}`);
154
+ }
155
+ return;
156
+ }
157
+
158
+ // Handle -c/--config
159
+ if (options.config) {
160
+ const [cmd, ...args] = options.config;
161
+ switch (cmd) {
162
+ case 'set':
163
+ if (args.length < 2) {
164
+ console.log('Usage: tools-cc -c set <key> <value>');
165
+ return;
166
+ }
167
+ await handleConfigSet(args[0], args[1]);
168
+ break;
169
+ case 'get':
170
+ if (args.length < 1) {
171
+ console.log('Usage: tools-cc -c get <key>');
172
+ return;
173
+ }
174
+ await handleConfigGet(args[0]);
175
+ break;
176
+ default:
177
+ console.log(`Unknown config command: ${cmd}`);
178
+ }
179
+ return;
180
+ }
181
+
182
+ // No options provided, show help
183
+ program.outputHelp();
184
+ });
185
+
186
+ program.parseAsync();
@@ -0,0 +1,27 @@
1
+ export interface SourceConfig {
2
+ type: 'git' | 'local';
3
+ url?: string;
4
+ path?: string;
5
+ }
6
+
7
+ export interface GlobalConfig {
8
+ sourcesDir: string;
9
+ sources: Record<string, SourceConfig>;
10
+ }
11
+
12
+ export interface ProjectConfig {
13
+ sources: string[];
14
+ links: string[];
15
+ }
16
+
17
+ export interface Manifest {
18
+ name: string;
19
+ version: string;
20
+ skills?: string[];
21
+ commands?: string[];
22
+ agents?: string[];
23
+ }
24
+
25
+ export interface ToolConfig {
26
+ linkName: string;
27
+ }
@@ -0,0 +1 @@
1
+ export * from './config';
@@ -0,0 +1,18 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+
4
+ export const GLOBAL_CONFIG_DIR = path.join(os.homedir(), '.tools-cc');
5
+ export const GLOBAL_CONFIG_FILE = path.join(GLOBAL_CONFIG_DIR, 'config.json');
6
+
7
+ export const DEFAULT_CONFIG = {
8
+ sourcesDir: path.join(GLOBAL_CONFIG_DIR, 'sources'),
9
+ sources: {}
10
+ };
11
+
12
+ export function getProjectConfigPath(projectDir: string): string {
13
+ return path.join(projectDir, 'tools-cc.json');
14
+ }
15
+
16
+ export function getToolsccDir(projectDir: string): string {
17
+ return path.join(projectDir, '.toolscc');
18
+ }
@@ -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
+ });
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { initProject, useSource, unuseSource, listUsedSources } from '../../src/core/project';
5
+
6
+ describe('Project Module', () => {
7
+ const testProjectDir = path.join(__dirname, '../fixtures/test-project');
8
+ const testSourceDir = path.join(__dirname, '../fixtures/test-source');
9
+
10
+ beforeEach(async () => {
11
+ await fs.ensureDir(testProjectDir);
12
+ await fs.ensureDir(path.join(testSourceDir, 'skills', 'test-skill'));
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await fs.remove(testProjectDir);
17
+ await fs.remove(testSourceDir);
18
+ });
19
+
20
+ it('should initialize project with .toolscc directory', async () => {
21
+ await initProject(testProjectDir);
22
+ expect(await fs.pathExists(path.join(testProjectDir, '.toolscc'))).toBe(true);
23
+ expect(await fs.pathExists(path.join(testProjectDir, 'tools-cc.json'))).toBe(true);
24
+ });
25
+
26
+ it('should use source and copy components', async () => {
27
+ await initProject(testProjectDir);
28
+ await useSource('test-source', testSourceDir, testProjectDir);
29
+
30
+ // skills should be flattened with prefix
31
+ expect(await fs.pathExists(path.join(testProjectDir, '.toolscc', 'skills', 'test-source-test-skill'))).toBe(true);
32
+ });
33
+
34
+ it('should unuse source and remove components', async () => {
35
+ await initProject(testProjectDir);
36
+ await useSource('test-source', testSourceDir, testProjectDir);
37
+ await unuseSource('test-source', testProjectDir);
38
+
39
+ const config = await fs.readJson(path.join(testProjectDir, 'tools-cc.json'));
40
+ expect(config.sources).not.toContain('test-source');
41
+ });
42
+
43
+ it('should list used sources', async () => {
44
+ await initProject(testProjectDir);
45
+ await useSource('test-source', testSourceDir, testProjectDir);
46
+
47
+ const sources = await listUsedSources(testProjectDir);
48
+ expect(sources).toContain('test-source');
49
+ });
50
+ });