tools-cc 1.0.7 → 1.0.8
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 +13 -0
- package/CHANGELOG_en.md +13 -0
- package/dist/commands/template.d.ts +18 -0
- package/dist/commands/template.js +154 -0
- package/dist/core/template.d.ts +17 -0
- package/dist/core/template.js +74 -0
- package/dist/index.js +32 -0
- package/dist/types/config.d.ts +10 -0
- package/dist/utils/path.d.ts +2 -0
- package/dist/utils/path.js +6 -1
- package/package.json +3 -3
- package/src/commands/template.ts +160 -0
- package/src/core/template.ts +91 -0
- package/src/index.ts +48 -11
- package/src/types/config.ts +112 -101
- package/src/utils/path.ts +23 -18
- package/tests/commands/export.test.ts +120 -0
- package/tests/commands/use.test.ts +326 -0
- package/tests/core/config.test.ts +37 -0
- package/tests/core/manifest.test.ts +37 -0
- package/tests/core/project.test.ts +317 -0
- package/tests/core/source.test.ts +75 -0
- package/tests/core/symlink.test.ts +39 -0
- package/tests/core/template.test.ts +103 -0
- package/tests/types/config.test.ts +232 -0
- package/tests/utils/parsePath.test.ts +235 -0
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 {
|
|
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')
|
package/src/types/config.ts
CHANGED
|
@@ -1,102 +1,113 @@
|
|
|
1
|
-
export interface SourceConfig {
|
|
2
|
-
type: 'git' | 'local';
|
|
3
|
-
url?: string;
|
|
4
|
-
path?: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface GlobalConfig {
|
|
8
|
-
sourcesDir: string;
|
|
9
|
-
sources: Record<string, SourceConfig>;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 源选择配置 - 指定从源中导入哪些 skills/commands/agents
|
|
14
|
-
*/
|
|
15
|
-
export interface SourceSelection {
|
|
16
|
-
skills: string[];
|
|
17
|
-
commands: string[];
|
|
18
|
-
agents: string[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 新版项目配置 - sources 使用 Record 格式支持部分导入
|
|
23
|
-
*/
|
|
24
|
-
export interface ProjectConfig {
|
|
25
|
-
sources: Record<string, SourceSelection>;
|
|
26
|
-
links: string[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 旧版项目配置 - sources 为字符串数组(向后兼容)
|
|
31
|
-
*/
|
|
32
|
-
export interface LegacyProjectConfig {
|
|
33
|
-
sources: string[];
|
|
34
|
-
links: string[];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* 判断值是否为有效的 SourceSelection 对象
|
|
39
|
-
*/
|
|
40
|
-
export function isSourceSelection(value: unknown): value is SourceSelection {
|
|
41
|
-
if (typeof value !== 'object' || value === null) return false;
|
|
42
|
-
const obj = value as Record<string, unknown>;
|
|
43
|
-
return (
|
|
44
|
-
Array.isArray(obj.skills) &&
|
|
45
|
-
Array.isArray(obj.commands) &&
|
|
46
|
-
Array.isArray(obj.agents)
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* 将旧版项目配置转换为新版格式
|
|
52
|
-
* 如果 sources 是字符串数组,转换为 Record 格式,每个源默认导入全部内容
|
|
53
|
-
*/
|
|
54
|
-
export function normalizeProjectConfig(
|
|
55
|
-
config: LegacyProjectConfig | ProjectConfig
|
|
56
|
-
): ProjectConfig {
|
|
57
|
-
// If sources is an array, convert to new format
|
|
58
|
-
if (Array.isArray(config.sources)) {
|
|
59
|
-
const newSources: Record<string, SourceSelection> = {};
|
|
60
|
-
for (const sourceName of config.sources) {
|
|
61
|
-
newSources[sourceName] = {
|
|
62
|
-
skills: ['*'],
|
|
63
|
-
commands: ['*'],
|
|
64
|
-
agents: ['*']
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
return { sources: newSources, links: config.links };
|
|
68
|
-
}
|
|
69
|
-
return config as ProjectConfig;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface Manifest {
|
|
73
|
-
name: string;
|
|
74
|
-
version: string;
|
|
75
|
-
skills?: string[];
|
|
76
|
-
commands?: string[];
|
|
77
|
-
agents?: string[];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface ToolConfig {
|
|
81
|
-
linkName: string;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* 项目配置导出格式
|
|
86
|
-
*/
|
|
87
|
-
export interface ExportConfig {
|
|
88
|
-
version: string;
|
|
89
|
-
type: 'project';
|
|
90
|
-
config: ProjectConfig;
|
|
91
|
-
exportedAt: string;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* 全局配置导出格式
|
|
96
|
-
*/
|
|
97
|
-
export interface GlobalExportConfig {
|
|
98
|
-
version: string;
|
|
99
|
-
type: 'global';
|
|
100
|
-
config: GlobalConfig;
|
|
101
|
-
exportedAt: string;
|
|
1
|
+
export interface SourceConfig {
|
|
2
|
+
type: 'git' | 'local';
|
|
3
|
+
url?: string;
|
|
4
|
+
path?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface GlobalConfig {
|
|
8
|
+
sourcesDir: string;
|
|
9
|
+
sources: Record<string, SourceConfig>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 源选择配置 - 指定从源中导入哪些 skills/commands/agents
|
|
14
|
+
*/
|
|
15
|
+
export interface SourceSelection {
|
|
16
|
+
skills: string[];
|
|
17
|
+
commands: string[];
|
|
18
|
+
agents: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 新版项目配置 - sources 使用 Record 格式支持部分导入
|
|
23
|
+
*/
|
|
24
|
+
export interface ProjectConfig {
|
|
25
|
+
sources: Record<string, SourceSelection>;
|
|
26
|
+
links: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 旧版项目配置 - sources 为字符串数组(向后兼容)
|
|
31
|
+
*/
|
|
32
|
+
export interface LegacyProjectConfig {
|
|
33
|
+
sources: string[];
|
|
34
|
+
links: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 判断值是否为有效的 SourceSelection 对象
|
|
39
|
+
*/
|
|
40
|
+
export function isSourceSelection(value: unknown): value is SourceSelection {
|
|
41
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
42
|
+
const obj = value as Record<string, unknown>;
|
|
43
|
+
return (
|
|
44
|
+
Array.isArray(obj.skills) &&
|
|
45
|
+
Array.isArray(obj.commands) &&
|
|
46
|
+
Array.isArray(obj.agents)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 将旧版项目配置转换为新版格式
|
|
52
|
+
* 如果 sources 是字符串数组,转换为 Record 格式,每个源默认导入全部内容
|
|
53
|
+
*/
|
|
54
|
+
export function normalizeProjectConfig(
|
|
55
|
+
config: LegacyProjectConfig | ProjectConfig
|
|
56
|
+
): ProjectConfig {
|
|
57
|
+
// If sources is an array, convert to new format
|
|
58
|
+
if (Array.isArray(config.sources)) {
|
|
59
|
+
const newSources: Record<string, SourceSelection> = {};
|
|
60
|
+
for (const sourceName of config.sources) {
|
|
61
|
+
newSources[sourceName] = {
|
|
62
|
+
skills: ['*'],
|
|
63
|
+
commands: ['*'],
|
|
64
|
+
agents: ['*']
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return { sources: newSources, links: config.links };
|
|
68
|
+
}
|
|
69
|
+
return config as ProjectConfig;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface Manifest {
|
|
73
|
+
name: string;
|
|
74
|
+
version: string;
|
|
75
|
+
skills?: string[];
|
|
76
|
+
commands?: string[];
|
|
77
|
+
agents?: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ToolConfig {
|
|
81
|
+
linkName: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 项目配置导出格式
|
|
86
|
+
*/
|
|
87
|
+
export interface ExportConfig {
|
|
88
|
+
version: string;
|
|
89
|
+
type: 'project';
|
|
90
|
+
config: ProjectConfig;
|
|
91
|
+
exportedAt: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 全局配置导出格式
|
|
96
|
+
*/
|
|
97
|
+
export interface GlobalExportConfig {
|
|
98
|
+
version: string;
|
|
99
|
+
type: 'global';
|
|
100
|
+
config: GlobalConfig;
|
|
101
|
+
exportedAt: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 模板配置格式
|
|
106
|
+
*/
|
|
107
|
+
export interface TemplateConfig {
|
|
108
|
+
version: string;
|
|
109
|
+
name: string;
|
|
110
|
+
sourceProject: string;
|
|
111
|
+
savedAt: string;
|
|
112
|
+
config: ProjectConfig;
|
|
102
113
|
}
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
});
|