tools-cc 1.0.0 → 1.0.1

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.
@@ -2,3 +2,4 @@ export declare function handleSourceAdd(name: string, pathOrUrl: string): Promis
2
2
  export declare function handleSourceList(): Promise<void>;
3
3
  export declare function handleSourceRemove(name: string): Promise<void>;
4
4
  export declare function handleSourceUpdate(name?: string): Promise<void>;
5
+ export declare function handleSourceScan(): Promise<void>;
@@ -7,6 +7,7 @@ exports.handleSourceAdd = handleSourceAdd;
7
7
  exports.handleSourceList = handleSourceList;
8
8
  exports.handleSourceRemove = handleSourceRemove;
9
9
  exports.handleSourceUpdate = handleSourceUpdate;
10
+ exports.handleSourceScan = handleSourceScan;
10
11
  const chalk_1 = __importDefault(require("chalk"));
11
12
  const source_1 = require("../core/source");
12
13
  const path_1 = require("../utils/path");
@@ -70,3 +71,31 @@ async function handleSourceUpdate(name) {
70
71
  console.log(chalk_1.default.red(`✗ ${error instanceof Error ? error.message : 'Unknown error'}`));
71
72
  }
72
73
  }
74
+ async function handleSourceScan() {
75
+ try {
76
+ console.log(chalk_1.default.bold('Scanning sources directory...'));
77
+ const result = await (0, source_1.scanSources)(path_1.GLOBAL_CONFIG_DIR);
78
+ if (result.added.length > 0) {
79
+ console.log(chalk_1.default.green(`\n✓ Added ${result.added.length} source(s):`));
80
+ for (const name of result.added) {
81
+ console.log(chalk_1.default.gray(` + ${name}`));
82
+ }
83
+ }
84
+ if (result.updated.length > 0) {
85
+ console.log(chalk_1.default.yellow(`\n⚡ Updated ${result.updated.length} source(s):`));
86
+ for (const name of result.updated) {
87
+ console.log(chalk_1.default.gray(` ~ ${name}`));
88
+ }
89
+ }
90
+ if (result.skipped.length > 0) {
91
+ console.log(chalk_1.default.gray(`\n→ Skipped ${result.skipped.length} existing source(s)`));
92
+ }
93
+ if (result.added.length === 0 && result.updated.length === 0 && result.skipped.length === 0) {
94
+ console.log(chalk_1.default.gray('No sources found in sources directory.'));
95
+ }
96
+ console.log(chalk_1.default.green(`\n✓ Scan complete`));
97
+ }
98
+ catch (error) {
99
+ console.log(chalk_1.default.red(`✗ ${error instanceof Error ? error.message : 'Unknown error'}`));
100
+ }
101
+ }
@@ -4,3 +4,4 @@ export declare function handleUse(sourceNames: string[], options: {
4
4
  export declare function handleList(): Promise<void>;
5
5
  export declare function handleRemove(sourceName: string): Promise<void>;
6
6
  export declare function handleStatus(): Promise<void>;
7
+ export declare function handleProjectUpdate(sourceNames?: string[]): Promise<void>;
@@ -7,6 +7,7 @@ exports.handleUse = handleUse;
7
7
  exports.handleList = handleList;
8
8
  exports.handleRemove = handleRemove;
9
9
  exports.handleStatus = handleStatus;
10
+ exports.handleProjectUpdate = handleProjectUpdate;
10
11
  const chalk_1 = __importDefault(require("chalk"));
11
12
  const inquirer_1 = __importDefault(require("inquirer"));
12
13
  const project_1 = require("../core/project");
@@ -131,3 +132,40 @@ async function handleStatus() {
131
132
  }
132
133
  console.log();
133
134
  }
135
+ async function handleProjectUpdate(sourceNames) {
136
+ const projectDir = process.cwd();
137
+ const configFile = path_2.default.join(projectDir, 'tools-cc.json');
138
+ // 检查项目是否已初始化
139
+ if (!(await fs_extra_1.default.pathExists(configFile))) {
140
+ console.log(chalk_1.default.yellow('Project not initialized. Run `tools-cc use <source>` first.'));
141
+ return;
142
+ }
143
+ const config = await fs_extra_1.default.readJson(configFile);
144
+ let sourcesToUpdate = sourceNames && sourceNames.length > 0
145
+ ? sourceNames
146
+ : config.sources || [];
147
+ if (sourcesToUpdate.length === 0) {
148
+ console.log(chalk_1.default.gray('No sources to update.'));
149
+ return;
150
+ }
151
+ // 验证指定的源是否存在于项目配置中
152
+ if (sourceNames && sourceNames.length > 0) {
153
+ const invalidSources = sourceNames.filter((s) => !config.sources.includes(s));
154
+ if (invalidSources.length > 0) {
155
+ console.log(chalk_1.default.yellow(`Sources not in project: ${invalidSources.join(', ')}`));
156
+ }
157
+ sourcesToUpdate = sourcesToUpdate.filter((s) => config.sources.includes(s));
158
+ }
159
+ // 更新每个配置源
160
+ for (const sourceName of sourcesToUpdate) {
161
+ try {
162
+ const sourcePath = await (0, source_1.getSourcePath)(sourceName, path_1.GLOBAL_CONFIG_DIR);
163
+ await (0, project_1.useSource)(sourceName, sourcePath, projectDir);
164
+ console.log(chalk_1.default.green(`✓ Updated source: ${sourceName}`));
165
+ }
166
+ catch (error) {
167
+ console.log(chalk_1.default.red(`✗ Failed to update ${sourceName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
168
+ }
169
+ }
170
+ console.log(chalk_1.default.green(`\n✓ Project update complete`));
171
+ }
@@ -4,3 +4,9 @@ export declare function listSources(configDir: string): Promise<Record<string, S
4
4
  export declare function removeSource(name: string, configDir: string): Promise<void>;
5
5
  export declare function updateSource(name: string, configDir: string): Promise<void>;
6
6
  export declare function getSourcePath(name: string, configDir: string): Promise<string>;
7
+ export interface ScanResult {
8
+ added: string[];
9
+ updated: string[];
10
+ skipped: string[];
11
+ }
12
+ export declare function scanSources(configDir: string): Promise<ScanResult>;
@@ -8,6 +8,7 @@ exports.listSources = listSources;
8
8
  exports.removeSource = removeSource;
9
9
  exports.updateSource = updateSource;
10
10
  exports.getSourcePath = getSourcePath;
11
+ exports.scanSources = scanSources;
11
12
  const fs_extra_1 = __importDefault(require("fs-extra"));
12
13
  const path_1 = __importDefault(require("path"));
13
14
  const child_process_1 = require("child_process");
@@ -84,3 +85,80 @@ async function getSourcePath(name, configDir) {
84
85
  }
85
86
  return path_1.default.join(config.sourcesDir, name);
86
87
  }
88
+ async function scanSources(configDir) {
89
+ const config = await (0, config_1.loadGlobalConfig)(configDir);
90
+ const result = { added: [], updated: [], skipped: [] };
91
+ // 确保 sourcesDir 存在
92
+ if (!(await fs_extra_1.default.pathExists(config.sourcesDir))) {
93
+ await fs_extra_1.default.ensureDir(config.sourcesDir);
94
+ return result;
95
+ }
96
+ // 获取 sourcesDir 下的所有文件夹
97
+ const entries = await fs_extra_1.default.readdir(config.sourcesDir, { withFileTypes: true });
98
+ const directories = entries
99
+ .filter(entry => entry.isDirectory())
100
+ .map(entry => entry.name);
101
+ // 检查每个 git source 的克隆目录是否还存在
102
+ for (const [name, source] of Object.entries(config.sources)) {
103
+ if (source.type === 'git') {
104
+ const cloneDir = path_1.default.join(config.sourcesDir, name);
105
+ if (!(await fs_extra_1.default.pathExists(cloneDir))) {
106
+ // 克隆目录不存在,从配置中移除
107
+ delete config.sources[name];
108
+ console.log(`Removed missing git source: ${name}`);
109
+ }
110
+ }
111
+ }
112
+ // 扫描目录并更新配置
113
+ for (const dirName of directories) {
114
+ const dirPath = path_1.default.join(config.sourcesDir, dirName);
115
+ const existingSource = config.sources[dirName];
116
+ // 检查是否是 git 仓库
117
+ const isGitRepo = await fs_extra_1.default.pathExists(path_1.default.join(dirPath, '.git'));
118
+ if (existingSource) {
119
+ // 已存在配置
120
+ if (existingSource.type === 'git' && isGitRepo) {
121
+ result.skipped.push(dirName);
122
+ }
123
+ else if (existingSource.type === 'local' && existingSource.path === dirPath) {
124
+ result.skipped.push(dirName);
125
+ }
126
+ else {
127
+ // 配置不一致,更新为当前状态
128
+ if (isGitRepo) {
129
+ // 尝试获取远程 URL
130
+ try {
131
+ const remoteUrl = (0, child_process_1.execSync)(`git -C "${dirPath}" config --get remote.origin.url`, { encoding: 'utf-8' }).trim();
132
+ config.sources[dirName] = { type: 'git', url: remoteUrl };
133
+ }
134
+ catch {
135
+ config.sources[dirName] = { type: 'local', path: dirPath };
136
+ }
137
+ }
138
+ else {
139
+ config.sources[dirName] = { type: 'local', path: dirPath };
140
+ }
141
+ result.updated.push(dirName);
142
+ }
143
+ }
144
+ else {
145
+ // 新发现的目录,添加到配置
146
+ if (isGitRepo) {
147
+ // 尝试获取远程 URL
148
+ try {
149
+ const remoteUrl = (0, child_process_1.execSync)(`git -C "${dirPath}" config --get remote.origin.url`, { encoding: 'utf-8' }).trim();
150
+ config.sources[dirName] = { type: 'git', url: remoteUrl };
151
+ }
152
+ catch {
153
+ config.sources[dirName] = { type: 'local', path: dirPath };
154
+ }
155
+ }
156
+ else {
157
+ config.sources[dirName] = { type: 'local', path: dirPath };
158
+ }
159
+ result.added.push(dirName);
160
+ }
161
+ }
162
+ await (0, config_1.saveGlobalConfig)(config, configDir);
163
+ return result;
164
+ }
package/dist/index.js CHANGED
@@ -42,10 +42,17 @@ sourceCmd
42
42
  sourceCmd
43
43
  .command('update [name]')
44
44
  .alias('up')
45
- .description('Update source(s)')
45
+ .alias('upgrade')
46
+ .description('Update source(s) with git pull')
46
47
  .action(async (name) => {
47
48
  await (0, source_1.handleSourceUpdate)(name);
48
49
  });
50
+ sourceCmd
51
+ .command('scan')
52
+ .description('Scan sources directory and update configuration')
53
+ .action(async () => {
54
+ await (0, source_1.handleSourceScan)();
55
+ });
49
56
  // Config subcommands (full command version)
50
57
  const configCmd = program
51
58
  .command('config')
@@ -94,6 +101,12 @@ program
94
101
  .action(async () => {
95
102
  await (0, use_1.handleStatus)();
96
103
  });
104
+ program
105
+ .command('update [sources...]')
106
+ .description('Update source(s) in current project')
107
+ .action(async (sources) => {
108
+ await (0, use_1.handleProjectUpdate)(sources);
109
+ });
97
110
  // Help command
98
111
  program
99
112
  .command('help')
@@ -129,8 +142,12 @@ program
129
142
  break;
130
143
  case 'update':
131
144
  case 'up':
145
+ case 'upgrade':
132
146
  await (0, source_1.handleSourceUpdate)(args[0]);
133
147
  break;
148
+ case 'scan':
149
+ await (0, source_1.handleSourceScan)();
150
+ break;
134
151
  default:
135
152
  console.log(`Unknown source command: ${cmd}`);
136
153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tools-cc",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "tools-cc [options] <command> [args]",
5
5
  "main": "index.js",
6
6
  "directories": {
package/readme.md CHANGED
@@ -26,6 +26,9 @@ tools-cc -c set sourcesDir D:/skills-hub-sources
26
26
  tools-cc -s add my-skills https://github.com/user/my-skills.git
27
27
  tools-cc -s add local-skills D:/path/to/local-skills
28
28
 
29
+ # 或者扫描源目录自动发现并添加配置
30
+ tools-cc -s scan
31
+
29
32
  # 3. 查看已添加的配置源
30
33
  tools-cc -s list
31
34
 
@@ -39,32 +42,65 @@ tools-cc status
39
42
  # 6. 查看已启用的配置源
40
43
  tools-cc list
41
44
 
42
- # 7. 移除配置源
45
+ # 7. 更新配置源(从源目录同步最新内容到项目)
46
+ tools-cc update my-skills
47
+ tools-cc update # 更新全部
48
+
49
+ # 8. 移除配置源
43
50
  tools-cc rm my-skills
44
51
  ```
45
52
 
53
+ ## 工作流说明
54
+
55
+ ### 全局配置源管理 vs 项目配置
56
+
57
+ | 命令 | 作用域 | 说明 |
58
+ |------|--------|------|
59
+ | `tools-cc -s add/remove/list` | 全局 | 管理全局配置源 |
60
+ | `tools-cc -s update/upgrade` | 全局 | git pull 更新源代码 |
61
+ | `tools-cc -s scan` | 全局 | 扫描目录发现新源 |
62
+ | `tools-cc use/rm` | 项目 | 在项目中启用/禁用源 |
63
+ | `tools-cc update` | 项目 | 同步源内容到项目 |
64
+
65
+ ### 典型工作流
66
+
67
+ ```bash
68
+ # 场景1: 配置源有更新,需要同步到项目
69
+ tools-cc -s upgrade my-skills # 1. git pull 更新源代码
70
+ cd my-project
71
+ tools-cc update my-skills # 2. 同步到项目 .toolscc 目录
72
+
73
+ # 场景2: 批量更新所有源
74
+ tools-cc -s upgrade # 1. 更新所有 git 源
75
+ cd my-project
76
+ tools-cc update # 2. 同步所有源到项目
77
+ ```
78
+
46
79
  ## 命令列表
47
80
 
48
81
  ### Source 管理
49
82
 
50
83
  ```bash
51
84
  # 快捷方式 (-s)
52
- tools-cc -s add <name> <path-or-url> # 添加配置源
85
+ tools-cc -s add <name> <path-or-url> # 添加配置源(Git URL 或本地路径)
53
86
  tools-cc -s list # 列出所有配置源 (缩写: -s ls)
54
87
  tools-cc -s remove <name> # 移除配置源 (缩写: -s rm)
55
- tools-cc -s update [name] # 更新配置源 (缩写: -s up)
88
+ tools-cc -s update [name] # git pull 更新配置源代码 (缩写: -s up, -s upgrade)
89
+ tools-cc -s scan # 扫描 sourcesDir 目录,自动发现并添加配置源
56
90
 
57
91
  # 完整命令 (sources)
58
92
  tools-cc sources add <name> <path-or-url> # 添加配置源
59
93
  tools-cc sources list # 列出所有配置源 (缩写: sources ls)
60
94
  tools-cc sources remove <name> # 移除配置源 (缩写: sources rm)
61
- tools-cc sources update [name] # 更新配置源 (缩写: sources up)
95
+ tools-cc sources update [name] # git pull 更新配置源代码 (缩写: sources up, sources upgrade)
96
+ tools-cc sources scan # 扫描 sourcesDir 目录,自动发现并添加配置源
62
97
  ```
63
98
 
64
99
  ### 项目配置
65
100
 
66
101
  ```bash
67
- tools-cc use [sources...] [-p tools...] # 启用配置源并创建链接
102
+ tools-cc use [sources...] [-p tools...] # 启用配置源并创建符号链接
103
+ tools-cc update [sources...] # 同步配置源内容到项目 .toolscc 目录
68
104
  tools-cc list # 列出已启用的配置源
69
105
  tools-cc rm <source> # 禁用配置源
70
106
  tools-cc status # 查看项目状态
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { addSource, listSources, removeSource, updateSource } from '../core/source';
2
+ import { addSource, listSources, removeSource, updateSource, scanSources } from '../core/source';
3
3
  import { GLOBAL_CONFIG_DIR } from '../utils/path';
4
4
 
5
5
  export async function handleSourceAdd(name: string, pathOrUrl: string): Promise<void> {
@@ -61,3 +61,36 @@ export async function handleSourceUpdate(name?: string): Promise<void> {
61
61
  console.log(chalk.red(`✗ ${error instanceof Error ? error.message : 'Unknown error'}`));
62
62
  }
63
63
  }
64
+
65
+ export async function handleSourceScan(): Promise<void> {
66
+ try {
67
+ console.log(chalk.bold('Scanning sources directory...'));
68
+ const result = await scanSources(GLOBAL_CONFIG_DIR);
69
+
70
+ if (result.added.length > 0) {
71
+ console.log(chalk.green(`\n✓ Added ${result.added.length} source(s):`));
72
+ for (const name of result.added) {
73
+ console.log(chalk.gray(` + ${name}`));
74
+ }
75
+ }
76
+
77
+ if (result.updated.length > 0) {
78
+ console.log(chalk.yellow(`\n⚡ Updated ${result.updated.length} source(s):`));
79
+ for (const name of result.updated) {
80
+ console.log(chalk.gray(` ~ ${name}`));
81
+ }
82
+ }
83
+
84
+ if (result.skipped.length > 0) {
85
+ console.log(chalk.gray(`\n→ Skipped ${result.skipped.length} existing source(s)`));
86
+ }
87
+
88
+ if (result.added.length === 0 && result.updated.length === 0 && result.skipped.length === 0) {
89
+ console.log(chalk.gray('No sources found in sources directory.'));
90
+ }
91
+
92
+ console.log(chalk.green(`\n✓ Scan complete`));
93
+ } catch (error) {
94
+ console.log(chalk.red(`✗ ${error instanceof Error ? error.message : 'Unknown error'}`));
95
+ }
96
+ }
@@ -1,147 +1,190 @@
1
- import chalk from 'chalk';
2
- import inquirer from 'inquirer';
3
- import { useSource, unuseSource, listUsedSources, initProject } from '../core/project';
4
- import { getSourcePath, listSources } from '../core/source';
5
- import { createSymlink, isSymlink } from '../core/symlink';
6
- import { GLOBAL_CONFIG_DIR, getToolsccDir } from '../utils/path';
7
- import fs from 'fs-extra';
8
- import path from 'path';
9
-
10
- const SUPPORTED_TOOLS: Record<string, string> = {
11
- iflow: '.iflow',
12
- claude: '.claude',
13
- codebuddy: '.codebuddy',
14
- opencode: '.opencode'
15
- };
16
-
17
- export async function handleUse(
18
- sourceNames: string[],
19
- options: { projects?: string[] }
20
- ): Promise<void> {
21
- const projectDir = process.cwd();
22
-
23
- // 如果没有指定 source,进入交互模式
24
- if (sourceNames.length === 0) {
25
- const sources = await listSources(GLOBAL_CONFIG_DIR);
26
- const sourceList = Object.keys(sources);
27
-
28
- if (sourceList.length === 0) {
29
- console.log(chalk.yellow('No sources configured. Use `tools-cc -s add` to add one.'));
30
- return;
31
- }
32
-
33
- const answers = await inquirer.prompt([
34
- {
35
- type: 'checkbox',
36
- name: 'selectedSources',
37
- message: 'Select sources to use:',
38
- choices: sourceList
39
- }
40
- ]);
41
-
42
- sourceNames = answers.selectedSources;
43
- }
44
-
45
- if (sourceNames.length === 0) {
46
- console.log(chalk.gray('No sources selected.'));
47
- return;
48
- }
49
-
50
- // 初始化项目
51
- await initProject(projectDir);
52
-
53
- // 启用每个配置源
54
- for (const sourceName of sourceNames) {
55
- try {
56
- const sourcePath = await getSourcePath(sourceName, GLOBAL_CONFIG_DIR);
57
- await useSource(sourceName, sourcePath, projectDir);
58
- console.log(chalk.green(`✓ Using source: ${sourceName}`));
59
- } catch (error) {
60
- console.log(chalk.red(`✗ Failed to use ${sourceName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
61
- }
62
- }
63
-
64
- // 创建符号链接
65
- const tools = options.projects || Object.keys(SUPPORTED_TOOLS);
66
- const toolsccDir = getToolsccDir(projectDir);
67
-
68
- for (const tool of tools) {
69
- const linkName = SUPPORTED_TOOLS[tool];
70
- if (!linkName) {
71
- console.log(chalk.yellow(`Unknown tool: ${tool}`));
72
- continue;
73
- }
74
-
75
- const linkPath = path.join(projectDir, linkName);
76
-
77
- try {
78
- await createSymlink(toolsccDir, linkPath, true);
79
- console.log(chalk.green(`✓ Linked: ${linkName} -> .toolscc`));
80
- } catch (error) {
81
- console.log(chalk.red(`✗ Failed to link ${linkName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
82
- }
83
- }
84
-
85
- // 更新项目配置
86
- const configFile = path.join(projectDir, 'tools-cc.json');
87
- const config = await fs.readJson(configFile);
88
- const existingLinks = config.links || [];
89
- config.links = [...new Set([...existingLinks, ...tools])];
90
- await fs.writeJson(configFile, config, { spaces: 2 });
91
- }
92
-
93
- export async function handleList(): Promise<void> {
94
- const projectDir = process.cwd();
95
- const sources = await listUsedSources(projectDir);
96
-
97
- if (sources.length === 0) {
98
- console.log(chalk.gray('No sources in use. Run `tools-cc use <source-name>` to add one.'));
99
- return;
100
- }
101
-
102
- console.log(chalk.bold('Sources in use:'));
103
- for (const source of sources) {
104
- console.log(` ${chalk.cyan(source)}`);
105
- }
106
- }
107
-
108
- export async function handleRemove(sourceName: string): Promise<void> {
109
- const projectDir = process.cwd();
110
-
111
- try {
112
- await unuseSource(sourceName, projectDir);
113
- console.log(chalk.green(`✓ Removed source: ${sourceName}`));
114
- } catch (error) {
115
- console.log(chalk.red(`✗ ${error instanceof Error ? error.message : 'Unknown error'}`));
116
- }
117
- }
118
-
119
- export async function handleStatus(): Promise<void> {
120
- const projectDir = process.cwd();
121
- const sources = await listUsedSources(projectDir);
122
-
123
- console.log(chalk.bold('\nProject Status:'));
124
- console.log(chalk.gray(` Directory: ${projectDir}`));
125
-
126
- // 检查 .toolscc
127
- const toolsccDir = getToolsccDir(projectDir);
128
- console.log(` .toolscc: ${await fs.pathExists(toolsccDir) ? chalk.green('exists') : chalk.red('not found')}`);
129
-
130
- // 检查 sources
131
- console.log(` Sources: ${sources.length > 0 ? sources.map(s => chalk.cyan(s)).join(', ') : chalk.gray('none')}`);
132
-
133
- // 检查 links
134
- const configFile = path.join(projectDir, 'tools-cc.json');
135
- if (await fs.pathExists(configFile)) {
136
- const config = await fs.readJson(configFile);
137
- console.log(` Links:`);
138
- for (const tool of config.links || []) {
139
- const linkName = SUPPORTED_TOOLS[tool];
140
- if (!linkName) continue;
141
- const linkPath = path.join(projectDir, linkName);
142
- const isLink = await isSymlink(linkPath);
143
- console.log(` ${tool}: ${isLink ? chalk.green('linked') : chalk.red('not linked')}`);
144
- }
145
- }
146
- console.log();
147
- }
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import { useSource, unuseSource, listUsedSources, initProject } from '../core/project';
4
+ import { getSourcePath, listSources } from '../core/source';
5
+ import { createSymlink, isSymlink } from '../core/symlink';
6
+ import { GLOBAL_CONFIG_DIR, getToolsccDir } from '../utils/path';
7
+ import fs from 'fs-extra';
8
+ import path from 'path';
9
+
10
+ const SUPPORTED_TOOLS: Record<string, string> = {
11
+ iflow: '.iflow',
12
+ claude: '.claude',
13
+ codebuddy: '.codebuddy',
14
+ opencode: '.opencode'
15
+ };
16
+
17
+ export async function handleUse(
18
+ sourceNames: string[],
19
+ options: { projects?: string[] }
20
+ ): Promise<void> {
21
+ const projectDir = process.cwd();
22
+
23
+ // 如果没有指定 source,进入交互模式
24
+ if (sourceNames.length === 0) {
25
+ const sources = await listSources(GLOBAL_CONFIG_DIR);
26
+ const sourceList = Object.keys(sources);
27
+
28
+ if (sourceList.length === 0) {
29
+ console.log(chalk.yellow('No sources configured. Use `tools-cc -s add` to add one.'));
30
+ return;
31
+ }
32
+
33
+ const answers = await inquirer.prompt([
34
+ {
35
+ type: 'checkbox',
36
+ name: 'selectedSources',
37
+ message: 'Select sources to use:',
38
+ choices: sourceList
39
+ }
40
+ ]);
41
+
42
+ sourceNames = answers.selectedSources;
43
+ }
44
+
45
+ if (sourceNames.length === 0) {
46
+ console.log(chalk.gray('No sources selected.'));
47
+ return;
48
+ }
49
+
50
+ // 初始化项目
51
+ await initProject(projectDir);
52
+
53
+ // 启用每个配置源
54
+ for (const sourceName of sourceNames) {
55
+ try {
56
+ const sourcePath = await getSourcePath(sourceName, GLOBAL_CONFIG_DIR);
57
+ await useSource(sourceName, sourcePath, projectDir);
58
+ console.log(chalk.green(`✓ Using source: ${sourceName}`));
59
+ } catch (error) {
60
+ console.log(chalk.red(`✗ Failed to use ${sourceName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
61
+ }
62
+ }
63
+
64
+ // 创建符号链接
65
+ const tools = options.projects || Object.keys(SUPPORTED_TOOLS);
66
+ const toolsccDir = getToolsccDir(projectDir);
67
+
68
+ for (const tool of tools) {
69
+ const linkName = SUPPORTED_TOOLS[tool];
70
+ if (!linkName) {
71
+ console.log(chalk.yellow(`Unknown tool: ${tool}`));
72
+ continue;
73
+ }
74
+
75
+ const linkPath = path.join(projectDir, linkName);
76
+
77
+ try {
78
+ await createSymlink(toolsccDir, linkPath, true);
79
+ console.log(chalk.green(`✓ Linked: ${linkName} -> .toolscc`));
80
+ } catch (error) {
81
+ console.log(chalk.red(`✗ Failed to link ${linkName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
82
+ }
83
+ }
84
+
85
+ // 更新项目配置
86
+ const configFile = path.join(projectDir, 'tools-cc.json');
87
+ const config = await fs.readJson(configFile);
88
+ const existingLinks = config.links || [];
89
+ config.links = [...new Set([...existingLinks, ...tools])];
90
+ await fs.writeJson(configFile, config, { spaces: 2 });
91
+ }
92
+
93
+ export async function handleList(): Promise<void> {
94
+ const projectDir = process.cwd();
95
+ const sources = await listUsedSources(projectDir);
96
+
97
+ if (sources.length === 0) {
98
+ console.log(chalk.gray('No sources in use. Run `tools-cc use <source-name>` to add one.'));
99
+ return;
100
+ }
101
+
102
+ console.log(chalk.bold('Sources in use:'));
103
+ for (const source of sources) {
104
+ console.log(` ${chalk.cyan(source)}`);
105
+ }
106
+ }
107
+
108
+ export async function handleRemove(sourceName: string): Promise<void> {
109
+ const projectDir = process.cwd();
110
+
111
+ try {
112
+ await unuseSource(sourceName, projectDir);
113
+ console.log(chalk.green(`✓ Removed source: ${sourceName}`));
114
+ } catch (error) {
115
+ console.log(chalk.red(`✗ ${error instanceof Error ? error.message : 'Unknown error'}`));
116
+ }
117
+ }
118
+
119
+ export async function handleStatus(): Promise<void> {
120
+ const projectDir = process.cwd();
121
+ const sources = await listUsedSources(projectDir);
122
+
123
+ console.log(chalk.bold('\nProject Status:'));
124
+ console.log(chalk.gray(` Directory: ${projectDir}`));
125
+
126
+ // 检查 .toolscc
127
+ const toolsccDir = getToolsccDir(projectDir);
128
+ console.log(` .toolscc: ${await fs.pathExists(toolsccDir) ? chalk.green('exists') : chalk.red('not found')}`);
129
+
130
+ // 检查 sources
131
+ console.log(` Sources: ${sources.length > 0 ? sources.map(s => chalk.cyan(s)).join(', ') : chalk.gray('none')}`);
132
+
133
+ // 检查 links
134
+ const configFile = path.join(projectDir, 'tools-cc.json');
135
+ if (await fs.pathExists(configFile)) {
136
+ const config = await fs.readJson(configFile);
137
+ console.log(` Links:`);
138
+ for (const tool of config.links || []) {
139
+ const linkName = SUPPORTED_TOOLS[tool];
140
+ if (!linkName) continue;
141
+ const linkPath = path.join(projectDir, linkName);
142
+ const isLink = await isSymlink(linkPath);
143
+ console.log(` ${tool}: ${isLink ? chalk.green('linked') : chalk.red('not linked')}`);
144
+ }
145
+ }
146
+ console.log();
147
+ }
148
+
149
+ export async function handleProjectUpdate(sourceNames?: string[]): Promise<void> {
150
+ const projectDir = process.cwd();
151
+ const configFile = path.join(projectDir, 'tools-cc.json');
152
+
153
+ // 检查项目是否已初始化
154
+ if (!(await fs.pathExists(configFile))) {
155
+ console.log(chalk.yellow('Project not initialized. Run `tools-cc use <source>` first.'));
156
+ return;
157
+ }
158
+
159
+ const config = await fs.readJson(configFile);
160
+ let sourcesToUpdate = sourceNames && sourceNames.length > 0
161
+ ? sourceNames
162
+ : config.sources || [];
163
+
164
+ if (sourcesToUpdate.length === 0) {
165
+ console.log(chalk.gray('No sources to update.'));
166
+ return;
167
+ }
168
+
169
+ // 验证指定的源是否存在于项目配置中
170
+ if (sourceNames && sourceNames.length > 0) {
171
+ const invalidSources = sourceNames.filter((s: string) => !config.sources.includes(s));
172
+ if (invalidSources.length > 0) {
173
+ console.log(chalk.yellow(`Sources not in project: ${invalidSources.join(', ')}`));
174
+ }
175
+ sourcesToUpdate = sourcesToUpdate.filter((s: string) => config.sources.includes(s));
176
+ }
177
+
178
+ // 更新每个配置源
179
+ for (const sourceName of sourcesToUpdate) {
180
+ try {
181
+ const sourcePath = await getSourcePath(sourceName, GLOBAL_CONFIG_DIR);
182
+ await useSource(sourceName, sourcePath, projectDir);
183
+ console.log(chalk.green(`✓ Updated source: ${sourceName}`));
184
+ } catch (error) {
185
+ console.log(chalk.red(`✗ Failed to update ${sourceName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
186
+ }
187
+ }
188
+
189
+ console.log(chalk.green(`\n✓ Project update complete`));
190
+ }
@@ -1,100 +1,184 @@
1
- import fs from 'fs-extra';
2
- import path from 'path';
3
- import { execSync } from 'child_process';
4
- import { loadGlobalConfig, saveGlobalConfig } from './config';
5
- import { SourceConfig } from '../types';
6
-
7
- export async function addSource(
8
- name: string,
9
- sourcePath: string,
10
- configDir: string
11
- ): Promise<SourceConfig> {
12
- const config = await loadGlobalConfig(configDir);
13
-
14
- // 判断是 git url 还是本地路径
15
- const isGit = sourcePath.startsWith('http') || sourcePath.startsWith('git@');
16
-
17
- let sourceConfig: SourceConfig;
18
-
19
- if (isGit) {
20
- // Clone git repo
21
- const cloneDir = path.join(config.sourcesDir, name);
22
- console.log(`Cloning ${sourcePath} to ${cloneDir}...`);
23
-
24
- await fs.ensureDir(config.sourcesDir);
25
- execSync(`git clone ${sourcePath} "${cloneDir}"`, { stdio: 'inherit' });
26
-
27
- sourceConfig = { type: 'git', url: sourcePath };
28
- } else {
29
- // 本地路径
30
- const absolutePath = path.resolve(sourcePath);
31
- if (!(await fs.pathExists(absolutePath))) {
32
- throw new Error(`Path does not exist: ${absolutePath}`);
33
- }
34
- sourceConfig = { type: 'local', path: absolutePath };
35
- }
36
-
37
- config.sources[name] = sourceConfig;
38
- await saveGlobalConfig(config, configDir);
39
-
40
- return sourceConfig;
41
- }
42
-
43
- export async function listSources(configDir: string): Promise<Record<string, SourceConfig>> {
44
- const config = await loadGlobalConfig(configDir);
45
- return config.sources;
46
- }
47
-
48
- export async function removeSource(name: string, configDir: string): Promise<void> {
49
- const config = await loadGlobalConfig(configDir);
50
-
51
- if (!config.sources[name]) {
52
- throw new Error(`Source not found: ${name}`);
53
- }
54
-
55
- const source = config.sources[name];
56
-
57
- // 如果是 git 类型,清理克隆目录
58
- if (source.type === 'git') {
59
- const cloneDir = path.join(config.sourcesDir, name);
60
- if (await fs.pathExists(cloneDir)) {
61
- console.log(`Removing cloned directory: ${cloneDir}`);
62
- await fs.remove(cloneDir);
63
- }
64
- }
65
-
66
- delete config.sources[name];
67
- await saveGlobalConfig(config, configDir);
68
- }
69
-
70
- export async function updateSource(name: string, configDir: string): Promise<void> {
71
- const config = await loadGlobalConfig(configDir);
72
- const source = config.sources[name];
73
-
74
- if (!source) {
75
- throw new Error(`Source not found: ${name}`);
76
- }
77
-
78
- if (source.type === 'git') {
79
- const cloneDir = path.join(config.sourcesDir, name);
80
- console.log(`Updating ${name}...`);
81
- execSync(`git -C "${cloneDir}" pull`, { stdio: 'inherit' });
82
- } else {
83
- console.log(`Source ${name} is local, no update needed.`);
84
- }
85
- }
86
-
87
- export async function getSourcePath(name: string, configDir: string): Promise<string> {
88
- const config = await loadGlobalConfig(configDir);
89
- const source = config.sources[name];
90
-
91
- if (!source) {
92
- throw new Error(`Source not found: ${name}`);
93
- }
94
-
95
- if (source.type === 'local') {
96
- return source.path!;
97
- }
98
-
99
- return path.join(config.sourcesDir, name);
100
- }
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { execSync } from 'child_process';
4
+ import { loadGlobalConfig, saveGlobalConfig } from './config';
5
+ import { SourceConfig } from '../types';
6
+
7
+ export async function addSource(
8
+ name: string,
9
+ sourcePath: string,
10
+ configDir: string
11
+ ): Promise<SourceConfig> {
12
+ const config = await loadGlobalConfig(configDir);
13
+
14
+ // 判断是 git url 还是本地路径
15
+ const isGit = sourcePath.startsWith('http') || sourcePath.startsWith('git@');
16
+
17
+ let sourceConfig: SourceConfig;
18
+
19
+ if (isGit) {
20
+ // Clone git repo
21
+ const cloneDir = path.join(config.sourcesDir, name);
22
+ console.log(`Cloning ${sourcePath} to ${cloneDir}...`);
23
+
24
+ await fs.ensureDir(config.sourcesDir);
25
+ execSync(`git clone ${sourcePath} "${cloneDir}"`, { stdio: 'inherit' });
26
+
27
+ sourceConfig = { type: 'git', url: sourcePath };
28
+ } else {
29
+ // 本地路径
30
+ const absolutePath = path.resolve(sourcePath);
31
+ if (!(await fs.pathExists(absolutePath))) {
32
+ throw new Error(`Path does not exist: ${absolutePath}`);
33
+ }
34
+ sourceConfig = { type: 'local', path: absolutePath };
35
+ }
36
+
37
+ config.sources[name] = sourceConfig;
38
+ await saveGlobalConfig(config, configDir);
39
+
40
+ return sourceConfig;
41
+ }
42
+
43
+ export async function listSources(configDir: string): Promise<Record<string, SourceConfig>> {
44
+ const config = await loadGlobalConfig(configDir);
45
+ return config.sources;
46
+ }
47
+
48
+ export async function removeSource(name: string, configDir: string): Promise<void> {
49
+ const config = await loadGlobalConfig(configDir);
50
+
51
+ if (!config.sources[name]) {
52
+ throw new Error(`Source not found: ${name}`);
53
+ }
54
+
55
+ const source = config.sources[name];
56
+
57
+ // 如果是 git 类型,清理克隆目录
58
+ if (source.type === 'git') {
59
+ const cloneDir = path.join(config.sourcesDir, name);
60
+ if (await fs.pathExists(cloneDir)) {
61
+ console.log(`Removing cloned directory: ${cloneDir}`);
62
+ await fs.remove(cloneDir);
63
+ }
64
+ }
65
+
66
+ delete config.sources[name];
67
+ await saveGlobalConfig(config, configDir);
68
+ }
69
+
70
+ export async function updateSource(name: string, configDir: string): Promise<void> {
71
+ const config = await loadGlobalConfig(configDir);
72
+ const source = config.sources[name];
73
+
74
+ if (!source) {
75
+ throw new Error(`Source not found: ${name}`);
76
+ }
77
+
78
+ if (source.type === 'git') {
79
+ const cloneDir = path.join(config.sourcesDir, name);
80
+ console.log(`Updating ${name}...`);
81
+ execSync(`git -C "${cloneDir}" pull`, { stdio: 'inherit' });
82
+ } else {
83
+ console.log(`Source ${name} is local, no update needed.`);
84
+ }
85
+ }
86
+
87
+ export async function getSourcePath(name: string, configDir: string): Promise<string> {
88
+ const config = await loadGlobalConfig(configDir);
89
+ const source = config.sources[name];
90
+
91
+ if (!source) {
92
+ throw new Error(`Source not found: ${name}`);
93
+ }
94
+
95
+ if (source.type === 'local') {
96
+ return source.path!;
97
+ }
98
+
99
+ return path.join(config.sourcesDir, name);
100
+ }
101
+
102
+ export interface ScanResult {
103
+ added: string[];
104
+ updated: string[];
105
+ skipped: string[];
106
+ }
107
+
108
+ export async function scanSources(configDir: string): Promise<ScanResult> {
109
+ const config = await loadGlobalConfig(configDir);
110
+ const result: ScanResult = { added: [], updated: [], skipped: [] };
111
+
112
+ // 确保 sourcesDir 存在
113
+ if (!(await fs.pathExists(config.sourcesDir))) {
114
+ await fs.ensureDir(config.sourcesDir);
115
+ return result;
116
+ }
117
+
118
+ // 获取 sourcesDir 下的所有文件夹
119
+ const entries = await fs.readdir(config.sourcesDir, { withFileTypes: true });
120
+ const directories = entries
121
+ .filter(entry => entry.isDirectory())
122
+ .map(entry => entry.name);
123
+
124
+ // 检查每个 git source 的克隆目录是否还存在
125
+ for (const [name, source] of Object.entries(config.sources)) {
126
+ if (source.type === 'git') {
127
+ const cloneDir = path.join(config.sourcesDir, name);
128
+ if (!(await fs.pathExists(cloneDir))) {
129
+ // 克隆目录不存在,从配置中移除
130
+ delete config.sources[name];
131
+ console.log(`Removed missing git source: ${name}`);
132
+ }
133
+ }
134
+ }
135
+
136
+ // 扫描目录并更新配置
137
+ for (const dirName of directories) {
138
+ const dirPath = path.join(config.sourcesDir, dirName);
139
+ const existingSource = config.sources[dirName];
140
+
141
+ // 检查是否是 git 仓库
142
+ const isGitRepo = await fs.pathExists(path.join(dirPath, '.git'));
143
+
144
+ if (existingSource) {
145
+ // 已存在配置
146
+ if (existingSource.type === 'git' && isGitRepo) {
147
+ result.skipped.push(dirName);
148
+ } else if (existingSource.type === 'local' && existingSource.path === dirPath) {
149
+ result.skipped.push(dirName);
150
+ } else {
151
+ // 配置不一致,更新为当前状态
152
+ if (isGitRepo) {
153
+ // 尝试获取远程 URL
154
+ try {
155
+ const remoteUrl = execSync(`git -C "${dirPath}" config --get remote.origin.url`, { encoding: 'utf-8' }).trim();
156
+ config.sources[dirName] = { type: 'git', url: remoteUrl };
157
+ } catch {
158
+ config.sources[dirName] = { type: 'local', path: dirPath };
159
+ }
160
+ } else {
161
+ config.sources[dirName] = { type: 'local', path: dirPath };
162
+ }
163
+ result.updated.push(dirName);
164
+ }
165
+ } else {
166
+ // 新发现的目录,添加到配置
167
+ if (isGitRepo) {
168
+ // 尝试获取远程 URL
169
+ try {
170
+ const remoteUrl = execSync(`git -C "${dirPath}" config --get remote.origin.url`, { encoding: 'utf-8' }).trim();
171
+ config.sources[dirName] = { type: 'git', url: remoteUrl };
172
+ } catch {
173
+ config.sources[dirName] = { type: 'local', path: dirPath };
174
+ }
175
+ } else {
176
+ config.sources[dirName] = { type: 'local', path: dirPath };
177
+ }
178
+ result.added.push(dirName);
179
+ }
180
+ }
181
+
182
+ await saveGlobalConfig(config, configDir);
183
+ return result;
184
+ }
package/src/index.ts CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  import { Command } from 'commander';
4
4
  import { handleConfigSet, handleConfigGet, handleConfigList } from './commands/config';
5
- import { handleSourceAdd, handleSourceList, handleSourceRemove, handleSourceUpdate } from './commands/source';
6
- import { handleUse, handleList, handleRemove, handleStatus } from './commands/use';
5
+ import { handleSourceAdd, handleSourceList, handleSourceRemove, handleSourceUpdate, handleSourceScan } from './commands/source';
6
+ import { handleUse, handleList, handleRemove, handleStatus, handleProjectUpdate } from './commands/use';
7
7
  import { showHelp } from './commands/help';
8
8
  import { GLOBAL_CONFIG_DIR } from './utils/path';
9
9
 
@@ -50,11 +50,19 @@ sourceCmd
50
50
  sourceCmd
51
51
  .command('update [name]')
52
52
  .alias('up')
53
- .description('Update source(s)')
53
+ .alias('upgrade')
54
+ .description('Update source(s) with git pull')
54
55
  .action(async (name?: string) => {
55
56
  await handleSourceUpdate(name);
56
57
  });
57
58
 
59
+ sourceCmd
60
+ .command('scan')
61
+ .description('Scan sources directory and update configuration')
62
+ .action(async () => {
63
+ await handleSourceScan();
64
+ });
65
+
58
66
  // Config subcommands (full command version)
59
67
  const configCmd = program
60
68
  .command('config')
@@ -111,6 +119,13 @@ program
111
119
  await handleStatus();
112
120
  });
113
121
 
122
+ program
123
+ .command('update [sources...]')
124
+ .description('Update source(s) in current project')
125
+ .action(async (sources: string[]) => {
126
+ await handleProjectUpdate(sources);
127
+ });
128
+
114
129
  // Help command
115
130
  program
116
131
  .command('help')
@@ -147,8 +162,12 @@ program
147
162
  break;
148
163
  case 'update':
149
164
  case 'up':
165
+ case 'upgrade':
150
166
  await handleSourceUpdate(args[0]);
151
167
  break;
168
+ case 'scan':
169
+ await handleSourceScan();
170
+ break;
152
171
  default:
153
172
  console.log(`Unknown source command: ${cmd}`);
154
173
  }