tools-cc 1.0.6 → 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.
@@ -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
+ }
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
3
  import { useSource, unuseSource, listUsedSources, initProject, importProjectConfig } from '../core/project';
4
+ import { normalizeProjectConfig } from '../types/config';
4
5
  import { getSourcePath, listSources } from '../core/source';
5
6
  import { scanSource } from '../core/manifest';
6
7
  import { createSymlink, isSymlink } from '../core/symlink';
@@ -368,11 +369,16 @@ export async function handleProjectUpdate(sourceNames?: string[]): Promise<void>
368
369
  return;
369
370
  }
370
371
 
371
- const config = await fs.readJson(configFile);
372
+ const rawConfig = await fs.readJson(configFile);
373
+ // 使用 normalizeProjectConfig 处理旧版(数组)和新版(对象)格式
374
+ const config = normalizeProjectConfig(rawConfig);
372
375
  const configuredSources = Object.keys(config.sources || {});
373
376
 
374
- let sourcesToUpdate = sourceNames && sourceNames.length > 0
375
- ? sourceNames
377
+ // 过滤掉无效的源名称(如数字字符串),这些可能是 Commander.js 解析错误产生的
378
+ const validSourceNames = sourceNames?.filter(s => isNaN(Number(s))) || [];
379
+
380
+ let sourcesToUpdate = validSourceNames.length > 0
381
+ ? validSourceNames
376
382
  : configuredSources;
377
383
 
378
384
  if (sourcesToUpdate.length === 0) {
@@ -380,9 +386,9 @@ export async function handleProjectUpdate(sourceNames?: string[]): Promise<void>
380
386
  return;
381
387
  }
382
388
 
383
- // 验证指定的源是否存在于项目配置中
384
- if (sourceNames && sourceNames.length > 0) {
385
- const invalidSources = sourceNames.filter((s: string) => !configuredSources.includes(s));
389
+ // 验证指定的源是否存在于项目配置中(仅当用户明确指定了有效源名称时)
390
+ if (validSourceNames.length > 0) {
391
+ const invalidSources = validSourceNames.filter((s: string) => !configuredSources.includes(s));
386
392
  if (invalidSources.length > 0) {
387
393
  console.log(chalk.yellow(`Sources not in project: ${invalidSources.join(', ')}`));
388
394
  }
@@ -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')
@@ -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
- 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
+ }