claude-coder 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -125
- package/bin/cli.js +159 -161
- package/package.json +52 -47
- package/src/commands/auth.js +294 -0
- package/src/commands/setup-modules/helpers.js +105 -0
- package/src/commands/setup-modules/index.js +26 -0
- package/src/commands/setup-modules/mcp.js +95 -0
- package/src/commands/setup-modules/provider.js +261 -0
- package/src/commands/setup-modules/safety.js +62 -0
- package/src/commands/setup-modules/simplify.js +53 -0
- package/src/commands/setup.js +172 -0
- package/src/common/assets.js +192 -0
- package/src/{config.js → common/config.js} +138 -201
- package/src/common/constants.js +57 -0
- package/src/{indicator.js → common/indicator.js} +222 -217
- package/src/common/interaction.js +170 -0
- package/src/common/logging.js +77 -0
- package/src/common/sdk.js +51 -0
- package/src/{tasks.js → common/tasks.js} +157 -172
- package/src/common/utils.js +147 -0
- package/src/core/base.js +54 -0
- package/src/core/coding.js +55 -0
- package/src/core/context.js +132 -0
- package/src/core/hooks.js +529 -0
- package/src/{init.js → core/init.js} +163 -144
- package/src/core/plan.js +318 -0
- package/src/core/prompts.js +253 -0
- package/src/core/query.js +48 -0
- package/src/core/repair.js +58 -0
- package/src/{runner.js → core/runner.js} +352 -420
- package/src/core/scan.js +89 -0
- package/src/core/simplify.js +59 -0
- package/src/core/validator.js +138 -0
- package/{prompts/ADD_GUIDE.md → templates/addGuide.md} +98 -98
- package/templates/addUser.md +26 -0
- package/{prompts/CLAUDE.md → templates/agentProtocol.md} +195 -199
- package/templates/bash-process.md +5 -0
- package/{prompts/coding_user.md → templates/codingUser.md} +31 -23
- package/templates/guidance.json +35 -0
- package/templates/playwright.md +17 -0
- package/templates/requirements.example.md +56 -56
- package/{prompts/SCAN_PROTOCOL.md → templates/scanProtocol.md} +118 -118
- package/{prompts/scan_user.md → templates/scanUser.md} +17 -17
- package/templates/test_rule.md +194 -194
- package/prompts/add_user.md +0 -24
- package/src/auth.js +0 -245
- package/src/hooks.js +0 -160
- package/src/prompts.js +0 -295
- package/src/scanner.js +0 -62
- package/src/session.js +0 -352
- package/src/setup.js +0 -579
- package/src/validator.js +0 -181
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { localTimestamp } = require('./utils');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 处理 SDK 消息并写入日志流
|
|
7
|
+
* @param {object} message - SDK 消息对象
|
|
8
|
+
* @param {import('fs').WriteStream} logStream - 日志写入流
|
|
9
|
+
* @param {object} indicator - Indicator 实例(可选)
|
|
10
|
+
*/
|
|
11
|
+
function logMessage(message, logStream, indicator) {
|
|
12
|
+
if (message.type === 'assistant' && message.message?.content) {
|
|
13
|
+
for (const block of message.message.content) {
|
|
14
|
+
if (block.type === 'text' && block.text) {
|
|
15
|
+
if (indicator) indicator.updateActivity();
|
|
16
|
+
process.stdout.write(block.text);
|
|
17
|
+
if (logStream) logStream.write(block.text);
|
|
18
|
+
}
|
|
19
|
+
if (block.type === 'tool_use' && logStream) {
|
|
20
|
+
logStream.write(`[TOOL_USE] ${block.name}: ${JSON.stringify(block.input).slice(0, 300)}\n`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (message.type === 'tool_result') {
|
|
26
|
+
if (indicator) indicator.updateActivity();
|
|
27
|
+
if (logStream) {
|
|
28
|
+
const isErr = message.is_error || false;
|
|
29
|
+
const content = typeof message.content === 'string'
|
|
30
|
+
? message.content.slice(0, 500)
|
|
31
|
+
: JSON.stringify(message.content).slice(0, 500);
|
|
32
|
+
if (isErr) {
|
|
33
|
+
logStream.write(`[TOOL_ERROR] ${content}\n`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 从消息列表中提取结果消息
|
|
41
|
+
* @param {Array} messages - 消息列表
|
|
42
|
+
* @returns {object|null} 结果消息
|
|
43
|
+
*/
|
|
44
|
+
function extractResult(messages) {
|
|
45
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
46
|
+
if (messages[i].type === 'result') return messages[i];
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 从消息列表中提取结果文本
|
|
53
|
+
* @param {Array} messages - 消息列表
|
|
54
|
+
* @returns {string} 结果文本
|
|
55
|
+
*/
|
|
56
|
+
function extractResultText(messages) {
|
|
57
|
+
const result = extractResult(messages);
|
|
58
|
+
return result?.result || '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 写入 session 分隔符到日志
|
|
63
|
+
* @param {import('fs').WriteStream} logStream - 日志写入流
|
|
64
|
+
* @param {number} sessionNum - session 编号
|
|
65
|
+
* @param {string} label - 标签
|
|
66
|
+
*/
|
|
67
|
+
function writeSessionSeparator(logStream, sessionNum, label) {
|
|
68
|
+
const sep = '='.repeat(60);
|
|
69
|
+
logStream.write(`\n${sep}\n[Session ${sessionNum}] ${label} ${localTimestamp()}\n${sep}\n`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
logMessage,
|
|
74
|
+
extractResult,
|
|
75
|
+
extractResultText,
|
|
76
|
+
writeSessionSeparator,
|
|
77
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { log } = require('./config');
|
|
6
|
+
|
|
7
|
+
let _sdkModule = null;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 加载 Claude Agent SDK
|
|
11
|
+
*/
|
|
12
|
+
async function loadSDK() {
|
|
13
|
+
if (_sdkModule) return _sdkModule;
|
|
14
|
+
|
|
15
|
+
const pkgName = '@anthropic-ai/claude-agent-sdk';
|
|
16
|
+
const attempts = [
|
|
17
|
+
() => import(pkgName),
|
|
18
|
+
() => {
|
|
19
|
+
const { createRequire } = require('module');
|
|
20
|
+
const resolved = createRequire(__filename).resolve(pkgName);
|
|
21
|
+
return import(resolved);
|
|
22
|
+
},
|
|
23
|
+
() => {
|
|
24
|
+
const { createRequire } = require('module');
|
|
25
|
+
const resolved = createRequire(path.join(process.cwd(), 'noop.js')).resolve(pkgName);
|
|
26
|
+
return import(resolved);
|
|
27
|
+
},
|
|
28
|
+
() => {
|
|
29
|
+
const { execSync } = require('child_process');
|
|
30
|
+
const globalRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
|
|
31
|
+
const sdkDir = path.join(globalRoot, pkgName);
|
|
32
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(sdkDir, 'package.json'), 'utf8'));
|
|
33
|
+
const entry = pkgJson.exports?.['.'] || pkgJson.main || 'index.js';
|
|
34
|
+
const entryFile = typeof entry === 'object' ? (entry.import || entry.default || entry.node) : entry;
|
|
35
|
+
return import(path.join(sdkDir, entryFile));
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
for (const attempt of attempts) {
|
|
40
|
+
try {
|
|
41
|
+
_sdkModule = await attempt();
|
|
42
|
+
return _sdkModule;
|
|
43
|
+
} catch { /* try next */ }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
log('error', `未找到 ${pkgName}`);
|
|
47
|
+
log('error', `请先安装:npm install -g ${pkgName}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { loadSDK };
|
|
@@ -1,172 +1,157 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const TRANSITIONS =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
data
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
module.exports = {
|
|
161
|
-
VALID_STATUSES,
|
|
162
|
-
TRANSITIONS,
|
|
163
|
-
loadTasks,
|
|
164
|
-
saveTasks,
|
|
165
|
-
getFeatures,
|
|
166
|
-
findNextTask,
|
|
167
|
-
setStatus,
|
|
168
|
-
forceStatus,
|
|
169
|
-
addTask,
|
|
170
|
-
getStats,
|
|
171
|
-
showStatus,
|
|
172
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { log, COLOR } = require('./config');
|
|
4
|
+
const { assets } = require('./assets');
|
|
5
|
+
const { TASK_STATUSES, STATUS_TRANSITIONS } = require('./constants');
|
|
6
|
+
|
|
7
|
+
const VALID_STATUSES = TASK_STATUSES;
|
|
8
|
+
const TRANSITIONS = STATUS_TRANSITIONS;
|
|
9
|
+
|
|
10
|
+
function loadTasks() {
|
|
11
|
+
return assets.readJson('tasks', null);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function saveTasks(data) {
|
|
15
|
+
assets.writeJson('tasks', data);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getFeatures(data) {
|
|
19
|
+
return data?.features || [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function findNextTask(data) {
|
|
23
|
+
const features = getFeatures(data);
|
|
24
|
+
const failed = features.filter(f => f.status === 'failed')
|
|
25
|
+
.sort((a, b) => (a.priority || 999) - (b.priority || 999));
|
|
26
|
+
if (failed.length > 0) return failed[0];
|
|
27
|
+
|
|
28
|
+
const pending = features.filter(f => f.status === 'pending')
|
|
29
|
+
.filter(f => {
|
|
30
|
+
const deps = f.depends_on || [];
|
|
31
|
+
return deps.every(depId => {
|
|
32
|
+
const dep = features.find(x => x.id === depId);
|
|
33
|
+
return dep && dep.status === 'done';
|
|
34
|
+
});
|
|
35
|
+
})
|
|
36
|
+
.sort((a, b) => (a.priority || 999) - (b.priority || 999));
|
|
37
|
+
if (pending.length > 0) return pending[0];
|
|
38
|
+
|
|
39
|
+
const inProgress = features.filter(f => f.status === 'in_progress')
|
|
40
|
+
.sort((a, b) => (a.priority || 999) - (b.priority || 999));
|
|
41
|
+
return inProgress[0] || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function setStatus(data, taskId, newStatus) {
|
|
45
|
+
const features = getFeatures(data);
|
|
46
|
+
const task = features.find(f => f.id === taskId);
|
|
47
|
+
if (!task) throw new Error(`任务不存在: ${taskId}`);
|
|
48
|
+
if (!VALID_STATUSES.includes(newStatus)) throw new Error(`无效状态: ${newStatus}`);
|
|
49
|
+
|
|
50
|
+
const allowed = TRANSITIONS[task.status];
|
|
51
|
+
if (!allowed || !allowed.includes(newStatus)) {
|
|
52
|
+
throw new Error(`非法状态迁移: ${task.status} → ${newStatus}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
task.status = newStatus;
|
|
56
|
+
saveTasks(data);
|
|
57
|
+
return task;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function forceStatus(data, status) {
|
|
61
|
+
const features = getFeatures(data);
|
|
62
|
+
for (const f of features) {
|
|
63
|
+
if (f.status === 'in_progress') {
|
|
64
|
+
f.status = status;
|
|
65
|
+
saveTasks(data);
|
|
66
|
+
return f;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function addTask(data, task) {
|
|
73
|
+
if (!data) {
|
|
74
|
+
data = { project: '', created_at: new Date().toISOString().slice(0, 10), features: [] };
|
|
75
|
+
}
|
|
76
|
+
data.features.push(task);
|
|
77
|
+
saveTasks(data);
|
|
78
|
+
return data;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getStats(data) {
|
|
82
|
+
const features = getFeatures(data);
|
|
83
|
+
return {
|
|
84
|
+
total: features.length,
|
|
85
|
+
done: features.filter(f => f.status === 'done').length,
|
|
86
|
+
failed: features.filter(f => f.status === 'failed').length,
|
|
87
|
+
in_progress: features.filter(f => f.status === 'in_progress').length,
|
|
88
|
+
testing: features.filter(f => f.status === 'testing').length,
|
|
89
|
+
pending: features.filter(f => f.status === 'pending').length,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function printStats() {
|
|
94
|
+
const data = loadTasks();
|
|
95
|
+
if (!data) return;
|
|
96
|
+
const stats = getStats(data);
|
|
97
|
+
log('info', `进度: ${stats.done}/${stats.total} done, ${stats.in_progress} in_progress, ${stats.testing} testing, ${stats.failed} failed, ${stats.pending} pending`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function showStatus() {
|
|
101
|
+
const data = loadTasks();
|
|
102
|
+
if (!data) {
|
|
103
|
+
log('warn', '未找到 .claude-coder/tasks.json,请先运行 claude-coder run');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const stats = getStats(data);
|
|
108
|
+
const features = getFeatures(data);
|
|
109
|
+
|
|
110
|
+
console.log(`\n${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
111
|
+
console.log(` ${COLOR.blue}📋 任务状态${COLOR.reset} 项目: ${data.project || '(未命名)'}`);
|
|
112
|
+
console.log(`${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
113
|
+
|
|
114
|
+
const bar = stats.total > 0
|
|
115
|
+
? `[${'█'.repeat(Math.floor(stats.done / stats.total * 30))}${'░'.repeat(30 - Math.floor(stats.done / stats.total * 30))}]`
|
|
116
|
+
: '[░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]';
|
|
117
|
+
console.log(` 进度: ${bar} ${stats.done}/${stats.total}`);
|
|
118
|
+
|
|
119
|
+
console.log(`\n ${COLOR.green}✔ done: ${stats.done}${COLOR.reset} ${COLOR.yellow}⏳ pending: ${stats.pending}${COLOR.reset} ${COLOR.red}✘ failed: ${stats.failed}${COLOR.reset}`);
|
|
120
|
+
|
|
121
|
+
if (stats.in_progress > 0 || stats.testing > 0) {
|
|
122
|
+
console.log(` ▸ in_progress: ${stats.in_progress} ▸ testing: ${stats.testing}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const progress = assets.readJson('progress', null);
|
|
126
|
+
if (progress) {
|
|
127
|
+
const sessions = (progress.sessions || []).filter(s => typeof s.cost === 'number');
|
|
128
|
+
if (sessions.length > 0) {
|
|
129
|
+
const totalCost = sessions.reduce((sum, s) => sum + s.cost, 0);
|
|
130
|
+
console.log(`\n ${COLOR.blue}💰 累计成本${COLOR.reset}: $${totalCost.toFixed(4)} (${sessions.length} sessions)`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log(`\n ${'─'.repeat(45)}`);
|
|
135
|
+
for (const f of features) {
|
|
136
|
+
const icon = { done: '✔', pending: '○', in_progress: '▸', testing: '⟳', failed: '✘' }[f.status] || '?';
|
|
137
|
+
const color = { done: COLOR.green, failed: COLOR.red, in_progress: COLOR.blue, testing: COLOR.yellow }[f.status] || '';
|
|
138
|
+
console.log(` ${color}${icon}${COLOR.reset} [${f.id}] ${f.description} (${f.status})`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log(`${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}\n`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
VALID_STATUSES,
|
|
146
|
+
TRANSITIONS,
|
|
147
|
+
loadTasks,
|
|
148
|
+
saveTasks,
|
|
149
|
+
getFeatures,
|
|
150
|
+
findNextTask,
|
|
151
|
+
setStatus,
|
|
152
|
+
forceStatus,
|
|
153
|
+
addTask,
|
|
154
|
+
getStats,
|
|
155
|
+
printStats,
|
|
156
|
+
showStatus,
|
|
157
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
// 进程工具
|
|
116
|
+
// ─────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 休眠
|
|
120
|
+
* @param {number} ms - 毫秒
|
|
121
|
+
* @returns {Promise<void>}
|
|
122
|
+
*/
|
|
123
|
+
function sleep(ms) {
|
|
124
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
// ─────────────────────────────────────────────────────────────
|
|
129
|
+
// 日志工具 - 统一的日志处理
|
|
130
|
+
// ─────────────────────────────────────────────────────────────
|
|
131
|
+
function localTimestamp() {
|
|
132
|
+
const d = new Date();
|
|
133
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
134
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
135
|
+
const ss = String(d.getSeconds()).padStart(2, '0');
|
|
136
|
+
return `${hh}:${mm}:${ss}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
truncateMiddle,
|
|
141
|
+
truncatePath,
|
|
142
|
+
getGitHead,
|
|
143
|
+
isGitRepo,
|
|
144
|
+
appendGitignore,
|
|
145
|
+
sleep,
|
|
146
|
+
localTimestamp,
|
|
147
|
+
};
|
package/src/core/base.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { loadSDK } = require('../common/sdk');
|
|
4
|
+
const { writeSessionSeparator } = require('../common/logging');
|
|
5
|
+
const { SessionContext } = require('./context');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 通用 Session 执行器
|
|
9
|
+
* @param {string} type - session 类型
|
|
10
|
+
* @param {object} config - 配置
|
|
11
|
+
* @param {object} [config.externalCtx] - 外部传入的 SessionContext(共享日志和 indicator)
|
|
12
|
+
*/
|
|
13
|
+
async function runSession(type, config) {
|
|
14
|
+
const sdk = await loadSDK();
|
|
15
|
+
const ctx = config.externalCtx || new SessionContext(type, config.opts);
|
|
16
|
+
|
|
17
|
+
if (!config.externalCtx) {
|
|
18
|
+
ctx.initLogging(config.logFileName, config.logStream);
|
|
19
|
+
writeSessionSeparator(ctx.logStream, config.sessionNum || 0, config.label);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const stallTimeoutMin = ctx.initHooks(type);
|
|
23
|
+
ctx.startIndicator(config.sessionNum || 0, stallTimeoutMin);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const result = await config.execute(sdk, ctx);
|
|
27
|
+
if (config.onSuccess) {
|
|
28
|
+
await config.onSuccess(result, ctx);
|
|
29
|
+
}
|
|
30
|
+
// 只有非外部 ctx 才执行 finish
|
|
31
|
+
if (!config.externalCtx) {
|
|
32
|
+
ctx.finish();
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
exitCode: ctx.isStalled() ? 2 : 0,
|
|
36
|
+
logFile: ctx.logFile,
|
|
37
|
+
stalled: ctx.isStalled(),
|
|
38
|
+
...result,
|
|
39
|
+
};
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (!config.externalCtx) {
|
|
42
|
+
ctx.errorFinish(err);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
exitCode: 1,
|
|
46
|
+
error: err.message,
|
|
47
|
+
logFile: ctx.logFile,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
runSession,
|
|
54
|
+
};
|