tools-cc 1.0.0
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/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.js +56 -0
- package/dist/commands/help.d.ts +1 -0
- package/dist/commands/help.js +84 -0
- package/dist/commands/source.d.ts +4 -0
- package/dist/commands/source.js +72 -0
- package/dist/commands/use.d.ts +6 -0
- package/dist/commands/use.js +133 -0
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.js +37 -0
- package/dist/core/manifest.d.ts +3 -0
- package/dist/core/manifest.js +56 -0
- package/dist/core/project.d.ts +4 -0
- package/dist/core/project.js +118 -0
- package/dist/core/source.d.ts +6 -0
- package/dist/core/source.js +86 -0
- package/dist/core/symlink.d.ts +3 -0
- package/dist/core/symlink.js +56 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +165 -0
- package/dist/types/config.d.ts +23 -0
- package/dist/types/config.js +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +17 -0
- package/dist/utils/path.d.ts +8 -0
- package/dist/utils/path.js +22 -0
- package/docs/plans/2026-02-25-tools-cc-design.md +195 -0
- package/docs/plans/2026-02-25-tools-cc-impl.md +1600 -0
- package/package.json +44 -0
- package/readme.md +182 -0
- package/src/commands/config.ts +50 -0
- package/src/commands/help.ts +79 -0
- package/src/commands/source.ts +63 -0
- package/src/commands/use.ts +147 -0
- package/src/core/config.ts +37 -0
- package/src/core/manifest.ts +57 -0
- package/src/core/project.ts +136 -0
- package/src/core/source.ts +100 -0
- package/src/core/symlink.ts +56 -0
- package/src/index.ts +186 -0
- package/src/types/config.ts +27 -0
- package/src/types/index.ts +1 -0
- package/src/utils/path.ts +18 -0
- package/tests/core/config.test.ts +37 -0
- package/tests/core/manifest.test.ts +37 -0
- package/tests/core/project.test.ts +50 -0
- package/tests/core/source.test.ts +75 -0
- package/tests/core/symlink.test.ts +39 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,1600 @@
|
|
|
1
|
+
# tools-cc CLI 实现计划
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** 构建一个用于统一管理多个 AI 编程工具的 skills/commands/agents 配置的 CLI 工具
|
|
6
|
+
|
|
7
|
+
**Architecture:** 单体 CLI 架构,核心模块包括:配置管理、Source 管理、项目管理、符号链接处理。使用 TypeScript 开发,编译为 Node.js 可执行文件。
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Node.js, commander (CLI), inquirer (交互), fs-extra (文件操作)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Task 1: 项目初始化
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
- Create: `package.json`
|
|
17
|
+
- Create: `tsconfig.json`
|
|
18
|
+
- Create: `src/index.ts`
|
|
19
|
+
|
|
20
|
+
**Step 1: 初始化 npm 项目**
|
|
21
|
+
|
|
22
|
+
Run: `npm init -y`
|
|
23
|
+
|
|
24
|
+
**Step 2: 安装依赖**
|
|
25
|
+
|
|
26
|
+
Run: `npm install commander inquirer fs-extra chalk && npm install -D typescript @types/node @types/inquirer @types/fs-extra ts-node`
|
|
27
|
+
|
|
28
|
+
**Step 3: 创建 tsconfig.json**
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"compilerOptions": {
|
|
33
|
+
"target": "ES2020",
|
|
34
|
+
"module": "commonjs",
|
|
35
|
+
"lib": ["ES2020"],
|
|
36
|
+
"outDir": "./dist",
|
|
37
|
+
"rootDir": "./src",
|
|
38
|
+
"strict": true,
|
|
39
|
+
"esModuleInterop": true,
|
|
40
|
+
"skipLibCheck": true,
|
|
41
|
+
"forceConsistentCasingInFileNames": true,
|
|
42
|
+
"resolveJsonModule": true,
|
|
43
|
+
"declaration": true
|
|
44
|
+
},
|
|
45
|
+
"include": ["src/**/*"],
|
|
46
|
+
"exclude": ["node_modules", "dist"]
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Step 4: 创建基础入口文件**
|
|
51
|
+
|
|
52
|
+
Create: `src/index.ts`
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
#!/usr/bin/env node
|
|
56
|
+
|
|
57
|
+
import { Command } from 'commander';
|
|
58
|
+
|
|
59
|
+
const program = new Command();
|
|
60
|
+
|
|
61
|
+
program
|
|
62
|
+
.name('tools-cc')
|
|
63
|
+
.description('CLI tool for managing skills/commands/agents across multiple AI coding tools')
|
|
64
|
+
.version('0.0.1');
|
|
65
|
+
|
|
66
|
+
program.parse();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Step 5: 添加 npm scripts**
|
|
70
|
+
|
|
71
|
+
Modify `package.json`:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"scripts": {
|
|
76
|
+
"build": "tsc",
|
|
77
|
+
"dev": "ts-node src/index.ts",
|
|
78
|
+
"start": "node dist/index.js"
|
|
79
|
+
},
|
|
80
|
+
"bin": {
|
|
81
|
+
"tools-cc": "./dist/index.js"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Step 6: 验证基础 CLI**
|
|
87
|
+
|
|
88
|
+
Run: `npm run dev`
|
|
89
|
+
|
|
90
|
+
Expected: 显示帮助信息
|
|
91
|
+
|
|
92
|
+
**Step 7: Commit**
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
git add package.json package-lock.json tsconfig.json src/index.ts
|
|
96
|
+
git commit -m "chore: initialize project with TypeScript and commander"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Task 2: 配置管理模块 - 类型定义
|
|
102
|
+
|
|
103
|
+
**Files:**
|
|
104
|
+
- Create: `src/types/config.ts`
|
|
105
|
+
- Create: `src/types/index.ts`
|
|
106
|
+
|
|
107
|
+
**Step 1: 创建类型定义**
|
|
108
|
+
|
|
109
|
+
Create: `src/types/config.ts`
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
export interface SourceConfig {
|
|
113
|
+
type: 'git' | 'local';
|
|
114
|
+
url?: string;
|
|
115
|
+
path?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface GlobalConfig {
|
|
119
|
+
sourcesDir: string;
|
|
120
|
+
sources: Record<string, SourceConfig>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface ProjectConfig {
|
|
124
|
+
sources: string[];
|
|
125
|
+
links: string[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface Manifest {
|
|
129
|
+
name: string;
|
|
130
|
+
version: string;
|
|
131
|
+
skills?: string[];
|
|
132
|
+
commands?: string[];
|
|
133
|
+
agents?: string[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface ToolConfig {
|
|
137
|
+
linkName: string;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Create: `src/types/index.ts`
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
export * from './config';
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Step 2: Commit**
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
git add src/types/
|
|
151
|
+
git commit -m "feat: add type definitions for config and manifest"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Task 3: 配置管理模块 - 全局配置
|
|
157
|
+
|
|
158
|
+
**Files:**
|
|
159
|
+
- Create: `src/utils/path.ts`
|
|
160
|
+
- Create: `src/core/config.ts`
|
|
161
|
+
- Create: `tests/core/config.test.ts`
|
|
162
|
+
|
|
163
|
+
**Step 1: 创建路径工具**
|
|
164
|
+
|
|
165
|
+
Create: `src/utils/path.ts`
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import os from 'os';
|
|
169
|
+
import path from 'path';
|
|
170
|
+
|
|
171
|
+
export const GLOBAL_CONFIG_DIR = path.join(os.homedir(), '.tools-cc');
|
|
172
|
+
export const GLOBAL_CONFIG_FILE = path.join(GLOBAL_CONFIG_DIR, 'config.json');
|
|
173
|
+
|
|
174
|
+
export const DEFAULT_CONFIG = {
|
|
175
|
+
sourcesDir: path.join(GLOBAL_CONFIG_DIR, 'sources'),
|
|
176
|
+
sources: {}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export function getProjectConfigPath(projectDir: string): string {
|
|
180
|
+
return path.join(projectDir, 'tools-cc.json');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function getToolsccDir(projectDir: string): string {
|
|
184
|
+
return path.join(projectDir, '.toolscc');
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Step 2: 写配置读取测试**
|
|
189
|
+
|
|
190
|
+
Create: `tests/core/config.test.ts`
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
194
|
+
import fs from 'fs-extra';
|
|
195
|
+
import path from 'path';
|
|
196
|
+
import { loadGlobalConfig, saveGlobalConfig } from '../../src/core/config';
|
|
197
|
+
import { GLOBAL_CONFIG_FILE, GLOBAL_CONFIG_DIR } from '../../src/utils/path';
|
|
198
|
+
|
|
199
|
+
describe('Config Module', () => {
|
|
200
|
+
const testConfigDir = path.join(__dirname, '../fixtures/.tools-cc');
|
|
201
|
+
|
|
202
|
+
beforeEach(async () => {
|
|
203
|
+
await fs.ensureDir(testConfigDir);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
afterEach(async () => {
|
|
207
|
+
await fs.remove(testConfigDir);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should create default config if not exists', async () => {
|
|
211
|
+
const config = await loadGlobalConfig(testConfigDir);
|
|
212
|
+
expect(config.sourcesDir).toBeDefined();
|
|
213
|
+
expect(config.sources).toEqual({});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should save and load config correctly', async () => {
|
|
217
|
+
const testConfig = {
|
|
218
|
+
sourcesDir: '/test/sources',
|
|
219
|
+
sources: {
|
|
220
|
+
'test-source': { type: 'git' as const, url: 'https://github.com/test/repo.git' }
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
await saveGlobalConfig(testConfig, testConfigDir);
|
|
225
|
+
const loaded = await loadGlobalConfig(testConfigDir);
|
|
226
|
+
|
|
227
|
+
expect(loaded.sourcesDir).toBe('/test/sources');
|
|
228
|
+
expect(loaded.sources['test-source'].type).toBe('git');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Step 3: 运行测试确认失败**
|
|
234
|
+
|
|
235
|
+
Run: `npm test tests/core/config.test.ts`
|
|
236
|
+
|
|
237
|
+
Expected: FAIL (模块不存在)
|
|
238
|
+
|
|
239
|
+
**Step 4: 实现配置模块**
|
|
240
|
+
|
|
241
|
+
Create: `src/core/config.ts`
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import fs from 'fs-extra';
|
|
245
|
+
import path from 'path';
|
|
246
|
+
import { GlobalConfig, ProjectConfig } from '../types';
|
|
247
|
+
import { DEFAULT_CONFIG, getProjectConfigPath } from '../utils/path';
|
|
248
|
+
|
|
249
|
+
export async function loadGlobalConfig(configDir: string): Promise<GlobalConfig> {
|
|
250
|
+
const configFile = path.join(configDir, 'config.json');
|
|
251
|
+
|
|
252
|
+
if (!(await fs.pathExists(configFile))) {
|
|
253
|
+
await fs.ensureDir(configDir);
|
|
254
|
+
await fs.writeJson(configFile, DEFAULT_CONFIG, { spaces: 2 });
|
|
255
|
+
return DEFAULT_CONFIG;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return await fs.readJson(configFile);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function saveGlobalConfig(config: GlobalConfig, configDir: string): Promise<void> {
|
|
262
|
+
const configFile = path.join(configDir, 'config.json');
|
|
263
|
+
await fs.ensureDir(configDir);
|
|
264
|
+
await fs.writeJson(configFile, config, { spaces: 2 });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function loadProjectConfig(projectDir: string): Promise<ProjectConfig | null> {
|
|
268
|
+
const configFile = getProjectConfigPath(projectDir);
|
|
269
|
+
|
|
270
|
+
if (!(await fs.pathExists(configFile))) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return await fs.readJson(configFile);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function saveProjectConfig(config: ProjectConfig, projectDir: string): Promise<void> {
|
|
278
|
+
const configFile = getProjectConfigPath(projectDir);
|
|
279
|
+
await fs.writeJson(configFile, config, { spaces: 2 });
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Step 5: 安装测试框架**
|
|
284
|
+
|
|
285
|
+
Run: `npm install -D vitest`
|
|
286
|
+
|
|
287
|
+
Add to `package.json` scripts:
|
|
288
|
+
|
|
289
|
+
```json
|
|
290
|
+
"test": "vitest",
|
|
291
|
+
"test:run": "vitest run"
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Step 6: 运行测试确认通过**
|
|
295
|
+
|
|
296
|
+
Run: `npm run test:run tests/core/config.test.ts`
|
|
297
|
+
|
|
298
|
+
Expected: PASS
|
|
299
|
+
|
|
300
|
+
**Step 7: Commit**
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
git add src/utils/path.ts src/core/config.ts tests/core/config.test.ts package.json
|
|
304
|
+
git commit -m "feat: add global config management module with tests"
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Task 4: 配置管理模块 - Config 命令
|
|
310
|
+
|
|
311
|
+
**Files:**
|
|
312
|
+
- Create: `src/commands/config.ts`
|
|
313
|
+
- Modify: `src/index.ts`
|
|
314
|
+
|
|
315
|
+
**Step 1: 实现 config 命令**
|
|
316
|
+
|
|
317
|
+
Create: `src/commands/config.ts`
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
import chalk from 'chalk';
|
|
321
|
+
import { loadGlobalConfig, saveGlobalConfig } from '../core/config';
|
|
322
|
+
import { GLOBAL_CONFIG_DIR } from '../utils/path';
|
|
323
|
+
|
|
324
|
+
export async function handleConfigSet(key: string, value: string): Promise<void> {
|
|
325
|
+
const config = await loadGlobalConfig(GLOBAL_CONFIG_DIR);
|
|
326
|
+
|
|
327
|
+
if (key === 'sourcesDir') {
|
|
328
|
+
config.sourcesDir = value;
|
|
329
|
+
await saveGlobalConfig(config, GLOBAL_CONFIG_DIR);
|
|
330
|
+
console.log(chalk.green(`✓ Set sourcesDir to: ${value}`));
|
|
331
|
+
} else {
|
|
332
|
+
console.log(chalk.red(`✗ Unknown config key: ${key}`));
|
|
333
|
+
console.log(chalk.gray('Available keys: sourcesDir'));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export async function handleConfigGet(key: string): Promise<void> {
|
|
338
|
+
const config = await loadGlobalConfig(GLOBAL_CONFIG_DIR);
|
|
339
|
+
|
|
340
|
+
if (key === 'sourcesDir') {
|
|
341
|
+
console.log(config.sourcesDir);
|
|
342
|
+
} else if (key === 'all') {
|
|
343
|
+
console.log(JSON.stringify(config, null, 2));
|
|
344
|
+
} else {
|
|
345
|
+
console.log(chalk.red(`✗ Unknown config key: ${key}`));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**Step 2: 集成到主程序**
|
|
351
|
+
|
|
352
|
+
Modify `src/index.ts`:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
#!/usr/bin/env node
|
|
356
|
+
|
|
357
|
+
import { Command } from 'commander';
|
|
358
|
+
import { handleConfigSet, handleConfigGet } from './commands/config';
|
|
359
|
+
import { GLOBAL_CONFIG_DIR } from './utils/path';
|
|
360
|
+
|
|
361
|
+
const program = new Command();
|
|
362
|
+
|
|
363
|
+
program
|
|
364
|
+
.name('tools-cc')
|
|
365
|
+
.description('CLI tool for managing skills/commands/agents across multiple AI coding tools')
|
|
366
|
+
.version('0.0.1');
|
|
367
|
+
|
|
368
|
+
// Config commands
|
|
369
|
+
program
|
|
370
|
+
.command('config:set <key> <value>')
|
|
371
|
+
.alias('c:set')
|
|
372
|
+
.description('Set a config value')
|
|
373
|
+
.action(async (key: string, value: string) => {
|
|
374
|
+
await handleConfigSet(key, value);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
program
|
|
378
|
+
.command('config:get <key>')
|
|
379
|
+
.alias('c:get')
|
|
380
|
+
.description('Get a config value')
|
|
381
|
+
.action(async (key: string) => {
|
|
382
|
+
await handleConfigGet(key);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
program.parse();
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Step 3: 测试命令**
|
|
389
|
+
|
|
390
|
+
Run: `npm run dev -- c:set sourcesDir D:/test-sources`
|
|
391
|
+
|
|
392
|
+
Expected: `✓ Set sourcesDir to: D:/test-sources`
|
|
393
|
+
|
|
394
|
+
Run: `npm run dev -- c:get sourcesDir`
|
|
395
|
+
|
|
396
|
+
Expected: `D:/test-sources`
|
|
397
|
+
|
|
398
|
+
**Step 4: Commit**
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
git add src/commands/config.ts src/index.ts
|
|
402
|
+
git commit -m "feat: add config:set and config:get commands"
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Task 5: Source 管理模块 - 核心逻辑
|
|
408
|
+
|
|
409
|
+
**Files:**
|
|
410
|
+
- Create: `src/core/source.ts`
|
|
411
|
+
- Create: `tests/core/source.test.ts`
|
|
412
|
+
|
|
413
|
+
**Step 1: 写测试**
|
|
414
|
+
|
|
415
|
+
Create: `tests/core/source.test.ts`
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
419
|
+
import fs from 'fs-extra';
|
|
420
|
+
import path from 'path';
|
|
421
|
+
import { addSource, listSources, removeSource } from '../../src/core/source';
|
|
422
|
+
|
|
423
|
+
describe('Source Module', () => {
|
|
424
|
+
const testConfigDir = path.join(__dirname, '../fixtures/.tools-cc-test');
|
|
425
|
+
const testSourcesDir = path.join(__dirname, '../fixtures/sources');
|
|
426
|
+
|
|
427
|
+
beforeEach(async () => {
|
|
428
|
+
await fs.ensureDir(testConfigDir);
|
|
429
|
+
await fs.ensureDir(testSourcesDir);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
afterEach(async () => {
|
|
433
|
+
await fs.remove(testConfigDir);
|
|
434
|
+
await fs.remove(testSourcesDir);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('should add a local source', async () => {
|
|
438
|
+
const result = await addSource('test-local', testSourcesDir, testConfigDir);
|
|
439
|
+
expect(result.type).toBe('local');
|
|
440
|
+
expect(result.path).toBe(testSourcesDir);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should list sources', async () => {
|
|
444
|
+
await addSource('test-1', testSourcesDir, testConfigDir);
|
|
445
|
+
const sources = await listSources(testConfigDir);
|
|
446
|
+
expect(sources).toHaveProperty('test-1');
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('should remove a source', async () => {
|
|
450
|
+
await addSource('test-remove', testSourcesDir, testConfigDir);
|
|
451
|
+
await removeSource('test-remove', testConfigDir);
|
|
452
|
+
const sources = await listSources(testConfigDir);
|
|
453
|
+
expect(sources).not.toHaveProperty('test-remove');
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Step 2: 运行测试确认失败**
|
|
459
|
+
|
|
460
|
+
Run: `npm run test:run tests/core/source.test.ts`
|
|
461
|
+
|
|
462
|
+
Expected: FAIL
|
|
463
|
+
|
|
464
|
+
**Step 3: 实现 Source 模块**
|
|
465
|
+
|
|
466
|
+
Create: `src/core/source.ts`
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
import fs from 'fs-extra';
|
|
470
|
+
import path from 'path';
|
|
471
|
+
import { execSync } from 'child_process';
|
|
472
|
+
import { loadGlobalConfig, saveGlobalConfig } from './config';
|
|
473
|
+
import { SourceConfig } from '../types';
|
|
474
|
+
|
|
475
|
+
export async function addSource(
|
|
476
|
+
name: string,
|
|
477
|
+
sourcePath: string,
|
|
478
|
+
configDir: string
|
|
479
|
+
): Promise<SourceConfig> {
|
|
480
|
+
const config = await loadGlobalConfig(configDir);
|
|
481
|
+
|
|
482
|
+
// 判断是 git url 还是本地路径
|
|
483
|
+
const isGit = sourcePath.startsWith('http') || sourcePath.startsWith('git@');
|
|
484
|
+
|
|
485
|
+
let sourceConfig: SourceConfig;
|
|
486
|
+
|
|
487
|
+
if (isGit) {
|
|
488
|
+
// Clone git repo
|
|
489
|
+
const cloneDir = path.join(config.sourcesDir, name);
|
|
490
|
+
console.log(`Cloning ${sourcePath} to ${cloneDir}...`);
|
|
491
|
+
|
|
492
|
+
await fs.ensureDir(config.sourcesDir);
|
|
493
|
+
execSync(`git clone ${sourcePath} "${cloneDir}"`, { stdio: 'inherit' });
|
|
494
|
+
|
|
495
|
+
sourceConfig = { type: 'git', url: sourcePath };
|
|
496
|
+
} else {
|
|
497
|
+
// 本地路径
|
|
498
|
+
const absolutePath = path.resolve(sourcePath);
|
|
499
|
+
if (!(await fs.pathExists(absolutePath))) {
|
|
500
|
+
throw new Error(`Path does not exist: ${absolutePath}`);
|
|
501
|
+
}
|
|
502
|
+
sourceConfig = { type: 'local', path: absolutePath };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
config.sources[name] = sourceConfig;
|
|
506
|
+
await saveGlobalConfig(config, configDir);
|
|
507
|
+
|
|
508
|
+
return sourceConfig;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export async function listSources(configDir: string): Promise<Record<string, SourceConfig>> {
|
|
512
|
+
const config = await loadGlobalConfig(configDir);
|
|
513
|
+
return config.sources;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export async function removeSource(name: string, configDir: string): Promise<void> {
|
|
517
|
+
const config = await loadGlobalConfig(configDir);
|
|
518
|
+
|
|
519
|
+
if (!config.sources[name]) {
|
|
520
|
+
throw new Error(`Source not found: ${name}`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
delete config.sources[name];
|
|
524
|
+
await saveGlobalConfig(config, configDir);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export async function updateSource(name: string, configDir: string): Promise<void> {
|
|
528
|
+
const config = await loadGlobalConfig(configDir);
|
|
529
|
+
const source = config.sources[name];
|
|
530
|
+
|
|
531
|
+
if (!source) {
|
|
532
|
+
throw new Error(`Source not found: ${name}`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (source.type === 'git') {
|
|
536
|
+
const cloneDir = path.join(config.sourcesDir, name);
|
|
537
|
+
console.log(`Updating ${name}...`);
|
|
538
|
+
execSync(`git -C "${cloneDir}" pull`, { stdio: 'inherit' });
|
|
539
|
+
} else {
|
|
540
|
+
console.log(`Source ${name} is local, no update needed.`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export async function getSourcePath(name: string, configDir: string): Promise<string> {
|
|
545
|
+
const config = await loadGlobalConfig(configDir);
|
|
546
|
+
const source = config.sources[name];
|
|
547
|
+
|
|
548
|
+
if (!source) {
|
|
549
|
+
throw new Error(`Source not found: ${name}`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (source.type === 'local') {
|
|
553
|
+
return source.path!;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return path.join(config.sourcesDir, name);
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**Step 4: 运行测试确认通过**
|
|
561
|
+
|
|
562
|
+
Run: `npm run test:run tests/core/source.test.ts`
|
|
563
|
+
|
|
564
|
+
Expected: PASS
|
|
565
|
+
|
|
566
|
+
**Step 5: Commit**
|
|
567
|
+
|
|
568
|
+
```bash
|
|
569
|
+
git add src/core/source.ts tests/core/source.test.ts
|
|
570
|
+
git commit -m "feat: add source management core module with tests"
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
## Task 6: Source 命令实现
|
|
576
|
+
|
|
577
|
+
**Files:**
|
|
578
|
+
- Create: `src/commands/source.ts`
|
|
579
|
+
- Modify: `src/index.ts`
|
|
580
|
+
|
|
581
|
+
**Step 1: 实现 source 命令**
|
|
582
|
+
|
|
583
|
+
Create: `src/commands/source.ts`
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
import chalk from 'chalk';
|
|
587
|
+
import { addSource, listSources, removeSource, updateSource } from '../core/source';
|
|
588
|
+
import { GLOBAL_CONFIG_DIR } from '../utils/path';
|
|
589
|
+
|
|
590
|
+
export async function handleSourceAdd(name: string, pathOrUrl: string): Promise<void> {
|
|
591
|
+
try {
|
|
592
|
+
const result = await addSource(name, pathOrUrl, GLOBAL_CONFIG_DIR);
|
|
593
|
+
console.log(chalk.green(`✓ Added source: ${name}`));
|
|
594
|
+
console.log(chalk.gray(` Type: ${result.type}`));
|
|
595
|
+
if (result.type === 'git') {
|
|
596
|
+
console.log(chalk.gray(` URL: ${result.url}`));
|
|
597
|
+
} else {
|
|
598
|
+
console.log(chalk.gray(` Path: ${result.path}`));
|
|
599
|
+
}
|
|
600
|
+
} catch (error) {
|
|
601
|
+
console.log(chalk.red(`✗ ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export async function handleSourceList(): Promise<void> {
|
|
606
|
+
const sources = await listSources(GLOBAL_CONFIG_DIR);
|
|
607
|
+
const entries = Object.entries(sources);
|
|
608
|
+
|
|
609
|
+
if (entries.length === 0) {
|
|
610
|
+
console.log(chalk.gray('No sources configured.'));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
console.log(chalk.bold('Configured sources:'));
|
|
615
|
+
for (const [name, config] of entries) {
|
|
616
|
+
console.log(` ${chalk.cyan(name)} (${config.type})`);
|
|
617
|
+
if (config.type === 'git') {
|
|
618
|
+
console.log(chalk.gray(` ${config.url}`));
|
|
619
|
+
} else {
|
|
620
|
+
console.log(chalk.gray(` ${config.path}`));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export async function handleSourceRemove(name: string): Promise<void> {
|
|
626
|
+
try {
|
|
627
|
+
await removeSource(name, GLOBAL_CONFIG_DIR);
|
|
628
|
+
console.log(chalk.green(`✓ Removed source: ${name}`));
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.log(chalk.red(`✗ ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export async function handleSourceUpdate(name?: string): Promise<void> {
|
|
635
|
+
try {
|
|
636
|
+
if (name) {
|
|
637
|
+
await updateSource(name, GLOBAL_CONFIG_DIR);
|
|
638
|
+
} else {
|
|
639
|
+
const sources = await listSources(GLOBAL_CONFIG_DIR);
|
|
640
|
+
for (const sourceName of Object.keys(sources)) {
|
|
641
|
+
await updateSource(sourceName, GLOBAL_CONFIG_DIR);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
console.log(chalk.green(`✓ Update complete`));
|
|
645
|
+
} catch (error) {
|
|
646
|
+
console.log(chalk.red(`✗ ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
**Step 2: 集成到主程序**
|
|
652
|
+
|
|
653
|
+
Modify `src/index.ts`:
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
#!/usr/bin/env node
|
|
657
|
+
|
|
658
|
+
import { Command } from 'commander';
|
|
659
|
+
import { handleConfigSet, handleConfigGet } from './commands/config';
|
|
660
|
+
import { handleSourceAdd, handleSourceList, handleSourceRemove, handleSourceUpdate } from './commands/source';
|
|
661
|
+
import { GLOBAL_CONFIG_DIR } from './utils/path';
|
|
662
|
+
|
|
663
|
+
const program = new Command();
|
|
664
|
+
|
|
665
|
+
program
|
|
666
|
+
.name('tools-cc')
|
|
667
|
+
.description('CLI tool for managing skills/commands/agents across multiple AI coding tools')
|
|
668
|
+
.version('0.0.1');
|
|
669
|
+
|
|
670
|
+
// Source commands
|
|
671
|
+
program
|
|
672
|
+
.option('-s, --source <command> [args...]', 'Source management')
|
|
673
|
+
.action(async (options) => {
|
|
674
|
+
if (options.source) {
|
|
675
|
+
const [cmd, ...args] = options.source;
|
|
676
|
+
switch (cmd) {
|
|
677
|
+
case 'add':
|
|
678
|
+
if (args.length < 2) {
|
|
679
|
+
console.log('Usage: tools-cc -s add <name> <path-or-url>');
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
await handleSourceAdd(args[0], args[1]);
|
|
683
|
+
break;
|
|
684
|
+
case 'list':
|
|
685
|
+
case 'ls':
|
|
686
|
+
await handleSourceList();
|
|
687
|
+
break;
|
|
688
|
+
case 'remove':
|
|
689
|
+
case 'rm':
|
|
690
|
+
if (args.length < 1) {
|
|
691
|
+
console.log('Usage: tools-cc -s remove <name>');
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
await handleSourceRemove(args[0]);
|
|
695
|
+
break;
|
|
696
|
+
case 'update':
|
|
697
|
+
case 'up':
|
|
698
|
+
await handleSourceUpdate(args[0]);
|
|
699
|
+
break;
|
|
700
|
+
default:
|
|
701
|
+
console.log(`Unknown source command: ${cmd}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// Config commands
|
|
707
|
+
program
|
|
708
|
+
.command('config:set <key> <value>')
|
|
709
|
+
.alias('c:set')
|
|
710
|
+
.description('Set a config value')
|
|
711
|
+
.action(async (key: string, value: string) => {
|
|
712
|
+
await handleConfigSet(key, value);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
program
|
|
716
|
+
.command('config:get <key>')
|
|
717
|
+
.alias('c:get')
|
|
718
|
+
.description('Get a config value')
|
|
719
|
+
.action(async (key: string) => {
|
|
720
|
+
await handleConfigGet(key);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
program.parse();
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
**Step 3: 测试命令**
|
|
727
|
+
|
|
728
|
+
Run: `npm run dev -- -s list`
|
|
729
|
+
|
|
730
|
+
Expected: `No sources configured.`
|
|
731
|
+
|
|
732
|
+
**Step 4: Commit**
|
|
733
|
+
|
|
734
|
+
```bash
|
|
735
|
+
git add src/commands/source.ts src/index.ts
|
|
736
|
+
git commit -m "feat: add source management commands (-s add/list/remove/update)"
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
## Task 7: Manifest 解析模块
|
|
742
|
+
|
|
743
|
+
**Files:**
|
|
744
|
+
- Create: `src/core/manifest.ts`
|
|
745
|
+
- Create: `tests/core/manifest.test.ts`
|
|
746
|
+
|
|
747
|
+
**Step 1: 写测试**
|
|
748
|
+
|
|
749
|
+
Create: `tests/core/manifest.test.ts`
|
|
750
|
+
|
|
751
|
+
```typescript
|
|
752
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
753
|
+
import fs from 'fs-extra';
|
|
754
|
+
import path from 'path';
|
|
755
|
+
import { loadManifest, scanSource } from '../../src/core/manifest';
|
|
756
|
+
|
|
757
|
+
describe('Manifest Module', () => {
|
|
758
|
+
const testSourceDir = path.join(__dirname, '../fixtures/test-source');
|
|
759
|
+
|
|
760
|
+
beforeEach(async () => {
|
|
761
|
+
await fs.ensureDir(path.join(testSourceDir, 'skills', 'test-skill'));
|
|
762
|
+
await fs.ensureDir(path.join(testSourceDir, 'commands'));
|
|
763
|
+
await fs.ensureDir(path.join(testSourceDir, 'agents'));
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
afterEach(async () => {
|
|
767
|
+
await fs.remove(testSourceDir);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('should scan source directory without manifest', async () => {
|
|
771
|
+
const manifest = await scanSource(testSourceDir);
|
|
772
|
+
expect(manifest.name).toBe(path.basename(testSourceDir));
|
|
773
|
+
expect(manifest.skills).toContain('test-skill');
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it('should load existing manifest', async () => {
|
|
777
|
+
const manifestPath = path.join(testSourceDir, 'manifest.json');
|
|
778
|
+
await fs.writeJson(manifestPath, {
|
|
779
|
+
name: 'custom-name',
|
|
780
|
+
version: '2.0.0',
|
|
781
|
+
skills: ['skill1']
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
const manifest = await loadManifest(testSourceDir);
|
|
785
|
+
expect(manifest.name).toBe('custom-name');
|
|
786
|
+
expect(manifest.version).toBe('2.0.0');
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
**Step 2: 运行测试确认失败**
|
|
792
|
+
|
|
793
|
+
Run: `npm run test:run tests/core/manifest.test.ts`
|
|
794
|
+
|
|
795
|
+
Expected: FAIL
|
|
796
|
+
|
|
797
|
+
**Step 3: 实现 Manifest 模块**
|
|
798
|
+
|
|
799
|
+
Create: `src/core/manifest.ts`
|
|
800
|
+
|
|
801
|
+
```typescript
|
|
802
|
+
import fs from 'fs-extra';
|
|
803
|
+
import path from 'path';
|
|
804
|
+
import { Manifest } from '../types';
|
|
805
|
+
|
|
806
|
+
export async function loadManifest(sourceDir: string): Promise<Manifest> {
|
|
807
|
+
const manifestPath = path.join(sourceDir, 'manifest.json');
|
|
808
|
+
|
|
809
|
+
if (await fs.pathExists(manifestPath)) {
|
|
810
|
+
return await fs.readJson(manifestPath);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return scanSource(sourceDir);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export async function scanSource(sourceDir: string): Promise<Manifest> {
|
|
817
|
+
const name = path.basename(sourceDir);
|
|
818
|
+
const manifest: Manifest = {
|
|
819
|
+
name,
|
|
820
|
+
version: '0.0.0',
|
|
821
|
+
skills: [],
|
|
822
|
+
commands: [],
|
|
823
|
+
agents: []
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
// Scan skills
|
|
827
|
+
const skillsDir = path.join(sourceDir, 'skills');
|
|
828
|
+
if (await fs.pathExists(skillsDir)) {
|
|
829
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
830
|
+
manifest.skills = entries
|
|
831
|
+
.filter(e => e.isDirectory())
|
|
832
|
+
.map(e => e.name);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Scan commands
|
|
836
|
+
const commandsDir = path.join(sourceDir, 'commands');
|
|
837
|
+
if (await fs.pathExists(commandsDir)) {
|
|
838
|
+
const entries = await fs.readdir(commandsDir);
|
|
839
|
+
manifest.commands = entries
|
|
840
|
+
.filter(e => e.endsWith('.md'))
|
|
841
|
+
.map(e => e.replace('.md', ''));
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Scan agents
|
|
845
|
+
const agentsDir = path.join(sourceDir, 'agents');
|
|
846
|
+
if (await fs.pathExists(agentsDir)) {
|
|
847
|
+
const entries = await fs.readdir(agentsDir);
|
|
848
|
+
manifest.agents = entries
|
|
849
|
+
.filter(e => e.endsWith('.md'))
|
|
850
|
+
.map(e => e.replace('.md', ''));
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return manifest;
|
|
854
|
+
}
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
**Step 4: 运行测试确认通过**
|
|
858
|
+
|
|
859
|
+
Run: `npm run test:run tests/core/manifest.test.ts`
|
|
860
|
+
|
|
861
|
+
Expected: PASS
|
|
862
|
+
|
|
863
|
+
**Step 5: Commit**
|
|
864
|
+
|
|
865
|
+
```bash
|
|
866
|
+
git add src/core/manifest.ts tests/core/manifest.test.ts
|
|
867
|
+
git commit -m "feat: add manifest loading and scanning module"
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
---
|
|
871
|
+
|
|
872
|
+
## Task 8: 项目管理模块 - 核心逻辑
|
|
873
|
+
|
|
874
|
+
**Files:**
|
|
875
|
+
- Create: `src/core/project.ts`
|
|
876
|
+
- Create: `tests/core/project.test.ts`
|
|
877
|
+
|
|
878
|
+
**Step 1: 写测试**
|
|
879
|
+
|
|
880
|
+
Create: `tests/core/project.test.ts`
|
|
881
|
+
|
|
882
|
+
```typescript
|
|
883
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
884
|
+
import fs from 'fs-extra';
|
|
885
|
+
import path from 'path';
|
|
886
|
+
import { initProject, useSource, unuseSource } from '../../src/core/project';
|
|
887
|
+
|
|
888
|
+
describe('Project Module', () => {
|
|
889
|
+
const testProjectDir = path.join(__dirname, '../fixtures/test-project');
|
|
890
|
+
const testSourceDir = path.join(__dirname, '../fixtures/test-source');
|
|
891
|
+
|
|
892
|
+
beforeEach(async () => {
|
|
893
|
+
await fs.ensureDir(testProjectDir);
|
|
894
|
+
await fs.ensureDir(path.join(testSourceDir, 'skills', 'test-skill'));
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
afterEach(async () => {
|
|
898
|
+
await fs.remove(testProjectDir);
|
|
899
|
+
await fs.remove(testSourceDir);
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
it('should initialize project with .toolscc directory', async () => {
|
|
903
|
+
await initProject(testProjectDir);
|
|
904
|
+
expect(await fs.pathExists(path.join(testProjectDir, '.toolscc'))).toBe(true);
|
|
905
|
+
expect(await fs.pathExists(path.join(testProjectDir, 'tools-cc.json'))).toBe(true);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it('should use source and copy components', async () => {
|
|
909
|
+
await initProject(testProjectDir);
|
|
910
|
+
await useSource('test-source', testSourceDir, testProjectDir);
|
|
911
|
+
|
|
912
|
+
// skills should be flattened with prefix
|
|
913
|
+
expect(await fs.pathExists(path.join(testProjectDir, '.toolscc', 'skills', 'test-source-test-skill'))).toBe(true);
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it('should unuse source and remove components', async () => {
|
|
917
|
+
await initProject(testProjectDir);
|
|
918
|
+
await useSource('test-source', testSourceDir, testProjectDir);
|
|
919
|
+
await unuseSource('test-source', testProjectDir);
|
|
920
|
+
|
|
921
|
+
const config = await fs.readJson(path.join(testProjectDir, 'tools-cc.json'));
|
|
922
|
+
expect(config.sources).not.toContain('test-source');
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
**Step 2: 运行测试确认失败**
|
|
928
|
+
|
|
929
|
+
Run: `npm run test:run tests/core/project.test.ts`
|
|
930
|
+
|
|
931
|
+
Expected: FAIL
|
|
932
|
+
|
|
933
|
+
**Step 3: 实现项目模块**
|
|
934
|
+
|
|
935
|
+
Create: `src/core/project.ts`
|
|
936
|
+
|
|
937
|
+
```typescript
|
|
938
|
+
import fs from 'fs-extra';
|
|
939
|
+
import path from 'path';
|
|
940
|
+
import { ProjectConfig } from '../types';
|
|
941
|
+
import { loadManifest } from './manifest';
|
|
942
|
+
import { getToolsccDir, getProjectConfigPath } from '../utils/path';
|
|
943
|
+
|
|
944
|
+
export async function initProject(projectDir: string): Promise<void> {
|
|
945
|
+
const toolsccDir = getToolsccDir(projectDir);
|
|
946
|
+
const configFile = getProjectConfigPath(projectDir);
|
|
947
|
+
|
|
948
|
+
// Create .toolscc directory structure
|
|
949
|
+
await fs.ensureDir(path.join(toolsccDir, 'skills'));
|
|
950
|
+
await fs.ensureDir(path.join(toolsccDir, 'commands'));
|
|
951
|
+
await fs.ensureDir(path.join(toolsccDir, 'agents'));
|
|
952
|
+
|
|
953
|
+
// Create project config if not exists
|
|
954
|
+
if (!(await fs.pathExists(configFile))) {
|
|
955
|
+
const config: ProjectConfig = {
|
|
956
|
+
sources: [],
|
|
957
|
+
links: []
|
|
958
|
+
};
|
|
959
|
+
await fs.writeJson(configFile, config, { spaces: 2 });
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
export async function useSource(
|
|
964
|
+
sourceName: string,
|
|
965
|
+
sourceDir: string,
|
|
966
|
+
projectDir: string
|
|
967
|
+
): Promise<void> {
|
|
968
|
+
const toolsccDir = getToolsccDir(projectDir);
|
|
969
|
+
const manifest = await loadManifest(sourceDir);
|
|
970
|
+
|
|
971
|
+
// Ensure project is initialized
|
|
972
|
+
await initProject(projectDir);
|
|
973
|
+
|
|
974
|
+
// Copy/link skills (flattened with prefix)
|
|
975
|
+
const sourceSkillsDir = path.join(sourceDir, 'skills');
|
|
976
|
+
if (await fs.pathExists(sourceSkillsDir)) {
|
|
977
|
+
const skills = await fs.readdir(sourceSkillsDir);
|
|
978
|
+
for (const skill of skills) {
|
|
979
|
+
const srcPath = path.join(sourceSkillsDir, skill);
|
|
980
|
+
const destPath = path.join(toolsccDir, 'skills', `${sourceName}-${skill}`);
|
|
981
|
+
|
|
982
|
+
// Remove existing if exists
|
|
983
|
+
await fs.remove(destPath);
|
|
984
|
+
|
|
985
|
+
// Copy directory
|
|
986
|
+
await fs.copy(srcPath, destPath);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Copy commands (in subdirectory by source name)
|
|
991
|
+
const sourceCommandsDir = path.join(sourceDir, 'commands');
|
|
992
|
+
if (await fs.pathExists(sourceCommandsDir)) {
|
|
993
|
+
const destDir = path.join(toolsccDir, 'commands', sourceName);
|
|
994
|
+
await fs.remove(destDir);
|
|
995
|
+
await fs.copy(sourceCommandsDir, destDir);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Copy agents (in subdirectory by source name)
|
|
999
|
+
const sourceAgentsDir = path.join(sourceDir, 'agents');
|
|
1000
|
+
if (await fs.pathExists(sourceAgentsDir)) {
|
|
1001
|
+
const destDir = path.join(toolsccDir, 'agents', sourceName);
|
|
1002
|
+
await fs.remove(destDir);
|
|
1003
|
+
await fs.copy(sourceAgentsDir, destDir);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Update project config
|
|
1007
|
+
const configFile = getProjectConfigPath(projectDir);
|
|
1008
|
+
const config: ProjectConfig = await fs.readJson(configFile);
|
|
1009
|
+
if (!config.sources.includes(sourceName)) {
|
|
1010
|
+
config.sources.push(sourceName);
|
|
1011
|
+
}
|
|
1012
|
+
await fs.writeJson(configFile, config, { spaces: 2 });
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
export async function unuseSource(sourceName: string, projectDir: string): Promise<void> {
|
|
1016
|
+
const toolsccDir = getToolsccDir(projectDir);
|
|
1017
|
+
const configFile = getProjectConfigPath(projectDir);
|
|
1018
|
+
|
|
1019
|
+
// Remove skills with prefix
|
|
1020
|
+
const skillsDir = path.join(toolsccDir, 'skills');
|
|
1021
|
+
if (await fs.pathExists(skillsDir)) {
|
|
1022
|
+
const skills = await fs.readdir(skillsDir);
|
|
1023
|
+
for (const skill of skills) {
|
|
1024
|
+
if (skill.startsWith(`${sourceName}-`)) {
|
|
1025
|
+
await fs.remove(path.join(skillsDir, skill));
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Remove commands subdirectory
|
|
1031
|
+
await fs.remove(path.join(toolsccDir, 'commands', sourceName));
|
|
1032
|
+
|
|
1033
|
+
// Remove agents subdirectory
|
|
1034
|
+
await fs.remove(path.join(toolsccDir, 'agents', sourceName));
|
|
1035
|
+
|
|
1036
|
+
// Update project config
|
|
1037
|
+
const config: ProjectConfig = await fs.readJson(configFile);
|
|
1038
|
+
config.sources = config.sources.filter(s => s !== sourceName);
|
|
1039
|
+
await fs.writeJson(configFile, config, { spaces: 2 });
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
export async function listUsedSources(projectDir: string): Promise<string[]> {
|
|
1043
|
+
const configFile = getProjectConfigPath(projectDir);
|
|
1044
|
+
|
|
1045
|
+
if (!(await fs.pathExists(configFile))) {
|
|
1046
|
+
return [];
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const config: ProjectConfig = await fs.readJson(configFile);
|
|
1050
|
+
return config.sources;
|
|
1051
|
+
}
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
**Step 4: 运行测试确认通过**
|
|
1055
|
+
|
|
1056
|
+
Run: `npm run test:run tests/core/project.test.ts`
|
|
1057
|
+
|
|
1058
|
+
Expected: PASS
|
|
1059
|
+
|
|
1060
|
+
**Step 5: Commit**
|
|
1061
|
+
|
|
1062
|
+
```bash
|
|
1063
|
+
git add src/core/project.ts tests/core/project.test.ts
|
|
1064
|
+
git commit -m "feat: add project management module (init/use/unuse)"
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
---
|
|
1068
|
+
|
|
1069
|
+
## Task 9: 符号链接模块
|
|
1070
|
+
|
|
1071
|
+
**Files:**
|
|
1072
|
+
- Create: `src/core/symlink.ts`
|
|
1073
|
+
- Create: `tests/core/symlink.test.ts`
|
|
1074
|
+
|
|
1075
|
+
**Step 1: 写测试**
|
|
1076
|
+
|
|
1077
|
+
Create: `tests/core/symlink.test.ts`
|
|
1078
|
+
|
|
1079
|
+
```typescript
|
|
1080
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
1081
|
+
import fs from 'fs-extra';
|
|
1082
|
+
import path from 'path';
|
|
1083
|
+
import { createSymlink, removeSymlink, isSymlink } from '../../src/core/symlink';
|
|
1084
|
+
|
|
1085
|
+
describe('Symlink Module', () => {
|
|
1086
|
+
const testDir = path.join(__dirname, '../fixtures/symlink-test');
|
|
1087
|
+
const targetDir = path.join(testDir, 'target');
|
|
1088
|
+
const linkPath = path.join(testDir, 'link');
|
|
1089
|
+
|
|
1090
|
+
beforeEach(async () => {
|
|
1091
|
+
await fs.ensureDir(targetDir);
|
|
1092
|
+
await fs.writeJson(path.join(targetDir, 'test.json'), { test: true });
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
afterEach(async () => {
|
|
1096
|
+
await fs.remove(testDir);
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
it('should create symlink', async () => {
|
|
1100
|
+
await createSymlink(targetDir, linkPath);
|
|
1101
|
+
expect(await isSymlink(linkPath)).toBe(true);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it('should remove symlink', async () => {
|
|
1105
|
+
await createSymlink(targetDir, linkPath);
|
|
1106
|
+
await removeSymlink(linkPath);
|
|
1107
|
+
expect(await fs.pathExists(linkPath)).toBe(false);
|
|
1108
|
+
expect(await fs.pathExists(targetDir)).toBe(true);
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it('should replace existing directory with symlink', async () => {
|
|
1112
|
+
await fs.ensureDir(linkPath);
|
|
1113
|
+
await fs.writeFile(path.join(linkPath, 'old.txt'), 'old');
|
|
1114
|
+
|
|
1115
|
+
await createSymlink(targetDir, linkPath, true);
|
|
1116
|
+
expect(await isSymlink(linkPath)).toBe(true);
|
|
1117
|
+
});
|
|
1118
|
+
});
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
**Step 2: 运行测试确认失败**
|
|
1122
|
+
|
|
1123
|
+
Run: `npm run test:run tests/core/symlink.test.ts`
|
|
1124
|
+
|
|
1125
|
+
Expected: FAIL
|
|
1126
|
+
|
|
1127
|
+
**Step 3: 实现符号链接模块**
|
|
1128
|
+
|
|
1129
|
+
Create: `src/core/symlink.ts`
|
|
1130
|
+
|
|
1131
|
+
```typescript
|
|
1132
|
+
import fs from 'fs-extra';
|
|
1133
|
+
import path from 'path';
|
|
1134
|
+
|
|
1135
|
+
export async function createSymlink(
|
|
1136
|
+
target: string,
|
|
1137
|
+
linkPath: string,
|
|
1138
|
+
force: boolean = false
|
|
1139
|
+
): Promise<void> {
|
|
1140
|
+
// 如果目标已存在
|
|
1141
|
+
if (await fs.pathExists(linkPath)) {
|
|
1142
|
+
if (!force) {
|
|
1143
|
+
throw new Error(`Path already exists: ${linkPath}. Use force=true to overwrite.`);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// 检查是否已经是符号链接
|
|
1147
|
+
if (await isSymlink(linkPath)) {
|
|
1148
|
+
await fs.remove(linkPath);
|
|
1149
|
+
} else {
|
|
1150
|
+
// 是真实目录,删除
|
|
1151
|
+
await fs.remove(linkPath);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// 确保目标存在
|
|
1156
|
+
if (!(await fs.pathExists(target))) {
|
|
1157
|
+
throw new Error(`Target does not exist: ${target}`);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// 创建符号链接
|
|
1161
|
+
// Windows: 使用 junction (不需要管理员权限)
|
|
1162
|
+
// Linux/macOS: 使用 symlink
|
|
1163
|
+
const targetPath = path.resolve(target);
|
|
1164
|
+
|
|
1165
|
+
if (process.platform === 'win32') {
|
|
1166
|
+
// Windows: 使用 junction
|
|
1167
|
+
await fs.ensureSymlink(targetPath, linkPath, 'junction');
|
|
1168
|
+
} else {
|
|
1169
|
+
// Linux/macOS: 使用 dir symlink
|
|
1170
|
+
await fs.ensureSymlink(targetPath, linkPath, 'dir');
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
export async function removeSymlink(linkPath: string): Promise<void> {
|
|
1175
|
+
if (await isSymlink(linkPath)) {
|
|
1176
|
+
await fs.remove(linkPath);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
export async function isSymlink(path: string): Promise<boolean> {
|
|
1181
|
+
try {
|
|
1182
|
+
const stats = await fs.lstat(path);
|
|
1183
|
+
return stats.isSymbolicLink();
|
|
1184
|
+
} catch {
|
|
1185
|
+
return false;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
**Step 4: 运行测试确认通过**
|
|
1191
|
+
|
|
1192
|
+
Run: `npm run test:run tests/core/symlink.test.ts`
|
|
1193
|
+
|
|
1194
|
+
Expected: PASS
|
|
1195
|
+
|
|
1196
|
+
**Step 5: Commit**
|
|
1197
|
+
|
|
1198
|
+
```bash
|
|
1199
|
+
git add src/core/symlink.ts tests/core/symlink.test.ts
|
|
1200
|
+
git commit -m "feat: add symlink management module with Windows junction support"
|
|
1201
|
+
```
|
|
1202
|
+
|
|
1203
|
+
---
|
|
1204
|
+
|
|
1205
|
+
## Task 10: Use 命令实现
|
|
1206
|
+
|
|
1207
|
+
**Files:**
|
|
1208
|
+
- Create: `src/commands/use.ts`
|
|
1209
|
+
- Modify: `src/index.ts`
|
|
1210
|
+
|
|
1211
|
+
**Step 1: 实现 use 命令**
|
|
1212
|
+
|
|
1213
|
+
Create: `src/commands/use.ts`
|
|
1214
|
+
|
|
1215
|
+
```typescript
|
|
1216
|
+
import chalk from 'chalk';
|
|
1217
|
+
import inquirer from 'inquirer';
|
|
1218
|
+
import { useSource, unuseSource, listUsedSources, initProject } from '../core/project';
|
|
1219
|
+
import { getSourcePath, listSources } from '../core/source';
|
|
1220
|
+
import { createSymlink, removeSymlink, isSymlink } from '../core/symlink';
|
|
1221
|
+
import { GLOBAL_CONFIG_DIR, getToolsccDir } from '../utils/path';
|
|
1222
|
+
import fs from 'fs-extra';
|
|
1223
|
+
import path from 'path';
|
|
1224
|
+
|
|
1225
|
+
const SUPPORTED_TOOLS: Record<string, string> = {
|
|
1226
|
+
iflow: '.iflow',
|
|
1227
|
+
claude: '.claude',
|
|
1228
|
+
codebuddy: '.codebuddy',
|
|
1229
|
+
opencode: '.opencode'
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
export async function handleUse(
|
|
1233
|
+
sourceNames: string[],
|
|
1234
|
+
options: { p?: string[] }
|
|
1235
|
+
): Promise<void> {
|
|
1236
|
+
const projectDir = process.cwd();
|
|
1237
|
+
|
|
1238
|
+
// 如果没有指定 source,进入交互模式
|
|
1239
|
+
if (sourceNames.length === 0) {
|
|
1240
|
+
const sources = await listSources(GLOBAL_CONFIG_DIR);
|
|
1241
|
+
const sourceList = Object.keys(sources);
|
|
1242
|
+
|
|
1243
|
+
if (sourceList.length === 0) {
|
|
1244
|
+
console.log(chalk.yellow('No sources configured. Use `tools-cc -s add` to add one.'));
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const answers = await inquirer.prompt([
|
|
1249
|
+
{
|
|
1250
|
+
type: 'checkbox',
|
|
1251
|
+
name: 'selectedSources',
|
|
1252
|
+
message: 'Select sources to use:',
|
|
1253
|
+
choices: sourceList
|
|
1254
|
+
}
|
|
1255
|
+
]);
|
|
1256
|
+
|
|
1257
|
+
sourceNames = answers.selectedSources;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (sourceNames.length === 0) {
|
|
1261
|
+
console.log(chalk.gray('No sources selected.'));
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// 初始化项目
|
|
1266
|
+
await initProject(projectDir);
|
|
1267
|
+
|
|
1268
|
+
// 启用每个配置源
|
|
1269
|
+
for (const sourceName of sourceNames) {
|
|
1270
|
+
try {
|
|
1271
|
+
const sourcePath = await getSourcePath(sourceName, GLOBAL_CONFIG_DIR);
|
|
1272
|
+
await useSource(sourceName, sourcePath, projectDir);
|
|
1273
|
+
console.log(chalk.green(`✓ Using source: ${sourceName}`));
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
console.log(chalk.red(`✗ Failed to use ${sourceName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// 创建符号链接
|
|
1280
|
+
const tools = options.p || Object.keys(SUPPORTED_TOOLS);
|
|
1281
|
+
const toolsccDir = getToolsccDir(projectDir);
|
|
1282
|
+
|
|
1283
|
+
for (const tool of tools) {
|
|
1284
|
+
const linkName = SUPPORTED_TOOLS[tool];
|
|
1285
|
+
if (!linkName) {
|
|
1286
|
+
console.log(chalk.yellow(`Unknown tool: ${tool}`));
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const linkPath = path.join(projectDir, linkName);
|
|
1291
|
+
|
|
1292
|
+
try {
|
|
1293
|
+
await createSymlink(toolsccDir, linkPath, true);
|
|
1294
|
+
console.log(chalk.green(`✓ Linked: ${linkName} -> .toolscc`));
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
console.log(chalk.red(`✗ Failed to link ${linkName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// 更新项目配置
|
|
1301
|
+
const configFile = path.join(projectDir, 'tools-cc.json');
|
|
1302
|
+
const config = await fs.readJson(configFile);
|
|
1303
|
+
config.links = tools;
|
|
1304
|
+
await fs.writeJson(configFile, config, { spaces: 2 });
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
export async function handleList(): Promise<void> {
|
|
1308
|
+
const projectDir = process.cwd();
|
|
1309
|
+
const sources = await listUsedSources(projectDir);
|
|
1310
|
+
|
|
1311
|
+
if (sources.length === 0) {
|
|
1312
|
+
console.log(chalk.gray('No sources in use. Run `tools-cc use <source-name>` to add one.'));
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
console.log(chalk.bold('Sources in use:'));
|
|
1317
|
+
for (const source of sources) {
|
|
1318
|
+
console.log(` ${chalk.cyan(source)}`);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
export async function handleRemove(sourceName: string): Promise<void> {
|
|
1323
|
+
const projectDir = process.cwd();
|
|
1324
|
+
|
|
1325
|
+
try {
|
|
1326
|
+
await unuseSource(sourceName, projectDir);
|
|
1327
|
+
console.log(chalk.green(`✓ Removed source: ${sourceName}`));
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
console.log(chalk.red(`✗ ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
export async function handleStatus(): Promise<void> {
|
|
1334
|
+
const projectDir = process.cwd();
|
|
1335
|
+
const sources = await listUsedSources(projectDir);
|
|
1336
|
+
|
|
1337
|
+
console.log(chalk.bold('\nProject Status:'));
|
|
1338
|
+
console.log(chalk.gray(` Directory: ${projectDir}`));
|
|
1339
|
+
|
|
1340
|
+
// 检查 .toolscc
|
|
1341
|
+
const toolsccDir = getToolsccDir(projectDir);
|
|
1342
|
+
console.log(` .toolscc: ${await fs.pathExists(toolsccDir) ? chalk.green('exists') : chalk.red('not found')}`);
|
|
1343
|
+
|
|
1344
|
+
// 检查 sources
|
|
1345
|
+
console.log(` Sources: ${sources.length > 0 ? sources.map(s => chalk.cyan(s)).join(', ') : chalk.gray('none')}`);
|
|
1346
|
+
|
|
1347
|
+
// 检查 links
|
|
1348
|
+
const configFile = path.join(projectDir, 'tools-cc.json');
|
|
1349
|
+
if (await fs.pathExists(configFile)) {
|
|
1350
|
+
const config = await fs.readJson(configFile);
|
|
1351
|
+
console.log(` Links:`);
|
|
1352
|
+
for (const tool of config.links || []) {
|
|
1353
|
+
const linkName = SUPPORTED_TOOLS[tool];
|
|
1354
|
+
const linkPath = path.join(projectDir, linkName);
|
|
1355
|
+
const isLink = await isSymlink(linkPath);
|
|
1356
|
+
console.log(` ${tool}: ${isLink ? chalk.green('linked') : chalk.red('not linked')}`);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
console.log();
|
|
1360
|
+
}
|
|
1361
|
+
```
|
|
1362
|
+
|
|
1363
|
+
**Step 2: 重构主程序**
|
|
1364
|
+
|
|
1365
|
+
Modify `src/index.ts`:
|
|
1366
|
+
|
|
1367
|
+
```typescript
|
|
1368
|
+
#!/usr/bin/env node
|
|
1369
|
+
|
|
1370
|
+
import { Command } from 'commander';
|
|
1371
|
+
import { handleConfigSet, handleConfigGet } from './commands/config';
|
|
1372
|
+
import { handleSourceAdd, handleSourceList, handleSourceRemove, handleSourceUpdate } from './commands/source';
|
|
1373
|
+
import { handleUse, handleList, handleRemove, handleStatus } from './commands/use';
|
|
1374
|
+
import { GLOBAL_CONFIG_DIR } from './utils/path';
|
|
1375
|
+
|
|
1376
|
+
const program = new Command();
|
|
1377
|
+
|
|
1378
|
+
program
|
|
1379
|
+
.name('tools-cc')
|
|
1380
|
+
.description('CLI tool for managing skills/commands/agents across multiple AI coding tools')
|
|
1381
|
+
.version('0.0.1');
|
|
1382
|
+
|
|
1383
|
+
// Source management
|
|
1384
|
+
program
|
|
1385
|
+
.option('-s, --source <command> [args...]', 'Source management')
|
|
1386
|
+
.option('-c, --config <command> [args...]', 'Config management');
|
|
1387
|
+
|
|
1388
|
+
// Project commands
|
|
1389
|
+
program
|
|
1390
|
+
.command('use [sources...]')
|
|
1391
|
+
.description('Use sources in current project')
|
|
1392
|
+
.option('-p, --projects <tools...>', 'Tools to link (iflow, claude, codebuddy, opencode)')
|
|
1393
|
+
.action(async (sources: string[], options) => {
|
|
1394
|
+
await handleUse(sources, options);
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
program
|
|
1398
|
+
.command('list')
|
|
1399
|
+
.description('List sources in use')
|
|
1400
|
+
.action(async () => {
|
|
1401
|
+
await handleList();
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
program
|
|
1405
|
+
.command('rm <source>')
|
|
1406
|
+
.description('Remove a source from project')
|
|
1407
|
+
.action(async (source: string) => {
|
|
1408
|
+
await handleRemove(source);
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
program
|
|
1412
|
+
.command('status')
|
|
1413
|
+
.description('Show project status')
|
|
1414
|
+
.action(async () => {
|
|
1415
|
+
await handleStatus();
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
program.parseAsync().then(async () => {
|
|
1419
|
+
const options = program.opts();
|
|
1420
|
+
|
|
1421
|
+
// Handle -s/--source
|
|
1422
|
+
if (options.source) {
|
|
1423
|
+
const [cmd, ...args] = options.source;
|
|
1424
|
+
switch (cmd) {
|
|
1425
|
+
case 'add':
|
|
1426
|
+
if (args.length < 2) {
|
|
1427
|
+
console.log('Usage: tools-cc -s add <name> <path-or-url>');
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
await handleSourceAdd(args[0], args[1]);
|
|
1431
|
+
break;
|
|
1432
|
+
case 'list':
|
|
1433
|
+
case 'ls':
|
|
1434
|
+
await handleSourceList();
|
|
1435
|
+
break;
|
|
1436
|
+
case 'remove':
|
|
1437
|
+
case 'rm':
|
|
1438
|
+
if (args.length < 1) {
|
|
1439
|
+
console.log('Usage: tools-cc -s remove <name>');
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
await handleSourceRemove(args[0]);
|
|
1443
|
+
break;
|
|
1444
|
+
case 'update':
|
|
1445
|
+
case 'up':
|
|
1446
|
+
await handleSourceUpdate(args[0]);
|
|
1447
|
+
break;
|
|
1448
|
+
default:
|
|
1449
|
+
console.log(`Unknown source command: ${cmd}`);
|
|
1450
|
+
}
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// Handle -c/--config
|
|
1455
|
+
if (options.config) {
|
|
1456
|
+
const [cmd, ...args] = options.config;
|
|
1457
|
+
switch (cmd) {
|
|
1458
|
+
case 'set':
|
|
1459
|
+
if (args.length < 2) {
|
|
1460
|
+
console.log('Usage: tools-cc -c set <key> <value>');
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
await handleConfigSet(args[0], args[1]);
|
|
1464
|
+
break;
|
|
1465
|
+
case 'get':
|
|
1466
|
+
if (args.length < 1) {
|
|
1467
|
+
console.log('Usage: tools-cc -c get <key>');
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
await handleConfigGet(args[0]);
|
|
1471
|
+
break;
|
|
1472
|
+
default:
|
|
1473
|
+
console.log(`Unknown config command: ${cmd}`);
|
|
1474
|
+
}
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
```
|
|
1479
|
+
|
|
1480
|
+
**Step 3: 测试完整工作流**
|
|
1481
|
+
|
|
1482
|
+
Run: `npm run build`
|
|
1483
|
+
|
|
1484
|
+
Run: `npm link`
|
|
1485
|
+
|
|
1486
|
+
Run: `tools-cc status`
|
|
1487
|
+
|
|
1488
|
+
Expected: 显示项目状态
|
|
1489
|
+
|
|
1490
|
+
**Step 4: Commit**
|
|
1491
|
+
|
|
1492
|
+
```bash
|
|
1493
|
+
git add src/commands/use.ts src/index.ts
|
|
1494
|
+
git commit -m "feat: add use/list/rm/status commands for project management"
|
|
1495
|
+
```
|
|
1496
|
+
|
|
1497
|
+
---
|
|
1498
|
+
|
|
1499
|
+
## Task 11: 添加 .gitignore
|
|
1500
|
+
|
|
1501
|
+
**Files:**
|
|
1502
|
+
- Create: `.gitignore`
|
|
1503
|
+
|
|
1504
|
+
**Step 1: 创建 .gitignore**
|
|
1505
|
+
|
|
1506
|
+
```gitignore
|
|
1507
|
+
# Dependencies
|
|
1508
|
+
node_modules/
|
|
1509
|
+
|
|
1510
|
+
# Build
|
|
1511
|
+
dist/
|
|
1512
|
+
|
|
1513
|
+
# Test
|
|
1514
|
+
coverage/
|
|
1515
|
+
tests/fixtures/
|
|
1516
|
+
|
|
1517
|
+
# IDE
|
|
1518
|
+
.idea/
|
|
1519
|
+
.vscode/
|
|
1520
|
+
*.swp
|
|
1521
|
+
*.swo
|
|
1522
|
+
|
|
1523
|
+
# OS
|
|
1524
|
+
.DS_Store
|
|
1525
|
+
Thumbs.db
|
|
1526
|
+
|
|
1527
|
+
# Local config
|
|
1528
|
+
.tools-cc/
|
|
1529
|
+
*.local.json
|
|
1530
|
+
```
|
|
1531
|
+
|
|
1532
|
+
**Step 2: Commit**
|
|
1533
|
+
|
|
1534
|
+
```bash
|
|
1535
|
+
git add .gitignore
|
|
1536
|
+
git commit -m "chore: add .gitignore"
|
|
1537
|
+
```
|
|
1538
|
+
|
|
1539
|
+
---
|
|
1540
|
+
|
|
1541
|
+
## Task 12: 最终构建和测试
|
|
1542
|
+
|
|
1543
|
+
**Step 1: 运行所有测试**
|
|
1544
|
+
|
|
1545
|
+
Run: `npm run test:run`
|
|
1546
|
+
|
|
1547
|
+
Expected: All tests pass
|
|
1548
|
+
|
|
1549
|
+
**Step 2: 构建**
|
|
1550
|
+
|
|
1551
|
+
Run: `npm run build`
|
|
1552
|
+
|
|
1553
|
+
Expected: Build succeeds
|
|
1554
|
+
|
|
1555
|
+
**Step 3: 本地测试完整工作流**
|
|
1556
|
+
|
|
1557
|
+
```bash
|
|
1558
|
+
# 1. 设置配置
|
|
1559
|
+
tools-cc -c set sourcesDir D:/test-sources
|
|
1560
|
+
|
|
1561
|
+
# 2. 添加一个本地配置源
|
|
1562
|
+
tools-cc -s add test-source D:/path/to/existing/skills
|
|
1563
|
+
|
|
1564
|
+
# 3. 查看配置源
|
|
1565
|
+
tools-cc -s list
|
|
1566
|
+
|
|
1567
|
+
# 4. 在项目中使用
|
|
1568
|
+
cd /path/to/test/project
|
|
1569
|
+
tools-cc use test-source -p iflow claude
|
|
1570
|
+
|
|
1571
|
+
# 5. 查看状态
|
|
1572
|
+
tools-cc status
|
|
1573
|
+
|
|
1574
|
+
# 6. 查看已使用的配置源
|
|
1575
|
+
tools-cc list
|
|
1576
|
+
|
|
1577
|
+
# 7. 移除配置源
|
|
1578
|
+
tools-cc rm test-source
|
|
1579
|
+
```
|
|
1580
|
+
|
|
1581
|
+
**Step 4: 最终 Commit**
|
|
1582
|
+
|
|
1583
|
+
```bash
|
|
1584
|
+
git add .
|
|
1585
|
+
git commit -m "chore: final build and test"
|
|
1586
|
+
```
|
|
1587
|
+
|
|
1588
|
+
---
|
|
1589
|
+
|
|
1590
|
+
## 执行选项
|
|
1591
|
+
|
|
1592
|
+
计划已保存到 `docs/plans/2026-02-25-tools-cc-impl.md`
|
|
1593
|
+
|
|
1594
|
+
**两种执行方式:**
|
|
1595
|
+
|
|
1596
|
+
**1. Subagent-Driven (当前会话)** - 在此会话中逐任务派发子代理执行,任务间可审查,快速迭代
|
|
1597
|
+
|
|
1598
|
+
**2. Parallel Session (单独会话)** - 打开新会话使用 executing-plans,批量执行带检查点
|
|
1599
|
+
|
|
1600
|
+
**选择哪种方式?**
|