tools-cc 1.0.8 → 1.0.10
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 +19 -0
- package/CHANGELOG_en.md +19 -0
- package/README.md +31 -5
- package/README_en.md +31 -5
- package/dist/commands/help.js +18 -1
- package/dist/commands/template.d.ts +3 -1
- package/dist/commands/template.js +38 -6
- package/dist/commands/use.js +11 -1
- package/dist/core/manifest.js +10 -1
- package/dist/core/project.js +27 -1
- package/dist/index.js +3 -2
- package/dist/types/config.d.ts +3 -1
- package/dist/types/config.js +4 -2
- package/dist/utils/parsePath.d.ts +1 -1
- package/dist/utils/parsePath.js +5 -3
- package/package.json +1 -1
- package/src/commands/help.ts +18 -1
- package/src/commands/template.ts +202 -160
- package/src/commands/use.ts +11 -1
- package/src/core/manifest.ts +67 -57
- package/src/core/project.ts +304 -276
- package/src/index.ts +254 -253
- package/src/types/config.ts +116 -112
- package/src/utils/parsePath.ts +6 -4
- package/tests/commands/use.test.ts +12 -6
- package/tests/core/project.test.ts +324 -316
- package/tests/types/config.test.ts +44 -16
- package/tests/utils/parsePath.test.ts +18 -9
package/src/commands/template.ts
CHANGED
|
@@ -1,160 +1,202 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import inquirer from 'inquirer';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { saveTemplate, listTemplates, removeTemplate, getTemplate } from '../core/template';
|
|
5
|
-
import { loadProjectConfig } from '../core/config';
|
|
6
|
-
import { importProjectConfig } from '../core/project';
|
|
7
|
-
import { getSourcePath } from '../core/source';
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { saveTemplate, listTemplates, removeTemplate, getTemplate } from '../core/template';
|
|
5
|
+
import { loadProjectConfig } from '../core/config';
|
|
6
|
+
import { importProjectConfig } from '../core/project';
|
|
7
|
+
import { getSourcePath } from '../core/source';
|
|
8
|
+
import { createSymlink } from '../core/symlink';
|
|
9
|
+
import { TEMPLATES_DIR, getProjectConfigPath, getToolsccDir, GLOBAL_CONFIG_DIR } from '../utils/path';
|
|
10
|
+
import fs from 'fs-extra';
|
|
11
|
+
|
|
12
|
+
const SUPPORTED_TOOLS: Record<string, string> = {
|
|
13
|
+
iflow: '.iflow',
|
|
14
|
+
claude: '.claude',
|
|
15
|
+
codebuddy: '.codebuddy',
|
|
16
|
+
opencode: '.opencode',
|
|
17
|
+
codex: '.codex'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 处理 template save 命令
|
|
22
|
+
*/
|
|
23
|
+
export async function handleTemplateSave(options: { name?: string }): Promise<void> {
|
|
24
|
+
const projectDir = process.cwd();
|
|
25
|
+
const configFile = getProjectConfigPath(projectDir);
|
|
26
|
+
|
|
27
|
+
// 检查项目配置是否存在
|
|
28
|
+
if (!(await fs.pathExists(configFile))) {
|
|
29
|
+
console.log(chalk.yellow('Project not initialized. Run `tools-cc use <source>` first.'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// 读取项目配置
|
|
35
|
+
const config = await loadProjectConfig(projectDir);
|
|
36
|
+
if (!config) {
|
|
37
|
+
console.log(chalk.yellow('No project configuration found.'));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 确定模板名称
|
|
42
|
+
let templateName = options.name;
|
|
43
|
+
if (!templateName) {
|
|
44
|
+
templateName = path.basename(projectDir);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 检查是否已存在
|
|
48
|
+
const existing = await getTemplate(templateName, TEMPLATES_DIR);
|
|
49
|
+
if (existing) {
|
|
50
|
+
const answers = await inquirer.prompt([
|
|
51
|
+
{
|
|
52
|
+
type: 'confirm',
|
|
53
|
+
name: 'overwrite',
|
|
54
|
+
message: `Template "${templateName}" already exists. Overwrite?`,
|
|
55
|
+
default: false
|
|
56
|
+
}
|
|
57
|
+
]);
|
|
58
|
+
if (!answers.overwrite) {
|
|
59
|
+
console.log(chalk.gray('Cancelled.'));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 保存模板
|
|
65
|
+
const template = await saveTemplate(templateName, projectDir, config, TEMPLATES_DIR);
|
|
66
|
+
console.log(chalk.green(`✓ Template saved: ${template.name}`));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.log(chalk.red(`✗ Failed to save template: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 处理 template list 命令
|
|
74
|
+
*/
|
|
75
|
+
export async function handleTemplateList(): Promise<void> {
|
|
76
|
+
const templates = await listTemplates(TEMPLATES_DIR);
|
|
77
|
+
|
|
78
|
+
if (templates.length === 0) {
|
|
79
|
+
console.log(chalk.gray('No templates saved.'));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(chalk.bold('Saved templates:'));
|
|
84
|
+
for (const template of templates) {
|
|
85
|
+
const date = new Date(template.savedAt).toLocaleDateString();
|
|
86
|
+
console.log(` ${chalk.cyan(template.name.padEnd(20))} (saved: ${date})`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 处理 template rm 命令
|
|
92
|
+
*/
|
|
93
|
+
export async function handleTemplateRemove(name: string): Promise<void> {
|
|
94
|
+
try {
|
|
95
|
+
await removeTemplate(name, TEMPLATES_DIR);
|
|
96
|
+
console.log(chalk.green(`✓ Template removed: ${name}`));
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.log(chalk.red(`✗ Failed to remove template: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 处理 template use 命令
|
|
104
|
+
*/
|
|
105
|
+
export async function handleTemplateUse(name?: string, options?: { projects?: string[] }): Promise<void> {
|
|
106
|
+
const projectDir = process.cwd();
|
|
107
|
+
const toolsccDir = getToolsccDir(projectDir);
|
|
108
|
+
const configFile = getProjectConfigPath(projectDir);
|
|
109
|
+
|
|
110
|
+
// 如果没有指定名称,显示选择列表
|
|
111
|
+
if (!name) {
|
|
112
|
+
const templates = await listTemplates(TEMPLATES_DIR);
|
|
113
|
+
|
|
114
|
+
if (templates.length === 0) {
|
|
115
|
+
console.log(chalk.gray('No templates saved.'));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const answers = await inquirer.prompt([
|
|
120
|
+
{
|
|
121
|
+
type: 'list',
|
|
122
|
+
name: 'selectedTemplate',
|
|
123
|
+
message: 'Select a template to use:',
|
|
124
|
+
choices: templates.map(t => ({
|
|
125
|
+
name: `${t.name} (from: ${path.basename(t.sourceProject)})`,
|
|
126
|
+
value: t.name
|
|
127
|
+
}))
|
|
128
|
+
}
|
|
129
|
+
]);
|
|
130
|
+
name = answers.selectedTemplate as string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 获取模板
|
|
134
|
+
const template = await getTemplate(name as string, TEMPLATES_DIR);
|
|
135
|
+
if (!template) {
|
|
136
|
+
console.log(chalk.red(`✗ Template not found: ${name}`));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 定义源路径解析函数
|
|
141
|
+
const resolveSourcePath = async (sourceName: string): Promise<string> => {
|
|
142
|
+
return await getSourcePath(sourceName, GLOBAL_CONFIG_DIR);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// 创建临时配置文件
|
|
146
|
+
const tempConfigPath = path.join(projectDir, '.toolscc-template-temp.json');
|
|
147
|
+
const exportConfig = {
|
|
148
|
+
version: '1.0',
|
|
149
|
+
type: 'project' as const,
|
|
150
|
+
config: template.config,
|
|
151
|
+
exportedAt: new Date().toISOString()
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// 导入配置
|
|
155
|
+
try {
|
|
156
|
+
await fs.writeJson(tempConfigPath, exportConfig, { spaces: 2 });
|
|
157
|
+
await importProjectConfig(tempConfigPath, projectDir, resolveSourcePath);
|
|
158
|
+
console.log(chalk.green(`✓ Applied template: ${name}`));
|
|
159
|
+
|
|
160
|
+
// 创建符号链接
|
|
161
|
+
await createToolLinks(projectDir, toolsccDir, configFile, options?.projects);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.log(chalk.red(`✗ Failed to apply template: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
164
|
+
} finally {
|
|
165
|
+
await fs.remove(tempConfigPath);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 创建工具符号链接
|
|
171
|
+
*/
|
|
172
|
+
async function createToolLinks(
|
|
173
|
+
projectDir: string,
|
|
174
|
+
toolsccDir: string,
|
|
175
|
+
configFile: string,
|
|
176
|
+
projects?: string[]
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
const tools = projects || Object.keys(SUPPORTED_TOOLS);
|
|
179
|
+
|
|
180
|
+
for (const tool of tools) {
|
|
181
|
+
const linkName = SUPPORTED_TOOLS[tool];
|
|
182
|
+
if (!linkName) {
|
|
183
|
+
console.log(chalk.yellow(`Unknown tool: ${tool}`));
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const linkPath = path.join(projectDir, linkName);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await createSymlink(toolsccDir, linkPath, true);
|
|
191
|
+
console.log(chalk.green(`✓ Linked: ${linkName} -> .toolscc`));
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.log(chalk.red(`✗ Failed to link ${linkName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 更新项目配置
|
|
198
|
+
const config = await fs.readJson(configFile);
|
|
199
|
+
const existingLinks = config.links || [];
|
|
200
|
+
config.links = [...new Set([...existingLinks, ...tools])];
|
|
201
|
+
await fs.writeJson(configFile, config, { spaces: 2 });
|
|
202
|
+
}
|
package/src/commands/use.ts
CHANGED
|
@@ -195,6 +195,14 @@ async function handleInteractiveMode(
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
// Rules 区
|
|
199
|
+
if (manifest.rules && manifest.rules.length > 0) {
|
|
200
|
+
choices.push(new inquirer.Separator(`--- Rules (${manifest.rules.length}) ---`));
|
|
201
|
+
for (const rule of manifest.rules) {
|
|
202
|
+
choices.push({ name: rule, value: `rules/${rule}` });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
198
206
|
if (choices.length === 0) {
|
|
199
207
|
console.log(chalk.yellow(`No items found in source: ${sourceName}`));
|
|
200
208
|
return;
|
|
@@ -220,7 +228,8 @@ async function handleInteractiveMode(
|
|
|
220
228
|
const selection: SourceSelection = {
|
|
221
229
|
skills: [],
|
|
222
230
|
commands: [],
|
|
223
|
-
agents: []
|
|
231
|
+
agents: [],
|
|
232
|
+
rules: []
|
|
224
233
|
};
|
|
225
234
|
|
|
226
235
|
for (const item of answers.selectedItems) {
|
|
@@ -228,6 +237,7 @@ async function handleInteractiveMode(
|
|
|
228
237
|
if (type === 'skills') selection.skills.push(name);
|
|
229
238
|
else if (type === 'commands') selection.commands.push(name);
|
|
230
239
|
else if (type === 'agents') selection.agents.push(name);
|
|
240
|
+
else if (type === 'rules') selection.rules.push(name);
|
|
231
241
|
}
|
|
232
242
|
|
|
233
243
|
// 初始化项目并应用选择
|
package/src/core/manifest.ts
CHANGED
|
@@ -1,57 +1,67 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { Manifest } from '../types';
|
|
4
|
-
|
|
5
|
-
export async function loadManifest(sourceDir: string): Promise<Manifest> {
|
|
6
|
-
const manifestPath = path.join(sourceDir, 'manifest.json');
|
|
7
|
-
|
|
8
|
-
if (await fs.pathExists(manifestPath)) {
|
|
9
|
-
try {
|
|
10
|
-
return await fs.readJson(manifestPath);
|
|
11
|
-
} catch (error) {
|
|
12
|
-
throw new Error(`Failed to parse manifest.json: ${error instanceof Error ? error.message : 'Invalid JSON'}`);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return scanSource(sourceDir);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function scanSource(sourceDir: string): Promise<Manifest> {
|
|
20
|
-
const name = path.basename(sourceDir);
|
|
21
|
-
const manifest: Manifest = {
|
|
22
|
-
name,
|
|
23
|
-
version: '0.0.0',
|
|
24
|
-
skills: [],
|
|
25
|
-
commands: [],
|
|
26
|
-
agents: []
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Manifest } from '../types';
|
|
4
|
+
|
|
5
|
+
export async function loadManifest(sourceDir: string): Promise<Manifest> {
|
|
6
|
+
const manifestPath = path.join(sourceDir, 'manifest.json');
|
|
7
|
+
|
|
8
|
+
if (await fs.pathExists(manifestPath)) {
|
|
9
|
+
try {
|
|
10
|
+
return await fs.readJson(manifestPath);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
throw new Error(`Failed to parse manifest.json: ${error instanceof Error ? error.message : 'Invalid JSON'}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return scanSource(sourceDir);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function scanSource(sourceDir: string): Promise<Manifest> {
|
|
20
|
+
const name = path.basename(sourceDir);
|
|
21
|
+
const manifest: Manifest = {
|
|
22
|
+
name,
|
|
23
|
+
version: '0.0.0',
|
|
24
|
+
skills: [],
|
|
25
|
+
commands: [],
|
|
26
|
+
agents: [],
|
|
27
|
+
rules: []
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Scan skills
|
|
31
|
+
const skillsDir = path.join(sourceDir, 'skills');
|
|
32
|
+
if (await fs.pathExists(skillsDir)) {
|
|
33
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
34
|
+
manifest.skills = entries
|
|
35
|
+
.filter(e => e.isDirectory())
|
|
36
|
+
.map(e => e.name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Scan commands
|
|
40
|
+
const commandsDir = path.join(sourceDir, 'commands');
|
|
41
|
+
if (await fs.pathExists(commandsDir)) {
|
|
42
|
+
const entries = await fs.readdir(commandsDir, { withFileTypes: true });
|
|
43
|
+
manifest.commands = entries
|
|
44
|
+
.filter(e => e.isFile() && e.name.endsWith('.md'))
|
|
45
|
+
.map(e => e.name.replace('.md', ''));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Scan agents
|
|
49
|
+
const agentsDir = path.join(sourceDir, 'agents');
|
|
50
|
+
if (await fs.pathExists(agentsDir)) {
|
|
51
|
+
const entries = await fs.readdir(agentsDir, { withFileTypes: true });
|
|
52
|
+
manifest.agents = entries
|
|
53
|
+
.filter(e => e.isFile() && e.name.endsWith('.md'))
|
|
54
|
+
.map(e => e.name.replace('.md', ''));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Scan rules
|
|
58
|
+
const rulesDir = path.join(sourceDir, 'rules');
|
|
59
|
+
if (await fs.pathExists(rulesDir)) {
|
|
60
|
+
const entries = await fs.readdir(rulesDir, { withFileTypes: true });
|
|
61
|
+
manifest.rules = entries
|
|
62
|
+
.filter(e => e.isDirectory())
|
|
63
|
+
.map(e => e.name);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return manifest;
|
|
67
|
+
}
|