tools-cc 1.0.4 → 1.0.6
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 +20 -0
- package/CHANGELOG_en.md +18 -0
- package/README.md +63 -6
- package/dist/commands/export.d.ts +7 -0
- package/dist/commands/export.js +57 -0
- package/dist/commands/help.js +3 -2
- package/dist/commands/use.d.ts +18 -2
- package/dist/commands/use.js +211 -45
- package/dist/core/project.d.ts +10 -1
- package/dist/core/project.js +134 -17
- package/dist/index.js +12 -1
- package/dist/types/config.d.ts +45 -0
- package/dist/types/config.js +32 -0
- package/dist/utils/parsePath.d.ts +31 -0
- package/dist/utils/parsePath.js +86 -0
- package/package.json +5 -2
- package/src/commands/export.ts +60 -0
- package/src/commands/help.ts +3 -2
- package/src/commands/use.ts +261 -45
- package/src/core/project.ts +158 -18
- package/src/index.ts +209 -198
- package/src/types/config.ts +75 -0
- package/src/utils/parsePath.ts +108 -0
- package/docs/plans/2026-02-25-tools-cc-design.md +0 -195
- package/docs/plans/2026-02-25-tools-cc-impl.md +0 -1600
- package/tests/core/config.test.ts +0 -37
- package/tests/core/manifest.test.ts +0 -37
- package/tests/core/project.test.ts +0 -50
- package/tests/core/source.test.ts +0 -75
- package/tests/core/symlink.test.ts +0 -39
package/dist/core/project.js
CHANGED
|
@@ -7,10 +7,21 @@ exports.initProject = initProject;
|
|
|
7
7
|
exports.useSource = useSource;
|
|
8
8
|
exports.unuseSource = unuseSource;
|
|
9
9
|
exports.listUsedSources = listUsedSources;
|
|
10
|
+
exports.exportProjectConfig = exportProjectConfig;
|
|
11
|
+
exports.importProjectConfig = importProjectConfig;
|
|
10
12
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
11
13
|
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const types_1 = require("../types");
|
|
12
15
|
const manifest_1 = require("./manifest");
|
|
13
16
|
const path_2 = require("../utils/path");
|
|
17
|
+
/**
|
|
18
|
+
* 默认选择配置 - 导入所有内容
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_SELECTION = {
|
|
21
|
+
skills: ['*'],
|
|
22
|
+
commands: ['*'],
|
|
23
|
+
agents: ['*']
|
|
24
|
+
};
|
|
14
25
|
async function initProject(projectDir) {
|
|
15
26
|
const toolsccDir = (0, path_2.getToolsccDir)(projectDir);
|
|
16
27
|
const configFile = (0, path_2.getProjectConfigPath)(projectDir);
|
|
@@ -21,13 +32,37 @@ async function initProject(projectDir) {
|
|
|
21
32
|
// Create project config if not exists
|
|
22
33
|
if (!(await fs_extra_1.default.pathExists(configFile))) {
|
|
23
34
|
const config = {
|
|
24
|
-
sources:
|
|
35
|
+
sources: {},
|
|
25
36
|
links: []
|
|
26
37
|
};
|
|
27
38
|
await fs_extra_1.default.writeJson(configFile, config, { spaces: 2 });
|
|
28
39
|
}
|
|
29
40
|
}
|
|
30
|
-
|
|
41
|
+
/**
|
|
42
|
+
* 读取项目配置,自动处理新旧格式
|
|
43
|
+
*/
|
|
44
|
+
async function readProjectConfig(configFile) {
|
|
45
|
+
const rawConfig = await fs_extra_1.default.readJson(configFile);
|
|
46
|
+
return (0, types_1.normalizeProjectConfig)(rawConfig);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 获取源名称列表(兼容新旧格式)
|
|
50
|
+
*/
|
|
51
|
+
function getSourceNames(config) {
|
|
52
|
+
return Object.keys(config.sources);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 检查是否应该复制某项(根据选择配置)
|
|
56
|
+
*/
|
|
57
|
+
function shouldInclude(itemName, selection) {
|
|
58
|
+
// 如果选择包含通配符,包含所有项
|
|
59
|
+
if (selection.includes('*')) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
// 否则检查是否在选择列表中
|
|
63
|
+
return selection.includes(itemName);
|
|
64
|
+
}
|
|
65
|
+
async function useSource(sourceName, sourceDir, projectDir, selection) {
|
|
31
66
|
// Input validation
|
|
32
67
|
if (!sourceName || !sourceName.trim()) {
|
|
33
68
|
throw new Error('Source name is required');
|
|
@@ -40,11 +75,17 @@ async function useSource(sourceName, sourceDir, projectDir) {
|
|
|
40
75
|
const manifest = await (0, manifest_1.loadManifest)(sourceDir);
|
|
41
76
|
// Ensure project is initialized
|
|
42
77
|
await initProject(projectDir);
|
|
78
|
+
// 使用传入的选择配置或默认配置
|
|
79
|
+
const effectiveSelection = selection ?? DEFAULT_SELECTION;
|
|
43
80
|
// Copy/link skills (flattened with prefix)
|
|
44
81
|
const sourceSkillsDir = path_1.default.join(sourceDir, 'skills');
|
|
45
82
|
if (await fs_extra_1.default.pathExists(sourceSkillsDir)) {
|
|
46
83
|
const skills = await fs_extra_1.default.readdir(sourceSkillsDir);
|
|
47
84
|
for (const skill of skills) {
|
|
85
|
+
// 检查是否应该包含此 skill
|
|
86
|
+
if (!shouldInclude(skill, effectiveSelection.skills)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
48
89
|
const srcPath = path_1.default.join(sourceSkillsDir, skill);
|
|
49
90
|
const name = `${sourceName}` == `${skill}` ? skill : `${sourceName}-${skill}`;
|
|
50
91
|
const destPath = path_1.default.join(toolsccDir, 'skills', name);
|
|
@@ -57,23 +98,51 @@ async function useSource(sourceName, sourceDir, projectDir) {
|
|
|
57
98
|
// Copy commands (in subdirectory by source name)
|
|
58
99
|
const sourceCommandsDir = path_1.default.join(sourceDir, 'commands');
|
|
59
100
|
if (await fs_extra_1.default.pathExists(sourceCommandsDir)) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
101
|
+
// 检查是否有选择 commands
|
|
102
|
+
if (effectiveSelection.commands.includes('*')) {
|
|
103
|
+
// 复制所有 commands
|
|
104
|
+
const destDir = path_1.default.join(toolsccDir, 'commands', sourceName);
|
|
105
|
+
await fs_extra_1.default.remove(destDir);
|
|
106
|
+
await fs_extra_1.default.copy(sourceCommandsDir, destDir);
|
|
107
|
+
}
|
|
108
|
+
else if (effectiveSelection.commands.length > 0) {
|
|
109
|
+
// 只复制选中的 commands
|
|
110
|
+
const destDir = path_1.default.join(toolsccDir, 'commands', sourceName);
|
|
111
|
+
await fs_extra_1.default.ensureDir(destDir);
|
|
112
|
+
for (const cmdName of effectiveSelection.commands) {
|
|
113
|
+
const srcFile = path_1.default.join(sourceCommandsDir, `${cmdName}.md`);
|
|
114
|
+
if (await fs_extra_1.default.pathExists(srcFile)) {
|
|
115
|
+
await fs_extra_1.default.copy(srcFile, path_1.default.join(destDir, `${cmdName}.md`));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
63
119
|
}
|
|
64
120
|
// Copy agents (in subdirectory by source name)
|
|
65
121
|
const sourceAgentsDir = path_1.default.join(sourceDir, 'agents');
|
|
66
122
|
if (await fs_extra_1.default.pathExists(sourceAgentsDir)) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
123
|
+
// 检查是否有选择 agents
|
|
124
|
+
if (effectiveSelection.agents.includes('*')) {
|
|
125
|
+
// 复制所有 agents
|
|
126
|
+
const destDir = path_1.default.join(toolsccDir, 'agents', sourceName);
|
|
127
|
+
await fs_extra_1.default.remove(destDir);
|
|
128
|
+
await fs_extra_1.default.copy(sourceAgentsDir, destDir);
|
|
129
|
+
}
|
|
130
|
+
else if (effectiveSelection.agents.length > 0) {
|
|
131
|
+
// 只复制选中的 agents
|
|
132
|
+
const destDir = path_1.default.join(toolsccDir, 'agents', sourceName);
|
|
133
|
+
await fs_extra_1.default.ensureDir(destDir);
|
|
134
|
+
for (const agentName of effectiveSelection.agents) {
|
|
135
|
+
const srcFile = path_1.default.join(sourceAgentsDir, `${agentName}.md`);
|
|
136
|
+
if (await fs_extra_1.default.pathExists(srcFile)) {
|
|
137
|
+
await fs_extra_1.default.copy(srcFile, path_1.default.join(destDir, `${agentName}.md`));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
70
141
|
}
|
|
71
|
-
// Update project config
|
|
142
|
+
// Update project config - 保存实际使用的选择配置
|
|
72
143
|
const configFile = (0, path_2.getProjectConfigPath)(projectDir);
|
|
73
|
-
const config = await
|
|
74
|
-
|
|
75
|
-
config.sources.push(sourceName);
|
|
76
|
-
}
|
|
144
|
+
const config = await readProjectConfig(configFile);
|
|
145
|
+
config.sources[sourceName] = effectiveSelection;
|
|
77
146
|
await fs_extra_1.default.writeJson(configFile, config, { spaces: 2 });
|
|
78
147
|
}
|
|
79
148
|
async function unuseSource(sourceName, projectDir) {
|
|
@@ -100,13 +169,13 @@ async function unuseSource(sourceName, projectDir) {
|
|
|
100
169
|
// Update project config with error handling
|
|
101
170
|
let config;
|
|
102
171
|
try {
|
|
103
|
-
config = await
|
|
172
|
+
config = await readProjectConfig(configFile);
|
|
104
173
|
}
|
|
105
174
|
catch (error) {
|
|
106
175
|
// If config file doesn't exist or is invalid, nothing to update
|
|
107
176
|
return;
|
|
108
177
|
}
|
|
109
|
-
|
|
178
|
+
delete config.sources[sourceName];
|
|
110
179
|
await fs_extra_1.default.writeJson(configFile, config, { spaces: 2 });
|
|
111
180
|
}
|
|
112
181
|
async function listUsedSources(projectDir) {
|
|
@@ -114,6 +183,54 @@ async function listUsedSources(projectDir) {
|
|
|
114
183
|
if (!(await fs_extra_1.default.pathExists(configFile))) {
|
|
115
184
|
return [];
|
|
116
185
|
}
|
|
117
|
-
const config = await
|
|
118
|
-
return config
|
|
186
|
+
const config = await readProjectConfig(configFile);
|
|
187
|
+
return getSourceNames(config);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 导出项目配置到 JSON 文件
|
|
191
|
+
*/
|
|
192
|
+
async function exportProjectConfig(projectDir, outputPath) {
|
|
193
|
+
const configFile = (0, path_2.getProjectConfigPath)(projectDir);
|
|
194
|
+
if (!(await fs_extra_1.default.pathExists(configFile))) {
|
|
195
|
+
throw new Error('Project not initialized. Use `tools-cc use <source>` to get started.');
|
|
196
|
+
}
|
|
197
|
+
const config = await readProjectConfig(configFile);
|
|
198
|
+
const exportConfig = {
|
|
199
|
+
version: '1.0',
|
|
200
|
+
type: 'project',
|
|
201
|
+
config,
|
|
202
|
+
exportedAt: new Date().toISOString()
|
|
203
|
+
};
|
|
204
|
+
await fs_extra_1.default.writeJson(outputPath, exportConfig, { spaces: 2 });
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* 从 JSON 文件导入项目配置
|
|
208
|
+
*/
|
|
209
|
+
async function importProjectConfig(configPath, projectDir, resolveSourcePath) {
|
|
210
|
+
if (!(await fs_extra_1.default.pathExists(configPath))) {
|
|
211
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
212
|
+
}
|
|
213
|
+
const exportConfig = await fs_extra_1.default.readJson(configPath);
|
|
214
|
+
// Validate version
|
|
215
|
+
if (exportConfig.version !== '1.0') {
|
|
216
|
+
throw new Error(`Unsupported config version: ${exportConfig.version}`);
|
|
217
|
+
}
|
|
218
|
+
// Validate type
|
|
219
|
+
if (exportConfig.type !== 'project') {
|
|
220
|
+
throw new Error(`Invalid config type: ${exportConfig.type}. Expected 'project'.`);
|
|
221
|
+
}
|
|
222
|
+
// Initialize project
|
|
223
|
+
await initProject(projectDir);
|
|
224
|
+
// Apply each source
|
|
225
|
+
for (const [sourceName, selection] of Object.entries(exportConfig.config.sources)) {
|
|
226
|
+
const sourceDir = await resolveSourcePath(sourceName);
|
|
227
|
+
await useSource(sourceName, sourceDir, projectDir, selection);
|
|
228
|
+
}
|
|
229
|
+
// Update links if present
|
|
230
|
+
if (exportConfig.config.links && exportConfig.config.links.length > 0) {
|
|
231
|
+
const configFile = (0, path_2.getProjectConfigPath)(projectDir);
|
|
232
|
+
const config = await readProjectConfig(configFile);
|
|
233
|
+
config.links = exportConfig.config.links;
|
|
234
|
+
await fs_extra_1.default.writeJson(configFile, config, { spaces: 2 });
|
|
235
|
+
}
|
|
119
236
|
}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const commander_1 = require("commander");
|
|
|
5
5
|
const config_1 = require("./commands/config");
|
|
6
6
|
const source_1 = require("./commands/source");
|
|
7
7
|
const use_1 = require("./commands/use");
|
|
8
|
+
const export_1 = require("./commands/export");
|
|
8
9
|
const help_1 = require("./commands/help");
|
|
9
10
|
const program = new commander_1.Command();
|
|
10
11
|
program
|
|
@@ -79,7 +80,9 @@ configCmd
|
|
|
79
80
|
program
|
|
80
81
|
.command('use [sources...]')
|
|
81
82
|
.description('Use sources in current project')
|
|
82
|
-
.option('-p, --projects <tools...>', 'Tools to link (iflow, claude, codebuddy, opencode)')
|
|
83
|
+
.option('-p, --projects <tools...>', 'Tools to link (iflow, claude, codebuddy, opencode, codex)')
|
|
84
|
+
.option('-l, --ls', 'Interactive selection mode for single source')
|
|
85
|
+
.option('-c, --config <file>', 'Import from config file')
|
|
83
86
|
.action(async (sources, options) => {
|
|
84
87
|
await (0, use_1.handleUse)(sources, options);
|
|
85
88
|
});
|
|
@@ -107,6 +110,14 @@ program
|
|
|
107
110
|
.action(async (sources) => {
|
|
108
111
|
await (0, use_1.handleProjectUpdate)(sources);
|
|
109
112
|
});
|
|
113
|
+
program
|
|
114
|
+
.command('export')
|
|
115
|
+
.description('Export project or global config')
|
|
116
|
+
.option('-o, --output <file>', 'Output file path')
|
|
117
|
+
.option('--global', 'Export global config')
|
|
118
|
+
.action(async (options) => {
|
|
119
|
+
await (0, export_1.handleExport)(options);
|
|
120
|
+
});
|
|
110
121
|
// Help command
|
|
111
122
|
program
|
|
112
123
|
.command('help')
|
package/dist/types/config.d.ts
CHANGED
|
@@ -7,10 +7,37 @@ export interface GlobalConfig {
|
|
|
7
7
|
sourcesDir: string;
|
|
8
8
|
sources: Record<string, SourceConfig>;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* 源选择配置 - 指定从源中导入哪些 skills/commands/agents
|
|
12
|
+
*/
|
|
13
|
+
export interface SourceSelection {
|
|
14
|
+
skills: string[];
|
|
15
|
+
commands: string[];
|
|
16
|
+
agents: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 新版项目配置 - sources 使用 Record 格式支持部分导入
|
|
20
|
+
*/
|
|
10
21
|
export interface ProjectConfig {
|
|
22
|
+
sources: Record<string, SourceSelection>;
|
|
23
|
+
links: string[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 旧版项目配置 - sources 为字符串数组(向后兼容)
|
|
27
|
+
*/
|
|
28
|
+
export interface LegacyProjectConfig {
|
|
11
29
|
sources: string[];
|
|
12
30
|
links: string[];
|
|
13
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* 判断值是否为有效的 SourceSelection 对象
|
|
34
|
+
*/
|
|
35
|
+
export declare function isSourceSelection(value: unknown): value is SourceSelection;
|
|
36
|
+
/**
|
|
37
|
+
* 将旧版项目配置转换为新版格式
|
|
38
|
+
* 如果 sources 是字符串数组,转换为 Record 格式,每个源默认导入全部内容
|
|
39
|
+
*/
|
|
40
|
+
export declare function normalizeProjectConfig(config: LegacyProjectConfig | ProjectConfig): ProjectConfig;
|
|
14
41
|
export interface Manifest {
|
|
15
42
|
name: string;
|
|
16
43
|
version: string;
|
|
@@ -21,3 +48,21 @@ export interface Manifest {
|
|
|
21
48
|
export interface ToolConfig {
|
|
22
49
|
linkName: string;
|
|
23
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* 项目配置导出格式
|
|
53
|
+
*/
|
|
54
|
+
export interface ExportConfig {
|
|
55
|
+
version: string;
|
|
56
|
+
type: 'project';
|
|
57
|
+
config: ProjectConfig;
|
|
58
|
+
exportedAt: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 全局配置导出格式
|
|
62
|
+
*/
|
|
63
|
+
export interface GlobalExportConfig {
|
|
64
|
+
version: string;
|
|
65
|
+
type: 'global';
|
|
66
|
+
config: GlobalConfig;
|
|
67
|
+
exportedAt: string;
|
|
68
|
+
}
|
package/dist/types/config.js
CHANGED
|
@@ -1,2 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSourceSelection = isSourceSelection;
|
|
4
|
+
exports.normalizeProjectConfig = normalizeProjectConfig;
|
|
5
|
+
/**
|
|
6
|
+
* 判断值是否为有效的 SourceSelection 对象
|
|
7
|
+
*/
|
|
8
|
+
function isSourceSelection(value) {
|
|
9
|
+
if (typeof value !== 'object' || value === null)
|
|
10
|
+
return false;
|
|
11
|
+
const obj = value;
|
|
12
|
+
return (Array.isArray(obj.skills) &&
|
|
13
|
+
Array.isArray(obj.commands) &&
|
|
14
|
+
Array.isArray(obj.agents));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 将旧版项目配置转换为新版格式
|
|
18
|
+
* 如果 sources 是字符串数组,转换为 Record 格式,每个源默认导入全部内容
|
|
19
|
+
*/
|
|
20
|
+
function normalizeProjectConfig(config) {
|
|
21
|
+
// If sources is an array, convert to new format
|
|
22
|
+
if (Array.isArray(config.sources)) {
|
|
23
|
+
const newSources = {};
|
|
24
|
+
for (const sourceName of config.sources) {
|
|
25
|
+
newSources[sourceName] = {
|
|
26
|
+
skills: ['*'],
|
|
27
|
+
commands: ['*'],
|
|
28
|
+
agents: ['*']
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return { sources: newSources, links: config.links };
|
|
32
|
+
}
|
|
33
|
+
return config;
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { SourceSelection } from '../types/config';
|
|
2
|
+
/**
|
|
3
|
+
* 解析后的源路径
|
|
4
|
+
*/
|
|
5
|
+
export interface ParsedSourcePath {
|
|
6
|
+
sourceName: string;
|
|
7
|
+
type?: 'skills' | 'commands' | 'agents';
|
|
8
|
+
itemName?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 解析源路径字符串
|
|
12
|
+
*
|
|
13
|
+
* 支持的格式:
|
|
14
|
+
* - 'my-skills' → { sourceName: 'my-skills' } (整个源)
|
|
15
|
+
* - 'my-skills/skills/a-skill' → { sourceName: 'my-skills', type: 'skills', itemName: 'a-skill' }
|
|
16
|
+
* - 'my-skills/commands/test' → { sourceName: 'my-skills', type: 'commands', itemName: 'test' }
|
|
17
|
+
* - 'other/agents/reviewer' → { sourceName: 'other', type: 'agents', itemName: 'reviewer' }
|
|
18
|
+
*
|
|
19
|
+
* 无效路径只返回 sourceName
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseSourcePath(input: string): ParsedSourcePath;
|
|
22
|
+
/**
|
|
23
|
+
* 从路径数组构建选择配置
|
|
24
|
+
*
|
|
25
|
+
* 将多个路径转换为 Record<string, SourceSelection> 格式
|
|
26
|
+
*
|
|
27
|
+
* 示例:
|
|
28
|
+
* ['my-skills/skills/a', 'my-skills/skills/b', 'my-skills/commands/test']
|
|
29
|
+
* → { 'my-skills': { skills: ['a', 'b'], commands: ['test'], agents: [] } }
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildSelectionFromPaths(paths: string[]): Record<string, SourceSelection>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseSourcePath = parseSourcePath;
|
|
4
|
+
exports.buildSelectionFromPaths = buildSelectionFromPaths;
|
|
5
|
+
/**
|
|
6
|
+
* 解析源路径字符串
|
|
7
|
+
*
|
|
8
|
+
* 支持的格式:
|
|
9
|
+
* - 'my-skills' → { sourceName: 'my-skills' } (整个源)
|
|
10
|
+
* - 'my-skills/skills/a-skill' → { sourceName: 'my-skills', type: 'skills', itemName: 'a-skill' }
|
|
11
|
+
* - 'my-skills/commands/test' → { sourceName: 'my-skills', type: 'commands', itemName: 'test' }
|
|
12
|
+
* - 'other/agents/reviewer' → { sourceName: 'other', type: 'agents', itemName: 'reviewer' }
|
|
13
|
+
*
|
|
14
|
+
* 无效路径只返回 sourceName
|
|
15
|
+
*/
|
|
16
|
+
function parseSourcePath(input) {
|
|
17
|
+
const parts = input.split('/');
|
|
18
|
+
// 至少需要源名称
|
|
19
|
+
if (parts.length === 0 || input === '') {
|
|
20
|
+
return { sourceName: '' };
|
|
21
|
+
}
|
|
22
|
+
const sourceName = parts[0];
|
|
23
|
+
// 只有源名称,返回整个源
|
|
24
|
+
if (parts.length === 1) {
|
|
25
|
+
return { sourceName };
|
|
26
|
+
}
|
|
27
|
+
// 检查第二部分是否为有效类型
|
|
28
|
+
const validTypes = ['skills', 'commands', 'agents'];
|
|
29
|
+
const type = parts[1];
|
|
30
|
+
if (!validTypes.includes(type)) {
|
|
31
|
+
return { sourceName };
|
|
32
|
+
}
|
|
33
|
+
// 检查第三部分(项目名称)
|
|
34
|
+
if (parts.length < 3 || !parts[2]) {
|
|
35
|
+
return { sourceName };
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
sourceName,
|
|
39
|
+
type,
|
|
40
|
+
itemName: parts[2]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 从路径数组构建选择配置
|
|
45
|
+
*
|
|
46
|
+
* 将多个路径转换为 Record<string, SourceSelection> 格式
|
|
47
|
+
*
|
|
48
|
+
* 示例:
|
|
49
|
+
* ['my-skills/skills/a', 'my-skills/skills/b', 'my-skills/commands/test']
|
|
50
|
+
* → { 'my-skills': { skills: ['a', 'b'], commands: ['test'], agents: [] } }
|
|
51
|
+
*/
|
|
52
|
+
function buildSelectionFromPaths(paths) {
|
|
53
|
+
const result = {};
|
|
54
|
+
for (const path of paths) {
|
|
55
|
+
const parsed = parseSourcePath(path);
|
|
56
|
+
const { sourceName, type, itemName } = parsed;
|
|
57
|
+
// 跳过空源名称
|
|
58
|
+
if (!sourceName) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// 初始化源选择(如果不存在)
|
|
62
|
+
if (!result[sourceName]) {
|
|
63
|
+
result[sourceName] = {
|
|
64
|
+
skills: [],
|
|
65
|
+
commands: [],
|
|
66
|
+
agents: []
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// 如果没有指定类型和项目名称,表示整个源
|
|
70
|
+
if (!type || !itemName) {
|
|
71
|
+
result[sourceName] = {
|
|
72
|
+
skills: ['*'],
|
|
73
|
+
commands: ['*'],
|
|
74
|
+
agents: ['*']
|
|
75
|
+
};
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// 添加项目到对应的类型数组(去重)
|
|
79
|
+
const selection = result[sourceName];
|
|
80
|
+
const targetArray = selection[type];
|
|
81
|
+
if (!targetArray.includes(itemName)) {
|
|
82
|
+
targetArray.push(itemName);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tools-cc",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "tools-cc [options] <command> [args]",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
"start": "node dist/index.js",
|
|
13
13
|
"test": "vitest",
|
|
14
14
|
"test:run": "vitest run",
|
|
15
|
-
"
|
|
15
|
+
"tag:create": "git tag v%npm_package_version% && git push github v%npm_package_version%",
|
|
16
|
+
"publish:npm": "npm publish",
|
|
17
|
+
"release:gh": "gh release create v%npm_package_version% --title v%npm_package_version% --generate-notes",
|
|
18
|
+
"release": "npm run tag:create && npm run publish:npm && npm run release:gh"
|
|
16
19
|
},
|
|
17
20
|
"bin": {
|
|
18
21
|
"tools-cc": "./dist/index.js"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { exportProjectConfig } from '../core/project';
|
|
5
|
+
import { loadGlobalConfig } from '../core/config';
|
|
6
|
+
import { GLOBAL_CONFIG_DIR } from '../utils/path';
|
|
7
|
+
import { GlobalExportConfig } from '../types/config';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 导出项目或全局配置
|
|
11
|
+
*/
|
|
12
|
+
export async function handleExport(options: {
|
|
13
|
+
output?: string;
|
|
14
|
+
global?: boolean;
|
|
15
|
+
}): Promise<void> {
|
|
16
|
+
try {
|
|
17
|
+
if (options.global) {
|
|
18
|
+
// 导出全局配置
|
|
19
|
+
await exportGlobalConfig(options.output);
|
|
20
|
+
} else {
|
|
21
|
+
// 导出项目配置
|
|
22
|
+
await exportProjectConfigCmd(options.output);
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
console.log(chalk.red(`✗ Export failed: ${message}`));
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 导出项目配置
|
|
33
|
+
*/
|
|
34
|
+
async function exportProjectConfigCmd(outputPath?: string): Promise<void> {
|
|
35
|
+
const projectDir = process.cwd();
|
|
36
|
+
const outputFile = outputPath ?? path.join(projectDir, '.toolscc-export.json');
|
|
37
|
+
|
|
38
|
+
await exportProjectConfig(projectDir, outputFile);
|
|
39
|
+
console.log(chalk.green(`✓ Project config exported to: ${outputFile}`));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 导出全局配置
|
|
44
|
+
*/
|
|
45
|
+
async function exportGlobalConfig(outputPath?: string): Promise<void> {
|
|
46
|
+
const projectDir = process.cwd();
|
|
47
|
+
const outputFile = outputPath ?? path.join(projectDir, '.toolscc-global-export.json');
|
|
48
|
+
|
|
49
|
+
const config = await loadGlobalConfig(GLOBAL_CONFIG_DIR);
|
|
50
|
+
|
|
51
|
+
const exportConfig: GlobalExportConfig = {
|
|
52
|
+
version: '1.0',
|
|
53
|
+
type: 'global',
|
|
54
|
+
config,
|
|
55
|
+
exportedAt: new Date().toISOString()
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
await fs.writeJson(outputFile, exportConfig, { spaces: 2 });
|
|
59
|
+
console.log(chalk.green(`✓ Global config exported to: ${outputFile}`));
|
|
60
|
+
}
|
package/src/commands/help.ts
CHANGED
|
@@ -9,7 +9,7 @@ ${chalk.bold.cyan('════════════════════
|
|
|
9
9
|
|
|
10
10
|
${chalk.bold('DESCRIPTION / 描述')}
|
|
11
11
|
A CLI tool for managing skills/commands/agents configurations across multiple
|
|
12
|
-
AI coding tools (iflow, claude, codebuddy, opencode, etc.) via symlinks.
|
|
12
|
+
AI coding tools (iflow, claude, codebuddy, opencode, codex, etc.) via symlinks.
|
|
13
13
|
|
|
14
14
|
一个用于统一管理多个 AI 编程工具配置的命令行工具,通过符号链接机制避免重复配置。
|
|
15
15
|
|
|
@@ -50,6 +50,7 @@ ${chalk.bold('SUPPORTED TOOLS / 支持的工具')}
|
|
|
50
50
|
claude → .claude
|
|
51
51
|
codebuddy → .codebuddy
|
|
52
52
|
opencode → .opencode
|
|
53
|
+
codex → .codex
|
|
53
54
|
|
|
54
55
|
${chalk.bold('EXAMPLES / 示例')}
|
|
55
56
|
${chalk.gray('# Add a git source / 添加 Git 配置源')}
|
|
@@ -62,7 +63,7 @@ ${chalk.bold('EXAMPLES / 示例')}
|
|
|
62
63
|
tools-cc sources list
|
|
63
64
|
|
|
64
65
|
${chalk.gray('# Use sources in project / 在项目中启用配置源')}
|
|
65
|
-
tools-cc use my-skills -p iflow claude
|
|
66
|
+
tools-cc use my-skills -p iflow claude codex
|
|
66
67
|
|
|
67
68
|
${chalk.gray('# Check project status / 检查项目状态')}
|
|
68
69
|
tools-cc status
|