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/src/commands/use.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
|
-
import { useSource, unuseSource, listUsedSources, initProject } from '../core/project';
|
|
3
|
+
import { useSource, unuseSource, listUsedSources, initProject, importProjectConfig } from '../core/project';
|
|
4
4
|
import { getSourcePath, listSources } from '../core/source';
|
|
5
|
+
import { scanSource } from '../core/manifest';
|
|
5
6
|
import { createSymlink, isSymlink } from '../core/symlink';
|
|
6
7
|
import { GLOBAL_CONFIG_DIR, getToolsccDir, getProjectConfigPath } from '../utils/path';
|
|
8
|
+
import { parseSourcePath, buildSelectionFromPaths } from '../utils/parsePath';
|
|
9
|
+
import type { SourceSelection } from '../types/config';
|
|
7
10
|
import fs from 'fs-extra';
|
|
8
11
|
import path from 'path';
|
|
9
12
|
|
|
@@ -11,69 +14,279 @@ const SUPPORTED_TOOLS: Record<string, string> = {
|
|
|
11
14
|
iflow: '.iflow',
|
|
12
15
|
claude: '.claude',
|
|
13
16
|
codebuddy: '.codebuddy',
|
|
14
|
-
opencode: '.opencode'
|
|
17
|
+
opencode: '.opencode',
|
|
18
|
+
codex: '.codex'
|
|
15
19
|
};
|
|
16
20
|
|
|
21
|
+
/**
|
|
22
|
+
* use 命令选项
|
|
23
|
+
*/
|
|
24
|
+
export interface UseOptions {
|
|
25
|
+
projects?: string[];
|
|
26
|
+
ls?: boolean;
|
|
27
|
+
config?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 处理 use 命令
|
|
32
|
+
*
|
|
33
|
+
* 支持多种模式:
|
|
34
|
+
* 1. 配置导入模式: tools-cc use -c config.json
|
|
35
|
+
* 2. 交互选择模式: tools-cc use my-source --ls
|
|
36
|
+
* 3. 路径语法模式: tools-cc use my-source/skills/a-skill
|
|
37
|
+
* 4. 整体导入模式: tools-cc use my-source
|
|
38
|
+
* 5. 点模式: tools-cc use . (使用已配置源)
|
|
39
|
+
*/
|
|
17
40
|
export async function handleUse(
|
|
18
|
-
|
|
19
|
-
options:
|
|
41
|
+
sourceSpecs: string[],
|
|
42
|
+
options: UseOptions
|
|
20
43
|
): Promise<void> {
|
|
21
44
|
const projectDir = process.cwd();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
45
|
+
const toolsccDir = getToolsccDir(projectDir);
|
|
46
|
+
const configFile = getProjectConfigPath(projectDir);
|
|
47
|
+
|
|
48
|
+
// 1. 配置导入模式
|
|
49
|
+
if (options.config) {
|
|
50
|
+
await handleConfigImportMode(options.config, projectDir, options.projects);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 2. 点模式:使用当前项目已配置的源
|
|
55
|
+
if (sourceSpecs.length === 1 && sourceSpecs[0] === '.') {
|
|
56
|
+
await handleDotMode(projectDir, toolsccDir, configFile, options.projects);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 3. 交互选择模式:单个源 + --ls 选项
|
|
61
|
+
if (options.ls && sourceSpecs.length === 1) {
|
|
62
|
+
const parsed = parseSourcePath(sourceSpecs[0]);
|
|
63
|
+
// 只有源名称时才进入交互模式
|
|
64
|
+
if (!parsed.type && parsed.sourceName) {
|
|
65
|
+
await handleInteractiveMode(parsed.sourceName, projectDir, options.projects);
|
|
30
66
|
return;
|
|
31
67
|
}
|
|
32
|
-
|
|
33
|
-
const answers = await inquirer.prompt([
|
|
34
|
-
{
|
|
35
|
-
type: 'checkbox',
|
|
36
|
-
name: 'selectedSources',
|
|
37
|
-
message: 'Select sources to use:',
|
|
38
|
-
choices: sourceList
|
|
39
|
-
}
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
sourceNames = answers.selectedSources;
|
|
43
68
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
69
|
+
|
|
70
|
+
// 4. 无参数时显示源选择列表
|
|
71
|
+
if (sourceSpecs.length === 0) {
|
|
72
|
+
sourceSpecs = await selectSourcesInteractively();
|
|
73
|
+
if (sourceSpecs.length === 0) {
|
|
74
|
+
console.log(chalk.gray('No sources selected.'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
48
77
|
}
|
|
49
|
-
|
|
78
|
+
|
|
79
|
+
// 5. 解析路径语法并构建选择配置
|
|
80
|
+
const selectionMap = buildSelectionFromPaths(sourceSpecs);
|
|
81
|
+
|
|
50
82
|
// 初始化项目
|
|
51
83
|
await initProject(projectDir);
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
for (const sourceName of
|
|
84
|
+
|
|
85
|
+
// 应用每个源的选择配置
|
|
86
|
+
for (const [sourceName, selection] of Object.entries(selectionMap)) {
|
|
55
87
|
try {
|
|
56
88
|
const sourcePath = await getSourcePath(sourceName, GLOBAL_CONFIG_DIR);
|
|
57
|
-
await useSource(sourceName, sourcePath, projectDir);
|
|
89
|
+
await useSource(sourceName, sourcePath, projectDir, selection);
|
|
58
90
|
console.log(chalk.green(`✓ Using source: ${sourceName}`));
|
|
59
91
|
} catch (error) {
|
|
60
92
|
console.log(chalk.red(`✗ Failed to use ${sourceName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
61
93
|
}
|
|
62
94
|
}
|
|
63
|
-
|
|
95
|
+
|
|
64
96
|
// 创建符号链接
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
97
|
+
await createToolLinks(projectDir, toolsccDir, configFile, options.projects);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 配置导入模式
|
|
102
|
+
*/
|
|
103
|
+
async function handleConfigImportMode(
|
|
104
|
+
configPath: string,
|
|
105
|
+
projectDir: string,
|
|
106
|
+
projects?: string[]
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
try {
|
|
109
|
+
const toolsccDir = getToolsccDir(projectDir);
|
|
110
|
+
const configFile = getProjectConfigPath(projectDir);
|
|
111
|
+
|
|
112
|
+
// 解析配置文件路径
|
|
113
|
+
const resolvedPath = path.resolve(configPath);
|
|
114
|
+
|
|
115
|
+
// 定义源路径解析函数
|
|
116
|
+
const resolveSourcePath = async (sourceName: string): Promise<string> => {
|
|
117
|
+
return await getSourcePath(sourceName, GLOBAL_CONFIG_DIR);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// 导入配置
|
|
121
|
+
await importProjectConfig(resolvedPath, projectDir, resolveSourcePath);
|
|
122
|
+
console.log(chalk.green(`✓ Imported config from: ${resolvedPath}`));
|
|
123
|
+
|
|
124
|
+
// 创建符号链接
|
|
125
|
+
await createToolLinks(projectDir, toolsccDir, configFile, projects);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.log(chalk.red(`✗ Failed to import config: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 点模式:使用已配置源创建符号链接
|
|
133
|
+
*/
|
|
134
|
+
async function handleDotMode(
|
|
135
|
+
projectDir: string,
|
|
136
|
+
toolsccDir: string,
|
|
137
|
+
configFile: string,
|
|
138
|
+
projects?: string[]
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
if (!(await fs.pathExists(configFile))) {
|
|
141
|
+
console.log(chalk.yellow('Project not initialized. Run `tools-cc use <source>` first.'));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const config = await fs.readJson(configFile);
|
|
146
|
+
const configuredSources = Object.keys(config.sources || {});
|
|
147
|
+
|
|
148
|
+
if (configuredSources.length === 0) {
|
|
149
|
+
console.log(chalk.yellow('No sources configured in this project. Run `tools-cc use <source>` to add one.'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log(chalk.cyan(`Using existing sources: ${configuredSources.join(', ')}`));
|
|
154
|
+
await createToolLinks(projectDir, toolsccDir, configFile, projects);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 交互选择模式:显示技能/命令/代理选择列表
|
|
159
|
+
*/
|
|
160
|
+
async function handleInteractiveMode(
|
|
161
|
+
sourceName: string,
|
|
162
|
+
projectDir: string,
|
|
163
|
+
projects?: string[]
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
try {
|
|
166
|
+
const sourcePath = await getSourcePath(sourceName, GLOBAL_CONFIG_DIR);
|
|
167
|
+
const manifest = await scanSource(sourcePath);
|
|
168
|
+
|
|
169
|
+
// 构建选项列表
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
171
|
+
const choices: any[] = [];
|
|
172
|
+
|
|
173
|
+
// Skills 区
|
|
174
|
+
if (manifest.skills && manifest.skills.length > 0) {
|
|
175
|
+
choices.push(new inquirer.Separator(`--- Skills (${manifest.skills.length}) ---`));
|
|
176
|
+
for (const skill of manifest.skills) {
|
|
177
|
+
choices.push({ name: skill, value: `skills/${skill}` });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Commands 区
|
|
182
|
+
if (manifest.commands && manifest.commands.length > 0) {
|
|
183
|
+
choices.push(new inquirer.Separator(`--- Commands (${manifest.commands.length}) ---`));
|
|
184
|
+
for (const cmd of manifest.commands) {
|
|
185
|
+
choices.push({ name: cmd, value: `commands/${cmd}` });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Agents 区
|
|
190
|
+
if (manifest.agents && manifest.agents.length > 0) {
|
|
191
|
+
choices.push(new inquirer.Separator(`--- Agents (${manifest.agents.length}) ---`));
|
|
192
|
+
for (const agent of manifest.agents) {
|
|
193
|
+
choices.push({ name: agent, value: `agents/${agent}` });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (choices.length === 0) {
|
|
198
|
+
console.log(chalk.yellow(`No items found in source: ${sourceName}`));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 显示选择列表
|
|
203
|
+
const answers = await inquirer.prompt([
|
|
204
|
+
{
|
|
205
|
+
type: 'checkbox',
|
|
206
|
+
name: 'selectedItems',
|
|
207
|
+
message: `Select items from ${sourceName}:`,
|
|
208
|
+
choices,
|
|
209
|
+
pageSize: 15
|
|
210
|
+
}
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
if (answers.selectedItems.length === 0) {
|
|
214
|
+
console.log(chalk.gray('No items selected.'));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 将选择转换为 SourceSelection
|
|
219
|
+
const selection: SourceSelection = {
|
|
220
|
+
skills: [],
|
|
221
|
+
commands: [],
|
|
222
|
+
agents: []
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
for (const item of answers.selectedItems) {
|
|
226
|
+
const [type, name] = item.split('/');
|
|
227
|
+
if (type === 'skills') selection.skills.push(name);
|
|
228
|
+
else if (type === 'commands') selection.commands.push(name);
|
|
229
|
+
else if (type === 'agents') selection.agents.push(name);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 初始化项目并应用选择
|
|
233
|
+
await initProject(projectDir);
|
|
234
|
+
await useSource(sourceName, sourcePath, projectDir, selection);
|
|
235
|
+
console.log(chalk.green(`✓ Using source: ${sourceName}`));
|
|
236
|
+
|
|
237
|
+
// 创建符号链接
|
|
238
|
+
const toolsccDir = getToolsccDir(projectDir);
|
|
239
|
+
const configFile = getProjectConfigPath(projectDir);
|
|
240
|
+
await createToolLinks(projectDir, toolsccDir, configFile, projects);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.log(chalk.red(`✗ Failed to use ${sourceName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 显示源选择列表并返回用户选择
|
|
248
|
+
*/
|
|
249
|
+
async function selectSourcesInteractively(): Promise<string[]> {
|
|
250
|
+
const sources = await listSources(GLOBAL_CONFIG_DIR);
|
|
251
|
+
const sourceList = Object.keys(sources);
|
|
252
|
+
|
|
253
|
+
if (sourceList.length === 0) {
|
|
254
|
+
console.log(chalk.yellow('No sources configured. Use `tools-cc -s add` to add one.'));
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const answers = await inquirer.prompt([
|
|
259
|
+
{
|
|
260
|
+
type: 'checkbox',
|
|
261
|
+
name: 'selectedSources',
|
|
262
|
+
message: 'Select sources to use:',
|
|
263
|
+
choices: sourceList
|
|
264
|
+
}
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
return answers.selectedSources;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 创建工具符号链接
|
|
272
|
+
*/
|
|
273
|
+
async function createToolLinks(
|
|
274
|
+
projectDir: string,
|
|
275
|
+
toolsccDir: string,
|
|
276
|
+
configFile: string,
|
|
277
|
+
projects?: string[]
|
|
278
|
+
): Promise<void> {
|
|
279
|
+
const tools = projects || Object.keys(SUPPORTED_TOOLS);
|
|
280
|
+
|
|
68
281
|
for (const tool of tools) {
|
|
69
282
|
const linkName = SUPPORTED_TOOLS[tool];
|
|
70
283
|
if (!linkName) {
|
|
71
284
|
console.log(chalk.yellow(`Unknown tool: ${tool}`));
|
|
72
285
|
continue;
|
|
73
286
|
}
|
|
74
|
-
|
|
287
|
+
|
|
75
288
|
const linkPath = path.join(projectDir, linkName);
|
|
76
|
-
|
|
289
|
+
|
|
77
290
|
try {
|
|
78
291
|
await createSymlink(toolsccDir, linkPath, true);
|
|
79
292
|
console.log(chalk.green(`✓ Linked: ${linkName} -> .toolscc`));
|
|
@@ -81,9 +294,8 @@ export async function handleUse(
|
|
|
81
294
|
console.log(chalk.red(`✗ Failed to link ${linkName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
82
295
|
}
|
|
83
296
|
}
|
|
84
|
-
|
|
297
|
+
|
|
85
298
|
// 更新项目配置
|
|
86
|
-
const configFile = getProjectConfigPath(projectDir);
|
|
87
299
|
const config = await fs.readJson(configFile);
|
|
88
300
|
const existingLinks = config.links || [];
|
|
89
301
|
config.links = [...new Set([...existingLinks, ...tools])];
|
|
@@ -157,9 +369,11 @@ export async function handleProjectUpdate(sourceNames?: string[]): Promise<void>
|
|
|
157
369
|
}
|
|
158
370
|
|
|
159
371
|
const config = await fs.readJson(configFile);
|
|
372
|
+
const configuredSources = Object.keys(config.sources || {});
|
|
373
|
+
|
|
160
374
|
let sourcesToUpdate = sourceNames && sourceNames.length > 0
|
|
161
375
|
? sourceNames
|
|
162
|
-
:
|
|
376
|
+
: configuredSources;
|
|
163
377
|
|
|
164
378
|
if (sourcesToUpdate.length === 0) {
|
|
165
379
|
console.log(chalk.gray('No sources to update.'));
|
|
@@ -168,18 +382,20 @@ export async function handleProjectUpdate(sourceNames?: string[]): Promise<void>
|
|
|
168
382
|
|
|
169
383
|
// 验证指定的源是否存在于项目配置中
|
|
170
384
|
if (sourceNames && sourceNames.length > 0) {
|
|
171
|
-
const invalidSources = sourceNames.filter((s: string) => !
|
|
385
|
+
const invalidSources = sourceNames.filter((s: string) => !configuredSources.includes(s));
|
|
172
386
|
if (invalidSources.length > 0) {
|
|
173
387
|
console.log(chalk.yellow(`Sources not in project: ${invalidSources.join(', ')}`));
|
|
174
388
|
}
|
|
175
|
-
sourcesToUpdate = sourcesToUpdate.filter((s: string) =>
|
|
389
|
+
sourcesToUpdate = sourcesToUpdate.filter((s: string) => configuredSources.includes(s));
|
|
176
390
|
}
|
|
177
391
|
|
|
178
392
|
// 更新每个配置源
|
|
179
393
|
for (const sourceName of sourcesToUpdate) {
|
|
180
394
|
try {
|
|
181
395
|
const sourcePath = await getSourcePath(sourceName, GLOBAL_CONFIG_DIR);
|
|
182
|
-
|
|
396
|
+
// 使用保存的选择配置进行更新
|
|
397
|
+
const selection = config.sources[sourceName];
|
|
398
|
+
await useSource(sourceName, sourcePath, projectDir, selection);
|
|
183
399
|
console.log(chalk.green(`✓ Updated source: ${sourceName}`));
|
|
184
400
|
} catch (error) {
|
|
185
401
|
console.log(chalk.red(`✗ Failed to update ${sourceName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
@@ -187,4 +403,4 @@ export async function handleProjectUpdate(sourceNames?: string[]): Promise<void>
|
|
|
187
403
|
}
|
|
188
404
|
|
|
189
405
|
console.log(chalk.green(`\n✓ Project update complete`));
|
|
190
|
-
}
|
|
406
|
+
}
|
package/src/core/project.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { ProjectConfig } from '../types';
|
|
3
|
+
import { ProjectConfig, LegacyProjectConfig, normalizeProjectConfig, SourceSelection, ExportConfig } from '../types';
|
|
4
4
|
import { loadManifest } from './manifest';
|
|
5
5
|
import { getToolsccDir, getProjectConfigPath } from '../utils/path';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* 默认选择配置 - 导入所有内容
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_SELECTION: SourceSelection = {
|
|
11
|
+
skills: ['*'],
|
|
12
|
+
commands: ['*'],
|
|
13
|
+
agents: ['*']
|
|
14
|
+
};
|
|
15
|
+
|
|
7
16
|
export async function initProject(projectDir: string): Promise<void> {
|
|
8
17
|
const toolsccDir = getToolsccDir(projectDir);
|
|
9
18
|
const configFile = getProjectConfigPath(projectDir);
|
|
@@ -16,17 +25,45 @@ export async function initProject(projectDir: string): Promise<void> {
|
|
|
16
25
|
// Create project config if not exists
|
|
17
26
|
if (!(await fs.pathExists(configFile))) {
|
|
18
27
|
const config: ProjectConfig = {
|
|
19
|
-
sources:
|
|
28
|
+
sources: {},
|
|
20
29
|
links: []
|
|
21
30
|
};
|
|
22
31
|
await fs.writeJson(configFile, config, { spaces: 2 });
|
|
23
32
|
}
|
|
24
33
|
}
|
|
25
34
|
|
|
35
|
+
/**
|
|
36
|
+
* 读取项目配置,自动处理新旧格式
|
|
37
|
+
*/
|
|
38
|
+
async function readProjectConfig(configFile: string): Promise<ProjectConfig> {
|
|
39
|
+
const rawConfig = await fs.readJson(configFile);
|
|
40
|
+
return normalizeProjectConfig(rawConfig);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 获取源名称列表(兼容新旧格式)
|
|
45
|
+
*/
|
|
46
|
+
function getSourceNames(config: ProjectConfig): string[] {
|
|
47
|
+
return Object.keys(config.sources);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 检查是否应该复制某项(根据选择配置)
|
|
52
|
+
*/
|
|
53
|
+
function shouldInclude(itemName: string, selection: string[]): boolean {
|
|
54
|
+
// 如果选择包含通配符,包含所有项
|
|
55
|
+
if (selection.includes('*')) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
// 否则检查是否在选择列表中
|
|
59
|
+
return selection.includes(itemName);
|
|
60
|
+
}
|
|
61
|
+
|
|
26
62
|
export async function useSource(
|
|
27
63
|
sourceName: string,
|
|
28
64
|
sourceDir: string,
|
|
29
|
-
projectDir: string
|
|
65
|
+
projectDir: string,
|
|
66
|
+
selection?: SourceSelection
|
|
30
67
|
): Promise<void> {
|
|
31
68
|
// Input validation
|
|
32
69
|
if (!sourceName || !sourceName.trim()) {
|
|
@@ -44,11 +81,19 @@ export async function useSource(
|
|
|
44
81
|
// Ensure project is initialized
|
|
45
82
|
await initProject(projectDir);
|
|
46
83
|
|
|
84
|
+
// 使用传入的选择配置或默认配置
|
|
85
|
+
const effectiveSelection: SourceSelection = selection ?? DEFAULT_SELECTION;
|
|
86
|
+
|
|
47
87
|
// Copy/link skills (flattened with prefix)
|
|
48
88
|
const sourceSkillsDir = path.join(sourceDir, 'skills');
|
|
49
89
|
if (await fs.pathExists(sourceSkillsDir)) {
|
|
50
90
|
const skills = await fs.readdir(sourceSkillsDir);
|
|
51
91
|
for (const skill of skills) {
|
|
92
|
+
// 检查是否应该包含此 skill
|
|
93
|
+
if (!shouldInclude(skill, effectiveSelection.skills)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
52
97
|
const srcPath = path.join(sourceSkillsDir, skill);
|
|
53
98
|
const name = `${sourceName}` == `${skill}` ? skill : `${sourceName}-${skill}`;
|
|
54
99
|
const destPath = path.join(toolsccDir, 'skills', name);
|
|
@@ -64,25 +109,53 @@ export async function useSource(
|
|
|
64
109
|
// Copy commands (in subdirectory by source name)
|
|
65
110
|
const sourceCommandsDir = path.join(sourceDir, 'commands');
|
|
66
111
|
if (await fs.pathExists(sourceCommandsDir)) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
112
|
+
// 检查是否有选择 commands
|
|
113
|
+
if (effectiveSelection.commands.includes('*')) {
|
|
114
|
+
// 复制所有 commands
|
|
115
|
+
const destDir = path.join(toolsccDir, 'commands', sourceName);
|
|
116
|
+
await fs.remove(destDir);
|
|
117
|
+
await fs.copy(sourceCommandsDir, destDir);
|
|
118
|
+
} else if (effectiveSelection.commands.length > 0) {
|
|
119
|
+
// 只复制选中的 commands
|
|
120
|
+
const destDir = path.join(toolsccDir, 'commands', sourceName);
|
|
121
|
+
await fs.ensureDir(destDir);
|
|
122
|
+
|
|
123
|
+
for (const cmdName of effectiveSelection.commands) {
|
|
124
|
+
const srcFile = path.join(sourceCommandsDir, `${cmdName}.md`);
|
|
125
|
+
if (await fs.pathExists(srcFile)) {
|
|
126
|
+
await fs.copy(srcFile, path.join(destDir, `${cmdName}.md`));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
70
130
|
}
|
|
71
131
|
|
|
72
132
|
// Copy agents (in subdirectory by source name)
|
|
73
133
|
const sourceAgentsDir = path.join(sourceDir, 'agents');
|
|
74
134
|
if (await fs.pathExists(sourceAgentsDir)) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
135
|
+
// 检查是否有选择 agents
|
|
136
|
+
if (effectiveSelection.agents.includes('*')) {
|
|
137
|
+
// 复制所有 agents
|
|
138
|
+
const destDir = path.join(toolsccDir, 'agents', sourceName);
|
|
139
|
+
await fs.remove(destDir);
|
|
140
|
+
await fs.copy(sourceAgentsDir, destDir);
|
|
141
|
+
} else if (effectiveSelection.agents.length > 0) {
|
|
142
|
+
// 只复制选中的 agents
|
|
143
|
+
const destDir = path.join(toolsccDir, 'agents', sourceName);
|
|
144
|
+
await fs.ensureDir(destDir);
|
|
145
|
+
|
|
146
|
+
for (const agentName of effectiveSelection.agents) {
|
|
147
|
+
const srcFile = path.join(sourceAgentsDir, `${agentName}.md`);
|
|
148
|
+
if (await fs.pathExists(srcFile)) {
|
|
149
|
+
await fs.copy(srcFile, path.join(destDir, `${agentName}.md`));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
78
153
|
}
|
|
79
154
|
|
|
80
|
-
// Update project config
|
|
155
|
+
// Update project config - 保存实际使用的选择配置
|
|
81
156
|
const configFile = getProjectConfigPath(projectDir);
|
|
82
|
-
const config
|
|
83
|
-
|
|
84
|
-
config.sources.push(sourceName);
|
|
85
|
-
}
|
|
157
|
+
const config = await readProjectConfig(configFile);
|
|
158
|
+
config.sources[sourceName] = effectiveSelection;
|
|
86
159
|
await fs.writeJson(configFile, config, { spaces: 2 });
|
|
87
160
|
}
|
|
88
161
|
|
|
@@ -115,13 +188,13 @@ export async function unuseSource(sourceName: string, projectDir: string): Promi
|
|
|
115
188
|
// Update project config with error handling
|
|
116
189
|
let config: ProjectConfig;
|
|
117
190
|
try {
|
|
118
|
-
config = await
|
|
191
|
+
config = await readProjectConfig(configFile);
|
|
119
192
|
} catch (error) {
|
|
120
193
|
// If config file doesn't exist or is invalid, nothing to update
|
|
121
194
|
return;
|
|
122
195
|
}
|
|
123
196
|
|
|
124
|
-
|
|
197
|
+
delete config.sources[sourceName];
|
|
125
198
|
await fs.writeJson(configFile, config, { spaces: 2 });
|
|
126
199
|
}
|
|
127
200
|
|
|
@@ -132,6 +205,73 @@ export async function listUsedSources(projectDir: string): Promise<string[]> {
|
|
|
132
205
|
return [];
|
|
133
206
|
}
|
|
134
207
|
|
|
135
|
-
const config
|
|
136
|
-
return config
|
|
208
|
+
const config = await readProjectConfig(configFile);
|
|
209
|
+
return getSourceNames(config);
|
|
137
210
|
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 导出项目配置到 JSON 文件
|
|
214
|
+
*/
|
|
215
|
+
export async function exportProjectConfig(
|
|
216
|
+
projectDir: string,
|
|
217
|
+
outputPath: string
|
|
218
|
+
): Promise<void> {
|
|
219
|
+
const configFile = getProjectConfigPath(projectDir);
|
|
220
|
+
|
|
221
|
+
if (!(await fs.pathExists(configFile))) {
|
|
222
|
+
throw new Error('Project not initialized. Use `tools-cc use <source>` to get started.');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const config = await readProjectConfig(configFile);
|
|
226
|
+
|
|
227
|
+
const exportConfig: ExportConfig = {
|
|
228
|
+
version: '1.0',
|
|
229
|
+
type: 'project',
|
|
230
|
+
config,
|
|
231
|
+
exportedAt: new Date().toISOString()
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
await fs.writeJson(outputPath, exportConfig, { spaces: 2 });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 从 JSON 文件导入项目配置
|
|
239
|
+
*/
|
|
240
|
+
export async function importProjectConfig(
|
|
241
|
+
configPath: string,
|
|
242
|
+
projectDir: string,
|
|
243
|
+
resolveSourcePath: (sourceName: string) => Promise<string>
|
|
244
|
+
): Promise<void> {
|
|
245
|
+
if (!(await fs.pathExists(configPath))) {
|
|
246
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const exportConfig: ExportConfig = await fs.readJson(configPath);
|
|
250
|
+
|
|
251
|
+
// Validate version
|
|
252
|
+
if (exportConfig.version !== '1.0') {
|
|
253
|
+
throw new Error(`Unsupported config version: ${exportConfig.version}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Validate type
|
|
257
|
+
if (exportConfig.type !== 'project') {
|
|
258
|
+
throw new Error(`Invalid config type: ${exportConfig.type}. Expected 'project'.`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Initialize project
|
|
262
|
+
await initProject(projectDir);
|
|
263
|
+
|
|
264
|
+
// Apply each source
|
|
265
|
+
for (const [sourceName, selection] of Object.entries(exportConfig.config.sources)) {
|
|
266
|
+
const sourceDir = await resolveSourcePath(sourceName);
|
|
267
|
+
await useSource(sourceName, sourceDir, projectDir, selection);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Update links if present
|
|
271
|
+
if (exportConfig.config.links && exportConfig.config.links.length > 0) {
|
|
272
|
+
const configFile = getProjectConfigPath(projectDir);
|
|
273
|
+
const config = await readProjectConfig(configFile);
|
|
274
|
+
config.links = exportConfig.config.links;
|
|
275
|
+
await fs.writeJson(configFile, config, { spaces: 2 });
|
|
276
|
+
}
|
|
277
|
+
}
|