claude-coder 1.8.2 → 1.8.4
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 +167 -167
- package/bin/cli.js +172 -172
- package/package.json +53 -52
- package/recipes/_shared/roles/developer.md +11 -0
- package/recipes/_shared/roles/product.md +12 -0
- package/recipes/_shared/roles/tester.md +12 -0
- package/recipes/_shared/test/report-format.md +86 -0
- package/recipes/backend/base.md +27 -0
- package/recipes/backend/components/auth.md +18 -0
- package/recipes/backend/components/crud-api.md +18 -0
- package/recipes/backend/components/file-service.md +15 -0
- package/recipes/backend/manifest.json +20 -0
- package/recipes/backend/test/api-test.md +25 -0
- package/recipes/console/base.md +37 -0
- package/recipes/console/components/modal-form.md +20 -0
- package/recipes/console/components/pagination.md +17 -0
- package/recipes/console/components/search.md +17 -0
- package/recipes/console/components/table-list.md +18 -0
- package/recipes/console/components/tabs.md +14 -0
- package/recipes/console/components/tree.md +15 -0
- package/recipes/console/components/upload.md +15 -0
- package/recipes/console/manifest.json +24 -0
- package/recipes/console/test/crud-e2e.md +47 -0
- package/recipes/h5/base.md +26 -0
- package/recipes/h5/components/animation.md +11 -0
- package/recipes/h5/components/countdown.md +11 -0
- package/recipes/h5/components/share.md +11 -0
- package/recipes/h5/components/swiper.md +11 -0
- package/recipes/h5/manifest.json +21 -0
- package/recipes/h5/test/h5-e2e.md +20 -0
- package/src/commands/auth.js +290 -240
- package/src/commands/setup-modules/helpers.js +99 -99
- package/src/commands/setup-modules/index.js +25 -25
- package/src/commands/setup-modules/mcp.js +94 -94
- package/src/commands/setup-modules/provider.js +260 -260
- package/src/commands/setup-modules/safety.js +61 -61
- package/src/commands/setup-modules/simplify.js +52 -52
- package/src/commands/setup.js +172 -172
- package/src/common/assets.js +236 -236
- package/src/common/config.js +125 -125
- package/src/common/constants.js +55 -55
- package/src/common/indicator.js +222 -222
- package/src/common/interaction.js +170 -170
- package/src/common/logging.js +77 -77
- package/src/common/sdk.js +50 -50
- package/src/common/tasks.js +88 -88
- package/src/common/utils.js +161 -161
- package/src/core/coding.js +55 -55
- package/src/core/context.js +117 -117
- package/src/core/go.js +310 -310
- package/src/core/harness.js +484 -484
- package/src/core/hooks.js +533 -533
- package/src/core/init.js +171 -171
- package/src/core/plan.js +325 -325
- package/src/core/prompts.js +227 -227
- package/src/core/query.js +49 -49
- package/src/core/repair.js +46 -46
- package/src/core/runner.js +195 -195
- package/src/core/scan.js +89 -89
- package/src/core/session.js +56 -56
- package/src/core/simplify.js +53 -52
- package/templates/bash-process.md +12 -12
- package/templates/codingSystem.md +65 -65
- package/templates/codingUser.md +17 -17
- package/templates/coreProtocol.md +29 -29
- package/templates/goSystem.md +130 -130
- package/templates/guidance.json +52 -52
- package/templates/planSystem.md +78 -78
- package/templates/planUser.md +8 -8
- package/templates/playwright.md +16 -16
- package/templates/requirements.example.md +57 -57
- package/templates/scanSystem.md +120 -120
- package/templates/scanUser.md +10 -10
- package/templates/test_rule.md +194 -194
package/src/common/tasks.js
CHANGED
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { log, COLOR } = require('./config');
|
|
4
|
-
const { assets } = require('./assets');
|
|
5
|
-
|
|
6
|
-
function loadTasks() {
|
|
7
|
-
return assets.readJson('tasks', null);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function saveTasks(data) {
|
|
11
|
-
assets.writeJson('tasks', data);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function getFeatures(data) {
|
|
15
|
-
return data?.features || [];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function getStats(data) {
|
|
19
|
-
const features = getFeatures(data);
|
|
20
|
-
return {
|
|
21
|
-
total: features.length,
|
|
22
|
-
done: features.filter(f => f.status === 'done').length,
|
|
23
|
-
failed: features.filter(f => f.status === 'failed').length,
|
|
24
|
-
in_progress: features.filter(f => f.status === 'in_progress').length,
|
|
25
|
-
testing: features.filter(f => f.status === 'testing').length,
|
|
26
|
-
pending: features.filter(f => f.status === 'pending').length,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function printStats() {
|
|
31
|
-
const data = loadTasks();
|
|
32
|
-
if (!data) return;
|
|
33
|
-
const stats = getStats(data);
|
|
34
|
-
log('info', `进度: ${stats.done}/${stats.total} done, ${stats.in_progress} in_progress, ${stats.testing} testing, ${stats.failed} failed, ${stats.pending} pending`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function showStatus() {
|
|
38
|
-
const data = loadTasks();
|
|
39
|
-
if (!data) {
|
|
40
|
-
log('warn', '未找到 .claude-coder/tasks.json,请先运行 claude-coder run');
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const stats = getStats(data);
|
|
45
|
-
const features = getFeatures(data);
|
|
46
|
-
|
|
47
|
-
console.log(`\n${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
48
|
-
console.log(` ${COLOR.blue}📋 任务状态${COLOR.reset} 项目: ${data.project || '(未命名)'}`);
|
|
49
|
-
console.log(`${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
50
|
-
|
|
51
|
-
const bar = stats.total > 0
|
|
52
|
-
? `[${'█'.repeat(Math.floor(stats.done / stats.total * 30))}${'░'.repeat(30 - Math.floor(stats.done / stats.total * 30))}]`
|
|
53
|
-
: '[░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]';
|
|
54
|
-
console.log(` 进度: ${bar} ${stats.done}/${stats.total}`);
|
|
55
|
-
|
|
56
|
-
console.log(`\n ${COLOR.green}✔ done: ${stats.done}${COLOR.reset} ${COLOR.yellow}⏳ pending: ${stats.pending}${COLOR.reset} ${COLOR.red}✘ failed: ${stats.failed}${COLOR.reset}`);
|
|
57
|
-
|
|
58
|
-
if (stats.in_progress > 0 || stats.testing > 0) {
|
|
59
|
-
console.log(` ▸ in_progress: ${stats.in_progress} ▸ testing: ${stats.testing}`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const progress = assets.readJson('progress', null);
|
|
63
|
-
if (progress) {
|
|
64
|
-
const sessions = (progress.sessions || []).filter(s => typeof s.cost === 'number');
|
|
65
|
-
if (sessions.length > 0) {
|
|
66
|
-
const totalCost = sessions.reduce((sum, s) => sum + s.cost, 0);
|
|
67
|
-
console.log(`\n ${COLOR.blue}💰 累计成本${COLOR.reset}: $${totalCost.toFixed(4)} (${sessions.length} sessions)`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
console.log(`\n ${'─'.repeat(45)}`);
|
|
72
|
-
for (const f of features) {
|
|
73
|
-
const icon = { done: '✔', pending: '○', in_progress: '▸', testing: '⟳', failed: '✘' }[f.status] || '?';
|
|
74
|
-
const color = { done: COLOR.green, failed: COLOR.red, in_progress: COLOR.blue, testing: COLOR.yellow }[f.status] || '';
|
|
75
|
-
console.log(` ${color}${icon}${COLOR.reset} [${f.id}] ${f.description} (${f.status})`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
console.log(`${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}\n`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
module.exports = {
|
|
82
|
-
loadTasks,
|
|
83
|
-
saveTasks,
|
|
84
|
-
getFeatures,
|
|
85
|
-
getStats,
|
|
86
|
-
printStats,
|
|
87
|
-
showStatus,
|
|
88
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { log, COLOR } = require('./config');
|
|
4
|
+
const { assets } = require('./assets');
|
|
5
|
+
|
|
6
|
+
function loadTasks() {
|
|
7
|
+
return assets.readJson('tasks', null);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function saveTasks(data) {
|
|
11
|
+
assets.writeJson('tasks', data);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getFeatures(data) {
|
|
15
|
+
return data?.features || [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getStats(data) {
|
|
19
|
+
const features = getFeatures(data);
|
|
20
|
+
return {
|
|
21
|
+
total: features.length,
|
|
22
|
+
done: features.filter(f => f.status === 'done').length,
|
|
23
|
+
failed: features.filter(f => f.status === 'failed').length,
|
|
24
|
+
in_progress: features.filter(f => f.status === 'in_progress').length,
|
|
25
|
+
testing: features.filter(f => f.status === 'testing').length,
|
|
26
|
+
pending: features.filter(f => f.status === 'pending').length,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function printStats() {
|
|
31
|
+
const data = loadTasks();
|
|
32
|
+
if (!data) return;
|
|
33
|
+
const stats = getStats(data);
|
|
34
|
+
log('info', `进度: ${stats.done}/${stats.total} done, ${stats.in_progress} in_progress, ${stats.testing} testing, ${stats.failed} failed, ${stats.pending} pending`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function showStatus() {
|
|
38
|
+
const data = loadTasks();
|
|
39
|
+
if (!data) {
|
|
40
|
+
log('warn', '未找到 .claude-coder/tasks.json,请先运行 claude-coder run');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const stats = getStats(data);
|
|
45
|
+
const features = getFeatures(data);
|
|
46
|
+
|
|
47
|
+
console.log(`\n${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
48
|
+
console.log(` ${COLOR.blue}📋 任务状态${COLOR.reset} 项目: ${data.project || '(未命名)'}`);
|
|
49
|
+
console.log(`${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
50
|
+
|
|
51
|
+
const bar = stats.total > 0
|
|
52
|
+
? `[${'█'.repeat(Math.floor(stats.done / stats.total * 30))}${'░'.repeat(30 - Math.floor(stats.done / stats.total * 30))}]`
|
|
53
|
+
: '[░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]';
|
|
54
|
+
console.log(` 进度: ${bar} ${stats.done}/${stats.total}`);
|
|
55
|
+
|
|
56
|
+
console.log(`\n ${COLOR.green}✔ done: ${stats.done}${COLOR.reset} ${COLOR.yellow}⏳ pending: ${stats.pending}${COLOR.reset} ${COLOR.red}✘ failed: ${stats.failed}${COLOR.reset}`);
|
|
57
|
+
|
|
58
|
+
if (stats.in_progress > 0 || stats.testing > 0) {
|
|
59
|
+
console.log(` ▸ in_progress: ${stats.in_progress} ▸ testing: ${stats.testing}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const progress = assets.readJson('progress', null);
|
|
63
|
+
if (progress) {
|
|
64
|
+
const sessions = (progress.sessions || []).filter(s => typeof s.cost === 'number');
|
|
65
|
+
if (sessions.length > 0) {
|
|
66
|
+
const totalCost = sessions.reduce((sum, s) => sum + s.cost, 0);
|
|
67
|
+
console.log(`\n ${COLOR.blue}💰 累计成本${COLOR.reset}: $${totalCost.toFixed(4)} (${sessions.length} sessions)`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(`\n ${'─'.repeat(45)}`);
|
|
72
|
+
for (const f of features) {
|
|
73
|
+
const icon = { done: '✔', pending: '○', in_progress: '▸', testing: '⟳', failed: '✘' }[f.status] || '?';
|
|
74
|
+
const color = { done: COLOR.green, failed: COLOR.red, in_progress: COLOR.blue, testing: COLOR.yellow }[f.status] || '';
|
|
75
|
+
console.log(` ${color}${icon}${COLOR.reset} [${f.id}] ${f.description} (${f.status})`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(`${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}\n`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
loadTasks,
|
|
83
|
+
saveTasks,
|
|
84
|
+
getFeatures,
|
|
85
|
+
getStats,
|
|
86
|
+
printStats,
|
|
87
|
+
showStatus,
|
|
88
|
+
};
|
package/src/common/utils.js
CHANGED
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const { execSync } = require('child_process');
|
|
5
|
-
|
|
6
|
-
// ─────────────────────────────────────────────────────────────
|
|
7
|
-
// 字符串工具
|
|
8
|
-
// ─────────────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 中间截断字符串,保留首尾
|
|
12
|
-
* @param {string} str - 原字符串
|
|
13
|
-
* @param {number} maxLen - 最大长度
|
|
14
|
-
* @returns {string}
|
|
15
|
-
*/
|
|
16
|
-
function truncateMiddle(str, maxLen) {
|
|
17
|
-
if (!str || str.length <= maxLen) return str || '';
|
|
18
|
-
const startLen = Math.ceil((maxLen - 1) / 2);
|
|
19
|
-
const endLen = Math.floor((maxLen - 1) / 2);
|
|
20
|
-
return str.slice(0, startLen) + '…' + str.slice(-endLen);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* 路径感知截断:优先保留文件名,截断目录中间
|
|
25
|
-
* @param {string} path - 文件路径
|
|
26
|
-
* @param {number} maxLen - 最大长度
|
|
27
|
-
* @returns {string}
|
|
28
|
-
*/
|
|
29
|
-
function truncatePath(path, maxLen) {
|
|
30
|
-
if (!path || path.length <= maxLen) return path || '';
|
|
31
|
-
|
|
32
|
-
const lastSlash = path.lastIndexOf('/');
|
|
33
|
-
if (lastSlash === -1) {
|
|
34
|
-
return truncateMiddle(path, maxLen);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const fileName = path.slice(lastSlash + 1);
|
|
38
|
-
const dirPath = path.slice(0, lastSlash);
|
|
39
|
-
|
|
40
|
-
// 文件名本身超长,截断文件名
|
|
41
|
-
if (fileName.length >= maxLen - 2) {
|
|
42
|
-
return truncateMiddle(path, maxLen);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// 保留文件名,截断目录
|
|
46
|
-
const availableForDir = maxLen - fileName.length - 2; // -2 for '…/'
|
|
47
|
-
if (availableForDir <= 0) {
|
|
48
|
-
return '…/' + fileName.slice(0, maxLen - 2);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// 目录两端保留
|
|
52
|
-
const dirStart = Math.ceil(availableForDir / 2);
|
|
53
|
-
const dirEnd = Math.floor(availableForDir / 2);
|
|
54
|
-
const truncatedDir = dirPath.slice(0, dirStart) + '…' + (dirEnd > 0 ? dirPath.slice(-dirEnd) : '');
|
|
55
|
-
|
|
56
|
-
return truncatedDir + '/' + fileName;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ─────────────────────────────────────────────────────────────
|
|
60
|
-
// Git 工具
|
|
61
|
-
// ─────────────────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* 获取当前 git HEAD commit hash
|
|
65
|
-
* @param {string} cwd - 工作目录
|
|
66
|
-
* @returns {string} commit hash 或 'none'
|
|
67
|
-
*/
|
|
68
|
-
function getGitHead(cwd) {
|
|
69
|
-
try {
|
|
70
|
-
return execSync('git rev-parse HEAD', { cwd, encoding: 'utf8' }).trim();
|
|
71
|
-
} catch {
|
|
72
|
-
return 'none';
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* 检查是否在 git 仓库中
|
|
78
|
-
* @param {string} cwd - 工作目录
|
|
79
|
-
* @returns {boolean}
|
|
80
|
-
*/
|
|
81
|
-
function isGitRepo(cwd) {
|
|
82
|
-
try {
|
|
83
|
-
execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'ignore' });
|
|
84
|
-
return true;
|
|
85
|
-
} catch {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ─────────────────────────────────────────────────────────────
|
|
91
|
-
// .gitignore 工具
|
|
92
|
-
// ─────────────────────────────────────────────────────────────
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* 向 .gitignore 追加条目(如果不存在)
|
|
96
|
-
* @param {string} projectRoot - 项目根目录
|
|
97
|
-
* @param {string} entry - 要添加的条目
|
|
98
|
-
* @returns {boolean} 是否有新增
|
|
99
|
-
*/
|
|
100
|
-
function appendGitignore(projectRoot, entry) {
|
|
101
|
-
const path = require('path');
|
|
102
|
-
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
103
|
-
let content = '';
|
|
104
|
-
if (fs.existsSync(gitignorePath)) {
|
|
105
|
-
content = fs.readFileSync(gitignorePath, 'utf8');
|
|
106
|
-
}
|
|
107
|
-
if (content.includes(entry)) return false;
|
|
108
|
-
|
|
109
|
-
const suffix = content.endsWith('\n') || content === '' ? '' : '\n';
|
|
110
|
-
fs.appendFileSync(gitignorePath, `${suffix}${entry}\n`, 'utf8');
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* 确保 .gitignore 包含 claude-coder 的敏感文件条目
|
|
116
|
-
* @param {string} projectRoot - 项目根目录
|
|
117
|
-
* @returns {boolean} 是否有新增
|
|
118
|
-
*/
|
|
119
|
-
function ensureGitignore(projectRoot) {
|
|
120
|
-
const patterns = ['.claude-coder/.env', '.claude-coder/.runtime/'];
|
|
121
|
-
let added = false;
|
|
122
|
-
for (const p of patterns) {
|
|
123
|
-
if (appendGitignore(projectRoot, p)) added = true;
|
|
124
|
-
}
|
|
125
|
-
return added;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// ─────────────────────────────────────────────────────────────
|
|
129
|
-
// 进程工具
|
|
130
|
-
// ─────────────────────────────────────────────────────────────
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* 休眠
|
|
134
|
-
* @param {number} ms - 毫秒
|
|
135
|
-
* @returns {Promise<void>}
|
|
136
|
-
*/
|
|
137
|
-
function sleep(ms) {
|
|
138
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// ─────────────────────────────────────────────────────────────
|
|
143
|
-
// 日志工具 - 统一的日志处理
|
|
144
|
-
// ─────────────────────────────────────────────────────────────
|
|
145
|
-
function localTimestamp() {
|
|
146
|
-
const d = new Date();
|
|
147
|
-
const hh = String(d.getHours()).padStart(2, '0');
|
|
148
|
-
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
149
|
-
const ss = String(d.getSeconds()).padStart(2, '0');
|
|
150
|
-
return `${hh}:${mm}:${ss}`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
module.exports = {
|
|
154
|
-
truncateMiddle,
|
|
155
|
-
truncatePath,
|
|
156
|
-
getGitHead,
|
|
157
|
-
isGitRepo,
|
|
158
|
-
appendGitignore,
|
|
159
|
-
ensureGitignore,
|
|
160
|
-
sleep,
|
|
161
|
-
localTimestamp,
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
// ─────────────────────────────────────────────────────────────
|
|
7
|
+
// 字符串工具
|
|
8
|
+
// ─────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 中间截断字符串,保留首尾
|
|
12
|
+
* @param {string} str - 原字符串
|
|
13
|
+
* @param {number} maxLen - 最大长度
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
function truncateMiddle(str, maxLen) {
|
|
17
|
+
if (!str || str.length <= maxLen) return str || '';
|
|
18
|
+
const startLen = Math.ceil((maxLen - 1) / 2);
|
|
19
|
+
const endLen = Math.floor((maxLen - 1) / 2);
|
|
20
|
+
return str.slice(0, startLen) + '…' + str.slice(-endLen);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 路径感知截断:优先保留文件名,截断目录中间
|
|
25
|
+
* @param {string} path - 文件路径
|
|
26
|
+
* @param {number} maxLen - 最大长度
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
function truncatePath(path, maxLen) {
|
|
30
|
+
if (!path || path.length <= maxLen) return path || '';
|
|
31
|
+
|
|
32
|
+
const lastSlash = path.lastIndexOf('/');
|
|
33
|
+
if (lastSlash === -1) {
|
|
34
|
+
return truncateMiddle(path, maxLen);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const fileName = path.slice(lastSlash + 1);
|
|
38
|
+
const dirPath = path.slice(0, lastSlash);
|
|
39
|
+
|
|
40
|
+
// 文件名本身超长,截断文件名
|
|
41
|
+
if (fileName.length >= maxLen - 2) {
|
|
42
|
+
return truncateMiddle(path, maxLen);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 保留文件名,截断目录
|
|
46
|
+
const availableForDir = maxLen - fileName.length - 2; // -2 for '…/'
|
|
47
|
+
if (availableForDir <= 0) {
|
|
48
|
+
return '…/' + fileName.slice(0, maxLen - 2);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 目录两端保留
|
|
52
|
+
const dirStart = Math.ceil(availableForDir / 2);
|
|
53
|
+
const dirEnd = Math.floor(availableForDir / 2);
|
|
54
|
+
const truncatedDir = dirPath.slice(0, dirStart) + '…' + (dirEnd > 0 ? dirPath.slice(-dirEnd) : '');
|
|
55
|
+
|
|
56
|
+
return truncatedDir + '/' + fileName;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─────────────────────────────────────────────────────────────
|
|
60
|
+
// Git 工具
|
|
61
|
+
// ─────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 获取当前 git HEAD commit hash
|
|
65
|
+
* @param {string} cwd - 工作目录
|
|
66
|
+
* @returns {string} commit hash 或 'none'
|
|
67
|
+
*/
|
|
68
|
+
function getGitHead(cwd) {
|
|
69
|
+
try {
|
|
70
|
+
return execSync('git rev-parse HEAD', { cwd, encoding: 'utf8' }).trim();
|
|
71
|
+
} catch {
|
|
72
|
+
return 'none';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 检查是否在 git 仓库中
|
|
78
|
+
* @param {string} cwd - 工作目录
|
|
79
|
+
* @returns {boolean}
|
|
80
|
+
*/
|
|
81
|
+
function isGitRepo(cwd) {
|
|
82
|
+
try {
|
|
83
|
+
execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'ignore' });
|
|
84
|
+
return true;
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─────────────────────────────────────────────────────────────
|
|
91
|
+
// .gitignore 工具
|
|
92
|
+
// ─────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 向 .gitignore 追加条目(如果不存在)
|
|
96
|
+
* @param {string} projectRoot - 项目根目录
|
|
97
|
+
* @param {string} entry - 要添加的条目
|
|
98
|
+
* @returns {boolean} 是否有新增
|
|
99
|
+
*/
|
|
100
|
+
function appendGitignore(projectRoot, entry) {
|
|
101
|
+
const path = require('path');
|
|
102
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
103
|
+
let content = '';
|
|
104
|
+
if (fs.existsSync(gitignorePath)) {
|
|
105
|
+
content = fs.readFileSync(gitignorePath, 'utf8');
|
|
106
|
+
}
|
|
107
|
+
if (content.includes(entry)) return false;
|
|
108
|
+
|
|
109
|
+
const suffix = content.endsWith('\n') || content === '' ? '' : '\n';
|
|
110
|
+
fs.appendFileSync(gitignorePath, `${suffix}${entry}\n`, 'utf8');
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 确保 .gitignore 包含 claude-coder 的敏感文件条目
|
|
116
|
+
* @param {string} projectRoot - 项目根目录
|
|
117
|
+
* @returns {boolean} 是否有新增
|
|
118
|
+
*/
|
|
119
|
+
function ensureGitignore(projectRoot) {
|
|
120
|
+
const patterns = ['.claude-coder/.env', '.claude-coder/.runtime/'];
|
|
121
|
+
let added = false;
|
|
122
|
+
for (const p of patterns) {
|
|
123
|
+
if (appendGitignore(projectRoot, p)) added = true;
|
|
124
|
+
}
|
|
125
|
+
return added;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─────────────────────────────────────────────────────────────
|
|
129
|
+
// 进程工具
|
|
130
|
+
// ─────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 休眠
|
|
134
|
+
* @param {number} ms - 毫秒
|
|
135
|
+
* @returns {Promise<void>}
|
|
136
|
+
*/
|
|
137
|
+
function sleep(ms) {
|
|
138
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
// ─────────────────────────────────────────────────────────────
|
|
143
|
+
// 日志工具 - 统一的日志处理
|
|
144
|
+
// ─────────────────────────────────────────────────────────────
|
|
145
|
+
function localTimestamp() {
|
|
146
|
+
const d = new Date();
|
|
147
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
148
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
149
|
+
const ss = String(d.getSeconds()).padStart(2, '0');
|
|
150
|
+
return `${hh}:${mm}:${ss}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
truncateMiddle,
|
|
155
|
+
truncatePath,
|
|
156
|
+
getGitHead,
|
|
157
|
+
isGitRepo,
|
|
158
|
+
appendGitignore,
|
|
159
|
+
ensureGitignore,
|
|
160
|
+
sleep,
|
|
161
|
+
localTimestamp,
|
|
162
162
|
};
|
package/src/core/coding.js
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const { runSession } = require("./session");
|
|
4
|
-
const { buildQueryOptions } = require("./query");
|
|
5
|
-
const { buildSystemPrompt, buildCodingContext } = require("./prompts");
|
|
6
|
-
const { extractResult } = require("../common/logging");
|
|
7
|
-
const { log } = require("../common/config");
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* 内部:运行编码 Session
|
|
11
|
-
*/
|
|
12
|
-
async function runCodingSession(sessionNum, opts = {}) {
|
|
13
|
-
const taskId = opts.taskId || "unknown";
|
|
14
|
-
const dateStr = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
15
|
-
|
|
16
|
-
return runSession("coding", {
|
|
17
|
-
opts,
|
|
18
|
-
sessionNum,
|
|
19
|
-
logFileName: `${taskId}_session_${sessionNum}_${dateStr}.log`,
|
|
20
|
-
label: `coding task=${taskId}`,
|
|
21
|
-
|
|
22
|
-
async execute(sdk, ctx) {
|
|
23
|
-
const prompt = buildCodingContext(sessionNum, opts);
|
|
24
|
-
const queryOpts = buildQueryOptions(ctx.config, opts);
|
|
25
|
-
queryOpts.systemPrompt = buildSystemPrompt('coding');
|
|
26
|
-
queryOpts.hooks = ctx.hooks;
|
|
27
|
-
queryOpts.abortController = ctx.abortController;
|
|
28
|
-
queryOpts.disallowedTools = ['askUserQuestion'];
|
|
29
|
-
|
|
30
|
-
const collected = await ctx.runQuery(sdk, prompt, queryOpts);
|
|
31
|
-
const result = extractResult(collected);
|
|
32
|
-
const subtype = result?.subtype || "unknown";
|
|
33
|
-
|
|
34
|
-
if (subtype !== "success" && subtype !== "unknown") {
|
|
35
|
-
log(
|
|
36
|
-
"warn",
|
|
37
|
-
`session 结束原因: ${subtype} (turns: ${result?.num_turns ?? "?"})`,
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
if (ctx.logStream.writable) {
|
|
41
|
-
ctx.logStream.write(
|
|
42
|
-
`[${new Date().toISOString()}] SESSION_END subtype=${subtype} turns=${result?.num_turns ?? "?"} cost=${result?.total_cost_usd ?? "?"}\n`,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
cost: result?.total_cost_usd ?? null,
|
|
48
|
-
tokenUsage: result?.usage ?? null,
|
|
49
|
-
subtype,
|
|
50
|
-
};
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
module.exports = { runCodingSession };
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { runSession } = require("./session");
|
|
4
|
+
const { buildQueryOptions } = require("./query");
|
|
5
|
+
const { buildSystemPrompt, buildCodingContext } = require("./prompts");
|
|
6
|
+
const { extractResult } = require("../common/logging");
|
|
7
|
+
const { log } = require("../common/config");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 内部:运行编码 Session
|
|
11
|
+
*/
|
|
12
|
+
async function runCodingSession(sessionNum, opts = {}) {
|
|
13
|
+
const taskId = opts.taskId || "unknown";
|
|
14
|
+
const dateStr = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
15
|
+
|
|
16
|
+
return runSession("coding", {
|
|
17
|
+
opts,
|
|
18
|
+
sessionNum,
|
|
19
|
+
logFileName: `${taskId}_session_${sessionNum}_${dateStr}.log`,
|
|
20
|
+
label: `coding task=${taskId}`,
|
|
21
|
+
|
|
22
|
+
async execute(sdk, ctx) {
|
|
23
|
+
const prompt = buildCodingContext(sessionNum, opts);
|
|
24
|
+
const queryOpts = buildQueryOptions(ctx.config, opts);
|
|
25
|
+
queryOpts.systemPrompt = buildSystemPrompt('coding');
|
|
26
|
+
queryOpts.hooks = ctx.hooks;
|
|
27
|
+
queryOpts.abortController = ctx.abortController;
|
|
28
|
+
queryOpts.disallowedTools = ['askUserQuestion'];
|
|
29
|
+
|
|
30
|
+
const collected = await ctx.runQuery(sdk, prompt, queryOpts);
|
|
31
|
+
const result = extractResult(collected);
|
|
32
|
+
const subtype = result?.subtype || "unknown";
|
|
33
|
+
|
|
34
|
+
if (subtype !== "success" && subtype !== "unknown") {
|
|
35
|
+
log(
|
|
36
|
+
"warn",
|
|
37
|
+
`session 结束原因: ${subtype} (turns: ${result?.num_turns ?? "?"})`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (ctx.logStream.writable) {
|
|
41
|
+
ctx.logStream.write(
|
|
42
|
+
`[${new Date().toISOString()}] SESSION_END subtype=${subtype} turns=${result?.num_turns ?? "?"} cost=${result?.total_cost_usd ?? "?"}\n`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
cost: result?.total_cost_usd ?? null,
|
|
48
|
+
tokenUsage: result?.usage ?? null,
|
|
49
|
+
subtype,
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { runCodingSession };
|