@movemama/opencode-legacy 0.1.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 +53 -0
- package/index.js +19 -0
- package/legacy-rules.json +19 -0
- package/package.json +36 -0
- package/plugin-meta.js +14 -0
- package/tools/edit.js +56 -0
- package/tools/edit.ts +64 -0
- package/tools/grep.js +210 -0
- package/tools/legacy-codec.js +13 -0
- package/tools/legacy-edit-core.mjs +134 -0
- package/tools/legacy-router.mjs +149 -0
- package/tools/legacy-search-core.mjs +84 -0
- package/tools/legacy.js +78 -0
- package/tools/legacy.ts +230 -0
- package/tools/opencode-paths.mjs +41 -0
- package/tools/read.js +148 -0
- package/tools/read.ts +213 -0
- package/tools/script-edit-core.mjs +126 -0
- package/tools/script-edit.js +59 -0
- package/tools/script-edit.ts +59 -0
- package/tools/txt-gb2312-tool.mjs +392 -0
- package/tools/write.js +53 -0
- package/tools/write.ts +67 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
import { getKnownIconvCandidates } from './opencode-paths.mjs';
|
|
8
|
+
|
|
9
|
+
function normalizeWindowsPath(filePath) {
|
|
10
|
+
return filePath.replace(/\\/g, '/');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getDerivedIconvCandidates() {
|
|
14
|
+
const pathValue = process.env.Path || process.env.PATH || '';
|
|
15
|
+
const parts = pathValue.split(';').map((item) => item.trim()).filter(Boolean);
|
|
16
|
+
const derived = [];
|
|
17
|
+
|
|
18
|
+
for (const part of parts) {
|
|
19
|
+
const normalized = normalizeWindowsPath(part);
|
|
20
|
+
if (normalized.toLowerCase().endsWith('/git/cmd')) {
|
|
21
|
+
derived.push(normalized.replace(/\/cmd$/i, '/usr/bin/iconv.exe'));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return derived;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ICONV_CANDIDATES = [
|
|
29
|
+
'iconv',
|
|
30
|
+
process.env.OPENCODE_ICONV_PATH,
|
|
31
|
+
process.env.ICONV_PATH,
|
|
32
|
+
...getKnownIconvCandidates(),
|
|
33
|
+
...getDerivedIconvCandidates(),
|
|
34
|
+
].filter(Boolean);
|
|
35
|
+
|
|
36
|
+
const DEFAULT_ICONV_INSTALL_COMMAND = [
|
|
37
|
+
'winget',
|
|
38
|
+
'install',
|
|
39
|
+
'--id',
|
|
40
|
+
'mlocati.GetText',
|
|
41
|
+
'--source',
|
|
42
|
+
'winget',
|
|
43
|
+
'--accept-package-agreements',
|
|
44
|
+
'--accept-source-agreements',
|
|
45
|
+
'--disable-interactivity',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function runInstallCommand(command, args) {
|
|
49
|
+
return spawnSync(command, args, {
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
shell: process.platform === 'win32',
|
|
52
|
+
env: process.env,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function fail(message) {
|
|
57
|
+
process.stderr.write(`${message}\n`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getIconvCandidates() {
|
|
62
|
+
if (process.env.OPENCODE_ICONV_DISABLE_FALLBACKS === '1') {
|
|
63
|
+
return [process.env.OPENCODE_ICONV_PATH].filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
return ICONV_CANDIDATES;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function tryResolveIconvPath() {
|
|
69
|
+
const candidates = getIconvCandidates();
|
|
70
|
+
const diagnostics = [];
|
|
71
|
+
|
|
72
|
+
for (const candidate of candidates) {
|
|
73
|
+
const result = spawnSync(candidate, ['--version'], { encoding: 'utf8' });
|
|
74
|
+
diagnostics.push({
|
|
75
|
+
candidate,
|
|
76
|
+
status: result.status,
|
|
77
|
+
error: result.error?.message ?? null,
|
|
78
|
+
stdout: (result.stdout || '').slice(0, 120),
|
|
79
|
+
stderr: (result.stderr || '').slice(0, 120),
|
|
80
|
+
});
|
|
81
|
+
if (result.status === 0) {
|
|
82
|
+
return { resolvedPath: candidate, diagnostics };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { resolvedPath: null, diagnostics };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function tryAutoInstallIconv() {
|
|
90
|
+
if (process.env.OPENCODE_ICONV_SKIP_AUTO_INSTALL === '1') {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (process.env.OPENCODE_ICONV_INSTALLER_NODE_SCRIPT) {
|
|
95
|
+
const nodeCommand = process.env.OPENCODE_NODE_PATH || process.execPath || 'node';
|
|
96
|
+
const result = spawnSync(nodeCommand, [process.env.OPENCODE_ICONV_INSTALLER_NODE_SCRIPT], {
|
|
97
|
+
encoding: 'utf8',
|
|
98
|
+
env: process.env,
|
|
99
|
+
});
|
|
100
|
+
return result.status === 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const [command, ...args] = DEFAULT_ICONV_INSTALL_COMMAND;
|
|
104
|
+
const result = runInstallCommand(command, args);
|
|
105
|
+
|
|
106
|
+
return result.status === 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveIconvPath() {
|
|
110
|
+
const firstAttempt = tryResolveIconvPath();
|
|
111
|
+
const resolvedPath = firstAttempt.resolvedPath;
|
|
112
|
+
if (resolvedPath) {
|
|
113
|
+
return resolvedPath;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const installed = tryAutoInstallIconv();
|
|
117
|
+
if (installed) {
|
|
118
|
+
const secondAttempt = tryResolveIconvPath();
|
|
119
|
+
const installedPath = secondAttempt.resolvedPath;
|
|
120
|
+
if (installedPath) {
|
|
121
|
+
return installedPath;
|
|
122
|
+
}
|
|
123
|
+
fail(
|
|
124
|
+
[
|
|
125
|
+
'iconv 自动安装后仍不可用',
|
|
126
|
+
`cwd=${process.cwd()}`,
|
|
127
|
+
`OPENCODE_ICONV_PATH=${process.env.OPENCODE_ICONV_PATH || '<empty>'}`,
|
|
128
|
+
`ICONV_PATH=${process.env.ICONV_PATH || '<empty>'}`,
|
|
129
|
+
`PATH_SAMPLE=${(process.env.Path || process.env.PATH || '').slice(0, 300)}`,
|
|
130
|
+
`diagnostics=${JSON.stringify(secondAttempt.diagnostics)}`,
|
|
131
|
+
].join(' | '),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fail(
|
|
136
|
+
[
|
|
137
|
+
'iconv 不可用、未安装、路径不可访问、自动安装失败或执行失败',
|
|
138
|
+
`cwd=${process.cwd()}`,
|
|
139
|
+
`OPENCODE_ICONV_PATH=${process.env.OPENCODE_ICONV_PATH || '<empty>'}`,
|
|
140
|
+
`ICONV_PATH=${process.env.ICONV_PATH || '<empty>'}`,
|
|
141
|
+
`PATH_SAMPLE=${(process.env.Path || process.env.PATH || '').slice(0, 300)}`,
|
|
142
|
+
`diagnostics=${JSON.stringify(firstAttempt.diagnostics)}`,
|
|
143
|
+
].join(' | '),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function runIconv(iconvPath, args, options = {}) {
|
|
148
|
+
const result = spawnSync(iconvPath, args, {
|
|
149
|
+
encoding: 'buffer',
|
|
150
|
+
...options,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (result.status !== 0) {
|
|
154
|
+
const errorText = result.stderr ? result.stderr.toString('utf8') : 'iconv 执行失败';
|
|
155
|
+
fail(errorText.trim() || 'iconv 执行失败');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function detectNonGb2312Chars(iconvPath, text, limit = 8) {
|
|
162
|
+
const issues = [];
|
|
163
|
+
let line = 1;
|
|
164
|
+
let column = 1;
|
|
165
|
+
|
|
166
|
+
for (const char of text) {
|
|
167
|
+
if (char === '\r') {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (char === '\n') {
|
|
172
|
+
line += 1;
|
|
173
|
+
column = 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const probe = spawnSync(iconvPath, ['-f', 'UTF-8', '-t', 'GB2312'], {
|
|
178
|
+
input: Buffer.from(char, 'utf8'),
|
|
179
|
+
encoding: 'buffer',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (probe.status !== 0) {
|
|
183
|
+
issues.push({
|
|
184
|
+
char,
|
|
185
|
+
codePoint: `U+${char.codePointAt(0).toString(16).toUpperCase()}`,
|
|
186
|
+
line,
|
|
187
|
+
column,
|
|
188
|
+
});
|
|
189
|
+
if (issues.length >= limit) {
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
column += 1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return issues;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const GB2312_SAFE_REPLACEMENTS = new Map([
|
|
201
|
+
['🙂', '[表情]'],
|
|
202
|
+
['😀', '[表情]'],
|
|
203
|
+
['😂', '[表情]'],
|
|
204
|
+
['“', '"'],
|
|
205
|
+
['”', '"'],
|
|
206
|
+
['‘', "'"],
|
|
207
|
+
['’', "'"],
|
|
208
|
+
['—', '-'],
|
|
209
|
+
['–', '-'],
|
|
210
|
+
['…', '...'],
|
|
211
|
+
[' ', ' '],
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
export function suggestGb2312SafeText(text) {
|
|
215
|
+
let changed = false;
|
|
216
|
+
let preview = '';
|
|
217
|
+
|
|
218
|
+
for (const char of text) {
|
|
219
|
+
const replacement = GB2312_SAFE_REPLACEMENTS.get(char);
|
|
220
|
+
if (replacement !== undefined) {
|
|
221
|
+
preview += replacement;
|
|
222
|
+
changed = true;
|
|
223
|
+
} else {
|
|
224
|
+
preview += char;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { changed, preview };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function detectLineEnding(text) {
|
|
232
|
+
if (text.includes('\r\n')) {
|
|
233
|
+
return '\r\n';
|
|
234
|
+
}
|
|
235
|
+
return '\n';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function readCommand(filePath) {
|
|
239
|
+
const iconvPath = resolveIconvPath();
|
|
240
|
+
const result = runIconv(iconvPath, ['-f', 'GB2312', '-t', 'UTF-8', filePath]);
|
|
241
|
+
process.stdout.write(result.stdout.toString('utf8'));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function writeCommand(targetPath, sourcePath) {
|
|
245
|
+
const iconvPath = resolveIconvPath();
|
|
246
|
+
const sourceText = await readFile(sourcePath, 'utf8');
|
|
247
|
+
const lineEnding = detectLineEnding(sourceText);
|
|
248
|
+
const targetDir = path.dirname(targetPath);
|
|
249
|
+
|
|
250
|
+
await mkdir(targetDir, { recursive: true });
|
|
251
|
+
|
|
252
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), 'txt-gb2312-tool-'));
|
|
253
|
+
const utf8TempPath = path.join(tempDir, 'input-utf8.txt');
|
|
254
|
+
const gb2312TempPath = path.join(tempDir, 'output-gb2312.txt');
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
await writeFile(utf8TempPath, sourceText, 'utf8');
|
|
258
|
+
|
|
259
|
+
const convert = spawnSync(iconvPath, ['-f', 'UTF-8', '-t', 'GB2312', utf8TempPath], {
|
|
260
|
+
encoding: 'buffer',
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (convert.status !== 0) {
|
|
264
|
+
const issues = detectNonGb2312Chars(iconvPath, sourceText);
|
|
265
|
+
const suggestion = suggestGb2312SafeText(sourceText);
|
|
266
|
+
const issueText = issues.length > 0
|
|
267
|
+
? issues.map((item) => `字符 ${JSON.stringify(item.char)} (${item.codePoint}) 位于第 ${item.line} 行第 ${item.column} 列`).join(';')
|
|
268
|
+
: '未能自动定位具体字符';
|
|
269
|
+
fail(
|
|
270
|
+
[
|
|
271
|
+
'写入 GB2312 失败,内容中存在不可编码字符。',
|
|
272
|
+
issueText,
|
|
273
|
+
'建议:替换这些字符,或先写入 `.utf8` / `.md` 临时文件再整理为最终 `.txt`。',
|
|
274
|
+
suggestion.changed ? `安全替换建议预览:${suggestion.preview.slice(0, 200)}` : '当前未生成自动替换建议。',
|
|
275
|
+
`iconv 报错:${convert.stderr ? convert.stderr.toString('utf8').trim() : 'unknown error'}`,
|
|
276
|
+
].join(' '),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await writeFile(gb2312TempPath, convert.stdout);
|
|
281
|
+
|
|
282
|
+
runIconv(iconvPath, ['-f', 'GB2312', '-t', 'UTF-8', gb2312TempPath]);
|
|
283
|
+
|
|
284
|
+
await rename(gb2312TempPath, targetPath);
|
|
285
|
+
|
|
286
|
+
process.stdout.write(
|
|
287
|
+
[
|
|
288
|
+
`- 文件路径:\`${targetPath}\``,
|
|
289
|
+
'- 源编码:`UTF-8`',
|
|
290
|
+
'- 目标编码:`GB2312`',
|
|
291
|
+
`- 换行风格:\`${lineEnding === '\r\n' ? '\\r\\n' : '\\n'}\``,
|
|
292
|
+
'- 转换工具:`iconv`',
|
|
293
|
+
`- 转换命令:\`${iconvPath} -f UTF-8 -t GB2312 ${utf8TempPath} > ${gb2312TempPath}\``,
|
|
294
|
+
`- 验证命令:\`${iconvPath} -f GB2312 -t UTF-8 ${gb2312TempPath} > NUL\``,
|
|
295
|
+
'- 验证结果:通过',
|
|
296
|
+
'- 结论:已按 GB2312 编码保存,验证通过',
|
|
297
|
+
].join('\n') + '\n'
|
|
298
|
+
);
|
|
299
|
+
} finally {
|
|
300
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function verifyCommand(filePath) {
|
|
305
|
+
const iconvPath = resolveIconvPath();
|
|
306
|
+
const raw = await readFile(filePath);
|
|
307
|
+
const result = runIconv(iconvPath, ['-f', 'GB2312', '-t', 'UTF-8', filePath]);
|
|
308
|
+
const text = result.stdout.toString('utf8');
|
|
309
|
+
const lineEnding = raw.includes(0x0d) && raw.includes(0x0a) ? '\r\n' : '\n';
|
|
310
|
+
|
|
311
|
+
if (text.length === 0 && raw.length > 0) {
|
|
312
|
+
fail('GB2312 回读验证失败');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
process.stdout.write(
|
|
316
|
+
[
|
|
317
|
+
`- 文件路径:\`${filePath}\``,
|
|
318
|
+
'- 源编码:`GB2312`',
|
|
319
|
+
'- 目标编码:`GB2312`',
|
|
320
|
+
`- 换行风格:\`${lineEnding === '\r\n' ? '\\r\\n' : '\\n'}\``,
|
|
321
|
+
'- 转换工具:`iconv`',
|
|
322
|
+
'- 转换命令:`不适用`',
|
|
323
|
+
`- 验证命令:\`${iconvPath} -f GB2312 -t UTF-8 ${filePath} > NUL\``,
|
|
324
|
+
'- 验证结果:通过',
|
|
325
|
+
'- 结论:已按 GB2312 编码保存,验证通过',
|
|
326
|
+
].join('\n') + '\n'
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function main() {
|
|
331
|
+
const [command, arg1, arg2] = process.argv.slice(2);
|
|
332
|
+
|
|
333
|
+
if (!command) {
|
|
334
|
+
fail('用法:txt-gb2312-tool.mjs <read|write|verify> <path> [sourcePath]');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (command === 'read') {
|
|
338
|
+
if (!arg1) {
|
|
339
|
+
fail('read 子命令缺少文件路径');
|
|
340
|
+
}
|
|
341
|
+
await readCommand(arg1);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (command === 'write') {
|
|
346
|
+
if (!arg1 || !arg2) {
|
|
347
|
+
fail('write 子命令缺少目标路径或源文件路径');
|
|
348
|
+
}
|
|
349
|
+
await writeCommand(arg1, arg2);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (command === 'verify') {
|
|
354
|
+
if (!arg1) {
|
|
355
|
+
fail('verify 子命令缺少文件路径');
|
|
356
|
+
}
|
|
357
|
+
if (!existsSync(arg1)) {
|
|
358
|
+
fail('待验证文件不存在');
|
|
359
|
+
}
|
|
360
|
+
await verifyCommand(arg1);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fail(`未知子命令:${command}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function __testOnly_getIconvCandidatesForPath(pathValue) {
|
|
368
|
+
const previousPath = process.env.Path;
|
|
369
|
+
const previousPATH = process.env.PATH;
|
|
370
|
+
process.env.Path = pathValue;
|
|
371
|
+
process.env.PATH = pathValue;
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
return [
|
|
375
|
+
'iconv',
|
|
376
|
+
process.env.OPENCODE_ICONV_PATH,
|
|
377
|
+
process.env.ICONV_PATH,
|
|
378
|
+
'C:/Users/Administrator/AppData/Local/Programs/gettext-iconv/bin/iconv.exe',
|
|
379
|
+
'C:/Users/Administrator/AppData/Local/Atlassian/SourceTree/git_local/usr/bin/iconv.exe',
|
|
380
|
+
...getDerivedIconvCandidates(),
|
|
381
|
+
].filter(Boolean);
|
|
382
|
+
} finally {
|
|
383
|
+
process.env.Path = previousPath;
|
|
384
|
+
process.env.PATH = previousPATH;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const isDirectExecution = process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url;
|
|
389
|
+
|
|
390
|
+
if (isDirectExecution) {
|
|
391
|
+
await main();
|
|
392
|
+
}
|
package/tools/write.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin'
|
|
2
|
+
import { writeFile } from 'node:fs/promises'
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { createDefaultLegacyRules, matchLegacyRule } from './legacy-router.mjs'
|
|
6
|
+
import { getBundledLegacyRulesPath } from './opencode-paths.mjs'
|
|
7
|
+
import { encodeLegacyText } from './legacy-codec.js'
|
|
8
|
+
|
|
9
|
+
function loadLegacyRules(worktree) {
|
|
10
|
+
const candidates = [
|
|
11
|
+
path.join(worktree, '.opencode', 'legacy-rules.json'),
|
|
12
|
+
path.join(worktree, 'legacy-rules.json'),
|
|
13
|
+
getBundledLegacyRulesPath(),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
for (const filePath of candidates) {
|
|
17
|
+
if (existsSync(filePath)) {
|
|
18
|
+
return JSON.parse(readFileSync(filePath, 'utf8')).rules ?? createDefaultLegacyRules()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return createDefaultLegacyRules()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolvePath(filePath, worktree) {
|
|
26
|
+
return path.isAbsolute(filePath) ? filePath : path.join(worktree, filePath)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function writeLegacyFile(targetPath, content, encoding) {
|
|
30
|
+
const buffer = encodeLegacyText(content, encoding)
|
|
31
|
+
await writeFile(targetPath, buffer)
|
|
32
|
+
return `已按 ${encoding} 编码写入 ${targetPath}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default tool({
|
|
36
|
+
description: '写入文件,命中 legacy 规则时自动按对应编码处理',
|
|
37
|
+
args: {
|
|
38
|
+
filePath: tool.schema.string().describe('文件路径'),
|
|
39
|
+
content: tool.schema.string().describe('文件内容'),
|
|
40
|
+
},
|
|
41
|
+
async execute(args, context) {
|
|
42
|
+
const filePath = resolvePath(args.filePath, context.worktree)
|
|
43
|
+
const rules = loadLegacyRules(context.worktree)
|
|
44
|
+
const matched = matchLegacyRule(filePath, rules)
|
|
45
|
+
|
|
46
|
+
if (matched?.encoding) {
|
|
47
|
+
return writeLegacyFile(filePath, args.content, matched.encoding)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await writeFile(filePath, args.content, 'utf8')
|
|
51
|
+
return `已写入 ${filePath}`
|
|
52
|
+
},
|
|
53
|
+
})
|
package/tools/write.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin'
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
import { createDefaultLegacyRules, matchLegacyRule } from './legacy-router.mjs'
|
|
8
|
+
import { getGlobalLegacyRulesPath, getGlobalToolPath } from './opencode-paths.mjs'
|
|
9
|
+
|
|
10
|
+
function loadLegacyRules(worktree: string) {
|
|
11
|
+
const projectConfigPath = path.join(worktree, '.opencode', 'legacy-rules.json')
|
|
12
|
+
const globalConfigPath = getGlobalLegacyRulesPath()
|
|
13
|
+
|
|
14
|
+
if (existsSync(projectConfigPath)) {
|
|
15
|
+
return JSON.parse(readFileSync(projectConfigPath, 'utf8')).rules ?? createDefaultLegacyRules()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (existsSync(globalConfigPath)) {
|
|
19
|
+
return JSON.parse(readFileSync(globalConfigPath, 'utf8')).rules ?? createDefaultLegacyRules()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return createDefaultLegacyRules()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolvePath(filePath: string, worktree: string) {
|
|
26
|
+
return path.isAbsolute(filePath) ? filePath : path.join(worktree, filePath)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function writeViaTxtTool(targetPath: string, content: string, worktree: string) {
|
|
30
|
+
const toolPath = getGlobalToolPath('txt-gb2312-tool.mjs')
|
|
31
|
+
return mkdtemp(path.join(tmpdir(), 'global-write-')).then(async (tempDir) => {
|
|
32
|
+
const tempPath = path.join(tempDir, 'content.utf8')
|
|
33
|
+
try {
|
|
34
|
+
await writeFile(tempPath, content, 'utf8')
|
|
35
|
+
const result = spawnSync(process.execPath, [toolPath, 'write', targetPath, tempPath], {
|
|
36
|
+
cwd: worktree,
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
})
|
|
39
|
+
if (result.status !== 0) {
|
|
40
|
+
throw new Error(result.stderr || 'legacy txt 写入失败')
|
|
41
|
+
}
|
|
42
|
+
return result.stdout
|
|
43
|
+
} finally {
|
|
44
|
+
await rm(tempDir, { recursive: true, force: true })
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default tool({
|
|
50
|
+
description: '写入文件,命中 legacy 规则时自动按对应编码处理',
|
|
51
|
+
args: {
|
|
52
|
+
filePath: tool.schema.string().describe('文件路径'),
|
|
53
|
+
content: tool.schema.string().describe('文件内容'),
|
|
54
|
+
},
|
|
55
|
+
async execute(args, context) {
|
|
56
|
+
const filePath = resolvePath(args.filePath, context.worktree)
|
|
57
|
+
const rules = loadLegacyRules(context.worktree)
|
|
58
|
+
const matched = matchLegacyRule(filePath, rules)
|
|
59
|
+
|
|
60
|
+
if (matched?.tool === 'txt-gb2312' || matched?.encoding?.toLowerCase() === 'gb2312') {
|
|
61
|
+
return writeViaTxtTool(filePath, args.content, context.worktree)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await writeFile(filePath, args.content, 'utf8')
|
|
65
|
+
return `已写入 ${filePath}`
|
|
66
|
+
},
|
|
67
|
+
})
|