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
@@ -0,0 +1,91 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { TemplateConfig, ProjectConfig } from '../types';
4
+
5
+ /**
6
+ * 保存项目配置为模板
7
+ */
8
+ export async function saveTemplate(
9
+ name: string,
10
+ sourceProject: string,
11
+ config: ProjectConfig,
12
+ templatesDir: string
13
+ ): Promise<TemplateConfig> {
14
+ if (!name || !name.trim()) {
15
+ throw new Error('Template name is required');
16
+ }
17
+
18
+ await fs.ensureDir(templatesDir);
19
+
20
+ const template: TemplateConfig = {
21
+ version: '1.0',
22
+ name,
23
+ sourceProject,
24
+ savedAt: new Date().toISOString(),
25
+ config
26
+ };
27
+
28
+ const templatePath = path.join(templatesDir, `${name}.json`);
29
+ await fs.writeJson(templatePath, template, { spaces: 2 });
30
+
31
+ return template;
32
+ }
33
+
34
+ /**
35
+ * 列出所有模板
36
+ */
37
+ export async function listTemplates(templatesDir: string): Promise<TemplateConfig[]> {
38
+ if (!(await fs.pathExists(templatesDir))) {
39
+ return [];
40
+ }
41
+
42
+ const files = await fs.readdir(templatesDir);
43
+ const templates: TemplateConfig[] = [];
44
+
45
+ for (const file of files) {
46
+ if (file.endsWith('.json')) {
47
+ try {
48
+ const template = await fs.readJson(path.join(templatesDir, file));
49
+ if (template.version && template.name && template.config) {
50
+ templates.push(template);
51
+ }
52
+ } catch {
53
+ // Skip invalid files
54
+ }
55
+ }
56
+ }
57
+
58
+ return templates.sort((a, b) => b.savedAt.localeCompare(a.savedAt));
59
+ }
60
+
61
+ /**
62
+ * 获取指定模板
63
+ */
64
+ export async function getTemplate(
65
+ name: string,
66
+ templatesDir: string
67
+ ): Promise<TemplateConfig | null> {
68
+ const templatePath = path.join(templatesDir, `${name}.json`);
69
+
70
+ if (!(await fs.pathExists(templatePath))) {
71
+ return null;
72
+ }
73
+
74
+ return await fs.readJson(templatePath);
75
+ }
76
+
77
+ /**
78
+ * 删除模板
79
+ */
80
+ export async function removeTemplate(
81
+ name: string,
82
+ templatesDir: string
83
+ ): Promise<void> {
84
+ const templatePath = path.join(templatesDir, `${name}.json`);
85
+
86
+ if (!(await fs.pathExists(templatePath))) {
87
+ throw new Error(`Template not found: ${name}`);
88
+ }
89
+
90
+ await fs.remove(templatePath);
91
+ }
package/src/index.ts CHANGED
@@ -3,9 +3,10 @@
3
3
  import { Command } from 'commander';
4
4
  import { handleConfigSet, handleConfigGet, handleConfigList } from './commands/config';
5
5
  import { handleSourceAdd, handleSourceList, handleSourceRemove, handleSourceUpdate, handleSourceScan } from './commands/source';
6
- import { handleUse, handleList, handleRemove, handleStatus, handleProjectUpdate } from './commands/use';
7
- import { handleExport } from './commands/export';
8
- import { showHelp } from './commands/help';
6
+ import { handleUse, handleList, handleRemove, handleStatus, handleProjectUpdate } from './commands/use';
7
+ import { handleExport } from './commands/export';
8
+ import { handleTemplateSave, handleTemplateList, handleTemplateRemove, handleTemplateUse } from './commands/template';
9
+ import { showHelp } from './commands/help';
9
10
 
10
11
  const program = new Command();
11
12
 
@@ -89,16 +90,52 @@ configCmd
89
90
  await handleConfigList();
90
91
  });
91
92
 
93
+ // Template commands
94
+ const templateCmd = program
95
+ .command('template')
96
+ .description('Template management')
97
+ .alias('tpl');
98
+
99
+ templateCmd
100
+ .command('save')
101
+ .description('Save current project config as template')
102
+ .option('-n, --name <name>', 'Template name (default: project directory name)')
103
+ .action(async (options) => {
104
+ await handleTemplateSave(options);
105
+ });
106
+
107
+ templateCmd
108
+ .command('list')
109
+ .alias('ls')
110
+ .description('List all saved templates')
111
+ .action(async () => {
112
+ await handleTemplateList();
113
+ });
114
+
115
+ templateCmd
116
+ .command('rm <name>')
117
+ .description('Remove a template')
118
+ .action(async (name: string) => {
119
+ await handleTemplateRemove(name);
120
+ });
121
+
122
+ templateCmd
123
+ .command('use [name]')
124
+ .description('Apply a template to current project')
125
+ .action(async (name?: string) => {
126
+ await handleTemplateUse(name);
127
+ });
128
+
92
129
  // Project commands
93
130
  program
94
- .command('use [sources...]')
95
- .description('Use sources in current project')
96
- .option('-p, --projects <tools...>', 'Tools to link (iflow, claude, codebuddy, opencode, codex)')
97
- .option('-l, --ls', 'Interactive selection mode for single source')
98
- .option('-c, --config <file>', 'Import from config file')
99
- .action(async (sources: string[], options) => {
100
- await handleUse(sources, options);
101
- });
131
+ .command('use [sources...]')
132
+ .description('Use sources in current project')
133
+ .option('-p, --projects <tools...>', 'Tools to link (iflow, claude, codebuddy, opencode, codex)')
134
+ .option('-l, --ls', 'Interactive selection mode for single source')
135
+ .option('-c, --config <file>', 'Import from config file')
136
+ .action(async (sources: string[], options) => {
137
+ await handleUse(sources, options);
138
+ });
102
139
 
103
140
  program
104
141
  .command('list')
@@ -10,12 +10,13 @@ export interface GlobalConfig {
10
10
  }
11
11
 
12
12
  /**
13
- * 源选择配置 - 指定从源中导入哪些 skills/commands/agents
13
+ * 源选择配置 - 指定从源中导入哪些 skills/commands/agents/rules
14
14
  */
15
15
  export interface SourceSelection {
16
16
  skills: string[];
17
17
  commands: string[];
18
18
  agents: string[];
19
+ rules: string[];
19
20
  }
20
21
 
21
22
  /**
@@ -43,7 +44,8 @@ export function isSourceSelection(value: unknown): value is SourceSelection {
43
44
  return (
44
45
  Array.isArray(obj.skills) &&
45
46
  Array.isArray(obj.commands) &&
46
- Array.isArray(obj.agents)
47
+ Array.isArray(obj.agents) &&
48
+ Array.isArray(obj.rules)
47
49
  );
48
50
  }
49
51
 
@@ -61,7 +63,8 @@ export function normalizeProjectConfig(
61
63
  newSources[sourceName] = {
62
64
  skills: ['*'],
63
65
  commands: ['*'],
64
- agents: ['*']
66
+ agents: ['*'],
67
+ rules: ['*']
65
68
  };
66
69
  }
67
70
  return { sources: newSources, links: config.links };
@@ -75,6 +78,7 @@ export interface Manifest {
75
78
  skills?: string[];
76
79
  commands?: string[];
77
80
  agents?: string[];
81
+ rules?: string[];
78
82
  }
79
83
 
80
84
  export interface ToolConfig {
@@ -99,4 +103,15 @@ export interface GlobalExportConfig {
99
103
  type: 'global';
100
104
  config: GlobalConfig;
101
105
  exportedAt: string;
106
+ }
107
+
108
+ /**
109
+ * 模板配置格式
110
+ */
111
+ export interface TemplateConfig {
112
+ version: string;
113
+ name: string;
114
+ sourceProject: string;
115
+ savedAt: string;
116
+ config: ProjectConfig;
102
117
  }
@@ -5,7 +5,7 @@ import { SourceSelection } from '../types/config';
5
5
  */
6
6
  export interface ParsedSourcePath {
7
7
  sourceName: string;
8
- type?: 'skills' | 'commands' | 'agents';
8
+ type?: 'skills' | 'commands' | 'agents' | 'rules';
9
9
  itemName?: string;
10
10
  }
11
11
 
@@ -36,7 +36,7 @@ export function parseSourcePath(input: string): ParsedSourcePath {
36
36
  }
37
37
 
38
38
  // 检查第二部分是否为有效类型
39
- const validTypes = ['skills', 'commands', 'agents'] as const;
39
+ const validTypes = ['skills', 'commands', 'agents', 'rules'] as const;
40
40
  const type = parts[1] as typeof validTypes[number];
41
41
 
42
42
  if (!validTypes.includes(type)) {
@@ -81,7 +81,8 @@ export function buildSelectionFromPaths(paths: string[]): Record<string, SourceS
81
81
  result[sourceName] = {
82
82
  skills: [],
83
83
  commands: [],
84
- agents: []
84
+ agents: [],
85
+ rules: []
85
86
  };
86
87
  }
87
88
 
@@ -90,7 +91,8 @@ export function buildSelectionFromPaths(paths: string[]): Record<string, SourceS
90
91
  result[sourceName] = {
91
92
  skills: ['*'],
92
93
  commands: ['*'],
93
- agents: ['*']
94
+ agents: ['*'],
95
+ rules: ['*']
94
96
  };
95
97
  continue;
96
98
  }
package/src/utils/path.ts CHANGED
@@ -1,18 +1,23 @@
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 getToolsccDir(projectDir: string): string {
13
- return path.join(projectDir, '.toolscc');
14
- }
15
-
16
- export function getProjectConfigPath(projectDir: string): string {
17
- return path.join(getToolsccDir(projectDir), 'config.json');
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
+ export const TEMPLATES_DIR = path.join(GLOBAL_CONFIG_DIR, 'templates');
7
+
8
+ export const DEFAULT_CONFIG = {
9
+ sourcesDir: path.join(GLOBAL_CONFIG_DIR, 'sources'),
10
+ sources: {}
11
+ };
12
+
13
+ export function getTemplatePath(templateName: string): string {
14
+ return path.join(TEMPLATES_DIR, `${templateName}.json`);
15
+ }
16
+
17
+ export function getToolsccDir(projectDir: string): string {
18
+ return path.join(projectDir, '.toolscc');
19
+ }
20
+
21
+ export function getProjectConfigPath(projectDir: string): string {
22
+ return path.join(getToolsccDir(projectDir), 'config.json');
23
+ }
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { handleExport } from '../../src/commands/export';
5
+ import { initProject, useSource } from '../../src/core/project';
6
+ import { GLOBAL_CONFIG_DIR } from '../../src/utils/path';
7
+
8
+ describe('handleExport', () => {
9
+ const testProjectDir = path.join(__dirname, '../fixtures/test-export-cmd-project');
10
+ const testSourceDir = path.join(__dirname, '../fixtures/test-export-cmd-source');
11
+ const exportFilePath = path.join(__dirname, '../fixtures/test-export-cmd.json');
12
+ const globalExportPath = path.join(__dirname, '../fixtures/test-global-export-cmd.json');
13
+
14
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>;
15
+
16
+ beforeEach(async () => {
17
+ // Create test source
18
+ await fs.ensureDir(path.join(testSourceDir, 'skills', 'test-skill'));
19
+ await fs.writeFile(path.join(testSourceDir, 'skills', 'test-skill', 'test.md'), '# test skill');
20
+
21
+ // Create project directory
22
+ await fs.ensureDir(testProjectDir);
23
+
24
+ // Spy on console.log
25
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
26
+ });
27
+
28
+ afterEach(async () => {
29
+ await fs.remove(testProjectDir);
30
+ await fs.remove(testSourceDir);
31
+ await fs.remove(exportFilePath);
32
+ await fs.remove(globalExportPath);
33
+ consoleLogSpy.mockRestore();
34
+ });
35
+
36
+ it('should export project config to default path', async () => {
37
+ // Initialize project with a source
38
+ await initProject(testProjectDir);
39
+ await useSource('test-source', testSourceDir, testProjectDir);
40
+
41
+ // Change to project directory for the test
42
+ const originalCwd = process.cwd();
43
+ process.chdir(testProjectDir);
44
+
45
+ try {
46
+ await handleExport({});
47
+
48
+ // Check default export file was created
49
+ const defaultExportPath = path.join(testProjectDir, '.toolscc-export.json');
50
+ expect(await fs.pathExists(defaultExportPath)).toBe(true);
51
+
52
+ // Verify file content
53
+ const exported = await fs.readJson(defaultExportPath);
54
+ expect(exported.version).toBe('1.0');
55
+ expect(exported.type).toBe('project');
56
+
57
+ // Verify console output
58
+ expect(console.log).toHaveBeenCalledWith(
59
+ expect.stringContaining('✓ Project config exported to:')
60
+ );
61
+
62
+ // Cleanup
63
+ await fs.remove(defaultExportPath);
64
+ } finally {
65
+ process.chdir(originalCwd);
66
+ }
67
+ });
68
+
69
+ it('should export project config to custom path', async () => {
70
+ await initProject(testProjectDir);
71
+ await useSource('test-source', testSourceDir, testProjectDir);
72
+
73
+ const originalCwd = process.cwd();
74
+ process.chdir(testProjectDir);
75
+
76
+ try {
77
+ await handleExport({ output: exportFilePath });
78
+
79
+ expect(await fs.pathExists(exportFilePath)).toBe(true);
80
+
81
+ const exported = await fs.readJson(exportFilePath);
82
+ expect(exported.version).toBe('1.0');
83
+ expect(exported.type).toBe('project');
84
+ } finally {
85
+ process.chdir(originalCwd);
86
+ }
87
+ });
88
+
89
+ it('should export global config with --global flag', async () => {
90
+ const originalCwd = process.cwd();
91
+ process.chdir(testProjectDir);
92
+
93
+ try {
94
+ await handleExport({ global: true, output: globalExportPath });
95
+
96
+ expect(await fs.pathExists(globalExportPath)).toBe(true);
97
+
98
+ const exported = await fs.readJson(globalExportPath);
99
+ expect(exported.version).toBe('1.0');
100
+ expect(exported.type).toBe('global');
101
+ expect(exported.config).toBeDefined();
102
+ expect(exported.config.sourcesDir).toBeDefined();
103
+ } finally {
104
+ process.chdir(originalCwd);
105
+ }
106
+ });
107
+
108
+ it('should throw error when exporting non-initialized project', async () => {
109
+ const originalCwd = process.cwd();
110
+ process.chdir(testProjectDir);
111
+
112
+ try {
113
+ // Don't initialize project
114
+ await expect(handleExport({ output: exportFilePath }))
115
+ .rejects.toThrow();
116
+ } finally {
117
+ process.chdir(originalCwd);
118
+ }
119
+ });
120
+ });