tools-cc 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/CHANGELOG_en.md +22 -0
  3. package/README.md +31 -5
  4. package/README_en.md +31 -5
  5. package/dist/commands/help.js +18 -1
  6. package/dist/commands/template.d.ts +18 -0
  7. package/dist/commands/template.js +154 -0
  8. package/dist/commands/use.js +11 -1
  9. package/dist/core/manifest.js +10 -1
  10. package/dist/core/project.js +27 -1
  11. package/dist/core/template.d.ts +17 -0
  12. package/dist/core/template.js +74 -0
  13. package/dist/index.js +32 -0
  14. package/dist/types/config.d.ts +13 -1
  15. package/dist/types/config.js +4 -2
  16. package/dist/utils/parsePath.d.ts +1 -1
  17. package/dist/utils/parsePath.js +5 -3
  18. package/dist/utils/path.d.ts +2 -0
  19. package/dist/utils/path.js +6 -1
  20. package/package.json +3 -3
  21. package/src/commands/help.ts +18 -1
  22. package/src/commands/template.ts +160 -0
  23. package/src/commands/use.ts +11 -1
  24. package/src/core/manifest.ts +67 -57
  25. package/src/core/project.ts +304 -276
  26. package/src/core/template.ts +91 -0
  27. package/src/index.ts +48 -11
  28. package/src/types/config.ts +18 -3
  29. package/src/utils/parsePath.ts +6 -4
  30. package/src/utils/path.ts +23 -18
  31. package/tests/commands/export.test.ts +120 -0
  32. package/tests/commands/use.test.ts +332 -0
  33. package/tests/core/config.test.ts +37 -0
  34. package/tests/core/manifest.test.ts +37 -0
  35. package/tests/core/project.test.ts +325 -0
  36. package/tests/core/source.test.ts +75 -0
  37. package/tests/core/symlink.test.ts +39 -0
  38. package/tests/core/template.test.ts +103 -0
  39. package/tests/types/config.test.ts +260 -0
  40. package/tests/utils/parsePath.test.ts +244 -0
@@ -8,12 +8,13 @@ export interface GlobalConfig {
8
8
  sources: Record<string, SourceConfig>;
9
9
  }
10
10
  /**
11
- * 源选择配置 - 指定从源中导入哪些 skills/commands/agents
11
+ * 源选择配置 - 指定从源中导入哪些 skills/commands/agents/rules
12
12
  */
13
13
  export interface SourceSelection {
14
14
  skills: string[];
15
15
  commands: string[];
16
16
  agents: string[];
17
+ rules: string[];
17
18
  }
18
19
  /**
19
20
  * 新版项目配置 - sources 使用 Record 格式支持部分导入
@@ -44,6 +45,7 @@ export interface Manifest {
44
45
  skills?: string[];
45
46
  commands?: string[];
46
47
  agents?: string[];
48
+ rules?: string[];
47
49
  }
48
50
  export interface ToolConfig {
49
51
  linkName: string;
@@ -66,3 +68,13 @@ export interface GlobalExportConfig {
66
68
  config: GlobalConfig;
67
69
  exportedAt: string;
68
70
  }
71
+ /**
72
+ * 模板配置格式
73
+ */
74
+ export interface TemplateConfig {
75
+ version: string;
76
+ name: string;
77
+ sourceProject: string;
78
+ savedAt: string;
79
+ config: ProjectConfig;
80
+ }
@@ -11,7 +11,8 @@ function isSourceSelection(value) {
11
11
  const obj = value;
12
12
  return (Array.isArray(obj.skills) &&
13
13
  Array.isArray(obj.commands) &&
14
- Array.isArray(obj.agents));
14
+ Array.isArray(obj.agents) &&
15
+ Array.isArray(obj.rules));
15
16
  }
16
17
  /**
17
18
  * 将旧版项目配置转换为新版格式
@@ -25,7 +26,8 @@ function normalizeProjectConfig(config) {
25
26
  newSources[sourceName] = {
26
27
  skills: ['*'],
27
28
  commands: ['*'],
28
- agents: ['*']
29
+ agents: ['*'],
30
+ rules: ['*']
29
31
  };
30
32
  }
31
33
  return { sources: newSources, links: config.links };
@@ -4,7 +4,7 @@ import { SourceSelection } from '../types/config';
4
4
  */
5
5
  export interface ParsedSourcePath {
6
6
  sourceName: string;
7
- type?: 'skills' | 'commands' | 'agents';
7
+ type?: 'skills' | 'commands' | 'agents' | 'rules';
8
8
  itemName?: string;
9
9
  }
10
10
  /**
@@ -25,7 +25,7 @@ function parseSourcePath(input) {
25
25
  return { sourceName };
26
26
  }
27
27
  // 检查第二部分是否为有效类型
28
- const validTypes = ['skills', 'commands', 'agents'];
28
+ const validTypes = ['skills', 'commands', 'agents', 'rules'];
29
29
  const type = parts[1];
30
30
  if (!validTypes.includes(type)) {
31
31
  return { sourceName };
@@ -63,7 +63,8 @@ function buildSelectionFromPaths(paths) {
63
63
  result[sourceName] = {
64
64
  skills: [],
65
65
  commands: [],
66
- agents: []
66
+ agents: [],
67
+ rules: []
67
68
  };
68
69
  }
69
70
  // 如果没有指定类型和项目名称,表示整个源
@@ -71,7 +72,8 @@ function buildSelectionFromPaths(paths) {
71
72
  result[sourceName] = {
72
73
  skills: ['*'],
73
74
  commands: ['*'],
74
- agents: ['*']
75
+ agents: ['*'],
76
+ rules: ['*']
75
77
  };
76
78
  continue;
77
79
  }
@@ -1,8 +1,10 @@
1
1
  export declare const GLOBAL_CONFIG_DIR: string;
2
2
  export declare const GLOBAL_CONFIG_FILE: string;
3
+ export declare const TEMPLATES_DIR: string;
3
4
  export declare const DEFAULT_CONFIG: {
4
5
  sourcesDir: string;
5
6
  sources: {};
6
7
  };
8
+ export declare function getTemplatePath(templateName: string): string;
7
9
  export declare function getToolsccDir(projectDir: string): string;
8
10
  export declare function getProjectConfigPath(projectDir: string): string;
@@ -3,17 +3,22 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.DEFAULT_CONFIG = exports.GLOBAL_CONFIG_FILE = exports.GLOBAL_CONFIG_DIR = void 0;
6
+ exports.DEFAULT_CONFIG = exports.TEMPLATES_DIR = exports.GLOBAL_CONFIG_FILE = exports.GLOBAL_CONFIG_DIR = void 0;
7
+ exports.getTemplatePath = getTemplatePath;
7
8
  exports.getToolsccDir = getToolsccDir;
8
9
  exports.getProjectConfigPath = getProjectConfigPath;
9
10
  const os_1 = __importDefault(require("os"));
10
11
  const path_1 = __importDefault(require("path"));
11
12
  exports.GLOBAL_CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.tools-cc');
12
13
  exports.GLOBAL_CONFIG_FILE = path_1.default.join(exports.GLOBAL_CONFIG_DIR, 'config.json');
14
+ exports.TEMPLATES_DIR = path_1.default.join(exports.GLOBAL_CONFIG_DIR, 'templates');
13
15
  exports.DEFAULT_CONFIG = {
14
16
  sourcesDir: path_1.default.join(exports.GLOBAL_CONFIG_DIR, 'sources'),
15
17
  sources: {}
16
18
  };
19
+ function getTemplatePath(templateName) {
20
+ return path_1.default.join(exports.TEMPLATES_DIR, `${templateName}.json`);
21
+ }
17
22
  function getToolsccDir(projectDir) {
18
23
  return path_1.default.join(projectDir, '.toolscc');
19
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tools-cc",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "tools-cc [options] <command> [args]",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -18,14 +18,14 @@
18
18
  "release": "npm run tag:create && npm run publish:npm && npm run release:gh"
19
19
  },
20
20
  "bin": {
21
- "tools-cc": "./dist/index.js"
21
+ "tools-cc": "dist/index.js"
22
22
  },
23
23
  "keywords": [],
24
24
  "author": "",
25
25
  "license": "ISC",
26
26
  "repository": {
27
27
  "type": "git",
28
- "url": "https://github.com/q759410559/tools-cc.git"
28
+ "url": "git+https://github.com/q759410559/tools-cc.git"
29
29
  },
30
30
  "bugs": {
31
31
  "url": "https://github.com/q759410559/tools-cc/issues"
@@ -23,7 +23,7 @@ ${chalk.bold('COMMANDS / 命令')}
23
23
  tools-cc sources add <name> <path-or-url> Add a source / 添加配置源
24
24
  tools-cc sources list, ls List all sources / 列出所有配置源
25
25
  tools-cc sources remove, rm <name> Remove a source / 移除配置源
26
- tools-cc sources update, up [name] git pull update / 更新源代码
26
+ tools-cc sources update, up, upgrade [name] git pull update / 更新源代码
27
27
  tools-cc sources scan Scan dir for sources / 扫描发现新源
28
28
 
29
29
  ${chalk.gray('Shortcut: -s')} e.g., tools-cc -s add my-skills https://github.com/user/skills.git
@@ -43,6 +43,14 @@ ${chalk.bold('COMMANDS / 命令')}
43
43
  tools-cc status Show project status / 显示项目状态
44
44
  tools-cc export [options] Export config / 导出配置
45
45
 
46
+ ${chalk.cyan('Template Management / 模板管理')}
47
+ tools-cc template save [-n <name>] Save project config as template / 保存项目配置为模板
48
+ tools-cc template list, ls List all templates / 列出所有模板
49
+ tools-cc template rm <name> Delete template / 删除模板
50
+ tools-cc template use [name] Apply template to project / 应用模板到项目
51
+
52
+ ${chalk.gray('Shortcut: tpl')} e.g., tools-cc tpl save, tools-cc tpl list
53
+
46
54
  ${chalk.cyan('Help / 帮助')}
47
55
  tools-cc help Show this help / 显示此帮助信息
48
56
  tools-cc --help, -h Show command help / 显示命令帮助
@@ -95,6 +103,15 @@ ${chalk.bold('EXAMPLES / 示例')}
95
103
  ${chalk.gray('# Export project config / 导出项目配置')}
96
104
  tools-cc export -o my-config.json
97
105
 
106
+ ${chalk.gray('# Save project config as template / 保存项目配置为模板')}
107
+ tools-cc template save -n my-template
108
+
109
+ ${chalk.gray('# List all templates / 列出所有模板')}
110
+ tools-cc template list
111
+
112
+ ${chalk.gray('# Apply template to project / 应用模板到项目')}
113
+ tools-cc template use my-template
114
+
98
115
  ${chalk.gray('# Show full configuration / 显示完整配置')}
99
116
  tools-cc config list
100
117
 
@@ -0,0 +1,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 { TEMPLATES_DIR, getProjectConfigPath, GLOBAL_CONFIG_DIR } from '../utils/path';
9
+ import fs from 'fs-extra';
10
+
11
+ /**
12
+ * 处理 template save 命令
13
+ */
14
+ export async function handleTemplateSave(options: { name?: string }): Promise<void> {
15
+ const projectDir = process.cwd();
16
+ const configFile = getProjectConfigPath(projectDir);
17
+
18
+ // 检查项目配置是否存在
19
+ if (!(await fs.pathExists(configFile))) {
20
+ console.log(chalk.yellow('Project not initialized. Run `tools-cc use <source>` first.'));
21
+ return;
22
+ }
23
+
24
+ try {
25
+ // 读取项目配置
26
+ const config = await loadProjectConfig(projectDir);
27
+ if (!config) {
28
+ console.log(chalk.yellow('No project configuration found.'));
29
+ return;
30
+ }
31
+
32
+ // 确定模板名称
33
+ let templateName = options.name;
34
+ if (!templateName) {
35
+ templateName = path.basename(projectDir);
36
+ }
37
+
38
+ // 检查是否已存在
39
+ const existing = await getTemplate(templateName, TEMPLATES_DIR);
40
+ if (existing) {
41
+ const answers = await inquirer.prompt([
42
+ {
43
+ type: 'confirm',
44
+ name: 'overwrite',
45
+ message: `Template "${templateName}" already exists. Overwrite?`,
46
+ default: false
47
+ }
48
+ ]);
49
+ if (!answers.overwrite) {
50
+ console.log(chalk.gray('Cancelled.'));
51
+ return;
52
+ }
53
+ }
54
+
55
+ // 保存模板
56
+ const template = await saveTemplate(templateName, projectDir, config, TEMPLATES_DIR);
57
+ console.log(chalk.green(`✓ Template saved: ${template.name}`));
58
+ } catch (error) {
59
+ console.log(chalk.red(`✗ Failed to save template: ${error instanceof Error ? error.message : 'Unknown error'}`));
60
+ }
61
+ }
62
+
63
+ /**
64
+ * 处理 template list 命令
65
+ */
66
+ export async function handleTemplateList(): Promise<void> {
67
+ const templates = await listTemplates(TEMPLATES_DIR);
68
+
69
+ if (templates.length === 0) {
70
+ console.log(chalk.gray('No templates saved.'));
71
+ return;
72
+ }
73
+
74
+ console.log(chalk.bold('Saved templates:'));
75
+ for (const template of templates) {
76
+ const date = new Date(template.savedAt).toLocaleDateString();
77
+ console.log(` ${chalk.cyan(template.name.padEnd(20))} (saved: ${date})`);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * 处理 template rm 命令
83
+ */
84
+ export async function handleTemplateRemove(name: string): Promise<void> {
85
+ try {
86
+ await removeTemplate(name, TEMPLATES_DIR);
87
+ console.log(chalk.green(`✓ Template removed: ${name}`));
88
+ } catch (error) {
89
+ console.log(chalk.red(`✗ Failed to remove template: ${error instanceof Error ? error.message : 'Unknown error'}`));
90
+ }
91
+ }
92
+
93
+ /**
94
+ * 处理 template use 命令
95
+ */
96
+ export async function handleTemplateUse(name?: string): Promise<void> {
97
+ const projectDir = process.cwd();
98
+ const configFile = getProjectConfigPath(projectDir);
99
+
100
+ // 检查项目是否已初始化
101
+ if (!(await fs.pathExists(configFile))) {
102
+ console.log(chalk.yellow('Project not initialized. Run `tools-cc use <source>` first.'));
103
+ return;
104
+ }
105
+
106
+ // 如果没有指定名称,显示选择列表
107
+ if (!name) {
108
+ const templates = await listTemplates(TEMPLATES_DIR);
109
+
110
+ if (templates.length === 0) {
111
+ console.log(chalk.gray('No templates saved.'));
112
+ return;
113
+ }
114
+
115
+ const answers = await inquirer.prompt([
116
+ {
117
+ type: 'list',
118
+ name: 'selectedTemplate',
119
+ message: 'Select a template to use:',
120
+ choices: templates.map(t => ({
121
+ name: `${t.name} (from: ${path.basename(t.sourceProject)})`,
122
+ value: t.name
123
+ }))
124
+ }
125
+ ]);
126
+ name = answers.selectedTemplate as string;
127
+ }
128
+
129
+ // 获取模板
130
+ const template = await getTemplate(name as string, TEMPLATES_DIR);
131
+ if (!template) {
132
+ console.log(chalk.red(`✗ Template not found: ${name}`));
133
+ return;
134
+ }
135
+
136
+ // 定义源路径解析函数
137
+ const resolveSourcePath = async (sourceName: string): Promise<string> => {
138
+ return await getSourcePath(sourceName, GLOBAL_CONFIG_DIR);
139
+ };
140
+
141
+ // 创建临时配置文件
142
+ const tempConfigPath = path.join(projectDir, '.toolscc-template-temp.json');
143
+ const exportConfig = {
144
+ version: '1.0',
145
+ type: 'project' as const,
146
+ config: template.config,
147
+ exportedAt: new Date().toISOString()
148
+ };
149
+
150
+ // 导入配置
151
+ try {
152
+ await fs.writeJson(tempConfigPath, exportConfig, { spaces: 2 });
153
+ await importProjectConfig(tempConfigPath, projectDir, resolveSourcePath);
154
+ console.log(chalk.green(`✓ Applied template: ${name}`));
155
+ } catch (error) {
156
+ console.log(chalk.red(`✗ Failed to apply template: ${error instanceof Error ? error.message : 'Unknown error'}`));
157
+ } finally {
158
+ await fs.remove(tempConfigPath);
159
+ }
160
+ }
@@ -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
  // 初始化项目并应用选择
@@ -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
- // Scan skills
30
- const skillsDir = path.join(sourceDir, 'skills');
31
- if (await fs.pathExists(skillsDir)) {
32
- const entries = await fs.readdir(skillsDir, { withFileTypes: true });
33
- manifest.skills = entries
34
- .filter(e => e.isDirectory())
35
- .map(e => e.name);
36
- }
37
-
38
- // Scan commands
39
- const commandsDir = path.join(sourceDir, 'commands');
40
- if (await fs.pathExists(commandsDir)) {
41
- const entries = await fs.readdir(commandsDir, { withFileTypes: true });
42
- manifest.commands = entries
43
- .filter(e => e.isFile() && e.name.endsWith('.md'))
44
- .map(e => e.name.replace('.md', ''));
45
- }
46
-
47
- // Scan agents
48
- const agentsDir = path.join(sourceDir, 'agents');
49
- if (await fs.pathExists(agentsDir)) {
50
- const entries = await fs.readdir(agentsDir, { withFileTypes: true });
51
- manifest.agents = entries
52
- .filter(e => e.isFile() && e.name.endsWith('.md'))
53
- .map(e => e.name.replace('.md', ''));
54
- }
55
-
56
- return manifest;
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
+ }