tools-cc 1.0.0 → 1.0.3
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/AGENTS.md +164 -0
- package/CHANGELOG.md +63 -0
- package/{readme.md → README.md} +44 -8
- package/dist/commands/source.d.ts +1 -0
- package/dist/commands/source.js +29 -0
- package/dist/commands/use.d.ts +1 -0
- package/dist/commands/use.js +40 -2
- package/dist/core/source.d.ts +6 -0
- package/dist/core/source.js +78 -0
- package/dist/index.js +18 -1
- package/dist/utils/path.d.ts +1 -1
- package/dist/utils/path.js +4 -4
- package/package.json +1 -1
- package/src/commands/source.ts +34 -1
- package/src/commands/use.ts +190 -147
- package/src/core/source.ts +184 -100
- package/src/index.ts +22 -3
- package/src/utils/path.ts +4 -4
package/src/commands/use.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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, getProjectConfigPath } 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 = getProjectConfigPath(projectDir);
|
|
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 = getProjectConfigPath(projectDir);
|
|
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 = getProjectConfigPath(projectDir);
|
|
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
|
+
}
|
package/src/core/source.ts
CHANGED
|
@@ -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
|
-
.
|
|
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
|
}
|