ai-worktool 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.startTestAgent = startTestAgent;
7
+ const path_1 = __importDefault(require("path"));
8
+ const agents_1 = require("./agents");
9
+ const tools_1 = require("./tools");
10
+ async function startTestAgent(options) {
11
+ const ctrl = options.controller;
12
+ const log = options.onMessage || console.log;
13
+ let isErrorEnd = false;
14
+ log(`开始干活... 😎`);
15
+ console.log(options.projectRoot);
16
+ try {
17
+ // 工作流程:
18
+ // 1、检查当前项目单元测试工具是否安装好,否则安装测试工具搭建测试环境
19
+ const projectConfig = await (0, tools_1.detectProjectConfig)(options.projectRoot, options);
20
+ if (!projectConfig.isInstalled) {
21
+ log('项目初始化...');
22
+ await (0, tools_1.initProject)(options.projectRoot, projectConfig.packageManager);
23
+ }
24
+ await (0, tools_1.setupTestEnvironment)(projectConfig.projectRoot, projectConfig.packageManager, projectConfig.languages[0], projectConfig.testFramework);
25
+ await options.onReady?.(projectConfig);
26
+ let toolCalls = null;
27
+ let lastTestResult = null;
28
+ let lastFailedTest = null;
29
+ do {
30
+ if (!toolCalls) {
31
+ toolCalls = [];
32
+ }
33
+ // 2、先执行单测并生成单测覆盖报告
34
+ log('开始检查测试覆盖率...');
35
+ const result = await (0, tools_1.runTests)(projectConfig.projectRoot, projectConfig.testFramework, projectConfig.coverageDir, projectConfig.testConfigFile);
36
+ if (ctrl?.signal.aborted) {
37
+ break;
38
+ }
39
+ if (!result.success) {
40
+ // // 单测执行失败,没有发现任何需要测试的代码
41
+ // if (result.noTestsFound) {
42
+ // log('没有发现任何需要测试的源代码...');
43
+ // if (!options.fullCoverageEnd) {
44
+ // await wait(options.idleTimeout || 5000);
45
+ // continue;
46
+ // } else {
47
+ // break;
48
+ // }
49
+ // }
50
+ // 上一次执行必须有写入类操作,否则不会有新的单测覆盖率变化
51
+ if (lastTestResult?.stderr === result.stderr &&
52
+ toolCalls.every((toolCall) => !tools_1.modifyTools.includes(toolCall.functionName))) {
53
+ log('没有新的代码修改,等待中...');
54
+ await (0, tools_1.wait)(options.idleTimeout || 5000);
55
+ continue;
56
+ }
57
+ lastTestResult = result;
58
+ // 单测执行失败,比如代码编译不通过
59
+ log('单测执行失败,开始修复...');
60
+ await (0, agents_1.startAgent)(options.platform, 'fixconfig', (0, agents_1.fixErrorPrompt)(result.stderr), projectConfig, options.withConversation, ctrl, options.mode, options.onMessage, async (tool) => {
61
+ toolCalls.push(tool);
62
+ });
63
+ if (ctrl?.signal.aborted) {
64
+ break;
65
+ }
66
+ continue;
67
+ }
68
+ // 3、要是有失败的单测先修复单测代码,重复第2步
69
+ const failedTests = await (0, tools_1.getFailedTests)(projectConfig.projectRoot);
70
+ if (ctrl?.signal.aborted) {
71
+ break;
72
+ }
73
+ if (failedTests.length > 0) {
74
+ const failedTest = failedTests[0];
75
+ if (lastFailedTest?.errorMessages.join('\n') === failedTest.errorMessages.join('\n') &&
76
+ lastFailedTest.testFilePath === failedTest.testFilePath &&
77
+ lastFailedTest.testName === failedTest.testName &&
78
+ toolCalls.every((toolCall) => !tools_1.modifyTools.includes(toolCall.functionName))) {
79
+ log('没有新的单测失败,等待中...');
80
+ await (0, tools_1.wait)(options.idleTimeout || 5000);
81
+ continue;
82
+ }
83
+ lastFailedTest = failedTest;
84
+ // 不能并行修复,改完行号都会变,所以每次修复完需要重新执行单测
85
+ log(`发现有失败的单测${path_1.default.basename(failedTest.testFilePath)},开始修复...`);
86
+ await (0, agents_1.startAgent)(options.platform,
87
+ // 没有testName说明是修复单测代码本身问题
88
+ failedTest.testName ? 'fixbug' : 'fixconfig', await (0, agents_1.fixCodePrompt)(failedTest), projectConfig, options.withConversation, ctrl, options.mode, options.onMessage, async (tool) => {
89
+ toolCalls.push(tool);
90
+ });
91
+ if (ctrl?.signal.aborted) {
92
+ break;
93
+ }
94
+ continue;
95
+ }
96
+ // 4、要是单测都成功并且有代码修改提交本次修改
97
+ if (options.autoCommit) {
98
+ }
99
+ // 5、要是有未覆盖的代码生成新的单测用例代码,重复第2步
100
+ const { total, files } = await (0, tools_1.generateUncoveredLinesReport)(projectConfig.projectRoot, projectConfig.coverageDir);
101
+ if (ctrl?.signal.aborted) {
102
+ break;
103
+ }
104
+ log(`当前代码总覆盖率为${parseFloat(total[options.checkPct || 'branches'].pct.toString()) || 0}%`);
105
+ const finished = total[options.checkPct || 'branches'].pct >= 100;
106
+ if (!finished) {
107
+ for (const filePath of Object.keys(files)) {
108
+ const uncoveredLineFile = files[filePath];
109
+ if (uncoveredLineFile.uncoveredLines.length <= 0) {
110
+ continue;
111
+ }
112
+ log(`发现有未覆盖行,开始补充...`, filePath);
113
+ await (0, agents_1.startAgent)(options.platform, 'addtest', await (0, agents_1.addUncoveredLinePrompt)(filePath, uncoveredLineFile), projectConfig, options.withConversation, ctrl, options.mode, options.onMessage, async (tool) => {
114
+ toolCalls.push(tool);
115
+ });
116
+ if (ctrl?.signal.aborted) {
117
+ break;
118
+ }
119
+ }
120
+ }
121
+ else if (!options.fullCoverageEnd) {
122
+ await (0, tools_1.wait)(options.idleTimeout || 5000);
123
+ }
124
+ else {
125
+ break;
126
+ }
127
+ } while (!ctrl?.signal.aborted);
128
+ }
129
+ catch (err) {
130
+ if (err.name === 'AbortError') {
131
+ console.log('已终止');
132
+ }
133
+ else {
134
+ console.error(err);
135
+ log('发生错误,' + err.message);
136
+ isErrorEnd = true;
137
+ }
138
+ }
139
+ log(isErrorEnd ? '被下班~ 👻' : '下班下班 👋');
140
+ }
141
+ //# sourceMappingURL=testAgent.js.map
@@ -0,0 +1,543 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.parseOperations = parseOperations;
40
+ exports.searchFilesByExtension = searchFilesByExtension;
41
+ exports.searchFilesByText = searchFilesByText;
42
+ exports.readFile = readFile;
43
+ exports.writeFile = writeFile;
44
+ exports.renameFile = renameFile;
45
+ exports.modifyLine = modifyLine;
46
+ exports.insertLine = insertLine;
47
+ exports.getLine = getLine;
48
+ exports.deleteLine = deleteLine;
49
+ exports.batchModifyLines = batchModifyLines;
50
+ exports.deleteFolder = deleteFolder;
51
+ const fs = __importStar(require("fs/promises"));
52
+ const path_1 = __importDefault(require("path"));
53
+ /**
54
+ * 解析操作字符串为内部操作对象
55
+ * @param operations 操作字符串,格式:操作类型:行号范围:内容(仅增/改需要),多操作用逗号分隔
56
+ * 操作类型:i(插入)、u(更新)、d(删除)
57
+ * 行号范围:单行用数字,多行用数字-数字,如 3 或 5-7
58
+ * 内容:用双引号包裹,支持\n换行,如 "新内容" 或 "第一行\n第二行"
59
+ * @example 'd:5-7,u:3:"更新第3行",i:2:"插入新行"'
60
+ */
61
+ function parseOperations(operations) {
62
+ const operationList = [];
63
+ const regex = /([a-z]):(\d*(?:-\d+)?)(?::"((?:[^"\\]|\\.)*)")?/g;
64
+ let match;
65
+ while ((match = regex.exec(operations)) !== null) {
66
+ const op = match[0];
67
+ const type = match[1];
68
+ const lineRange = match[2]; // 行号或范围
69
+ let text = match[3] || null; // 可选的字符串值
70
+ if (text) {
71
+ // 处理转义字符
72
+ text = text.replace(/\\n/g, '\n');
73
+ }
74
+ if (type === undefined || !['i', 'u', 'd'].includes(type)) {
75
+ throw new Error(`无效的操作类型: ${type}`);
76
+ }
77
+ let startLine, endLine;
78
+ // 解析行号范围
79
+ if (lineRange.includes('-')) {
80
+ [startLine, endLine] = lineRange.split('-').map((txt) => (txt ? Number(txt) : NaN));
81
+ }
82
+ else if (lineRange) {
83
+ startLine = Number(lineRange);
84
+ endLine = startLine;
85
+ }
86
+ else {
87
+ startLine = NaN;
88
+ endLine = NaN;
89
+ }
90
+ // 验证行号
91
+ if (isNaN(startLine) || isNaN(endLine) || startLine < 0 || endLine < startLine) {
92
+ throw new Error(`无效的行号范围: ${lineRange}`);
93
+ }
94
+ // 处理文本内容(转义字符)
95
+ let content = text?.startsWith('"') && text?.endsWith('"') ? text?.slice(1, -1) : text;
96
+ if (content) {
97
+ content = content.replace(/\\"/g, '"').replace(/\\n/g, '\n');
98
+ }
99
+ // 验证必要参数
100
+ if ((type === 'i' || type === 'u') && !content) {
101
+ throw new Error(`操作 ${op} 缺少文本内容, 文本内容需要用双引号包裹`);
102
+ }
103
+ if (type === 'd' && content) {
104
+ throw new Error(`删除操作 ${op} 不应包含文本内容`);
105
+ }
106
+ operationList.push({
107
+ type: type,
108
+ startLine,
109
+ endLine,
110
+ text: content
111
+ });
112
+ }
113
+ if (operationList.length === 0 && operations) {
114
+ throw new Error(`无效的操作格式: ${operations}`);
115
+ }
116
+ return operationList;
117
+ }
118
+ /**
119
+ * 在目录中递归搜索特定扩展名的文件
120
+ * @param directory 目录路径,如 '/src/components'
121
+ * @param extensions 文件扩展名数组,如 '.js|.jsx'
122
+ * @param includeIgnoreFiles 是否包含忽略文件,默认为false
123
+ * @returns 匹配的文件路径数组
124
+ */
125
+ async function searchFilesByExtension(directory, extensions, includeIgnoreFiles = false) {
126
+ try {
127
+ await fs.access(directory); // 检查目录是否存在
128
+ }
129
+ catch {
130
+ throw new Error(`目录不存在: ${directory}`);
131
+ }
132
+ const files = [];
133
+ const extensionsList = extensions?.split('|') || [];
134
+ // 通用忽略目录
135
+ const commonIgnoreDirs = [
136
+ 'node_modules',
137
+ '.git',
138
+ 'dist',
139
+ 'build',
140
+ 'vendor',
141
+ '.idea',
142
+ '.vscode',
143
+ 'tmp',
144
+ 'temp',
145
+ 'logs',
146
+ 'docs',
147
+ 'examples',
148
+ '.lock'
149
+ ];
150
+ // 其他语言忽略目录
151
+ const javaFamilyIgnoreDirs = ['target', 'bin', 'out', '.classpath', '.project', '.settings'];
152
+ const goIgnoreDirs = ['pkg', 'vendor', '.cache', 'bin'];
153
+ const pythonIgnoreDirs = ['__pycache__', 'venv', '.venv', 'env', 'build', 'dist', 'egg-info'];
154
+ const dotnetIgnoreDirs = ['obj', 'bin', 'packages', '.vs'];
155
+ const rubyIgnoreDirs = ['vendor/bundle', '.bundle', 'tmp', 'log'];
156
+ const rustIgnoreDirs = ['target'];
157
+ const swiftIgnoreDirs = ['.build', 'Carthage', 'Pods'];
158
+ // 合并所有忽略目录
159
+ const ignoreDirs = [
160
+ ...commonIgnoreDirs,
161
+ ...javaFamilyIgnoreDirs,
162
+ ...goIgnoreDirs,
163
+ ...pythonIgnoreDirs,
164
+ ...dotnetIgnoreDirs,
165
+ ...rubyIgnoreDirs,
166
+ ...rustIgnoreDirs,
167
+ ...swiftIgnoreDirs
168
+ ];
169
+ async function search(dir) {
170
+ const entries = await fs.readdir(dir, { withFileTypes: true });
171
+ for (const entry of entries) {
172
+ const entryPath = path_1.default.join(dir, entry.name);
173
+ if (entry.isDirectory()) {
174
+ if (!includeIgnoreFiles && ignoreDirs.includes(entry.name)) {
175
+ continue;
176
+ }
177
+ await search(entryPath);
178
+ }
179
+ else if (extensionsList.some((ext) => entry.name.endsWith(ext))) {
180
+ files.push(entryPath);
181
+ }
182
+ }
183
+ }
184
+ await search(directory);
185
+ return files;
186
+ }
187
+ /**
188
+ * 在目录中递归搜索包含特定文本的文件
189
+ * @param directory 目录路径,如 '/src'
190
+ * @param text 要搜索的文本,如 'console.log'
191
+ * @param extensions 可选的文件扩展名过滤,如 '.js|.ts'
192
+ * @returns 匹配的文件路径数组
193
+ */
194
+ async function searchFilesByText(directory, text, extensions) {
195
+ try {
196
+ await fs.access(directory);
197
+ }
198
+ catch {
199
+ throw new Error(`目录不存在: ${directory}`);
200
+ }
201
+ const files = [];
202
+ const extensionsList = extensions?.split('|') || [];
203
+ async function search(dir) {
204
+ const entries = await fs.readdir(dir, { withFileTypes: true });
205
+ for (const entry of entries) {
206
+ const entryPath = path_1.default.join(dir, entry.name);
207
+ if (entry.isDirectory()) {
208
+ await search(entryPath);
209
+ }
210
+ else if (!extensionsList.length || extensionsList.some((ext) => entry.name.endsWith(ext))) {
211
+ try {
212
+ const content = await fs.readFile(entryPath, 'utf-8');
213
+ if (content.includes(text)) {
214
+ files.push(entryPath);
215
+ }
216
+ }
217
+ catch (error) {
218
+ console.warn(`无法读取文件 ${entryPath}: ${error.message}`);
219
+ }
220
+ }
221
+ }
222
+ }
223
+ await search(directory);
224
+ if (files.length === 0) {
225
+ throw new Error('未找到包含指定文本的文件');
226
+ }
227
+ return files;
228
+ }
229
+ /**
230
+ * 读取文件内容
231
+ * @param filePath 文件路径,如 'src/config/app.json'
232
+ * @param encoding 文件编码,默认为 'utf8'
233
+ * @param withRowNo 是否添加行号,默认为 false
234
+ * @returns 文件内容字符串
235
+ * @example
236
+ * const content = await readFile('example.txt');
237
+ * const contentWithLines = await readFile('example.txt', 'utf8', true);
238
+ */
239
+ async function readFile(filePath, encoding = 'utf8', withRowNo = false) {
240
+ try {
241
+ encoding = encoding || 'utf8';
242
+ await fs.access(filePath);
243
+ const content = await fs.readFile(filePath, { encoding });
244
+ if (withRowNo) {
245
+ const lines = content.replace(/\r\n/g, '\n').split('\n');
246
+ return lines.map((line, index) => `${index + 1}:${line}`).join('\n');
247
+ }
248
+ else {
249
+ return content.replace(/\r\n/g, '\n');
250
+ }
251
+ }
252
+ catch (error) {
253
+ if (error.code === 'ENOENT') {
254
+ throw new Error(`文件不存在: ${filePath}`);
255
+ }
256
+ throw new Error(`读取文件失败: ${error.message}`);
257
+ }
258
+ }
259
+ /**
260
+ * 写入文件
261
+ * @param filePath 文件路径,如 'src/data/output.txt'
262
+ * @param content 文本内容
263
+ * @param encoding 文件编码,默认为 'utf8'
264
+ * @param overwrite 是否覆盖现有文件,默认为 false
265
+ * @example
266
+ * await writeFile('new.txt', 'Hello World');
267
+ * await writeFile('existing.txt', 'New content', 'utf8', true);
268
+ */
269
+ async function writeFile(filePath, content, encoding = 'utf8', overwrite = true) {
270
+ let exists = false;
271
+ overwrite = overwrite ?? true;
272
+ try {
273
+ await fs.access(filePath);
274
+ exists = true;
275
+ }
276
+ catch { }
277
+ if (exists && !overwrite) {
278
+ throw new Error(`写入文件失败,文件已存在: ${filePath}`);
279
+ }
280
+ try {
281
+ encoding = encoding || 'utf8';
282
+ const dir = path_1.default.dirname(filePath);
283
+ try {
284
+ await fs.access(dir);
285
+ }
286
+ catch {
287
+ await fs.mkdir(dir, { recursive: true });
288
+ }
289
+ await fs.writeFile(filePath, content.replace(/\r\n/g, '\n'), encoding);
290
+ }
291
+ catch (error) {
292
+ throw new Error(`写入文件失败: ${error.message}`);
293
+ }
294
+ }
295
+ /**
296
+ * 给文件或目录改名
297
+ * @param oldPath 原文件/目录路径
298
+ * @param newPath 新文件/目录路径
299
+ * @param overwrite 如果新路径已存在,是否覆盖,默认为 false
300
+ * @example
301
+ * await renameFile('old.txt', 'new.txt');
302
+ * await renameFile('docs', 'documentation', true);
303
+ */
304
+ async function renameFile(oldPath, newPath, overwrite = false) {
305
+ try {
306
+ // 检查原路径是否存在
307
+ await fs.access(oldPath);
308
+ }
309
+ catch {
310
+ throw new Error(`原路径不存在: ${oldPath}`);
311
+ }
312
+ try {
313
+ // 检查新路径是否存在
314
+ await fs.access(newPath);
315
+ if (!overwrite) {
316
+ throw new Error(`新路径已存在: ${newPath},如需覆盖请设置 overwrite 为 true`);
317
+ }
318
+ // 若新路径存在且允许覆盖,先删除新路径
319
+ const stats = await fs.stat(newPath);
320
+ if (stats.isDirectory()) {
321
+ await deleteFolder(newPath, { safeMode: false });
322
+ }
323
+ else {
324
+ await fs.unlink(newPath);
325
+ }
326
+ }
327
+ catch (error) {
328
+ // 如果是因为新路径不存在而抛出的错误,则忽略
329
+ if (error.code !== 'ENOENT') {
330
+ throw error;
331
+ }
332
+ }
333
+ try {
334
+ // 创建新路径所在的目录(如果不存在)
335
+ const newDir = path_1.default.dirname(newPath);
336
+ await fs.mkdir(newDir, { recursive: true });
337
+ // 执行重命名操作
338
+ await fs.rename(oldPath, newPath);
339
+ }
340
+ catch (error) {
341
+ throw new Error(`重命名失败: ${error.message}`);
342
+ }
343
+ }
344
+ /**
345
+ * 修改指定行的文本内容(支持连续多行)
346
+ * @param filePath 文件路径,如 'src/main.js'
347
+ * @param startLine 起始行号(从1开始)
348
+ * @param endLine 结束行号(可选,默认为startLine)
349
+ * @param newText 新的文本内容(字符串或字符串数组)
350
+ * @param encoding 文件编码,默认为 'utf8'
351
+ * @example
352
+ * await modifyLine('file.txt', 5, 5, 'New content for line 5');
353
+ * await modifyLine('file.txt', 3, 5, ['Line 3', 'Line 4', 'Line 5']);
354
+ */
355
+ async function modifyLine(filePath, startLine, endLine = startLine, newText, encoding = 'utf8') {
356
+ endLine = endLine || startLine;
357
+ if (startLine < 1 || endLine < startLine) {
358
+ throw new Error(`行号参数错误: startLine=${startLine}, endLine=${endLine}`);
359
+ }
360
+ encoding = encoding || 'utf8';
361
+ const lines = (await readFile(filePath, encoding, false)).split('\n');
362
+ const lineCount = lines.length;
363
+ if (endLine > lineCount) {
364
+ throw new Error(`行号超出范围: ${endLine} (总行数: ${lineCount})`);
365
+ }
366
+ const replacementLines = Array.isArray(newText) ? newText : [newText];
367
+ const replaceCount = endLine - startLine + 1;
368
+ lines.splice(startLine - 1, replaceCount, ...replacementLines);
369
+ await writeFile(filePath, lines.join('\n'), encoding, true);
370
+ }
371
+ /**
372
+ * 在指定位置插入一行文本
373
+ * @param filePath 文件路径,如 'src/config.js'
374
+ * @param lineNumber 插入位置(从1开始,0表示在文件开头插入)
375
+ * @param text 要插入的文本
376
+ * @param encoding 文件编码,默认为 'utf8'
377
+ * @example
378
+ * await insertLine('file.txt', 0, 'New first line');
379
+ * await insertLine('file.txt', 3, 'Inserted after line 3');
380
+ */
381
+ async function insertLine(filePath, lineNumber, text, encoding = 'utf8') {
382
+ encoding = encoding || 'utf8';
383
+ const lines = (await readFile(filePath, encoding, false)).split('\n');
384
+ lines.splice(lineNumber, 0, text);
385
+ await writeFile(filePath, lines.join('\n'), encoding, true);
386
+ }
387
+ /**
388
+ * 查找指定行的文本内容
389
+ * @param filePath 文件路径,如 'src/data.txt'
390
+ * @param lineNumber 行号(从1开始)
391
+ * @param encoding 文件编码,默认为 'utf8'
392
+ * @returns 指定行的文本内容
393
+ * @example
394
+ * const line5 = await getLine('file.txt', 5);
395
+ */
396
+ async function getLine(filePath, lineNumber, encoding = 'utf8') {
397
+ encoding = encoding || 'utf8';
398
+ const lines = (await readFile(filePath, encoding, false)).split('\n');
399
+ if (lineNumber < 1 || lineNumber > lines.length) {
400
+ throw new Error(`行号超出范围: ${lineNumber} (总行数: ${lines.length})`);
401
+ }
402
+ return lines[lineNumber - 1];
403
+ }
404
+ /**
405
+ * 删除指定行(支持连续多行)
406
+ * @param filePath 文件路径,如 'src/temp.txt'
407
+ * @param startLine 起始行号(从1开始)
408
+ * @param endLine 结束行号(可选,默认为startLine)
409
+ * @param encoding 文件编码,默认为 'utf8'
410
+ * @example
411
+ * await deleteLine('file.txt', 5);
412
+ * await deleteLine('file.txt', 3, 7);
413
+ */
414
+ async function deleteLine(filePath, startLine, endLine = startLine, encoding = 'utf8') {
415
+ if (startLine < 1 || endLine < startLine) {
416
+ throw new Error(`行号参数错误: startLine=${startLine}, endLine=${endLine}`);
417
+ }
418
+ encoding = encoding || 'utf8';
419
+ const lines = (await readFile(filePath, encoding, false)).split('\n');
420
+ const lineCount = lines.length;
421
+ if (endLine > lineCount) {
422
+ throw new Error(`行号超出范围: ${endLine} (总行数: ${lineCount})`);
423
+ }
424
+ const deleteCount = endLine - startLine + 1;
425
+ lines.splice(startLine - 1, deleteCount);
426
+ await writeFile(filePath, lines.join('\n'), encoding, true);
427
+ }
428
+ /**
429
+ * 多行批量操作,支持同时插入、更新和删除操作非连续行。
430
+ * @param filePath 文件路径,如 'src/config/app.config'
431
+ * @param operations 操作字符串,格式:命令:行号:"内容"(d:删除,u:更新,i:插入,后边是行号范围或行号,后边是更新内容或插入内容,内容用双引号包围),逗号分割多个命令 如 'd:5-7,u:10-12:"行10\n行11\n行12",i:1:"文件开头"'
432
+ * @param encoding 文件编码,默认为 'utf8'
433
+ * @example
434
+ * await batchModifyLines('file.txt', 'd:5-7,u:3:"更新第3行",i:2:"插入新行"');
435
+ * await batchModifyLines('file.txt', 'u:10-12:"行10\n行11\n行12",i:1:"文件开头"');
436
+ */
437
+ async function batchModifyLines(filePath, operations, encoding = 'utf8') {
438
+ if (!operations || operations.trim() === '') {
439
+ throw new Error('没有提供任何操作');
440
+ }
441
+ // 解析操作字符串
442
+ const ops = parseOperations(operations);
443
+ encoding = encoding || 'utf8';
444
+ const originalContent = await readFile(filePath, encoding, false);
445
+ let lines = originalContent.split('\n');
446
+ const originalLineCount = lines.length;
447
+ // 验证所有操作的行号有效性
448
+ ops.forEach((op) => {
449
+ if (op.type === 'i') {
450
+ if (op.startLine < 0) {
451
+ // || op.startLine > originalLineCount
452
+ throw new Error(`插入操作行号超出范围: ${op.startLine} (原始总行数: ${originalLineCount})`);
453
+ }
454
+ }
455
+ else {
456
+ if (op.startLine < 1 || op.endLine > originalLineCount) {
457
+ throw new Error(`${op.type === 'u' ? '更新' : '删除'}操作行号超出范围: ${op.startLine}-${op.endLine} (原始总行数: ${originalLineCount})`);
458
+ }
459
+ }
460
+ });
461
+ // 分离并排序操作
462
+ const deleteOps = ops.filter((op) => op.type === 'd').sort((a, b) => b.startLine - a.startLine);
463
+ const updateOps = ops.filter((op) => op.type === 'u');
464
+ const insertOps = ops.filter((op) => op.type === 'i').sort((a, b) => a.startLine - b.startLine);
465
+ // 应用删除操作
466
+ deleteOps.forEach((op) => {
467
+ const lineCount = op.endLine - op.startLine + 1;
468
+ const deletedLinesBefore = deleteOps
469
+ .filter((d) => d.startLine > op.endLine)
470
+ .reduce((sum, d) => sum + (d.endLine - d.startLine + 1), 0);
471
+ const actualStartLine = op.startLine - 1 - deletedLinesBefore;
472
+ lines.splice(actualStartLine, lineCount);
473
+ });
474
+ // 应用更新操作
475
+ updateOps.forEach((op) => {
476
+ const lineCount = op.endLine - op.startLine + 1;
477
+ const deletedLinesBefore = deleteOps
478
+ .filter((d) => d.endLine < op.startLine)
479
+ .reduce((sum, d) => sum + (d.endLine - d.startLine + 1), 0);
480
+ const actualStartLine = op.startLine - 1 - deletedLinesBefore;
481
+ const replacementLines = op.text?.split('\n') || [];
482
+ lines.splice(actualStartLine, lineCount, ...replacementLines);
483
+ });
484
+ // 应用插入操作
485
+ insertOps.forEach((op) => {
486
+ // const deletedLinesBefore = deleteOps
487
+ // .filter((d) => d.endLine <= op.startLine)
488
+ // .reduce((sum, d) => sum + (d.endLine - d.startLine + 1), 0);
489
+ // const insertedLinesBefore = insertOps
490
+ // .filter((i) => i.startLine < op.startLine)
491
+ // .reduce((sum, i) => sum + (i.text?.split('\n').length || 0), 0);
492
+ const actualStartLine = op.startLine - 1; // - deletedLinesBefore + insertedLinesBefore;
493
+ const insertLines = op.text?.split('\n') || [];
494
+ lines.splice(actualStartLine, 0, ...insertLines);
495
+ });
496
+ // 写回文件
497
+ await writeFile(filePath, lines.join('\n'), encoding, true);
498
+ }
499
+ /**
500
+ * 安全递归删除文件夹及其内容
501
+ * @param targetPath 要删除的目录路径,如 'dist'
502
+ * @param options 配置选项
503
+ * @example
504
+ * await deleteFolder('dist');
505
+ * await deleteFolder('temp', { safeMode: false });
506
+ */
507
+ async function deleteFolder(targetPath, options = {}) {
508
+ const { safeMode = true, allowedRoots = ['dist', 'build', 'temp', 'test-output'], logger = console.log } = options;
509
+ const normalizedPath = path_1.default.normalize(targetPath);
510
+ if (safeMode) {
511
+ const isAllowedRoot = allowedRoots.some((root) => normalizedPath.includes(path_1.default.sep + root + path_1.default.sep) || normalizedPath.endsWith(path_1.default.sep + root));
512
+ if (!isAllowedRoot) {
513
+ throw new Error(`删除操作已被阻止:路径 '${normalizedPath}' 不在允许的根目录列表中`);
514
+ }
515
+ if (normalizedPath === '.' || normalizedPath === '..' || normalizedPath === '/') {
516
+ throw new Error(`安全限制:不允许删除根目录或相对根路径`);
517
+ }
518
+ }
519
+ try {
520
+ const stats = await fs.stat(normalizedPath);
521
+ if (stats.isDirectory()) {
522
+ const entries = await fs.readdir(normalizedPath);
523
+ for (const entry of entries) {
524
+ const entryPath = path_1.default.join(normalizedPath, entry);
525
+ await deleteFolder(entryPath, { safeMode, allowedRoots, logger });
526
+ }
527
+ await fs.rmdir(normalizedPath);
528
+ logger(`已删除目录: ${normalizedPath}`);
529
+ }
530
+ else {
531
+ await fs.unlink(normalizedPath);
532
+ logger(`已删除文件: ${normalizedPath}`);
533
+ }
534
+ }
535
+ catch (error) {
536
+ if (error.code === 'ENOENT') {
537
+ logger(`路径不存在: ${normalizedPath}`);
538
+ return;
539
+ }
540
+ throw error;
541
+ }
542
+ }
543
+ //# sourceMappingURL=file.js.map