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.
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.js +56 -0
- package/dist/commands/help.d.ts +1 -0
- package/dist/commands/help.js +84 -0
- package/dist/commands/source.d.ts +4 -0
- package/dist/commands/source.js +72 -0
- package/dist/commands/use.d.ts +6 -0
- package/dist/commands/use.js +133 -0
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.js +37 -0
- package/dist/core/manifest.d.ts +3 -0
- package/dist/core/manifest.js +56 -0
- package/dist/core/project.d.ts +4 -0
- package/dist/core/project.js +118 -0
- package/dist/core/source.d.ts +6 -0
- package/dist/core/source.js +86 -0
- package/dist/core/symlink.d.ts +3 -0
- package/dist/core/symlink.js +56 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +165 -0
- package/dist/types/config.d.ts +23 -0
- package/dist/types/config.js +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +17 -0
- package/dist/utils/path.d.ts +8 -0
- package/dist/utils/path.js +22 -0
- package/docs/plans/2026-02-25-tools-cc-design.md +195 -0
- package/docs/plans/2026-02-25-tools-cc-impl.md +1600 -0
- package/package.json +44 -0
- package/readme.md +182 -0
- package/src/commands/config.ts +50 -0
- package/src/commands/help.ts +79 -0
- package/src/commands/source.ts +63 -0
- package/src/commands/use.ts +147 -0
- package/src/core/config.ts +37 -0
- package/src/core/manifest.ts +57 -0
- package/src/core/project.ts +136 -0
- package/src/core/source.ts +100 -0
- package/src/core/symlink.ts +56 -0
- package/src/index.ts +186 -0
- package/src/types/config.ts +27 -0
- package/src/types/index.ts +1 -0
- package/src/utils/path.ts +18 -0
- package/tests/core/config.test.ts +37 -0
- package/tests/core/manifest.test.ts +37 -0
- package/tests/core/project.test.ts +50 -0
- package/tests/core/source.test.ts +75 -0
- package/tests/core/symlink.test.ts +39 -0
- 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
|
+
});
|