ethan-skill 1.2.1 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +300 -137
- package/dist/cli/config.d.ts +2 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/index.d.ts +5 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +1786 -56
- package/dist/cli/index.js.map +1 -1
- package/dist/git/utils.d.ts +34 -0
- package/dist/git/utils.d.ts.map +1 -0
- package/dist/git/utils.js +94 -0
- package/dist/git/utils.js.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +238 -6
- package/dist/mcp/server.js.map +1 -1
- package/dist/templates/copilot-md.template.js +82 -33
- package/dist/templates/copilot-md.template.js.map +1 -1
- package/dist/templates/cursor-mdc.template.d.ts.map +1 -1
- package/dist/templates/cursor-mdc.template.js +18 -8
- package/dist/templates/cursor-mdc.template.js.map +1 -1
- package/dist/workflow/state.d.ts +13 -9
- package/dist/workflow/state.d.ts.map +1 -1
- package/dist/workflow/state.js +42 -16
- package/dist/workflow/state.js.map +1 -1
- package/package.json +1 -1
- package/rules/claude-code/CLAUDE.md +2 -2
- package/rules/cline/.clinerules +9 -2
- package/rules/codebuddy/CODEBUDDY.md +2 -2
- package/rules/continue/.continuerules +9 -2
- package/rules/copilot/copilot-instructions.md +581 -57
- package/rules/cursor/.cursorrules +10 -3
- package/rules/cursor/smart-flow.mdc +12 -9
- package/rules/jetbrains/smart-flow.md +580 -57
- package/rules/lingma/smart-flow.md +2 -2
- package/rules/windsurf/.windsurf/rules/smart-flow.md +586 -57
- package/rules/zed/smart-flow.rules +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
/**
|
|
4
4
|
* npx ethan CLI
|
|
5
|
-
* 命令:install | list | mcp | validate | pipeline | doctor | stats | init | run | workflow
|
|
5
|
+
* 命令:install | list | mcp | validate | pipeline | doctor | stats (show/leaderboard/reset) | init | run | workflow
|
|
6
|
+
* commit | review | pr | standup | changelog
|
|
7
|
+
* scan | explain | test-case | naming | readme | roast
|
|
8
|
+
* oncall | schedule (add/list/remove) | hooks (install/list/remove)
|
|
9
|
+
* memory (add/search/show/list/export/remove) | estimate | retro | pipeline-init
|
|
6
10
|
*/
|
|
7
11
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
12
|
if (k2 === undefined) k2 = k;
|
|
@@ -46,6 +50,7 @@ const child_process_1 = require("child_process");
|
|
|
46
50
|
const index_1 = require("../skills/index");
|
|
47
51
|
const update_check_1 = require("./update-check");
|
|
48
52
|
const config_1 = require("./config");
|
|
53
|
+
const utils_1 = require("../git/utils");
|
|
49
54
|
// ─── 剪贴板工具函数(不经过 shell,避免 backtick 命令注入) ─────────────────
|
|
50
55
|
function copyToClipboard(text) {
|
|
51
56
|
try {
|
|
@@ -85,7 +90,8 @@ const STATS_FILE = path.join(os.homedir(), '.ethan-stats.json');
|
|
|
85
90
|
function readStats() {
|
|
86
91
|
try {
|
|
87
92
|
if (fs.existsSync(STATS_FILE)) {
|
|
88
|
-
|
|
93
|
+
const raw = JSON.parse(fs.readFileSync(STATS_FILE, 'utf-8'));
|
|
94
|
+
return raw.usage || raw;
|
|
89
95
|
}
|
|
90
96
|
}
|
|
91
97
|
catch {
|
|
@@ -95,7 +101,20 @@ function readStats() {
|
|
|
95
101
|
}
|
|
96
102
|
function writeStats(stats) {
|
|
97
103
|
try {
|
|
98
|
-
|
|
104
|
+
// 合并到 v2 格式
|
|
105
|
+
const existing = (() => {
|
|
106
|
+
try {
|
|
107
|
+
if (fs.existsSync(STATS_FILE)) {
|
|
108
|
+
const raw = JSON.parse(fs.readFileSync(STATS_FILE, 'utf-8'));
|
|
109
|
+
if (raw.usage)
|
|
110
|
+
return raw;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch { /* ignore */ }
|
|
114
|
+
return { usage: {}, streak: { current: 0, best: 0, lastDate: '' }, dailyLog: {} };
|
|
115
|
+
})();
|
|
116
|
+
existing.usage = stats;
|
|
117
|
+
fs.writeFileSync(STATS_FILE, JSON.stringify(existing, null, 2), 'utf-8');
|
|
99
118
|
}
|
|
100
119
|
catch {
|
|
101
120
|
// ignore write errors
|
|
@@ -364,6 +383,130 @@ pluginCmd
|
|
|
364
383
|
(0, config_1.writeConfig)({ ...config, plugins }, process.cwd());
|
|
365
384
|
console.log(`\n✅ 插件 ${name} 已卸载\n`);
|
|
366
385
|
});
|
|
386
|
+
pluginCmd
|
|
387
|
+
.command('publish')
|
|
388
|
+
.description('将当前目录的自定义 Skill 打包并发布到 npm(Prompt OS 插件体系)')
|
|
389
|
+
.option('--dry-run', '只预览,不实际发布')
|
|
390
|
+
.action(async (options) => {
|
|
391
|
+
const cwd = process.cwd();
|
|
392
|
+
const skillsDir = path.join(cwd, '.ethan', 'skills');
|
|
393
|
+
// 检查是否有自定义 Skill
|
|
394
|
+
const { loadCustomSkills } = await Promise.resolve().then(() => __importStar(require('../loader/custom-skill-loader')));
|
|
395
|
+
const customSkills = loadCustomSkills(cwd);
|
|
396
|
+
if (customSkills.length === 0) {
|
|
397
|
+
console.error('\n❌ 当前项目没有自定义 Skill(.ethan/skills/ 目录为空或不存在)');
|
|
398
|
+
console.log(' 使用 ethan skill new <name> 创建新 Skill\n');
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
// 检查 package.json
|
|
402
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
403
|
+
if (!fs.existsSync(pkgPath)) {
|
|
404
|
+
console.error('\n❌ 当前目录没有 package.json,无法发布到 npm\n');
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
408
|
+
if (!pkgJson.name?.startsWith('ethan-') && !pkgJson.name?.startsWith('@')) {
|
|
409
|
+
console.log('\n⚠️ 建议将包名设为 ethan-<your-skill-name> 格式,便于社区发现');
|
|
410
|
+
}
|
|
411
|
+
console.log(`\n📦 准备发布 Ethan 插件包\n`);
|
|
412
|
+
console.log(` 包名:${pkgJson.name || '(未设置)'}`);
|
|
413
|
+
console.log(` 版本:${pkgJson.version || '(未设置)'}`);
|
|
414
|
+
console.log(` 自定义 Skill 数量:${customSkills.length}`);
|
|
415
|
+
console.log(`\n 包含的 Skill:`);
|
|
416
|
+
for (const s of customSkills) {
|
|
417
|
+
console.log(` - ${s.id}(${s.name})`);
|
|
418
|
+
}
|
|
419
|
+
if (options.dryRun) {
|
|
420
|
+
console.log('\n🔍 Dry run 模式,不实际发布。\n');
|
|
421
|
+
console.log('发布清单 checklist:');
|
|
422
|
+
console.log(' ✅ 确认 package.json 中有 "ethan" 关键词(便于被发现)');
|
|
423
|
+
console.log(' ✅ 确认 .ethan/skills/ 目录存在且包含有效 Skill 定义');
|
|
424
|
+
console.log(' ✅ 确认 README.md 描述了插件用途和安装方法');
|
|
425
|
+
console.log(' ✅ 确认 main 字段指向正确的入口文件');
|
|
426
|
+
console.log('\n 运行 ethan plugin publish 实际发布\n');
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
// 生成安装说明
|
|
430
|
+
const installGuide = `\n💡 发布成功后,其他用户可通过以下命令安装:\n ethan plugin install ${pkgJson.name}\n`;
|
|
431
|
+
try {
|
|
432
|
+
const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
433
|
+
execSync('npm publish', { stdio: 'inherit', cwd });
|
|
434
|
+
console.log(installGuide);
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
console.error('\n❌ 发布失败,请检查 npm 登录状态(npm login)\n');
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
pluginCmd
|
|
442
|
+
.command('registry')
|
|
443
|
+
.description('管理私有 Skill 插件注册表')
|
|
444
|
+
.option('--set <url>', '设置私有注册表 URL')
|
|
445
|
+
.option('--unset', '移除私有注册表配置')
|
|
446
|
+
.option('--show', '显示当前注册表配置')
|
|
447
|
+
.action((options) => {
|
|
448
|
+
const config = (0, config_1.readConfig)(process.cwd());
|
|
449
|
+
if (options.show || (!options.set && !options.unset)) {
|
|
450
|
+
const registry = config.registry;
|
|
451
|
+
console.log('\n📋 插件注册表配置');
|
|
452
|
+
console.log('─'.repeat(40));
|
|
453
|
+
console.log(` 公共注册表:https://registry.npmjs.org(默认)`);
|
|
454
|
+
console.log(` 私有注册表:${registry || '(未配置)'}`);
|
|
455
|
+
console.log('\n配置方式:');
|
|
456
|
+
console.log(' ethan plugin registry --set https://your-registry.com');
|
|
457
|
+
console.log(' ethan plugin registry --unset\n');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (options.set) {
|
|
461
|
+
(0, config_1.writeConfig)({ ...config, registry: options.set }, process.cwd());
|
|
462
|
+
console.log(`\n✅ 私有注册表已设置为:${options.set}`);
|
|
463
|
+
console.log(' 后续 ethan plugin install 将优先从此注册表安装\n');
|
|
464
|
+
}
|
|
465
|
+
if (options.unset) {
|
|
466
|
+
const { registry: _, ...rest } = config;
|
|
467
|
+
(0, config_1.writeConfig)(rest, process.cwd());
|
|
468
|
+
console.log('\n✅ 私有注册表配置已移除\n');
|
|
469
|
+
void _;
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
pluginCmd
|
|
473
|
+
.command('search <keyword>')
|
|
474
|
+
.description('在 npm 上搜索 ethan- 前缀的插件包')
|
|
475
|
+
.option('-n, --limit <n>', '显示条数', '10')
|
|
476
|
+
.action(async (keyword, options) => {
|
|
477
|
+
console.log(`\n🔍 搜索 npm 插件:${keyword}...\n`);
|
|
478
|
+
try {
|
|
479
|
+
const https = await Promise.resolve().then(() => __importStar(require('https')));
|
|
480
|
+
const query = encodeURIComponent(`ethan-${keyword} keywords:ethan`);
|
|
481
|
+
const limit = parseInt(options.limit, 10) || 10;
|
|
482
|
+
const data = await new Promise((resolve, reject) => {
|
|
483
|
+
const req = https.get(`https://registry.npmjs.org/-/v1/search?text=${query}&size=${limit}`, (res) => {
|
|
484
|
+
let body = '';
|
|
485
|
+
res.on('data', (chunk) => body += chunk.toString());
|
|
486
|
+
res.on('end', () => resolve(body));
|
|
487
|
+
});
|
|
488
|
+
req.on('error', reject);
|
|
489
|
+
req.setTimeout(5000, () => { req.destroy(); reject(new Error('timeout')); });
|
|
490
|
+
});
|
|
491
|
+
const result = JSON.parse(data);
|
|
492
|
+
const packages = result.objects || [];
|
|
493
|
+
if (packages.length === 0) {
|
|
494
|
+
console.log(` 未找到匹配的 ethan 插件\n`);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
console.log(`找到 ${packages.length} 个插件:\n`);
|
|
498
|
+
console.log('─'.repeat(60));
|
|
499
|
+
for (const { package: pkg } of packages) {
|
|
500
|
+
console.log(`\n 📦 ${pkg.name} v${pkg.version}`);
|
|
501
|
+
console.log(` ${pkg.description || '无描述'}`);
|
|
502
|
+
console.log(` 安装:ethan plugin install ${pkg.name}`);
|
|
503
|
+
}
|
|
504
|
+
console.log('\n' + '─'.repeat(60) + '\n');
|
|
505
|
+
}
|
|
506
|
+
catch (e) {
|
|
507
|
+
console.error(`\n❌ 搜索失败(需要网络连接):${e.message}\n`);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
367
510
|
// ─── mcp 命令 ───────────────────────────────────────────────────────────────
|
|
368
511
|
program
|
|
369
512
|
.command('mcp')
|
|
@@ -578,39 +721,6 @@ program
|
|
|
578
721
|
console.log('\n✅ 所有检查完成\n');
|
|
579
722
|
}
|
|
580
723
|
});
|
|
581
|
-
// ─── stats 命令 ─────────────────────────────────────────────────────────────
|
|
582
|
-
program
|
|
583
|
-
.command('stats')
|
|
584
|
-
.description('查看 Skill 使用频次统计')
|
|
585
|
-
.option('--reset', '清空统计数据')
|
|
586
|
-
.action((options) => {
|
|
587
|
-
if (options.reset) {
|
|
588
|
-
writeStats({});
|
|
589
|
-
console.log('✅ 统计数据已清空');
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
const stats = readStats();
|
|
593
|
-
const entries = Object.entries(stats).sort(([, a], [, b]) => b - a);
|
|
594
|
-
if (entries.length === 0) {
|
|
595
|
-
console.log('\n📊 暂无使用记录(运行 pipeline run 命令后将记录使用频次)\n');
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
const maxCount = Math.max(...entries.map(([, v]) => v));
|
|
599
|
-
const BAR_WIDTH = 30;
|
|
600
|
-
console.log('\n📊 Ethan Skill 使用频次\n');
|
|
601
|
-
console.log('─'.repeat(60));
|
|
602
|
-
for (const [skillId, count] of entries) {
|
|
603
|
-
const skill = index_1.ALL_SKILLS.find((s) => s.id === skillId);
|
|
604
|
-
const name = skill ? skill.name : skillId;
|
|
605
|
-
const barLen = Math.round((count / maxCount) * BAR_WIDTH);
|
|
606
|
-
const bar = '█'.repeat(barLen);
|
|
607
|
-
const label = name.padEnd(12);
|
|
608
|
-
console.log(` ${label} ${bar} ${count}`);
|
|
609
|
-
}
|
|
610
|
-
const total = entries.reduce((sum, [, v]) => sum + v, 0);
|
|
611
|
-
console.log('─'.repeat(60));
|
|
612
|
-
console.log(` Total executions: ${total}\n`);
|
|
613
|
-
});
|
|
614
724
|
// ─── serve 命令(Web UI Dashboard) ─────────────────────────────────────────
|
|
615
725
|
program
|
|
616
726
|
.command('serve')
|
|
@@ -769,24 +879,50 @@ workflowCmd
|
|
|
769
879
|
.command('start [pipelineId]')
|
|
770
880
|
.description('启动工作流会话(默认 dev-workflow),输出第一步提示词')
|
|
771
881
|
.option('-c, --context <context>', '初始任务上下文', '')
|
|
882
|
+
.option('-n, --name <name>', '具名会话(存至 .ethan/sessions/<name>.json,可并行多个工作流)')
|
|
772
883
|
.action(async (pipelineId, options) => {
|
|
773
884
|
const { loadSession, createSession, buildStepPrompt, calcProgress, } = await Promise.resolve().then(() => __importStar(require('../workflow/state')));
|
|
774
885
|
const { resolvePipeline, PIPELINES } = await Promise.resolve().then(() => __importStar(require('../skills/pipeline')));
|
|
775
|
-
// 检查是否已有进行中的 session
|
|
776
|
-
const existing = loadSession(process.cwd());
|
|
886
|
+
// 检查是否已有进行中的 session(具名会话不覆盖默认)
|
|
887
|
+
const existing = loadSession(process.cwd(), options.name);
|
|
777
888
|
if (existing && !existing.completed) {
|
|
778
889
|
console.log('\n⚠️ 已有进行中的工作流:');
|
|
779
890
|
console.log(` Pipeline: ${existing.pipelineName}`);
|
|
891
|
+
if (existing.name)
|
|
892
|
+
console.log(` 会话名: ${existing.name}`);
|
|
780
893
|
console.log(` 进度: ${calcProgress(existing)}%`);
|
|
781
894
|
console.log('\n💡 使用 ethan workflow status 查看进度');
|
|
782
895
|
console.log(' 使用 ethan workflow reset 重置后再启动新工作流\n');
|
|
783
896
|
return;
|
|
784
897
|
}
|
|
785
898
|
const id = pipelineId ?? 'dev-workflow';
|
|
786
|
-
|
|
899
|
+
let resolved = resolvePipeline(id);
|
|
900
|
+
// 尝试从自定义 YAML pipeline 加载
|
|
901
|
+
if (!resolved) {
|
|
902
|
+
const customPipelines = loadCustomPipelines(process.cwd());
|
|
903
|
+
const custom = customPipelines.find((p) => p.id === id);
|
|
904
|
+
if (custom) {
|
|
905
|
+
const customSkills = custom.skillIds
|
|
906
|
+
.map((sid) => index_1.ALL_SKILLS.find((s) => s.id === sid))
|
|
907
|
+
.filter((s) => s !== null);
|
|
908
|
+
if (customSkills.length > 0) {
|
|
909
|
+
resolved = {
|
|
910
|
+
pipeline: {
|
|
911
|
+
id: custom.id,
|
|
912
|
+
name: custom.name,
|
|
913
|
+
description: custom.description,
|
|
914
|
+
skillIds: custom.skillIds,
|
|
915
|
+
},
|
|
916
|
+
skills: customSkills,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
787
921
|
if (!resolved) {
|
|
922
|
+
const customPipelines = loadCustomPipelines(process.cwd());
|
|
923
|
+
const allIds = [...PIPELINES.map((p) => p.id), ...customPipelines.map((p) => p.id)];
|
|
788
924
|
console.error(`Unknown pipeline: ${id}`);
|
|
789
|
-
console.error(`Available: ${
|
|
925
|
+
console.error(`Available: ${allIds.join(' | ')}`);
|
|
790
926
|
process.exit(1);
|
|
791
927
|
}
|
|
792
928
|
const { pipeline, skills } = resolved;
|
|
@@ -805,11 +941,13 @@ workflowCmd
|
|
|
805
941
|
process.exit(1);
|
|
806
942
|
}
|
|
807
943
|
}
|
|
808
|
-
const session = createSession(pipeline, context, process.cwd());
|
|
944
|
+
const session = createSession(pipeline, context, process.cwd(), options.name);
|
|
809
945
|
const firstStep = session.steps[0];
|
|
810
946
|
const firstSkill = skills[0];
|
|
811
947
|
console.log(`\n🚀 工作流已启动:${pipeline.name}`);
|
|
812
948
|
console.log(` ID: ${session.id}`);
|
|
949
|
+
if (session.name)
|
|
950
|
+
console.log(` 会话名: ${session.name}`);
|
|
813
951
|
console.log(` 共 ${session.steps.length} 步\n`);
|
|
814
952
|
console.log('─'.repeat(60));
|
|
815
953
|
const prompt = buildStepPrompt(session, firstStep, firstSkill);
|
|
@@ -819,7 +957,7 @@ workflowCmd
|
|
|
819
957
|
if (copyToClipboard(prompt)) {
|
|
820
958
|
console.log('\n✅ 提示词已复制到剪贴板!粘贴到你的 AI 编辑器中执行。');
|
|
821
959
|
}
|
|
822
|
-
console.log(`\n💡 完成本步后,运行:ethan workflow done "你的本步摘要"\n`);
|
|
960
|
+
console.log(`\n💡 完成本步后,运行:ethan workflow done "你的本步摘要"${session.name ? ` --name ${session.name}` : ''}\n`);
|
|
823
961
|
// 记录使用统计
|
|
824
962
|
const stats = readStats();
|
|
825
963
|
stats[firstSkill.id] = (stats[firstSkill.id] || 0) + 1;
|
|
@@ -828,10 +966,11 @@ workflowCmd
|
|
|
828
966
|
workflowCmd
|
|
829
967
|
.command('done [summary]')
|
|
830
968
|
.description('完成当前步骤,传入本步摘要,自动推进到下一步')
|
|
831
|
-
.
|
|
969
|
+
.option('-n, --name <name>', '具名会话名称')
|
|
970
|
+
.action(async (summary, options) => {
|
|
832
971
|
const { loadSession, markStepDone, buildStepPrompt, getCurrentStep, getCurrentStepIndex, calcProgress, } = await Promise.resolve().then(() => __importStar(require('../workflow/state')));
|
|
833
972
|
const { resolvePipeline } = await Promise.resolve().then(() => __importStar(require('../skills/pipeline')));
|
|
834
|
-
const session = loadSession(process.cwd());
|
|
973
|
+
const session = loadSession(process.cwd(), options.name);
|
|
835
974
|
if (!session) {
|
|
836
975
|
console.error('\n❌ 未找到进行中的工作流。运行 ethan workflow start 启动。\n');
|
|
837
976
|
process.exit(1);
|
|
@@ -862,6 +1001,8 @@ workflowCmd
|
|
|
862
1001
|
}
|
|
863
1002
|
const nextStep = markStepDone(session, stepSummary, process.cwd());
|
|
864
1003
|
const progress = calcProgress(session);
|
|
1004
|
+
// 自动归档到 Skill Memory(T16)
|
|
1005
|
+
archiveWorkflowToMemory(session.id, currentStep.skillId, session.pipelineName, stepSummary, process.cwd());
|
|
865
1006
|
if (!nextStep) {
|
|
866
1007
|
console.log('\n🎉 恭喜!工作流全部完成!');
|
|
867
1008
|
console.log(` Pipeline: ${session.pipelineName}`);
|
|
@@ -890,7 +1031,7 @@ workflowCmd
|
|
|
890
1031
|
if (copyToClipboard(prompt)) {
|
|
891
1032
|
console.log('\n✅ 下一步提示词已复制到剪贴板!');
|
|
892
1033
|
}
|
|
893
|
-
console.log(`\n💡 完成本步后,运行:ethan workflow done "你的本步摘要"\n`);
|
|
1034
|
+
console.log(`\n💡 完成本步后,运行:ethan workflow done "你的本步摘要"${session.name ? ` --name ${session.name}` : ''}\n`);
|
|
894
1035
|
// 记录使用统计
|
|
895
1036
|
const stats = readStats();
|
|
896
1037
|
stats[nextSkill.id] = (stats[nextSkill.id] || 0) + 1;
|
|
@@ -899,12 +1040,13 @@ workflowCmd
|
|
|
899
1040
|
workflowCmd
|
|
900
1041
|
.command('status')
|
|
901
1042
|
.description('查看当前工作流进度看板')
|
|
902
|
-
.
|
|
1043
|
+
.option('-n, --name <name>', '具名会话名称')
|
|
1044
|
+
.action(async (options) => {
|
|
903
1045
|
const { loadSession, getCurrentStepIndex, calcProgress, } = await Promise.resolve().then(() => __importStar(require('../workflow/state')));
|
|
904
|
-
const session = loadSession(process.cwd());
|
|
1046
|
+
const session = loadSession(process.cwd(), options.name);
|
|
905
1047
|
if (!session) {
|
|
906
1048
|
console.log('\n📋 当前目录暂无工作流会话。\n');
|
|
907
|
-
console.log('
|
|
1049
|
+
console.log(' 运行 ethan workflow start 启动新工作流\n');
|
|
908
1050
|
return;
|
|
909
1051
|
}
|
|
910
1052
|
const progress = calcProgress(session);
|
|
@@ -919,6 +1061,8 @@ workflowCmd
|
|
|
919
1061
|
console.log('─'.repeat(60));
|
|
920
1062
|
console.log(` Pipeline : ${session.pipelineName}`);
|
|
921
1063
|
console.log(` Session : ${session.id}`);
|
|
1064
|
+
if (session.name)
|
|
1065
|
+
console.log(` 会话名 : ${session.name}`);
|
|
922
1066
|
console.log(` 创建时间 : ${session.createdAt.slice(0, 19).replace('T', ' ')}`);
|
|
923
1067
|
console.log(` 更新时间 : ${session.updatedAt.slice(0, 19).replace('T', ' ')}`);
|
|
924
1068
|
console.log(` 总进度 : [${'█'.repeat(Math.round(progress / 5))}${'░'.repeat(20 - Math.round(progress / 5))}] ${progress}%`);
|
|
@@ -944,15 +1088,16 @@ workflowCmd
|
|
|
944
1088
|
}
|
|
945
1089
|
else {
|
|
946
1090
|
console.log(`\n💡 当前任务背景:${session.initialContext}`);
|
|
947
|
-
console.log(` 完成当前步骤后运行:ethan workflow done "你的摘要"\n`);
|
|
1091
|
+
console.log(` 完成当前步骤后运行:ethan workflow done "你的摘要"${session.name ? ` --name ${session.name}` : ''}\n`);
|
|
948
1092
|
}
|
|
949
1093
|
});
|
|
950
1094
|
workflowCmd
|
|
951
1095
|
.command('reset')
|
|
952
1096
|
.description('清除当前工作流会话(不可恢复)')
|
|
953
|
-
.
|
|
1097
|
+
.option('-n, --name <name>', '具名会话名称')
|
|
1098
|
+
.action(async (options) => {
|
|
954
1099
|
const { loadSession, deleteSession, calcProgress } = await Promise.resolve().then(() => __importStar(require('../workflow/state')));
|
|
955
|
-
const session = loadSession(process.cwd());
|
|
1100
|
+
const session = loadSession(process.cwd(), options.name);
|
|
956
1101
|
if (!session) {
|
|
957
1102
|
console.log('\n📋 当前目录无工作流会话,无需重置。\n');
|
|
958
1103
|
return;
|
|
@@ -969,15 +1114,38 @@ workflowCmd
|
|
|
969
1114
|
console.log('\n已取消重置。\n');
|
|
970
1115
|
return;
|
|
971
1116
|
}
|
|
972
|
-
deleteSession(process.cwd());
|
|
1117
|
+
deleteSession(process.cwd(), options.name);
|
|
973
1118
|
console.log('\n✅ 工作流已重置。运行 ethan workflow start 开始新工作流。\n');
|
|
974
1119
|
});
|
|
975
1120
|
workflowCmd
|
|
976
1121
|
.command('list')
|
|
977
|
-
.description('列出所有可用的工作流 Pipeline')
|
|
978
|
-
.
|
|
1122
|
+
.description('列出所有可用的工作流 Pipeline 及进行中的具名会话')
|
|
1123
|
+
.option('--sessions', '只列出所有具名会话(不列 Pipeline)')
|
|
1124
|
+
.action(async (options) => {
|
|
979
1125
|
const { PIPELINES } = await Promise.resolve().then(() => __importStar(require('../skills/pipeline')));
|
|
980
|
-
const { loadSession, calcProgress } = await Promise.resolve().then(() => __importStar(require('../workflow/state')));
|
|
1126
|
+
const { loadSession, calcProgress, listNamedSessions } = await Promise.resolve().then(() => __importStar(require('../workflow/state')));
|
|
1127
|
+
// ── 列出具名会话 ─────────────────────────────────────────────────────────
|
|
1128
|
+
const namedSessions = listNamedSessions(process.cwd());
|
|
1129
|
+
if (namedSessions.length > 0 || options.sessions) {
|
|
1130
|
+
console.log('\n📂 具名会话\n');
|
|
1131
|
+
console.log('─'.repeat(60));
|
|
1132
|
+
if (namedSessions.length === 0) {
|
|
1133
|
+
console.log(' 暂无具名会话。使用 ethan workflow start --name <name> 创建。');
|
|
1134
|
+
}
|
|
1135
|
+
else {
|
|
1136
|
+
for (const s of namedSessions) {
|
|
1137
|
+
const pct = calcProgress(s);
|
|
1138
|
+
const statusLabel = s.completed ? '🎉 已完成' : `${pct}% 进行中`;
|
|
1139
|
+
console.log(`\n 📌 ${s.name || s.id} [${statusLabel}]`);
|
|
1140
|
+
console.log(` Pipeline: ${s.pipelineName}`);
|
|
1141
|
+
console.log(` 背景: ${s.initialContext.slice(0, 60)}${s.initialContext.length > 60 ? '…' : ''}`);
|
|
1142
|
+
console.log(` 更新: ${s.updatedAt.slice(0, 19).replace('T', ' ')}`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
console.log('\n' + '─'.repeat(60));
|
|
1146
|
+
if (options.sessions)
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
981
1149
|
const current = loadSession(process.cwd());
|
|
982
1150
|
console.log('\n🔄 可用工作流\n');
|
|
983
1151
|
console.log('─'.repeat(60));
|
|
@@ -990,7 +1158,8 @@ workflowCmd
|
|
|
990
1158
|
console.log(` 步骤:${p.skillIds.join(' → ')}`);
|
|
991
1159
|
}
|
|
992
1160
|
console.log('\n' + '─'.repeat(60));
|
|
993
|
-
console.log('\n启动工作流:ethan workflow start <pipeline-id> -c "任务描述"
|
|
1161
|
+
console.log('\n启动工作流:ethan workflow start <pipeline-id> -c "任务描述"');
|
|
1162
|
+
console.log('具名会话: ethan workflow start <pipeline-id> --name <name> -c "任务描述"\n');
|
|
994
1163
|
});
|
|
995
1164
|
workflowCmd
|
|
996
1165
|
.command('report')
|
|
@@ -1112,5 +1281,1566 @@ workflowCmd
|
|
|
1112
1281
|
console.log('\n' + report);
|
|
1113
1282
|
}
|
|
1114
1283
|
});
|
|
1284
|
+
// ─── scan 命令(T06)────────────────────────────────────────────────────────
|
|
1285
|
+
program
|
|
1286
|
+
.command('scan')
|
|
1287
|
+
.description('扫描项目代码健康状况:TODO/FIXME、高频修改热点、过期依赖等')
|
|
1288
|
+
.option('--todo', '只扫描 TODO / FIXME / HACK / XXX 注释')
|
|
1289
|
+
.option('--deps', '只检查依赖过期情况(读取 package.json)')
|
|
1290
|
+
.option('--no-copy', '不复制到剪贴板,直接打印')
|
|
1291
|
+
.action((options) => {
|
|
1292
|
+
const cwd = process.cwd();
|
|
1293
|
+
// ── TODO 扫描 ──────────────────────────────────────────────────────────
|
|
1294
|
+
const todoResult = (0, child_process_1.spawnSync)('grep', ['-rn', '--include=*.ts', '--include=*.js', '--include=*.tsx', '--include=*.jsx',
|
|
1295
|
+
'--exclude-dir=node_modules', '--exclude-dir=dist', '--exclude-dir=.git',
|
|
1296
|
+
'-E', '(TODO|FIXME|HACK|XXX):', '.'], { encoding: 'utf-8', cwd });
|
|
1297
|
+
const todoLines = (todoResult.stdout || '').trim().split('\n').filter(Boolean);
|
|
1298
|
+
// ── 依赖检查 ────────────────────────────────────────────────────────────
|
|
1299
|
+
let depsSection = '';
|
|
1300
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
1301
|
+
if (fs.existsSync(pkgPath)) {
|
|
1302
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
1303
|
+
const allDeps = {
|
|
1304
|
+
...pkgJson.dependencies,
|
|
1305
|
+
...pkgJson.devDependencies,
|
|
1306
|
+
};
|
|
1307
|
+
const depList = Object.entries(allDeps)
|
|
1308
|
+
.map(([name, ver]) => ` ${name}: ${ver}`)
|
|
1309
|
+
.join('\n');
|
|
1310
|
+
depsSection = `\n## 当前依赖\n\`\`\`\n${depList}\n\`\`\`\n`;
|
|
1311
|
+
}
|
|
1312
|
+
if (options.deps && !options.todo) {
|
|
1313
|
+
if (!depsSection) {
|
|
1314
|
+
console.error('❌ 当前目录未找到 package.json');
|
|
1315
|
+
process.exit(1);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
const todoSection = todoLines.length > 0
|
|
1319
|
+
? `\n## TODO / FIXME 注释(${todoLines.length} 条)\n\`\`\`\n${todoLines.slice(0, 50).join('\n')}${todoLines.length > 50 ? `\n[...还有 ${todoLines.length - 50} 条]` : ''}\n\`\`\`\n`
|
|
1320
|
+
: '\n## TODO / FIXME 注释\n无\n';
|
|
1321
|
+
const prompt = `你是一名代码质量工程师,请根据以下项目扫描结果给出改进建议。
|
|
1322
|
+
|
|
1323
|
+
## 任务
|
|
1324
|
+
分析代码库的健康状况,输出优先级排序的改进建议清单。
|
|
1325
|
+
|
|
1326
|
+
## 项目路径
|
|
1327
|
+
${cwd}
|
|
1328
|
+
${options.todo && !options.deps ? todoSection : options.deps && !options.todo ? depsSection : todoSection + depsSection}
|
|
1329
|
+
## 输出格式
|
|
1330
|
+
1. **健康评分**(0-100,含简短评语)
|
|
1331
|
+
2. **高优先级问题**(需立即处理)
|
|
1332
|
+
3. **中优先级建议**(本周内处理)
|
|
1333
|
+
4. **低优先级优化**(有时间再说)
|
|
1334
|
+
5. **总结**(一句话)
|
|
1335
|
+
|
|
1336
|
+
请给出具体可操作的建议,每条建议说明原因和影响。`;
|
|
1337
|
+
if (options.copy !== false) {
|
|
1338
|
+
const ok = copyToClipboard(prompt);
|
|
1339
|
+
console.log(`\n✅ 扫描报告提示词已复制到剪贴板(${todoLines.length} 个 TODO)\n`);
|
|
1340
|
+
if (!ok)
|
|
1341
|
+
console.log(prompt);
|
|
1342
|
+
}
|
|
1343
|
+
else {
|
|
1344
|
+
console.log('\n' + prompt + '\n');
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
// ─── explain 命令(T07)─────────────────────────────────────────────────────
|
|
1348
|
+
program
|
|
1349
|
+
.command('explain [file]')
|
|
1350
|
+
.description('解释代码文件或指定行范围,生成易读解释提示词')
|
|
1351
|
+
.option('--lines <range>', '行范围,如 10-50')
|
|
1352
|
+
.option('--level <level>', '解释深度:junior(入门)| senior(深度)| rubber-duck(橡皮鸭调试)', 'senior')
|
|
1353
|
+
.option('--no-copy', '不复制到剪贴板,直接打印')
|
|
1354
|
+
.action((file, options) => {
|
|
1355
|
+
let code = '';
|
|
1356
|
+
let codeLabel = '';
|
|
1357
|
+
if (file) {
|
|
1358
|
+
const filePath = path.resolve(process.cwd(), file);
|
|
1359
|
+
if (!fs.existsSync(filePath)) {
|
|
1360
|
+
console.error(`❌ 文件不存在:${filePath}`);
|
|
1361
|
+
process.exit(1);
|
|
1362
|
+
}
|
|
1363
|
+
const allLines = fs.readFileSync(filePath, 'utf-8').split('\n');
|
|
1364
|
+
if (options.lines) {
|
|
1365
|
+
const [start, end] = options.lines.split('-').map(Number);
|
|
1366
|
+
code = allLines.slice((start || 1) - 1, end || allLines.length).join('\n');
|
|
1367
|
+
codeLabel = `${file}(第 ${start}-${end} 行)`;
|
|
1368
|
+
}
|
|
1369
|
+
else {
|
|
1370
|
+
code = allLines.join('\n');
|
|
1371
|
+
codeLabel = file;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
else {
|
|
1375
|
+
console.error('❌ 请提供文件路径,如:ethan explain src/utils.ts --lines 1-50');
|
|
1376
|
+
process.exit(1);
|
|
1377
|
+
}
|
|
1378
|
+
const levelGuide = {
|
|
1379
|
+
junior: '用简单直白的语言解释,避免术语,适合初级开发者理解',
|
|
1380
|
+
senior: '深入分析设计意图、架构决策和潜在问题,适合有经验的开发者',
|
|
1381
|
+
'rubber-duck': '像对橡皮鸭调试一样逐行解释,帮助理解代码执行流程和找出 bug',
|
|
1382
|
+
};
|
|
1383
|
+
const guide = levelGuide[options.level] || levelGuide['senior'];
|
|
1384
|
+
const truncatedCode = code.length > 6000 ? code.slice(0, 6000) + '\n[...代码已截断...]' : code;
|
|
1385
|
+
const prompt = `你是一名经验丰富的工程师,请解释以下代码。
|
|
1386
|
+
|
|
1387
|
+
## 解释风格
|
|
1388
|
+
${guide}
|
|
1389
|
+
|
|
1390
|
+
## 代码来源
|
|
1391
|
+
${codeLabel}
|
|
1392
|
+
|
|
1393
|
+
## 代码
|
|
1394
|
+
\`\`\`
|
|
1395
|
+
${truncatedCode}
|
|
1396
|
+
\`\`\`
|
|
1397
|
+
|
|
1398
|
+
## 输出格式
|
|
1399
|
+
1. **核心功能**(一句话)
|
|
1400
|
+
2. **逐块解析**(每个关键部分的作用)
|
|
1401
|
+
3. **关键技术点**(使用的设计模式/算法/API)
|
|
1402
|
+
4. **潜在问题或改进点**(若有)`;
|
|
1403
|
+
if (options.copy !== false) {
|
|
1404
|
+
const ok = copyToClipboard(prompt);
|
|
1405
|
+
console.log(`\n✅ 代码解释提示词已复制到剪贴板(${codeLabel})\n`);
|
|
1406
|
+
if (!ok)
|
|
1407
|
+
console.log(prompt);
|
|
1408
|
+
}
|
|
1409
|
+
else {
|
|
1410
|
+
console.log('\n' + prompt + '\n');
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
// ─── test-case 命令(T08)───────────────────────────────────────────────────
|
|
1414
|
+
program
|
|
1415
|
+
.command('test-case <file>')
|
|
1416
|
+
.description('为源文件生成测试用例提示词')
|
|
1417
|
+
.option('--framework <fw>', '测试框架:vitest | jest | mocha | jasmine | pytest | go-test', 'vitest')
|
|
1418
|
+
.option('--coverage <level>', '覆盖目标:basic | full | edge-cases', 'full')
|
|
1419
|
+
.option('--no-copy', '不复制到剪贴板,直接打印')
|
|
1420
|
+
.action((file, options) => {
|
|
1421
|
+
const filePath = path.resolve(process.cwd(), file);
|
|
1422
|
+
if (!fs.existsSync(filePath)) {
|
|
1423
|
+
console.error(`❌ 文件不存在:${filePath}`);
|
|
1424
|
+
process.exit(1);
|
|
1425
|
+
}
|
|
1426
|
+
const code = fs.readFileSync(filePath, 'utf-8');
|
|
1427
|
+
const truncatedCode = code.length > 6000 ? code.slice(0, 6000) + '\n[...已截断...]' : code;
|
|
1428
|
+
const coverageGuide = {
|
|
1429
|
+
basic: '覆盖主要功能路径,确保 happy path 通过',
|
|
1430
|
+
full: '覆盖所有分支(if/else/switch)、正常路径和错误路径',
|
|
1431
|
+
'edge-cases': '重点覆盖边界条件:空值、极值、并发、异常抛出、类型异常等',
|
|
1432
|
+
};
|
|
1433
|
+
const prompt = `你是一名测试工程师,请为以下代码生成完整的测试用例。
|
|
1434
|
+
|
|
1435
|
+
## 要求
|
|
1436
|
+
- **框架**:${options.framework}
|
|
1437
|
+
- **覆盖目标**:${coverageGuide[options.coverage] || coverageGuide['full']}
|
|
1438
|
+
- 每个测试用例包含:describe 描述、it/test 名称、arrange/act/assert 结构
|
|
1439
|
+
- 对异步代码使用 async/await
|
|
1440
|
+
- Mock 外部依赖(文件系统、网络请求、数据库等)
|
|
1441
|
+
|
|
1442
|
+
## 源文件
|
|
1443
|
+
${file}
|
|
1444
|
+
|
|
1445
|
+
## 源代码
|
|
1446
|
+
\`\`\`
|
|
1447
|
+
${truncatedCode}
|
|
1448
|
+
\`\`\`
|
|
1449
|
+
|
|
1450
|
+
## 输出格式
|
|
1451
|
+
直接输出完整可运行的测试文件,包含所有 import 语句,使用 ${options.framework} 语法。
|
|
1452
|
+
文件名建议:${path.basename(file, path.extname(file))}.test${path.extname(file)}`;
|
|
1453
|
+
if (options.copy !== false) {
|
|
1454
|
+
const ok = copyToClipboard(prompt);
|
|
1455
|
+
console.log(`\n✅ 测试用例提示词已复制到剪贴板(${file})\n`);
|
|
1456
|
+
if (!ok)
|
|
1457
|
+
console.log(prompt);
|
|
1458
|
+
}
|
|
1459
|
+
else {
|
|
1460
|
+
console.log('\n' + prompt + '\n');
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
// ─── naming 命令(T09)──────────────────────────────────────────────────────
|
|
1464
|
+
program
|
|
1465
|
+
.command('naming <description>')
|
|
1466
|
+
.description('根据描述生成命名候选(变量/函数/组件/文件等)')
|
|
1467
|
+
.option('--style <style>', '命名风格:camelCase | PascalCase | snake_case | kebab-case | all', 'all')
|
|
1468
|
+
.option('--lang <lang>', '语言上下文:ts | js | python | go | rust | java', 'ts')
|
|
1469
|
+
.option('--count <n>', '每种类型生成几个候选', '5')
|
|
1470
|
+
.option('--no-copy', '不复制到剪贴板,直接打印')
|
|
1471
|
+
.action((description, options) => {
|
|
1472
|
+
const count = parseInt(options.count, 10) || 5;
|
|
1473
|
+
const styleGuide = options.style === 'all'
|
|
1474
|
+
? '对每种命名类型(变量/函数/组件/文件/常量),同时提供 camelCase、PascalCase、snake_case 三种风格的候选'
|
|
1475
|
+
: `使用 ${options.style} 风格`;
|
|
1476
|
+
const prompt = `你是一名命名专家,擅长为代码元素起简洁、准确、符合惯例的名称。
|
|
1477
|
+
|
|
1478
|
+
## 需求描述
|
|
1479
|
+
${description}
|
|
1480
|
+
|
|
1481
|
+
## 要求
|
|
1482
|
+
- 语言/框架上下文:${options.lang}
|
|
1483
|
+
- 命名风格:${styleGuide}
|
|
1484
|
+
- 每种类型提供 ${count} 个候选,从最推荐到可接受排序
|
|
1485
|
+
- 每个候选附简短说明(为什么选这个名字)
|
|
1486
|
+
|
|
1487
|
+
## 输出格式(Markdown 表格)
|
|
1488
|
+
|
|
1489
|
+
### 变量名
|
|
1490
|
+
| 候选 | 风格 | 说明 |
|
|
1491
|
+
|------|------|------|
|
|
1492
|
+
|
|
1493
|
+
### 函数/方法名
|
|
1494
|
+
| 候选 | 风格 | 说明 |
|
|
1495
|
+
|------|------|------|
|
|
1496
|
+
|
|
1497
|
+
### 类/组件名
|
|
1498
|
+
| 候选 | 风格 | 说明 |
|
|
1499
|
+
|------|------|------|
|
|
1500
|
+
|
|
1501
|
+
### 文件/模块名
|
|
1502
|
+
| 候选 | 风格 | 说明 |
|
|
1503
|
+
|------|------|------|
|
|
1504
|
+
|
|
1505
|
+
### 常量名
|
|
1506
|
+
| 候选 | 风格 | 说明 |
|
|
1507
|
+
|------|------|------|
|
|
1508
|
+
|
|
1509
|
+
最后给出你的 **最终推荐**(每类各一个,并说明理由)。`;
|
|
1510
|
+
if (options.copy !== false) {
|
|
1511
|
+
const ok = copyToClipboard(prompt);
|
|
1512
|
+
console.log(`\n✅ 命名建议提示词已复制到剪贴板\n`);
|
|
1513
|
+
if (!ok)
|
|
1514
|
+
console.log(prompt);
|
|
1515
|
+
}
|
|
1516
|
+
else {
|
|
1517
|
+
console.log('\n' + prompt + '\n');
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
// ─── readme 命令(T10)──────────────────────────────────────────────────────
|
|
1521
|
+
program
|
|
1522
|
+
.command('readme')
|
|
1523
|
+
.description('扫描项目结构,生成 README 起草提示词')
|
|
1524
|
+
.option('--template <tpl>', '模板类型:library | cli | webapp | api | monorepo', 'library')
|
|
1525
|
+
.option('--no-copy', '不复制到剪贴板,直接打印')
|
|
1526
|
+
.action((options) => {
|
|
1527
|
+
const cwd = process.cwd();
|
|
1528
|
+
// 读取 package.json
|
|
1529
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
1530
|
+
let pkgInfo = '';
|
|
1531
|
+
if (fs.existsSync(pkgPath)) {
|
|
1532
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
1533
|
+
pkgInfo = `\n## package.json 信息\n- name: ${pkgJson.name || 'N/A'}\n- version: ${pkgJson.version || 'N/A'}\n- description: ${pkgJson.description || 'N/A'}\n- scripts: ${Object.keys(pkgJson.scripts || {}).join(', ') || 'N/A'}\n`;
|
|
1534
|
+
}
|
|
1535
|
+
// 获取目录结构(2层)
|
|
1536
|
+
const treeResult = (0, child_process_1.spawnSync)('find', ['.', '-maxdepth', '2', '-not', '-path', '*/node_modules/*', '-not', '-path', '*/.git/*', '-not', '-path', '*/dist/*'], {
|
|
1537
|
+
encoding: 'utf-8', cwd
|
|
1538
|
+
});
|
|
1539
|
+
const tree = (treeResult.stdout || '').trim().split('\n').slice(0, 60).join('\n');
|
|
1540
|
+
const templateGuide = {
|
|
1541
|
+
library: 'NPM 库 / SDK,包含:简介、安装、快速上手、API 文档、贡献指南',
|
|
1542
|
+
cli: 'CLI 工具,包含:简介、安装、命令列表(表格)、使用示例、配置说明',
|
|
1543
|
+
webapp: 'Web 应用,包含:简介、技术栈、本地开发、部署说明、截图区位占位',
|
|
1544
|
+
api: 'REST/GraphQL API 服务,包含:简介、接口列表、认证方式、部署',
|
|
1545
|
+
monorepo: 'Monorepo,包含:仓库结构、各包说明、开发工作流、发布流程',
|
|
1546
|
+
};
|
|
1547
|
+
const prompt = `你是一名技术写作者,请根据以下项目信息生成一份专业的 README.md。
|
|
1548
|
+
|
|
1549
|
+
## 项目类型
|
|
1550
|
+
${options.template}(${templateGuide[options.template] || templateGuide['library']})
|
|
1551
|
+
${pkgInfo}
|
|
1552
|
+
## 项目文件结构
|
|
1553
|
+
\`\`\`
|
|
1554
|
+
${tree}
|
|
1555
|
+
\`\`\`
|
|
1556
|
+
|
|
1557
|
+
## 要求
|
|
1558
|
+
1. 使用中文(技术术语保持英文)
|
|
1559
|
+
2. 开头放 badges 占位(如 npm version / build status / license)
|
|
1560
|
+
3. 结构清晰,每个 section 有实质内容(不要空占位)
|
|
1561
|
+
4. 安装和使用示例务必给出真实的命令(根据 package.json 推断)
|
|
1562
|
+
5. 末尾加 License section
|
|
1563
|
+
|
|
1564
|
+
请直接输出完整的 README.md 内容。`;
|
|
1565
|
+
if (options.copy !== false) {
|
|
1566
|
+
const ok = copyToClipboard(prompt);
|
|
1567
|
+
console.log(`\n✅ README 生成提示词已复制到剪贴板(${options.template} 模板)\n`);
|
|
1568
|
+
if (!ok)
|
|
1569
|
+
console.log(prompt);
|
|
1570
|
+
}
|
|
1571
|
+
else {
|
|
1572
|
+
console.log('\n' + prompt + '\n');
|
|
1573
|
+
}
|
|
1574
|
+
});
|
|
1575
|
+
// ─── roast 命令(T11)───────────────────────────────────────────────────────
|
|
1576
|
+
program
|
|
1577
|
+
.command('roast [file]')
|
|
1578
|
+
.description('以幽默吐槽方式 Review 代码(带 --pr 则 roast 当前 PR diff)')
|
|
1579
|
+
.option('--pr', '吐槽当前分支 PR diff')
|
|
1580
|
+
.option('--level <level>', '毒舌程度:mild(温和)| spicy(辛辣)| savage(毒舌)', 'spicy')
|
|
1581
|
+
.option('--no-copy', '不复制到剪贴板,直接打印')
|
|
1582
|
+
.action((file, options) => {
|
|
1583
|
+
let code = '';
|
|
1584
|
+
let codeLabel = '';
|
|
1585
|
+
if (options.pr) {
|
|
1586
|
+
if (!(0, utils_1.isGitRepo)()) {
|
|
1587
|
+
console.error('❌ 当前目录不是 Git 仓库');
|
|
1588
|
+
process.exit(1);
|
|
1589
|
+
}
|
|
1590
|
+
const base = (0, utils_1.getDefaultBranch)();
|
|
1591
|
+
code = (0, utils_1.truncateDiff)((0, utils_1.getBranchDiff)(base), 6000);
|
|
1592
|
+
codeLabel = `PR diff(${base}...${(0, utils_1.getCurrentBranch)()})`;
|
|
1593
|
+
}
|
|
1594
|
+
else if (file) {
|
|
1595
|
+
const filePath = path.resolve(process.cwd(), file);
|
|
1596
|
+
if (!fs.existsSync(filePath)) {
|
|
1597
|
+
console.error(`❌ 文件不存在:${filePath}`);
|
|
1598
|
+
process.exit(1);
|
|
1599
|
+
}
|
|
1600
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1601
|
+
code = content.length > 6000 ? content.slice(0, 6000) + '\n[...已截断...]' : content;
|
|
1602
|
+
codeLabel = file;
|
|
1603
|
+
}
|
|
1604
|
+
else {
|
|
1605
|
+
console.error('❌ 请提供文件路径或使用 --pr 吐槽当前 PR');
|
|
1606
|
+
process.exit(1);
|
|
1607
|
+
}
|
|
1608
|
+
const levelGuide = {
|
|
1609
|
+
mild: '温和友善,用轻松的玩笑指出问题,像好朋友之间的调侃',
|
|
1610
|
+
spicy: '辛辣直接,用夸张的比喻和反问揭露代码问题,但最终还是提出改进建议',
|
|
1611
|
+
savage: '毒舌模式全开,像脱口秀演员一样无情吐槽,但每个槽点都有实质的改进建议(最后给个鼓励)',
|
|
1612
|
+
};
|
|
1613
|
+
const prompt = `你是一名资深工程师,同时也是个擅长代码吐槽的脱口秀演员。请对以下代码进行幽默的 Roast Review。
|
|
1614
|
+
|
|
1615
|
+
## 毒舌程度
|
|
1616
|
+
${levelGuide[options.level] || levelGuide['spicy']}
|
|
1617
|
+
|
|
1618
|
+
## 代码来源
|
|
1619
|
+
${codeLabel}
|
|
1620
|
+
|
|
1621
|
+
## 代码 / Diff
|
|
1622
|
+
\`\`\`
|
|
1623
|
+
${code}
|
|
1624
|
+
\`\`\`
|
|
1625
|
+
|
|
1626
|
+
## 输出要求
|
|
1627
|
+
1. **开场白**(一句毒舌的总结)
|
|
1628
|
+
2. **逐条吐槽**(每条格式:💀 [文件/函数名] 吐槽内容 → 实质改进建议)
|
|
1629
|
+
3. **最佳槽点**(评选本次 Roast 的 MVP 代码片段,附详细解释)
|
|
1630
|
+
4. **结语**(${options.level === 'savage' ? '狠狠骂完后给一句温暖鼓励' : '调侃中带着肯定'})
|
|
1631
|
+
|
|
1632
|
+
注意:毒舌只是形式,每个问题都要有实质的技术建议,这不是人身攻击!`;
|
|
1633
|
+
if (options.copy !== false) {
|
|
1634
|
+
const ok = copyToClipboard(prompt);
|
|
1635
|
+
console.log(`\n✅ Roast Review 提示词已复制到剪贴板(${options.level} 模式)\n`);
|
|
1636
|
+
if (!ok)
|
|
1637
|
+
console.log(prompt);
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
console.log('\n' + prompt + '\n');
|
|
1641
|
+
}
|
|
1642
|
+
});
|
|
1643
|
+
// ─── commit 命令(T01)──────────────────────────────────────────────────────
|
|
1644
|
+
program
|
|
1645
|
+
.command('commit')
|
|
1646
|
+
.description('根据 git staged diff 生成 Commit Message 提示词,复制到剪贴板')
|
|
1647
|
+
.option('--type <type>', '强制指定 commit 类型(feat/fix/docs/refactor/test/chore 等)')
|
|
1648
|
+
.option('--emoji', '在 commit 类型前添加 Gitmoji')
|
|
1649
|
+
.option('--no-copy', '不复制到剪贴板,直接打印')
|
|
1650
|
+
.action((options) => {
|
|
1651
|
+
if (!(0, utils_1.isGitRepo)()) {
|
|
1652
|
+
console.error('❌ 当前目录不是 Git 仓库');
|
|
1653
|
+
process.exit(1);
|
|
1654
|
+
}
|
|
1655
|
+
const stagedFiles = (0, utils_1.getStagedFiles)();
|
|
1656
|
+
if (stagedFiles.length === 0) {
|
|
1657
|
+
console.error('❌ 暂存区为空,请先 git add 要提交的文件');
|
|
1658
|
+
process.exit(1);
|
|
1659
|
+
}
|
|
1660
|
+
const diff = (0, utils_1.truncateDiff)((0, utils_1.getStagedDiff)(), 6000);
|
|
1661
|
+
const typeHint = options.type ? `\n\n**强制类型**:${options.type}` : '';
|
|
1662
|
+
const emojiHint = options.emoji
|
|
1663
|
+
? '\n\n**输出格式**:在 type 前加 Gitmoji,例如 ✨ feat: ...'
|
|
1664
|
+
: '';
|
|
1665
|
+
const prompt = `你是一名经验丰富的工程师,请根据以下 Git staged diff 生成一条规范的 Commit Message。
|
|
1666
|
+
|
|
1667
|
+
## 要求
|
|
1668
|
+
- 遵循 Conventional Commits 规范(type(scope): subject)
|
|
1669
|
+
- type 可选:feat / fix / docs / style / refactor / perf / test / chore / ci / build
|
|
1670
|
+
- subject 用中文或英文均可,简洁描述"做了什么"(≤50字)
|
|
1671
|
+
- 如果改动涉及多个关注点,可附 body(每行一个要点)${typeHint}${emojiHint}
|
|
1672
|
+
|
|
1673
|
+
## 涉及文件
|
|
1674
|
+
${stagedFiles.map((f) => `- ${f}`).join('\n')}
|
|
1675
|
+
|
|
1676
|
+
## Staged Diff
|
|
1677
|
+
\`\`\`diff
|
|
1678
|
+
${diff}
|
|
1679
|
+
\`\`\`
|
|
1680
|
+
|
|
1681
|
+
请直接输出 Commit Message,不要额外解释。`;
|
|
1682
|
+
if (options.copy !== false) {
|
|
1683
|
+
const ok = copyToClipboard(prompt);
|
|
1684
|
+
console.log(`\n✅ Commit 提示词已复制到剪贴板(${stagedFiles.length} 个文件)\n`);
|
|
1685
|
+
if (!ok)
|
|
1686
|
+
console.log(prompt);
|
|
1687
|
+
}
|
|
1688
|
+
else {
|
|
1689
|
+
console.log('\n' + prompt + '\n');
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
// ─── review 命令(T02)──────────────────────────────────────────────────────
|
|
1693
|
+
program
|
|
1694
|
+
.command('review')
|
|
1695
|
+
.description('对 git diff 执行 Code Review,生成提示词复制到剪贴板')
|
|
1696
|
+
.option('--focus <focus>', '重点关注方向(security/performance/style/logic)')
|
|
1697
|
+
.option('--pr', '审查当前分支相对默认分支的完整 PR diff')
|
|
1698
|
+
.option('--base <branch>', '与指定分支做对比(优先级高于 --pr)')
|
|
1699
|
+
.option('--no-copy', '不复制到剪贴板,直接打印')
|
|
1700
|
+
.action((options) => {
|
|
1701
|
+
if (!(0, utils_1.isGitRepo)()) {
|
|
1702
|
+
console.error('❌ 当前目录不是 Git 仓库');
|
|
1703
|
+
process.exit(1);
|
|
1704
|
+
}
|
|
1705
|
+
let diff;
|
|
1706
|
+
let diffLabel;
|
|
1707
|
+
if (options.base) {
|
|
1708
|
+
diff = (0, utils_1.getBranchDiff)(options.base);
|
|
1709
|
+
diffLabel = `分支对比:${options.base}...HEAD`;
|
|
1710
|
+
}
|
|
1711
|
+
else if (options.pr) {
|
|
1712
|
+
const base = (0, utils_1.getDefaultBranch)();
|
|
1713
|
+
diff = (0, utils_1.getBranchDiff)(base);
|
|
1714
|
+
diffLabel = `PR diff:${base}...HEAD`;
|
|
1715
|
+
}
|
|
1716
|
+
else {
|
|
1717
|
+
diff = (0, utils_1.getStagedDiff)();
|
|
1718
|
+
if (!diff) {
|
|
1719
|
+
console.error('❌ 暂存区为空。请使用 --pr 或 --base <branch> 审查分支差异。');
|
|
1720
|
+
process.exit(1);
|
|
1721
|
+
}
|
|
1722
|
+
diffLabel = 'staged diff';
|
|
1723
|
+
}
|
|
1724
|
+
if (!diff) {
|
|
1725
|
+
console.error('❌ 未找到任何差异,无需 Review。');
|
|
1726
|
+
process.exit(1);
|
|
1727
|
+
}
|
|
1728
|
+
const focusHint = options.focus
|
|
1729
|
+
? `\n\n**重点关注**:${options.focus}(请在该方向给出更深入的分析)`
|
|
1730
|
+
: '';
|
|
1731
|
+
const prompt = `你是一名资深工程师,请对以下代码变更进行系统性 Code Review。
|
|
1732
|
+
|
|
1733
|
+
## Review 维度
|
|
1734
|
+
按以下顺序逐层分析,每个问题标注严重级别:
|
|
1735
|
+
- 🔴 **Blocker**:必须修复才能合并(正确性 / 安全漏洞 / 数据丢失风险)
|
|
1736
|
+
- 🟡 **Major**:强烈建议修复(性能 / 可维护性 / 明显不规范)
|
|
1737
|
+
- 🟢 **Minor**:可选优化(代码风格 / 命名 / 小的可读性问题)${focusHint}
|
|
1738
|
+
|
|
1739
|
+
## 变更来源
|
|
1740
|
+
${diffLabel}(当前分支:${(0, utils_1.getCurrentBranch)()})
|
|
1741
|
+
|
|
1742
|
+
## Diff
|
|
1743
|
+
\`\`\`diff
|
|
1744
|
+
${(0, utils_1.truncateDiff)(diff, 8000)}
|
|
1745
|
+
\`\`\`
|
|
1746
|
+
|
|
1747
|
+
## 输出格式
|
|
1748
|
+
1. **总体评价**(1-2句)
|
|
1749
|
+
2. **问题列表**(按 Blocker → Major → Minor 排列,每条格式:\`[文件:行号] 级别:描述 → 建议\`)
|
|
1750
|
+
3. **值得肯定的设计**(若有)
|
|
1751
|
+
4. **Review 结论**(通过 / 需修改后通过 / 需重大修改)`;
|
|
1752
|
+
if (options.copy !== false) {
|
|
1753
|
+
const ok = copyToClipboard(prompt);
|
|
1754
|
+
console.log(`\n✅ Code Review 提示词已复制到剪贴板(${diffLabel})\n`);
|
|
1755
|
+
if (!ok)
|
|
1756
|
+
console.log(prompt);
|
|
1757
|
+
}
|
|
1758
|
+
else {
|
|
1759
|
+
console.log('\n' + prompt + '\n');
|
|
1760
|
+
}
|
|
1761
|
+
});
|
|
1762
|
+
// ─── pr 命令(T03)───────────────────────────────────────────────────────────
|
|
1763
|
+
program
|
|
1764
|
+
.command('pr')
|
|
1765
|
+
.description('根据分支 diff 生成 PR 描述提示词')
|
|
1766
|
+
.option('--base <branch>', '对比的目标分支(默认自动检测 main/master)')
|
|
1767
|
+
.option('--out <file>', '将提示词写入文件(如 PR_DRAFT.md)')
|
|
1768
|
+
.option('--no-copy', '不复制到剪贴板,直接打印')
|
|
1769
|
+
.action((options) => {
|
|
1770
|
+
if (!(0, utils_1.isGitRepo)()) {
|
|
1771
|
+
console.error('❌ 当前目录不是 Git 仓库');
|
|
1772
|
+
process.exit(1);
|
|
1773
|
+
}
|
|
1774
|
+
const base = options.base || (0, utils_1.getDefaultBranch)();
|
|
1775
|
+
const currentBranch = (0, utils_1.getCurrentBranch)();
|
|
1776
|
+
if (currentBranch === base) {
|
|
1777
|
+
console.error(`❌ 当前已在 ${base} 分支,请切换到功能分支后再生成 PR 描述。`);
|
|
1778
|
+
process.exit(1);
|
|
1779
|
+
}
|
|
1780
|
+
const diff = (0, utils_1.getBranchDiff)(base);
|
|
1781
|
+
if (!diff) {
|
|
1782
|
+
console.error(`❌ 与 ${base} 分支相比没有差异,无需创建 PR。`);
|
|
1783
|
+
process.exit(1);
|
|
1784
|
+
}
|
|
1785
|
+
const prompt = `你是一名经验丰富的工程师,请根据以下 Git diff 生成一份规范的 Pull Request 描述。
|
|
1786
|
+
|
|
1787
|
+
## PR 信息
|
|
1788
|
+
- **当前分支**:${currentBranch}
|
|
1789
|
+
- **目标分支**:${base}
|
|
1790
|
+
|
|
1791
|
+
## 要求
|
|
1792
|
+
生成包含以下结构的 PR 描述(Markdown 格式):
|
|
1793
|
+
|
|
1794
|
+
### ✨ 变更概述
|
|
1795
|
+
(1-3 句话,说明这个 PR 做了什么)
|
|
1796
|
+
|
|
1797
|
+
### 📋 变更详情
|
|
1798
|
+
(分点列出主要改动)
|
|
1799
|
+
|
|
1800
|
+
### 🧪 测试说明
|
|
1801
|
+
(如何验证这些改动,包括手动测试步骤)
|
|
1802
|
+
|
|
1803
|
+
### 🔗 关联 Issue
|
|
1804
|
+
(如有,填写 "Closes #xxx" 或 "N/A")
|
|
1805
|
+
|
|
1806
|
+
### ⚠️ 注意事项
|
|
1807
|
+
(Reviewer 需要特别关注的地方,或部署注意事项)
|
|
1808
|
+
|
|
1809
|
+
## Diff(${base}...${currentBranch})
|
|
1810
|
+
\`\`\`diff
|
|
1811
|
+
${(0, utils_1.truncateDiff)(diff, 8000)}
|
|
1812
|
+
\`\`\`
|
|
1813
|
+
|
|
1814
|
+
请直接输出 PR 描述内容,使用 Markdown 格式,不要额外解释。`;
|
|
1815
|
+
if (options.out) {
|
|
1816
|
+
const outPath = path.resolve(process.cwd(), options.out);
|
|
1817
|
+
fs.writeFileSync(outPath, prompt, 'utf-8');
|
|
1818
|
+
console.log(`\n✅ PR 提示词已写入:${outPath}\n`);
|
|
1819
|
+
}
|
|
1820
|
+
else if (options.copy !== false) {
|
|
1821
|
+
const ok = copyToClipboard(prompt);
|
|
1822
|
+
console.log(`\n✅ PR 描述提示词已复制到剪贴板(${base}...${currentBranch})\n`);
|
|
1823
|
+
if (!ok)
|
|
1824
|
+
console.log(prompt);
|
|
1825
|
+
}
|
|
1826
|
+
else {
|
|
1827
|
+
console.log('\n' + prompt + '\n');
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
// ─── standup 命令(T04)─────────────────────────────────────────────────────
|
|
1831
|
+
program
|
|
1832
|
+
.command('standup')
|
|
1833
|
+
.description('根据最近 24h 的 git log 生成日报 / 站会稿')
|
|
1834
|
+
.option('--since <time>', '查询时间范围(默认 "24 hours ago")', '24 hours ago')
|
|
1835
|
+
.option('--author <author>', '只统计指定作者的提交(默认当前 git user)')
|
|
1836
|
+
.option('--format <format>', '输出格式:standup(站会稿)| daily(日报)| brief(一句话)', 'standup')
|
|
1837
|
+
.option('--no-copy', '不复制到剪贴板,直接打印')
|
|
1838
|
+
.action((options) => {
|
|
1839
|
+
if (!(0, utils_1.isGitRepo)()) {
|
|
1840
|
+
console.error('❌ 当前目录不是 Git 仓库');
|
|
1841
|
+
process.exit(1);
|
|
1842
|
+
}
|
|
1843
|
+
// 默认使用 git config 中的 user.name
|
|
1844
|
+
const authorResult = (0, child_process_1.spawnSync)('git', ['config', 'user.name'], { encoding: 'utf-8' });
|
|
1845
|
+
const author = options.author || (authorResult.stdout || '').trim() || undefined;
|
|
1846
|
+
const log = (0, utils_1.getCommitLogSince)(options.since, author);
|
|
1847
|
+
if (!log) {
|
|
1848
|
+
console.log(`\n⚠️ 在 "${options.since}" 内未找到任何提交记录。\n`);
|
|
1849
|
+
process.exit(0);
|
|
1850
|
+
}
|
|
1851
|
+
const formatGuide = {
|
|
1852
|
+
standup: `输出为站会发言稿,包含:
|
|
1853
|
+
1. **昨日完成**(根据 commit log 推断)
|
|
1854
|
+
2. **今日计划**(根据 commit 趋势合理推断 1-3 条)
|
|
1855
|
+
3. **阻塞 / 风险**(如有,否则填"无")
|
|
1856
|
+
格式简洁,适合口头朗读(60-120字)`,
|
|
1857
|
+
daily: `输出为日报,包含:
|
|
1858
|
+
1. **完成事项**(分条)
|
|
1859
|
+
2. **遗留 / 待处理**
|
|
1860
|
+
3. **明日计划**
|
|
1861
|
+
适合发送到工作群`,
|
|
1862
|
+
brief: `用一句话总结今天的主要工作(≤30字)`,
|
|
1863
|
+
};
|
|
1864
|
+
const guide = formatGuide[options.format] || formatGuide['standup'];
|
|
1865
|
+
const prompt = `你是一名工程师助手,请根据以下 Git 提交记录,生成${options.format === 'standup' ? '站会发言稿' : options.format === 'daily' ? '日报' : '工作简报'}。
|
|
1866
|
+
|
|
1867
|
+
## 提交记录(${options.since} 至今)
|
|
1868
|
+
\`\`\`
|
|
1869
|
+
${log}
|
|
1870
|
+
\`\`\`
|
|
1871
|
+
|
|
1872
|
+
## 要求
|
|
1873
|
+
${guide}
|
|
1874
|
+
|
|
1875
|
+
请直接输出内容,不要解释格式。`;
|
|
1876
|
+
if (options.copy !== false) {
|
|
1877
|
+
const ok = copyToClipboard(prompt);
|
|
1878
|
+
console.log(`\n✅ 站会 / 日报提示词已复制到剪贴板\n`);
|
|
1879
|
+
if (!ok)
|
|
1880
|
+
console.log(prompt);
|
|
1881
|
+
}
|
|
1882
|
+
else {
|
|
1883
|
+
console.log('\n' + prompt + '\n');
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
// ─── changelog 命令(T05)───────────────────────────────────────────────────
|
|
1887
|
+
program
|
|
1888
|
+
.command('changelog')
|
|
1889
|
+
.description('根据 tag 区间的 commit log 生成 CHANGELOG 提示词')
|
|
1890
|
+
.option('--from <tag>', '起始 tag(默认最新 tag)')
|
|
1891
|
+
.option('--to <ref>', '结束 ref(默认 HEAD)', 'HEAD')
|
|
1892
|
+
.option('--out <file>', '将提示词写入文件')
|
|
1893
|
+
.option('--append', '追加到 --out 文件而非覆盖')
|
|
1894
|
+
.option('--no-copy', '不复制到剪贴板,直接打印')
|
|
1895
|
+
.action((options) => {
|
|
1896
|
+
if (!(0, utils_1.isGitRepo)()) {
|
|
1897
|
+
console.error('❌ 当前目录不是 Git 仓库');
|
|
1898
|
+
process.exit(1);
|
|
1899
|
+
}
|
|
1900
|
+
const from = options.from || (0, utils_1.getLatestTag)();
|
|
1901
|
+
if (!from) {
|
|
1902
|
+
console.error('❌ 未找到任何 tag,请使用 --from <tag> 指定起始点。');
|
|
1903
|
+
process.exit(1);
|
|
1904
|
+
}
|
|
1905
|
+
const tags = (0, utils_1.getTags)();
|
|
1906
|
+
const log = (0, utils_1.getCommitRange)(from, options.to);
|
|
1907
|
+
if (!log) {
|
|
1908
|
+
console.error(`❌ ${from}..${options.to} 之间没有新的提交。`);
|
|
1909
|
+
process.exit(1);
|
|
1910
|
+
}
|
|
1911
|
+
const tagsInfo = tags.length > 0 ? `最近 tag:${tags.slice(0, 5).join(', ')}` : '';
|
|
1912
|
+
const prompt = `你是一名技术写作者,请根据以下 Git commit log 生成规范的 CHANGELOG 内容。
|
|
1913
|
+
|
|
1914
|
+
## 版本信息
|
|
1915
|
+
- **起始**:${from}
|
|
1916
|
+
- **结束**:${options.to}
|
|
1917
|
+
${tagsInfo ? `- **${tagsInfo}**` : ''}
|
|
1918
|
+
|
|
1919
|
+
## 要求
|
|
1920
|
+
输出格式(Markdown):
|
|
1921
|
+
\`\`\`
|
|
1922
|
+
## [版本号] - YYYY-MM-DD
|
|
1923
|
+
|
|
1924
|
+
### ✨ Features
|
|
1925
|
+
- commit subject(去掉 feat: 前缀)
|
|
1926
|
+
|
|
1927
|
+
### 🐛 Bug Fixes
|
|
1928
|
+
- commit subject(去掉 fix: 前缀)
|
|
1929
|
+
|
|
1930
|
+
### 📝 Documentation
|
|
1931
|
+
- ...
|
|
1932
|
+
|
|
1933
|
+
### ♻️ Refactor
|
|
1934
|
+
- ...
|
|
1935
|
+
|
|
1936
|
+
### 🔧 Chore
|
|
1937
|
+
- ...
|
|
1938
|
+
\`\`\`
|
|
1939
|
+
|
|
1940
|
+
规则:
|
|
1941
|
+
1. 按 Conventional Commits type 分类
|
|
1942
|
+
2. 跳过 merge commit 和无意义的 chore(如 bump version)
|
|
1943
|
+
3. 语言与 commit message 保持一致
|
|
1944
|
+
4. 如果 commit 没有规范 type 前缀,根据内容判断分类
|
|
1945
|
+
|
|
1946
|
+
## Commit Log(${from}..${options.to})
|
|
1947
|
+
\`\`\`
|
|
1948
|
+
${log}
|
|
1949
|
+
\`\`\`
|
|
1950
|
+
|
|
1951
|
+
请直接输出 CHANGELOG 内容,不要额外说明。`;
|
|
1952
|
+
if (options.out) {
|
|
1953
|
+
const outPath = path.resolve(process.cwd(), options.out);
|
|
1954
|
+
if (options.append && fs.existsSync(outPath)) {
|
|
1955
|
+
const existing = fs.readFileSync(outPath, 'utf-8');
|
|
1956
|
+
fs.writeFileSync(outPath, prompt + '\n\n---\n\n' + existing, 'utf-8');
|
|
1957
|
+
}
|
|
1958
|
+
else {
|
|
1959
|
+
fs.writeFileSync(outPath, prompt, 'utf-8');
|
|
1960
|
+
}
|
|
1961
|
+
console.log(`\n✅ CHANGELOG 提示词已写入:${outPath}\n`);
|
|
1962
|
+
}
|
|
1963
|
+
else if (options.copy !== false) {
|
|
1964
|
+
const ok = copyToClipboard(prompt);
|
|
1965
|
+
console.log(`\n✅ CHANGELOG 提示词已复制到剪贴板(${from}..${options.to})\n`);
|
|
1966
|
+
if (!ok)
|
|
1967
|
+
console.log(prompt);
|
|
1968
|
+
}
|
|
1969
|
+
else {
|
|
1970
|
+
console.log('\n' + prompt + '\n');
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
// ─── oncall 命令(T13)──────────────────────────────────────────────────────
|
|
1974
|
+
program
|
|
1975
|
+
.command('oncall')
|
|
1976
|
+
.description('启动故障响应工作流,生成事故排查提示词')
|
|
1977
|
+
.option('--severity <level>', '严重程度:P0(全站不可用)| P1(核心功能受损)| P2(局部影响)', 'P1')
|
|
1978
|
+
.option('-d, --desc <description>', '故障描述(现象、影响范围、触发时间)')
|
|
1979
|
+
.option('--postmortem', '生成事后复盘报告提示词(事故已解决后使用)')
|
|
1980
|
+
.option('--no-copy', '不复制到剪贴板,直接打印')
|
|
1981
|
+
.action((options) => {
|
|
1982
|
+
const desc = options.desc || '(请描述故障现象、影响用户范围和触发时间)';
|
|
1983
|
+
const severity = options.severity || 'P1';
|
|
1984
|
+
const severityGuide = {
|
|
1985
|
+
P0: '全站不可用 / 核心服务宕机,所有用户受影响,需立即处理',
|
|
1986
|
+
P1: '核心功能受损(如登录、支付、核心 API),部分用户受影响',
|
|
1987
|
+
P2: '非核心功能异常,影响范围有限,可在当天内处理',
|
|
1988
|
+
};
|
|
1989
|
+
if (options.postmortem) {
|
|
1990
|
+
const prompt = `你是一名 SRE 工程师,请帮我撰写事后复盘报告(Postmortem)。
|
|
1991
|
+
|
|
1992
|
+
## 事故信息
|
|
1993
|
+
- **严重程度**:${severity}(${severityGuide[severity] || '未知'})
|
|
1994
|
+
- **故障描述**:${desc}
|
|
1995
|
+
|
|
1996
|
+
## 复盘报告结构
|
|
1997
|
+
|
|
1998
|
+
### 1. 事故摘要
|
|
1999
|
+
(一段话描述:发生了什么、影响了什么、持续多久)
|
|
2000
|
+
|
|
2001
|
+
### 2. 事故时间线
|
|
2002
|
+
| 时间 | 事件 |
|
|
2003
|
+
|------|------|
|
|
2004
|
+
| | |
|
|
2005
|
+
|
|
2006
|
+
### 3. 根本原因分析(5 Why)
|
|
2007
|
+
- Why 1:为什么故障发生?
|
|
2008
|
+
- Why 2:...
|
|
2009
|
+
- Why 3:...
|
|
2010
|
+
- Why 4:...
|
|
2011
|
+
- Why 5:根本原因
|
|
2012
|
+
|
|
2013
|
+
### 4. 影响评估
|
|
2014
|
+
- 受影响用户数 / 请求数
|
|
2015
|
+
- 业务损失(可量化的部分)
|
|
2016
|
+
- SLA 影响
|
|
2017
|
+
|
|
2018
|
+
### 5. 解决措施
|
|
2019
|
+
- **临时措施**(已执行):
|
|
2020
|
+
- **永久修复**(已执行或计划):
|
|
2021
|
+
|
|
2022
|
+
### 6. 改进措施(Action Items)
|
|
2023
|
+
| 措施 | 负责人 | 截止日期 | 优先级 |
|
|
2024
|
+
|------|--------|----------|--------|
|
|
2025
|
+
|
|
2026
|
+
### 7. 经验教训
|
|
2027
|
+
|
|
2028
|
+
请根据上述信息,帮我补全这份复盘报告,对空白处提供建议填写方向。`;
|
|
2029
|
+
if (options.copy !== false) {
|
|
2030
|
+
copyToClipboard(prompt);
|
|
2031
|
+
console.log(`\n✅ 复盘报告提示词已复制到剪贴板\n`);
|
|
2032
|
+
}
|
|
2033
|
+
else {
|
|
2034
|
+
console.log('\n' + prompt + '\n');
|
|
2035
|
+
}
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
const prompt = `你是一名经验丰富的 SRE / On-Call 工程师,正在处理一起生产故障。
|
|
2039
|
+
|
|
2040
|
+
## 故障信息
|
|
2041
|
+
- **严重程度**:${severity}(${severityGuide[severity] || '未知'})
|
|
2042
|
+
- **当前状态**:正在响应中
|
|
2043
|
+
|
|
2044
|
+
## 故障现象
|
|
2045
|
+
${desc}
|
|
2046
|
+
|
|
2047
|
+
## 请按照以下框架协助我进行故障排查:
|
|
2048
|
+
|
|
2049
|
+
### Phase 1:快速评估(2 分钟内)
|
|
2050
|
+
1. **现象确认**:故障现象是否与描述一致?需要补充哪些关键信息?
|
|
2051
|
+
2. **影响范围**:受影响的服务 / 用户 / 区域
|
|
2052
|
+
3. **初步假设**:最可能的 3 个根因(按概率排序)
|
|
2053
|
+
|
|
2054
|
+
### Phase 2:假设验证(逐一排查)
|
|
2055
|
+
对每个假设:
|
|
2056
|
+
- 验证方法(命令 / 指标 / 日志查询)
|
|
2057
|
+
- 期望结果 vs 实际结果
|
|
2058
|
+
- 结论(排除 / 确认)
|
|
2059
|
+
|
|
2060
|
+
### Phase 3:解决方案
|
|
2061
|
+
- **立即止血**(最快恢复服务的临时措施)
|
|
2062
|
+
- **根治方案**(解决根本原因)
|
|
2063
|
+
- **预防措施**(防止复现)
|
|
2064
|
+
|
|
2065
|
+
### Phase 4:复盘准备
|
|
2066
|
+
- 关键时间节点记录
|
|
2067
|
+
- 待填写的 Postmortem 模板框架
|
|
2068
|
+
|
|
2069
|
+
请开始评估,首先告诉我需要哪些关键信息来锁定根因。`;
|
|
2070
|
+
if (options.copy !== false) {
|
|
2071
|
+
copyToClipboard(prompt);
|
|
2072
|
+
console.log(`\n✅ 故障排查提示词已复制到剪贴板(${severity})\n`);
|
|
2073
|
+
}
|
|
2074
|
+
else {
|
|
2075
|
+
console.log('\n' + prompt + '\n');
|
|
2076
|
+
}
|
|
2077
|
+
});
|
|
2078
|
+
// ─── schedule 命令(T14)────────────────────────────────────────────────────
|
|
2079
|
+
const scheduleCmd = program
|
|
2080
|
+
.command('schedule')
|
|
2081
|
+
.description('定时任务管理:将 ethan 命令加入 crontab(add/list/remove)');
|
|
2082
|
+
scheduleCmd
|
|
2083
|
+
.command('add <command>')
|
|
2084
|
+
.description('将 ethan 命令添加到 crontab(如:ethan standup --no-copy)')
|
|
2085
|
+
.option('--cron <expr>', 'cron 表达式(默认:每天早上 9:00)', '0 9 * * 1-5')
|
|
2086
|
+
.action((command, options) => {
|
|
2087
|
+
if (process.platform === 'win32') {
|
|
2088
|
+
console.error('❌ schedule 命令暂不支持 Windows(请使用任务计划程序)');
|
|
2089
|
+
process.exit(1);
|
|
2090
|
+
}
|
|
2091
|
+
const ethanBin = process.execPath.replace('node', '') + 'ethan';
|
|
2092
|
+
const fullCmd = `ethan ${command}`;
|
|
2093
|
+
const cronLine = `${options.cron} ${fullCmd} # ethan-schedule`;
|
|
2094
|
+
// 读取现有 crontab
|
|
2095
|
+
const existing = (0, child_process_1.spawnSync)('crontab', ['-l'], { encoding: 'utf-8' });
|
|
2096
|
+
const currentCron = existing.status === 0 ? (existing.stdout || '') : '';
|
|
2097
|
+
if (currentCron.includes(fullCmd)) {
|
|
2098
|
+
console.log(`\n⚠️ 已存在相同的定时任务:${fullCmd}\n`);
|
|
2099
|
+
process.exit(0);
|
|
2100
|
+
}
|
|
2101
|
+
const newCron = (currentCron.trimEnd() + '\n' + cronLine + '\n').trimStart();
|
|
2102
|
+
const result = (0, child_process_1.spawnSync)('crontab', ['-'], { input: newCron, encoding: 'utf-8' });
|
|
2103
|
+
if (result.status !== 0) {
|
|
2104
|
+
console.error(`❌ 添加 crontab 失败:${result.stderr}`);
|
|
2105
|
+
process.exit(1);
|
|
2106
|
+
}
|
|
2107
|
+
console.log(`\n✅ 已添加定时任务`);
|
|
2108
|
+
console.log(` 时间:${options.cron} (周一至周五 09:00)`);
|
|
2109
|
+
console.log(` 命令:${fullCmd}`);
|
|
2110
|
+
console.log(`\n💡 使用 ethan schedule list 查看所有任务\n`);
|
|
2111
|
+
void ethanBin;
|
|
2112
|
+
});
|
|
2113
|
+
scheduleCmd
|
|
2114
|
+
.command('list')
|
|
2115
|
+
.description('列出所有 ethan 定时任务')
|
|
2116
|
+
.action(() => {
|
|
2117
|
+
if (process.platform === 'win32') {
|
|
2118
|
+
console.error('❌ schedule 命令暂不支持 Windows');
|
|
2119
|
+
process.exit(1);
|
|
2120
|
+
}
|
|
2121
|
+
const result = (0, child_process_1.spawnSync)('crontab', ['-l'], { encoding: 'utf-8' });
|
|
2122
|
+
const lines = (result.stdout || '').split('\n').filter((l) => l.includes('# ethan-schedule'));
|
|
2123
|
+
if (lines.length === 0) {
|
|
2124
|
+
console.log('\n📋 暂无 ethan 定时任务。使用 ethan schedule add 添加。\n');
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
console.log(`\n📋 ethan 定时任务(${lines.length} 个)\n`);
|
|
2128
|
+
lines.forEach((l, i) => {
|
|
2129
|
+
const parts = l.replace('# ethan-schedule', '').trim().split(/\s+/);
|
|
2130
|
+
const cronExpr = parts.slice(0, 5).join(' ');
|
|
2131
|
+
const cmd = parts.slice(5).join(' ');
|
|
2132
|
+
console.log(` ${i + 1}. [${cronExpr}] ${cmd}`);
|
|
2133
|
+
});
|
|
2134
|
+
console.log('');
|
|
2135
|
+
});
|
|
2136
|
+
scheduleCmd
|
|
2137
|
+
.command('remove <command>')
|
|
2138
|
+
.description('移除 ethan 定时任务(匹配命令关键字)')
|
|
2139
|
+
.action((command) => {
|
|
2140
|
+
if (process.platform === 'win32') {
|
|
2141
|
+
console.error('❌ schedule 命令暂不支持 Windows');
|
|
2142
|
+
process.exit(1);
|
|
2143
|
+
}
|
|
2144
|
+
const result = (0, child_process_1.spawnSync)('crontab', ['-l'], { encoding: 'utf-8' });
|
|
2145
|
+
const currentCron = result.status === 0 ? (result.stdout || '') : '';
|
|
2146
|
+
const newCron = currentCron
|
|
2147
|
+
.split('\n')
|
|
2148
|
+
.filter((l) => !(l.includes(command) && l.includes('# ethan-schedule')))
|
|
2149
|
+
.join('\n');
|
|
2150
|
+
if (newCron === currentCron) {
|
|
2151
|
+
console.log(`\n⚠️ 未找到匹配的定时任务:${command}\n`);
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
const writeResult = (0, child_process_1.spawnSync)('crontab', ['-'], { input: newCron, encoding: 'utf-8' });
|
|
2155
|
+
if (writeResult.status !== 0) {
|
|
2156
|
+
console.error(`❌ 移除失败:${writeResult.stderr}`);
|
|
2157
|
+
process.exit(1);
|
|
2158
|
+
}
|
|
2159
|
+
console.log(`\n✅ 已移除包含 "${command}" 的定时任务\n`);
|
|
2160
|
+
});
|
|
2161
|
+
// ─── init --hooks 命令(T15)────────────────────────────────────────────────
|
|
2162
|
+
// init 命令已存在,在此添加 --hooks 支持(通过修改现有 init action)
|
|
2163
|
+
// 由于无法直接 patch 已注册的 action,以独立命令方式实现 hooks 管理
|
|
2164
|
+
const hooksCmd = program
|
|
2165
|
+
.command('hooks')
|
|
2166
|
+
.description('Git Hook 集成:将 ethan 命令注入到 git hooks(install/remove/list)');
|
|
2167
|
+
hooksCmd
|
|
2168
|
+
.command('install')
|
|
2169
|
+
.description('安装 ethan git hooks(pre-commit / commit-msg / post-merge)')
|
|
2170
|
+
.option('--pre-commit', '在 pre-commit 时运行 ethan scan(扫描代码健康)')
|
|
2171
|
+
.option('--commit-msg', '在 commit-msg 时提示 ethan commit(生成 commit 建议)')
|
|
2172
|
+
.option('--post-merge', '在 post-merge 时运行 ethan standup(更新站会稿)')
|
|
2173
|
+
.option('--all', '安装全部三个 hooks')
|
|
2174
|
+
.action((options) => {
|
|
2175
|
+
if (!(0, utils_1.isGitRepo)()) {
|
|
2176
|
+
console.error('❌ 当前目录不是 Git 仓库');
|
|
2177
|
+
process.exit(1);
|
|
2178
|
+
}
|
|
2179
|
+
const gitDir = (0, child_process_1.spawnSync)('git', ['rev-parse', '--git-dir'], { encoding: 'utf-8' }).stdout.trim();
|
|
2180
|
+
const hooksDir = path.join(process.cwd(), gitDir, 'hooks');
|
|
2181
|
+
if (!fs.existsSync(hooksDir))
|
|
2182
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
2183
|
+
const installed = [];
|
|
2184
|
+
if (options.all || options.preCommit) {
|
|
2185
|
+
const hookPath = path.join(hooksDir, 'pre-commit');
|
|
2186
|
+
const hookContent = `#!/bin/sh
|
|
2187
|
+
# ethan-hook: pre-commit
|
|
2188
|
+
# 运行 ethan scan 检查代码健康(仅警告,不阻止提交)
|
|
2189
|
+
if command -v ethan &> /dev/null; then
|
|
2190
|
+
ethan scan --no-copy 2>&1 | grep -E "⚠️|❌" || true
|
|
2191
|
+
fi
|
|
2192
|
+
`;
|
|
2193
|
+
fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
|
|
2194
|
+
installed.push('pre-commit');
|
|
2195
|
+
}
|
|
2196
|
+
if (options.all || options.commitMsg) {
|
|
2197
|
+
const hookPath = path.join(hooksDir, 'commit-msg');
|
|
2198
|
+
const hookContent = `#!/bin/sh
|
|
2199
|
+
# ethan-hook: commit-msg
|
|
2200
|
+
# 如果 commit message 太短(<10字符),提示使用 ethan commit
|
|
2201
|
+
MSG=$(cat "$1")
|
|
2202
|
+
if [ \${#MSG} -lt 10 ]; then
|
|
2203
|
+
echo "⚠️ Commit message 太短(<10字符)。提示:运行 'ethan commit' 生成规范的 Commit Message"
|
|
2204
|
+
fi
|
|
2205
|
+
exit 0
|
|
2206
|
+
`;
|
|
2207
|
+
fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
|
|
2208
|
+
installed.push('commit-msg');
|
|
2209
|
+
}
|
|
2210
|
+
if (options.all || options.postMerge) {
|
|
2211
|
+
const hookPath = path.join(hooksDir, 'post-merge');
|
|
2212
|
+
const hookContent = `#!/bin/sh
|
|
2213
|
+
# ethan-hook: post-merge
|
|
2214
|
+
# merge 后自动提示生成站会稿
|
|
2215
|
+
if command -v ethan &> /dev/null; then
|
|
2216
|
+
echo "\\n💡 已完成 merge,运行 'ethan standup' 生成站会稿"
|
|
2217
|
+
fi
|
|
2218
|
+
`;
|
|
2219
|
+
fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
|
|
2220
|
+
installed.push('post-merge');
|
|
2221
|
+
}
|
|
2222
|
+
if (installed.length === 0) {
|
|
2223
|
+
console.log('\n💡 请指定要安装的 hook:--pre-commit | --commit-msg | --post-merge | --all\n');
|
|
2224
|
+
process.exit(0);
|
|
2225
|
+
}
|
|
2226
|
+
console.log(`\n✅ 已安装 ${installed.length} 个 git hook:${installed.join(', ')}`);
|
|
2227
|
+
console.log(` 路径:${hooksDir}\n`);
|
|
2228
|
+
});
|
|
2229
|
+
hooksCmd
|
|
2230
|
+
.command('list')
|
|
2231
|
+
.description('列出当前项目已安装的 ethan git hooks')
|
|
2232
|
+
.action(() => {
|
|
2233
|
+
if (!(0, utils_1.isGitRepo)()) {
|
|
2234
|
+
console.error('❌ 当前目录不是 Git 仓库');
|
|
2235
|
+
process.exit(1);
|
|
2236
|
+
}
|
|
2237
|
+
const gitDir = (0, child_process_1.spawnSync)('git', ['rev-parse', '--git-dir'], { encoding: 'utf-8' }).stdout.trim();
|
|
2238
|
+
const hooksDir = path.join(process.cwd(), gitDir, 'hooks');
|
|
2239
|
+
const hookNames = ['pre-commit', 'commit-msg', 'post-merge', 'pre-push'];
|
|
2240
|
+
console.log('\n🪝 ethan git hooks\n');
|
|
2241
|
+
for (const hook of hookNames) {
|
|
2242
|
+
const hookPath = path.join(hooksDir, hook);
|
|
2243
|
+
if (fs.existsSync(hookPath)) {
|
|
2244
|
+
const content = fs.readFileSync(hookPath, 'utf-8');
|
|
2245
|
+
const isEthan = content.includes('ethan-hook');
|
|
2246
|
+
console.log(` ${isEthan ? '✅' : '⚪'} ${hook}${isEthan ? ' [ethan]' : ''}`);
|
|
2247
|
+
}
|
|
2248
|
+
else {
|
|
2249
|
+
console.log(` ⬜ ${hook} (未安装)`);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
console.log('');
|
|
2253
|
+
});
|
|
2254
|
+
hooksCmd
|
|
2255
|
+
.command('remove [hook]')
|
|
2256
|
+
.description('移除 ethan git hooks(不指定则移除所有)')
|
|
2257
|
+
.action((hook) => {
|
|
2258
|
+
if (!(0, utils_1.isGitRepo)()) {
|
|
2259
|
+
console.error('❌ 当前目录不是 Git 仓库');
|
|
2260
|
+
process.exit(1);
|
|
2261
|
+
}
|
|
2262
|
+
const gitDir = (0, child_process_1.spawnSync)('git', ['rev-parse', '--git-dir'], { encoding: 'utf-8' }).stdout.trim();
|
|
2263
|
+
const hooksDir = path.join(process.cwd(), gitDir, 'hooks');
|
|
2264
|
+
const targets = hook ? [hook] : ['pre-commit', 'commit-msg', 'post-merge'];
|
|
2265
|
+
const removed = [];
|
|
2266
|
+
for (const h of targets) {
|
|
2267
|
+
const hookPath = path.join(hooksDir, h);
|
|
2268
|
+
if (fs.existsSync(hookPath)) {
|
|
2269
|
+
const content = fs.readFileSync(hookPath, 'utf-8');
|
|
2270
|
+
if (content.includes('ethan-hook')) {
|
|
2271
|
+
fs.unlinkSync(hookPath);
|
|
2272
|
+
removed.push(h);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
if (removed.length === 0) {
|
|
2277
|
+
console.log('\n⚠️ 未找到 ethan git hooks\n');
|
|
2278
|
+
}
|
|
2279
|
+
else {
|
|
2280
|
+
console.log(`\n✅ 已移除 ${removed.length} 个 hook:${removed.join(', ')}\n`);
|
|
2281
|
+
}
|
|
2282
|
+
});
|
|
2283
|
+
// ─── memory 命令(T16 Skill Memory)────────────────────────────────────────
|
|
2284
|
+
// 自动归档工作流输出,支持搜索、展示、导出
|
|
2285
|
+
// 存储位置:~/.ethan-memory/ (全局) 或 .ethan/memory/ (项目级)
|
|
2286
|
+
const GLOBAL_MEMORY_DIR = path.join(os.homedir(), '.ethan-memory');
|
|
2287
|
+
function getMemoryDir(global, cwd) {
|
|
2288
|
+
return global ? GLOBAL_MEMORY_DIR : path.join(cwd || process.cwd(), '.ethan', 'memory');
|
|
2289
|
+
}
|
|
2290
|
+
function loadMemoryEntries(dir) {
|
|
2291
|
+
if (!fs.existsSync(dir))
|
|
2292
|
+
return [];
|
|
2293
|
+
return fs
|
|
2294
|
+
.readdirSync(dir)
|
|
2295
|
+
.filter((f) => f.endsWith('.json'))
|
|
2296
|
+
.map((f) => {
|
|
2297
|
+
try {
|
|
2298
|
+
return JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
|
|
2299
|
+
}
|
|
2300
|
+
catch {
|
|
2301
|
+
return null;
|
|
2302
|
+
}
|
|
2303
|
+
})
|
|
2304
|
+
.filter((e) => e !== null)
|
|
2305
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
2306
|
+
}
|
|
2307
|
+
function saveMemoryEntry(entry, dir) {
|
|
2308
|
+
if (!fs.existsSync(dir))
|
|
2309
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2310
|
+
const file = path.join(dir, `${entry.id}.json`);
|
|
2311
|
+
fs.writeFileSync(file, JSON.stringify(entry, null, 2), 'utf-8');
|
|
2312
|
+
}
|
|
2313
|
+
/** 归档工作流会话 summary 到 memory(workflow done 后自动调用) */
|
|
2314
|
+
function archiveWorkflowToMemory(sessionId, skillId, pipelineName, summary, cwd) {
|
|
2315
|
+
const dir = getMemoryDir(false, cwd);
|
|
2316
|
+
const entry = {
|
|
2317
|
+
id: `${Date.now().toString(36)}-${skillId}`,
|
|
2318
|
+
type: 'workflow',
|
|
2319
|
+
skillId,
|
|
2320
|
+
pipelineId: pipelineName,
|
|
2321
|
+
title: `[${pipelineName}] ${skillId} — ${summary.slice(0, 60)}`,
|
|
2322
|
+
content: summary,
|
|
2323
|
+
tags: [skillId, pipelineName],
|
|
2324
|
+
project: path.basename(cwd),
|
|
2325
|
+
createdAt: new Date().toISOString(),
|
|
2326
|
+
};
|
|
2327
|
+
saveMemoryEntry(entry, dir);
|
|
2328
|
+
}
|
|
2329
|
+
const memoryCmd = program.command('memory').description('Skill Memory:归档、搜索、展示 AI 工作产出');
|
|
2330
|
+
memoryCmd
|
|
2331
|
+
.command('add <content>')
|
|
2332
|
+
.description('手动添加一条记忆(内容支持多行,用引号包裹)')
|
|
2333
|
+
.option('--title <title>', '记忆标题')
|
|
2334
|
+
.option('--tags <tags>', '标签(逗号分隔)')
|
|
2335
|
+
.option('--global', '存至全局 ~/.ethan-memory/(默认存项目级 .ethan/memory/)')
|
|
2336
|
+
.action((content, options) => {
|
|
2337
|
+
const dir = getMemoryDir(!!options.global);
|
|
2338
|
+
const tags = options.tags ? options.tags.split(',').map((t) => t.trim()) : [];
|
|
2339
|
+
const entry = {
|
|
2340
|
+
id: Date.now().toString(36),
|
|
2341
|
+
type: 'manual',
|
|
2342
|
+
title: options.title || content.slice(0, 60),
|
|
2343
|
+
content,
|
|
2344
|
+
tags,
|
|
2345
|
+
project: path.basename(process.cwd()),
|
|
2346
|
+
createdAt: new Date().toISOString(),
|
|
2347
|
+
};
|
|
2348
|
+
saveMemoryEntry(entry, dir);
|
|
2349
|
+
console.log(`\n✅ 记忆已保存:${entry.title}\n ID: ${entry.id}\n`);
|
|
2350
|
+
});
|
|
2351
|
+
memoryCmd
|
|
2352
|
+
.command('search <keyword>')
|
|
2353
|
+
.description('在记忆库中搜索关键词(标题 + 内容 + 标签)')
|
|
2354
|
+
.option('--global', '搜索全局记忆库')
|
|
2355
|
+
.option('--tag <tag>', '按标签过滤')
|
|
2356
|
+
.option('-n, --limit <n>', '最多显示 N 条', '10')
|
|
2357
|
+
.action((keyword, options) => {
|
|
2358
|
+
const dir = getMemoryDir(!!options.global);
|
|
2359
|
+
const entries = loadMemoryEntries(dir);
|
|
2360
|
+
const kw = keyword.toLowerCase();
|
|
2361
|
+
const limit = parseInt(options.limit, 10) || 10;
|
|
2362
|
+
let results = entries.filter((e) => {
|
|
2363
|
+
const matchKw = e.title.toLowerCase().includes(kw) ||
|
|
2364
|
+
e.content.toLowerCase().includes(kw) ||
|
|
2365
|
+
e.tags.some((t) => t.toLowerCase().includes(kw));
|
|
2366
|
+
const matchTag = options.tag ? e.tags.includes(options.tag) : true;
|
|
2367
|
+
return matchKw && matchTag;
|
|
2368
|
+
});
|
|
2369
|
+
results = results.slice(0, limit);
|
|
2370
|
+
if (results.length === 0) {
|
|
2371
|
+
console.log(`\n🔍 未找到匹配 "${keyword}" 的记忆\n`);
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
console.log(`\n🔍 找到 ${results.length} 条记忆(关键词:"${keyword}")\n`);
|
|
2375
|
+
console.log('─'.repeat(60));
|
|
2376
|
+
for (const e of results) {
|
|
2377
|
+
const preview = e.content.length > 100 ? e.content.slice(0, 100) + '…' : e.content;
|
|
2378
|
+
console.log(`\n 📌 ${e.title}`);
|
|
2379
|
+
console.log(` ID: ${e.id} | ${e.createdAt.slice(0, 10)} | 标签:${e.tags.join(', ') || '无'}`);
|
|
2380
|
+
console.log(` ${preview}`);
|
|
2381
|
+
}
|
|
2382
|
+
console.log('\n' + '─'.repeat(60));
|
|
2383
|
+
console.log(`\n💡 用 ethan memory show <id> 查看完整内容\n`);
|
|
2384
|
+
});
|
|
2385
|
+
memoryCmd
|
|
2386
|
+
.command('show <id>')
|
|
2387
|
+
.description('展示一条记忆的完整内容')
|
|
2388
|
+
.option('--global', '从全局记忆库读取')
|
|
2389
|
+
.option('--no-copy', '不复制到剪贴板')
|
|
2390
|
+
.action((id, options) => {
|
|
2391
|
+
const dir = getMemoryDir(!!options.global);
|
|
2392
|
+
const entries = loadMemoryEntries(dir);
|
|
2393
|
+
const entry = entries.find((e) => e.id === id || e.id.startsWith(id));
|
|
2394
|
+
if (!entry) {
|
|
2395
|
+
console.error(`❌ 未找到 ID 为 "${id}" 的记忆`);
|
|
2396
|
+
process.exit(1);
|
|
2397
|
+
}
|
|
2398
|
+
console.log(`\n📖 ${entry.title}`);
|
|
2399
|
+
console.log(` 类型:${entry.type} | ${entry.createdAt.slice(0, 19).replace('T', ' ')}`);
|
|
2400
|
+
if (entry.tags.length > 0)
|
|
2401
|
+
console.log(` 标签:${entry.tags.join(', ')}`);
|
|
2402
|
+
console.log('\n' + '─'.repeat(60) + '\n');
|
|
2403
|
+
console.log(entry.content);
|
|
2404
|
+
console.log('\n' + '─'.repeat(60));
|
|
2405
|
+
if (options.copy !== false) {
|
|
2406
|
+
copyToClipboard(entry.content);
|
|
2407
|
+
console.log('\n✅ 内容已复制到剪贴板\n');
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
2410
|
+
memoryCmd
|
|
2411
|
+
.command('list')
|
|
2412
|
+
.description('列出最近的记忆条目')
|
|
2413
|
+
.option('--global', '列出全局记忆库')
|
|
2414
|
+
.option('-n, --limit <n>', '显示条数', '20')
|
|
2415
|
+
.option('--tag <tag>', '按标签过滤')
|
|
2416
|
+
.action((options) => {
|
|
2417
|
+
const dir = getMemoryDir(!!options.global);
|
|
2418
|
+
let entries = loadMemoryEntries(dir);
|
|
2419
|
+
if (options.tag)
|
|
2420
|
+
entries = entries.filter((e) => e.tags.includes(options.tag));
|
|
2421
|
+
entries = entries.slice(0, parseInt(options.limit, 10) || 20);
|
|
2422
|
+
if (entries.length === 0) {
|
|
2423
|
+
console.log('\n📋 记忆库为空。使用 ethan memory add 或工作流自动归档。\n');
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
console.log(`\n🧠 Skill Memory(${options.global ? '全局' : '项目'},${entries.length} 条)\n`);
|
|
2427
|
+
console.log('─'.repeat(60));
|
|
2428
|
+
for (const e of entries) {
|
|
2429
|
+
const icon = e.type === 'workflow' ? '🔄' : e.type === 'skill' ? '⚡' : '📝';
|
|
2430
|
+
console.log(` ${icon} [${e.id}] ${e.title.slice(0, 55)}`);
|
|
2431
|
+
console.log(` ${e.createdAt.slice(0, 10)} ${e.tags.join(' #') ? '#' + e.tags.join(' #') : ''}`);
|
|
2432
|
+
}
|
|
2433
|
+
console.log('\n' + '─'.repeat(60));
|
|
2434
|
+
console.log('\n用 ethan memory search <keyword> 搜索,ethan memory show <id> 查看详情\n');
|
|
2435
|
+
});
|
|
2436
|
+
memoryCmd
|
|
2437
|
+
.command('export')
|
|
2438
|
+
.description('导出记忆库为 Markdown 文件')
|
|
2439
|
+
.option('--global', '导出全局记忆库')
|
|
2440
|
+
.option('--out <file>', '输出文件路径', 'ethan-memory-export.md')
|
|
2441
|
+
.option('--tag <tag>', '只导出指定标签')
|
|
2442
|
+
.action((options) => {
|
|
2443
|
+
const dir = getMemoryDir(!!options.global);
|
|
2444
|
+
let entries = loadMemoryEntries(dir);
|
|
2445
|
+
if (options.tag)
|
|
2446
|
+
entries = entries.filter((e) => e.tags.includes(options.tag));
|
|
2447
|
+
if (entries.length === 0) {
|
|
2448
|
+
console.log('\n📋 记忆库为空,无需导出。\n');
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
const lines = [
|
|
2452
|
+
`# Ethan Memory Export`,
|
|
2453
|
+
``,
|
|
2454
|
+
`- **导出时间**:${new Date().toISOString().slice(0, 19).replace('T', ' ')}`,
|
|
2455
|
+
`- **条目数**:${entries.length}`,
|
|
2456
|
+
`- **范围**:${options.global ? '全局' : '项目级'}`,
|
|
2457
|
+
``,
|
|
2458
|
+
`---`,
|
|
2459
|
+
``,
|
|
2460
|
+
];
|
|
2461
|
+
for (const e of entries) {
|
|
2462
|
+
lines.push(`## ${e.title}`);
|
|
2463
|
+
lines.push(``);
|
|
2464
|
+
lines.push(`- **ID**: \`${e.id}\``);
|
|
2465
|
+
lines.push(`- **类型**: ${e.type}`);
|
|
2466
|
+
lines.push(`- **时间**: ${e.createdAt.slice(0, 10)}`);
|
|
2467
|
+
if (e.tags.length > 0)
|
|
2468
|
+
lines.push(`- **标签**: ${e.tags.map((t) => `\`${t}\``).join(' ')}`);
|
|
2469
|
+
lines.push(``);
|
|
2470
|
+
lines.push(e.content);
|
|
2471
|
+
lines.push(``);
|
|
2472
|
+
lines.push(`---`);
|
|
2473
|
+
lines.push(``);
|
|
2474
|
+
}
|
|
2475
|
+
const outPath = path.resolve(process.cwd(), options.out);
|
|
2476
|
+
fs.writeFileSync(outPath, lines.join('\n'), 'utf-8');
|
|
2477
|
+
console.log(`\n✅ 已导出 ${entries.length} 条记忆到:${outPath}\n`);
|
|
2478
|
+
});
|
|
2479
|
+
memoryCmd
|
|
2480
|
+
.command('remove <id>')
|
|
2481
|
+
.description('删除一条记忆')
|
|
2482
|
+
.option('--global', '从全局记忆库删除')
|
|
2483
|
+
.action((id, options) => {
|
|
2484
|
+
const dir = getMemoryDir(!!options.global);
|
|
2485
|
+
const entries = loadMemoryEntries(dir);
|
|
2486
|
+
const entry = entries.find((e) => e.id === id || e.id.startsWith(id));
|
|
2487
|
+
if (!entry) {
|
|
2488
|
+
console.error(`❌ 未找到 ID 为 "${id}" 的记忆`);
|
|
2489
|
+
process.exit(1);
|
|
2490
|
+
}
|
|
2491
|
+
const filePath = path.join(dir, `${entry.id}.json`);
|
|
2492
|
+
fs.unlinkSync(filePath);
|
|
2493
|
+
console.log(`\n✅ 已删除:${entry.title}\n`);
|
|
2494
|
+
});
|
|
2495
|
+
// ─── estimate 命令(T17 Estimation)────────────────────────────────────────
|
|
2496
|
+
program
|
|
2497
|
+
.command('estimate')
|
|
2498
|
+
.description('任务工时估算:用三点估算法生成结构化评估提示词')
|
|
2499
|
+
.option('-t, --task <task>', '任务描述')
|
|
2500
|
+
.option('--style <style>', '估算方式:story-points | hours | t-shirt(S/M/L/XL)', 'hours')
|
|
2501
|
+
.option('--team <size>', '团队规模(影响并行度评估)', '1')
|
|
2502
|
+
.option('--no-copy', '不复制到剪贴板,直接打印')
|
|
2503
|
+
.action((options) => {
|
|
2504
|
+
const task = options.task || '(请描述要估算的任务或功能)';
|
|
2505
|
+
const style = options.style || 'hours';
|
|
2506
|
+
const styleGuide = {
|
|
2507
|
+
hours: '用工时(人天)估算,分 乐观/标准/悲观 三个场景,给出加权平均(PERT 公式:(O + 4M + P) / 6)',
|
|
2508
|
+
'story-points': '用故事点(Fibonacci 数列:1/2/3/5/8/13/21)估算,基于复杂度和不确定性',
|
|
2509
|
+
't-shirt': '用 T 恤尺码(XS / S / M / L / XL / XXL)给出快速相对估算,适合 backlog 梳理',
|
|
2510
|
+
};
|
|
2511
|
+
const prompt = `你是一名有丰富经验的工程估算专家,请对以下任务进行结构化工时评估。
|
|
2512
|
+
|
|
2513
|
+
## 任务描述
|
|
2514
|
+
${task}
|
|
2515
|
+
|
|
2516
|
+
## 团队信息
|
|
2517
|
+
- 团队规模:${options.team} 人
|
|
2518
|
+
- 估算单位:${styleGuide[style] || styleGuide['hours']}
|
|
2519
|
+
|
|
2520
|
+
## 请按以下结构输出评估结果:
|
|
2521
|
+
|
|
2522
|
+
### 1. 任务拆解(Work Breakdown Structure)
|
|
2523
|
+
将任务分解为子任务,每个子任务单独估算
|
|
2524
|
+
|
|
2525
|
+
### 2. 风险识别
|
|
2526
|
+
| 风险项 | 概率 | 影响 | 缓解措施 |
|
|
2527
|
+
|--------|------|------|----------|
|
|
2528
|
+
|
|
2529
|
+
### 3. 估算结果
|
|
2530
|
+
| 子任务 | 乐观 | 标准 | 悲观 | 加权平均 |
|
|
2531
|
+
|--------|------|------|------|----------|
|
|
2532
|
+
| ... | | | | |
|
|
2533
|
+
| **合计** | | | | **X.X 天** |
|
|
2534
|
+
|
|
2535
|
+
### 4. 关键假设
|
|
2536
|
+
(哪些条件成立,估算才有效)
|
|
2537
|
+
|
|
2538
|
+
### 5. 建议缓冲
|
|
2539
|
+
(基于风险,建议增加 X% 缓冲,总估算约 Y 天)
|
|
2540
|
+
|
|
2541
|
+
### 6. 置信区间
|
|
2542
|
+
(80% 置信度范围:X - Y 天)
|
|
2543
|
+
|
|
2544
|
+
请给出完整评估,并在最后说明最大不确定因素是什么。`;
|
|
2545
|
+
if (options.copy !== false) {
|
|
2546
|
+
copyToClipboard(prompt);
|
|
2547
|
+
console.log(`\n✅ 估算提示词已复制到剪贴板(${style} 模式)\n`);
|
|
2548
|
+
}
|
|
2549
|
+
else {
|
|
2550
|
+
console.log('\n' + prompt + '\n');
|
|
2551
|
+
}
|
|
2552
|
+
});
|
|
2553
|
+
// ─── retro 命令(T17 Retrospective)────────────────────────────────────────
|
|
2554
|
+
program
|
|
2555
|
+
.command('retro')
|
|
2556
|
+
.description('迭代复盘:生成回顾会议提示词 / 总结报告')
|
|
2557
|
+
.option('--sprint <name>', '迭代/Sprint 名称或编号')
|
|
2558
|
+
.option('--format <fmt>', '格式:4l(4L 回顾)| starfish(海星)| mad-sad-glad | start-stop-continue', '4l')
|
|
2559
|
+
.option('--from-workflow', '从当前工作流会话读取上下文(自动填充完成内容)')
|
|
2560
|
+
.option('--no-copy', '不复制到剪贴板,直接打印')
|
|
2561
|
+
.action(async (options) => {
|
|
2562
|
+
const sprint = options.sprint || '本次迭代';
|
|
2563
|
+
const fmt = options.format || '4l';
|
|
2564
|
+
let workflowContext = '';
|
|
2565
|
+
if (options.fromWorkflow) {
|
|
2566
|
+
const { loadSession, calcProgress } = await Promise.resolve().then(() => __importStar(require('../workflow/state')));
|
|
2567
|
+
const session = loadSession(process.cwd());
|
|
2568
|
+
if (session) {
|
|
2569
|
+
const progress = calcProgress(session);
|
|
2570
|
+
const doneSteps = session.steps
|
|
2571
|
+
.filter((s) => s.status === 'done')
|
|
2572
|
+
.map((s) => `- ${s.skillId}:${s.summary || '(无摘要)'}`)
|
|
2573
|
+
.join('\n');
|
|
2574
|
+
workflowContext = `\n## 工作流上下文\n- Pipeline:${session.pipelineName}\n- 进度:${progress}%\n- 已完成步骤:\n${doneSteps}\n`;
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
const formatGuide = {
|
|
2578
|
+
'4l': `使用 **4L 回顾框架**:
|
|
2579
|
+
- **Liked(喜欢的)**:哪些做得好、值得保留?
|
|
2580
|
+
- **Learned(学到的)**:获得了哪些新知识或经验?
|
|
2581
|
+
- **Lacked(缺少的)**:哪些该有但没有的?
|
|
2582
|
+
- **Longed For(期望的)**:希望在下个迭代中改进的?`,
|
|
2583
|
+
starfish: `使用 **海星回顾框架**:
|
|
2584
|
+
- **Keep(继续)**:效果好,继续做
|
|
2585
|
+
- **Less(减少)**:有一定效果,但可以少做
|
|
2586
|
+
- **More(增加)**:效果好,应该多做
|
|
2587
|
+
- **Start(开始)**:还没做但应该开始
|
|
2588
|
+
- **Stop(停止)**:没有效果,应该停止`,
|
|
2589
|
+
'mad-sad-glad': `使用 **Mad/Sad/Glad 情绪回顾**:
|
|
2590
|
+
- **Mad(令人烦恼的)**:让团队沮丧或愤怒的事情
|
|
2591
|
+
- **Sad(令人遗憾的)**:可以更好但没做到的
|
|
2592
|
+
- **Glad(令人开心的)**:做得好、值得庆祝的`,
|
|
2593
|
+
'start-stop-continue': `使用 **Start/Stop/Continue 框架**:
|
|
2594
|
+
- **Start**:应该开始做的新事情
|
|
2595
|
+
- **Stop**:应该停止的无效做法
|
|
2596
|
+
- **Continue**:应该继续保持的做法`,
|
|
2597
|
+
};
|
|
2598
|
+
const guide = formatGuide[fmt] || formatGuide['4l'];
|
|
2599
|
+
const prompt = `你是一名敏捷教练,请帮助团队对"${sprint}"进行回顾总结。
|
|
2600
|
+
${workflowContext}
|
|
2601
|
+
## 回顾框架
|
|
2602
|
+
|
|
2603
|
+
${guide}
|
|
2604
|
+
|
|
2605
|
+
## 请输出以下内容:
|
|
2606
|
+
|
|
2607
|
+
### 1. 回顾会议引导提问
|
|
2608
|
+
(针对本框架各维度,给出 3-5 个引导团队讨论的开放性问题)
|
|
2609
|
+
|
|
2610
|
+
### 2. 回顾报告模板
|
|
2611
|
+
(按框架结构,提供可填写的模板,每项有举例说明)
|
|
2612
|
+
|
|
2613
|
+
### 3. 行动项建议格式
|
|
2614
|
+
| 问题 | 根因 | 行动项 | 负责人 | 截止 |
|
|
2615
|
+
|------|------|--------|--------|------|
|
|
2616
|
+
|
|
2617
|
+
### 4. 下次迭代目标(OKR 式)
|
|
2618
|
+
- **目标(O)**:...
|
|
2619
|
+
- **关键结果(KR)**:...
|
|
2620
|
+
|
|
2621
|
+
请以鼓励、积极的基调引导回顾,关注改进而非批评。`;
|
|
2622
|
+
if (options.copy !== false) {
|
|
2623
|
+
copyToClipboard(prompt);
|
|
2624
|
+
console.log(`\n✅ 迭代复盘提示词已复制到剪贴板(${fmt} 框架)\n`);
|
|
2625
|
+
}
|
|
2626
|
+
else {
|
|
2627
|
+
console.log('\n' + prompt + '\n');
|
|
2628
|
+
}
|
|
2629
|
+
});
|
|
2630
|
+
function readStatsV2() {
|
|
2631
|
+
try {
|
|
2632
|
+
if (fs.existsSync(STATS_FILE)) {
|
|
2633
|
+
const raw = JSON.parse(fs.readFileSync(STATS_FILE, 'utf-8'));
|
|
2634
|
+
// 向上兼容旧格式(纯 Record<string, number>)
|
|
2635
|
+
if (raw.usage)
|
|
2636
|
+
return raw;
|
|
2637
|
+
return { usage: raw, streak: { current: 0, best: 0, lastDate: '' }, dailyLog: {} };
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
catch { /* ignore */ }
|
|
2641
|
+
return { usage: {}, streak: { current: 0, best: 0, lastDate: '' }, dailyLog: {} };
|
|
2642
|
+
}
|
|
2643
|
+
function writeStatsV2(data) {
|
|
2644
|
+
try {
|
|
2645
|
+
fs.writeFileSync(STATS_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
2646
|
+
}
|
|
2647
|
+
catch { /* ignore */ }
|
|
2648
|
+
}
|
|
2649
|
+
/** 记录使用并更新连续使用天数 */
|
|
2650
|
+
function trackUsageWithStreak(skillId) {
|
|
2651
|
+
const data = readStatsV2();
|
|
2652
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
2653
|
+
// 使用次数
|
|
2654
|
+
data.usage[skillId] = (data.usage[skillId] || 0) + 1;
|
|
2655
|
+
// 每日日志
|
|
2656
|
+
if (!data.dailyLog[today])
|
|
2657
|
+
data.dailyLog[today] = [];
|
|
2658
|
+
if (!data.dailyLog[today].includes(skillId))
|
|
2659
|
+
data.dailyLog[today].push(skillId);
|
|
2660
|
+
// Streak 计算
|
|
2661
|
+
const lastDate = data.streak.lastDate;
|
|
2662
|
+
if (lastDate === today) {
|
|
2663
|
+
// 当天已经记录,无需更新 streak
|
|
2664
|
+
}
|
|
2665
|
+
else if (lastDate === '') {
|
|
2666
|
+
data.streak.current = 1;
|
|
2667
|
+
data.streak.best = 1;
|
|
2668
|
+
data.streak.lastDate = today;
|
|
2669
|
+
}
|
|
2670
|
+
else {
|
|
2671
|
+
const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
|
|
2672
|
+
if (lastDate === yesterday) {
|
|
2673
|
+
data.streak.current += 1;
|
|
2674
|
+
data.streak.best = Math.max(data.streak.best, data.streak.current);
|
|
2675
|
+
}
|
|
2676
|
+
else {
|
|
2677
|
+
// 断签
|
|
2678
|
+
data.streak.current = 1;
|
|
2679
|
+
}
|
|
2680
|
+
data.streak.lastDate = today;
|
|
2681
|
+
}
|
|
2682
|
+
writeStatsV2(data);
|
|
2683
|
+
}
|
|
2684
|
+
const statsCmd = program.command('stats').description('查看 Skill 使用频次统计和连续使用天数');
|
|
2685
|
+
statsCmd
|
|
2686
|
+
.command('show')
|
|
2687
|
+
.description('显示 ASCII 条形图统计(默认命令)')
|
|
2688
|
+
.option('--reset', '清空统计数据')
|
|
2689
|
+
.action((options) => {
|
|
2690
|
+
if (options.reset) {
|
|
2691
|
+
fs.writeFileSync(STATS_FILE, JSON.stringify({ usage: {}, streak: { current: 0, best: 0, lastDate: '' }, dailyLog: {} }, null, 2), 'utf-8');
|
|
2692
|
+
console.log('\n✅ 统计数据已清空\n');
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
const data = readStatsV2();
|
|
2696
|
+
const usage = data.usage;
|
|
2697
|
+
if (Object.keys(usage).length === 0) {
|
|
2698
|
+
console.log('\n📊 暂无使用记录。运行任意 Skill 后会自动记录。\n');
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
const sorted = Object.entries(usage).sort((a, b) => b[1] - a[1]);
|
|
2702
|
+
const max = sorted[0][1];
|
|
2703
|
+
console.log('\n📊 Skill 使用统计\n');
|
|
2704
|
+
console.log('─'.repeat(60));
|
|
2705
|
+
for (const [id, count] of sorted) {
|
|
2706
|
+
const bar = '█'.repeat(Math.round((count / max) * 30));
|
|
2707
|
+
const skill = index_1.ALL_SKILLS.find((s) => s.id === id);
|
|
2708
|
+
const label = skill ? `${skill.name}` : id;
|
|
2709
|
+
console.log(` ${label.padEnd(14)} ${bar.padEnd(30)} ${count}`);
|
|
2710
|
+
}
|
|
2711
|
+
console.log('─'.repeat(60));
|
|
2712
|
+
console.log(`\n 🔥 连续使用:${data.streak.current} 天 | 最长连续:${data.streak.best} 天\n`);
|
|
2713
|
+
});
|
|
2714
|
+
statsCmd
|
|
2715
|
+
.command('leaderboard')
|
|
2716
|
+
.description('显示使用排行榜 + 连续天数 + 日历热图')
|
|
2717
|
+
.option('--top <n>', '显示前 N 名', '5')
|
|
2718
|
+
.action((options) => {
|
|
2719
|
+
const data = readStatsV2();
|
|
2720
|
+
const usage = data.usage;
|
|
2721
|
+
const top = parseInt(options.top, 10) || 5;
|
|
2722
|
+
console.log('\n🏆 Ethan Leaderboard\n');
|
|
2723
|
+
console.log('─'.repeat(60));
|
|
2724
|
+
// ── 排行榜 ──────────────────────────────────────────────────────────────
|
|
2725
|
+
const sorted = Object.entries(usage).sort((a, b) => b[1] - a[1]).slice(0, top);
|
|
2726
|
+
const medals = ['🥇', '🥈', '🥉', '4️⃣ ', '5️⃣ '];
|
|
2727
|
+
if (sorted.length === 0) {
|
|
2728
|
+
console.log(' 暂无记录\n');
|
|
2729
|
+
}
|
|
2730
|
+
else {
|
|
2731
|
+
console.log('\n Top Skills\n');
|
|
2732
|
+
sorted.forEach(([id, count], i) => {
|
|
2733
|
+
const skill = index_1.ALL_SKILLS.find((s) => s.id === id);
|
|
2734
|
+
const name = skill ? skill.name : id;
|
|
2735
|
+
const bar = '▰'.repeat(Math.min(count, 20));
|
|
2736
|
+
console.log(` ${medals[i] || `${i + 1}. `} ${name.padEnd(12)} ${bar} ${count}次`);
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
// ── 连续天数 ────────────────────────────────────────────────────────────
|
|
2740
|
+
console.log('\n Streak\n');
|
|
2741
|
+
const streakBar = '🔥'.repeat(Math.min(data.streak.current, 7));
|
|
2742
|
+
console.log(` 当前:${data.streak.current} 天 ${streakBar}`);
|
|
2743
|
+
console.log(` 最长:${data.streak.best} 天`);
|
|
2744
|
+
if (data.streak.lastDate)
|
|
2745
|
+
console.log(` 最后使用:${data.streak.lastDate}`);
|
|
2746
|
+
// ── 近 7 天热图 ─────────────────────────────────────────────────────────
|
|
2747
|
+
console.log('\n 近 7 天活跃度\n');
|
|
2748
|
+
const today = new Date();
|
|
2749
|
+
const days = [];
|
|
2750
|
+
for (let i = 6; i >= 0; i--) {
|
|
2751
|
+
const d = new Date(today);
|
|
2752
|
+
d.setDate(d.getDate() - i);
|
|
2753
|
+
days.push(d.toISOString().slice(0, 10));
|
|
2754
|
+
}
|
|
2755
|
+
const heatRow = days
|
|
2756
|
+
.map((d) => {
|
|
2757
|
+
const count = (data.dailyLog[d] || []).length;
|
|
2758
|
+
if (count === 0)
|
|
2759
|
+
return '⬜';
|
|
2760
|
+
if (count === 1)
|
|
2761
|
+
return '🟩';
|
|
2762
|
+
if (count <= 3)
|
|
2763
|
+
return '🟨';
|
|
2764
|
+
return '🟥';
|
|
2765
|
+
})
|
|
2766
|
+
.join(' ');
|
|
2767
|
+
const dayLabels = days.map((d) => d.slice(5)).join(' ');
|
|
2768
|
+
console.log(` ${heatRow}`);
|
|
2769
|
+
console.log(` ${dayLabels}`);
|
|
2770
|
+
console.log('\n' + '─'.repeat(60));
|
|
2771
|
+
const total = Object.values(usage).reduce((a, b) => a + b, 0);
|
|
2772
|
+
console.log(`\n 总计使用 ${total} 次 | 覆盖 ${Object.keys(usage).length} 个 Skills\n`);
|
|
2773
|
+
});
|
|
2774
|
+
statsCmd
|
|
2775
|
+
.command('reset')
|
|
2776
|
+
.description('清空所有统计数据')
|
|
2777
|
+
.action(() => {
|
|
2778
|
+
const data = { usage: {}, streak: { current: 0, best: 0, lastDate: '' }, dailyLog: {} };
|
|
2779
|
+
writeStatsV2(data);
|
|
2780
|
+
console.log('\n✅ 统计数据已清空\n');
|
|
2781
|
+
});
|
|
2782
|
+
function loadCustomPipelines(cwd) {
|
|
2783
|
+
const dir = path.join(cwd, '.ethan', 'pipelines');
|
|
2784
|
+
if (!fs.existsSync(dir))
|
|
2785
|
+
return [];
|
|
2786
|
+
const results = [];
|
|
2787
|
+
for (const file of fs.readdirSync(dir).filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'))) {
|
|
2788
|
+
try {
|
|
2789
|
+
// 简单的 YAML 解析(仅支持内置 Pipeline YAML 格式,不依赖 js-yaml)
|
|
2790
|
+
const raw = fs.readFileSync(path.join(dir, file), 'utf-8');
|
|
2791
|
+
// 使用 js-yaml(已在 dependencies 中)
|
|
2792
|
+
const yaml = require('js-yaml');
|
|
2793
|
+
const parsed = yaml.load(raw);
|
|
2794
|
+
if (parsed && parsed.id && parsed.skillIds)
|
|
2795
|
+
results.push(parsed);
|
|
2796
|
+
}
|
|
2797
|
+
catch { /* ignore parse errors */ }
|
|
2798
|
+
}
|
|
2799
|
+
return results;
|
|
2800
|
+
}
|
|
2801
|
+
program
|
|
2802
|
+
.command('pipeline-init')
|
|
2803
|
+
.description('在 .ethan/pipelines/ 生成自定义 Pipeline YAML 模板')
|
|
2804
|
+
.option('--name <name>', 'Pipeline 名称(用于文件名)', 'my-pipeline')
|
|
2805
|
+
.action((options) => {
|
|
2806
|
+
const dir = path.join(process.cwd(), '.ethan', 'pipelines');
|
|
2807
|
+
if (!fs.existsSync(dir))
|
|
2808
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2809
|
+
const filename = `${options.name}.yaml`;
|
|
2810
|
+
const filePath = path.join(dir, filename);
|
|
2811
|
+
if (fs.existsSync(filePath)) {
|
|
2812
|
+
console.error(`❌ 文件已存在:${filePath}`);
|
|
2813
|
+
process.exit(1);
|
|
2814
|
+
}
|
|
2815
|
+
const template = `# Ethan 自定义 Pipeline 定义
|
|
2816
|
+
# 放置在 .ethan/pipelines/ 目录下,ethan 自动加载
|
|
2817
|
+
|
|
2818
|
+
id: ${options.name}
|
|
2819
|
+
name: 自定义工作流名称
|
|
2820
|
+
description: 这个 Pipeline 的描述
|
|
2821
|
+
|
|
2822
|
+
# 引用的 Skill ID 列表(按执行顺序)
|
|
2823
|
+
# 可用 Skill ID:requirement-understanding | task-breakdown | solution-design
|
|
2824
|
+
# implementation | progress-tracking | task-report | weekly-report
|
|
2825
|
+
# code-review | debug | tech-research
|
|
2826
|
+
skillIds:
|
|
2827
|
+
- requirement-understanding
|
|
2828
|
+
- solution-design
|
|
2829
|
+
- implementation
|
|
2830
|
+
|
|
2831
|
+
# (可选)输出结构定义:每个字段对应 AI 需要输出的内容
|
|
2832
|
+
# outputSchema:
|
|
2833
|
+
# featureSpec:
|
|
2834
|
+
# type: string
|
|
2835
|
+
# description: 需求规格说明书
|
|
2836
|
+
# required: true
|
|
2837
|
+
# designDoc:
|
|
2838
|
+
# type: string
|
|
2839
|
+
# description: 技术方案设计文档
|
|
2840
|
+
`;
|
|
2841
|
+
fs.writeFileSync(filePath, template, 'utf-8');
|
|
2842
|
+
console.log(`\n✅ 自定义 Pipeline 模板已生成:${filePath}`);
|
|
2843
|
+
console.log(`\n💡 编辑后运行:ethan workflow start ${options.name} -c "任务描述"\n`);
|
|
2844
|
+
});
|
|
1115
2845
|
program.parse(process.argv);
|
|
1116
2846
|
//# sourceMappingURL=index.js.map
|