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.
Files changed (49) hide show
  1. package/dist/commands/config.d.ts +3 -0
  2. package/dist/commands/config.js +56 -0
  3. package/dist/commands/help.d.ts +1 -0
  4. package/dist/commands/help.js +84 -0
  5. package/dist/commands/source.d.ts +4 -0
  6. package/dist/commands/source.js +72 -0
  7. package/dist/commands/use.d.ts +6 -0
  8. package/dist/commands/use.js +133 -0
  9. package/dist/core/config.d.ts +5 -0
  10. package/dist/core/config.js +37 -0
  11. package/dist/core/manifest.d.ts +3 -0
  12. package/dist/core/manifest.js +56 -0
  13. package/dist/core/project.d.ts +4 -0
  14. package/dist/core/project.js +118 -0
  15. package/dist/core/source.d.ts +6 -0
  16. package/dist/core/source.js +86 -0
  17. package/dist/core/symlink.d.ts +3 -0
  18. package/dist/core/symlink.js +56 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +165 -0
  21. package/dist/types/config.d.ts +23 -0
  22. package/dist/types/config.js +2 -0
  23. package/dist/types/index.d.ts +1 -0
  24. package/dist/types/index.js +17 -0
  25. package/dist/utils/path.d.ts +8 -0
  26. package/dist/utils/path.js +22 -0
  27. package/docs/plans/2026-02-25-tools-cc-design.md +195 -0
  28. package/docs/plans/2026-02-25-tools-cc-impl.md +1600 -0
  29. package/package.json +44 -0
  30. package/readme.md +182 -0
  31. package/src/commands/config.ts +50 -0
  32. package/src/commands/help.ts +79 -0
  33. package/src/commands/source.ts +63 -0
  34. package/src/commands/use.ts +147 -0
  35. package/src/core/config.ts +37 -0
  36. package/src/core/manifest.ts +57 -0
  37. package/src/core/project.ts +136 -0
  38. package/src/core/source.ts +100 -0
  39. package/src/core/symlink.ts +56 -0
  40. package/src/index.ts +186 -0
  41. package/src/types/config.ts +27 -0
  42. package/src/types/index.ts +1 -0
  43. package/src/utils/path.ts +18 -0
  44. package/tests/core/config.test.ts +37 -0
  45. package/tests/core/manifest.test.ts +37 -0
  46. package/tests/core/project.test.ts +50 -0
  47. package/tests/core/source.test.ts +75 -0
  48. package/tests/core/symlink.test.ts +39 -0
  49. 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
+ **选择哪种方式?**