@trim21/personal-pi-extensions 0.0.391 → 0.0.392
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/package.json +1 -1
- package/src/lib/write-guard.ts +10 -6
- package/src/opencode/edit-engine.ts +49 -15
- package/src/opencode/files.ts +113 -128
- package/src/opencode/index.ts +3 -1
package/package.json
CHANGED
package/src/lib/write-guard.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { basename, isAbsolute, relative, sep } from "node:path";
|
|
|
18
18
|
|
|
19
19
|
import { generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import { applyEdit, normalizeToLF } from "../opencode/edit-engine.js";
|
|
22
22
|
|
|
23
23
|
const ALWAYS_ALLOW = ["/tmp"];
|
|
24
24
|
const MAX_PREVIEW_LINES = 100;
|
|
@@ -82,11 +82,15 @@ export async function buildDiffPreview(
|
|
|
82
82
|
// line-numbered patch when it matches. Fall back to a parameter diff when the
|
|
83
83
|
// edit cannot be applied (oldText not found, ambiguous, or no file).
|
|
84
84
|
try {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
const applied = applyEdit(oldContent, change.oldText, change.newText, change.replaceAll);
|
|
86
|
+
return wrapDiff(
|
|
87
|
+
generateUnifiedPatch(
|
|
88
|
+
basename(resolvedPath),
|
|
89
|
+
normalizeToLF(applied.contentOld),
|
|
90
|
+
normalizeToLF(applied.contentNew),
|
|
91
|
+
2,
|
|
92
|
+
),
|
|
93
|
+
);
|
|
90
94
|
} catch {
|
|
91
95
|
const removed = change.oldText.split("\n").map((line) => `-${line}`);
|
|
92
96
|
const added = change.newText.split("\n").map((line) => `+${line}`);
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Opencode edit matching engine.
|
|
2
|
+
* Opencode v1 edit matching engine.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* and wrapped in a pi extension so the behaviour is identical to opencode.
|
|
4
|
+
* Replacers and replace() follow packages/opencode/src/tool/edit.ts.
|
|
5
|
+
* applyEdit() matches the official preprocessing: convert oldString/newString
|
|
6
|
+
* to the file's line endings, then replace on the original file content.
|
|
8
7
|
*
|
|
9
|
-
* Shared by
|
|
10
|
-
* - opencode-edit.ts — the edit tool implementation
|
|
11
|
-
* - lib/write-guard.ts — the diff preview shown in the approval dialog
|
|
8
|
+
* Shared by the edit tool and lib/write-guard.ts (approval-dialog preview).
|
|
12
9
|
*/
|
|
13
10
|
|
|
14
11
|
// ── BOM & line ending helpers ─────────────────────────────────────────────────
|
|
@@ -27,16 +24,52 @@ export function normalizeToLF(text: string): string {
|
|
|
27
24
|
return text.replaceAll("\r\n", "\n");
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
/** Convert LF text to the file's line ending. Input must already be LF. */
|
|
28
|
+
export function convertToLineEnding(text: string, ending: "\r\n" | "\n"): string {
|
|
29
|
+
if (ending === "\n") return text;
|
|
30
|
+
return text.replaceAll("\n", "\r\n");
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
export const restoreLineEndings = convertToLineEnding;
|
|
34
|
+
|
|
35
|
+
/** Strip BOM and normalize line endings to LF. */
|
|
35
36
|
export function normalizeForEdit(content: string): string {
|
|
36
37
|
const { text } = stripBom(content);
|
|
37
38
|
return normalizeToLF(text);
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
export interface AppliedEdit {
|
|
42
|
+
/** File text without BOM, original line endings. */
|
|
43
|
+
contentOld: string;
|
|
44
|
+
/** Replaced text without BOM, original line endings. */
|
|
45
|
+
contentNew: string;
|
|
46
|
+
/** Bytes to write: desiredBom + contentNew. */
|
|
47
|
+
finalContent: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Official v1 edit path: strip BOM, convert params to the file's line endings,
|
|
52
|
+
* replace on the original content, then desiredBom = source.bom || next.bom.
|
|
53
|
+
*/
|
|
54
|
+
export function applyEdit(
|
|
55
|
+
rawContent: string,
|
|
56
|
+
oldString: string,
|
|
57
|
+
newString: string,
|
|
58
|
+
replaceAll = false,
|
|
59
|
+
): AppliedEdit {
|
|
60
|
+
const { bom: sourceBom, text: contentOld } = stripBom(rawContent);
|
|
61
|
+
const ending = detectLineEnding(contentOld);
|
|
62
|
+
const old = convertToLineEnding(normalizeToLF(oldString), ending);
|
|
63
|
+
const replacement = convertToLineEnding(normalizeToLF(newString), ending);
|
|
64
|
+
const replaced = replace(contentOld, old, replacement, replaceAll);
|
|
65
|
+
const { bom: nextBom, text: contentNew } = stripBom(replaced);
|
|
66
|
+
return {
|
|
67
|
+
contentOld,
|
|
68
|
+
contentNew,
|
|
69
|
+
finalContent: (sourceBom || nextBom) + contentNew,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
40
73
|
// ── copied from opencode ──────────────────────────────────────────────────────
|
|
41
74
|
|
|
42
75
|
function levenshtein(a: string, b: string): number {
|
|
@@ -417,9 +450,9 @@ function isDisproportionateMatch(search: string, oldString: string) {
|
|
|
417
450
|
}
|
|
418
451
|
|
|
419
452
|
/**
|
|
420
|
-
* Replace `oldString` with `newString` in `content
|
|
421
|
-
*
|
|
422
|
-
* Throws when
|
|
453
|
+
* Replace `oldString` with `newString` in `content`.
|
|
454
|
+
* `oldString`/`newString` must already use the same line endings as `content`
|
|
455
|
+
* (see `applyEdit`). Throws when the match is missing or ambiguous.
|
|
423
456
|
*/
|
|
424
457
|
export function replace(
|
|
425
458
|
content: string,
|
|
@@ -459,7 +492,8 @@ export function replace(
|
|
|
459
492
|
);
|
|
460
493
|
}
|
|
461
494
|
if (replaceAll) {
|
|
462
|
-
|
|
495
|
+
// eslint-disable-next-line unicorn/no-unsafe-string-replacement -- match opencode v1 $& / $$ interpolation
|
|
496
|
+
return content.replaceAll(search, newString);
|
|
463
497
|
}
|
|
464
498
|
const lastIndex = content.lastIndexOf(search);
|
|
465
499
|
if (index !== lastIndex) continue;
|
package/src/opencode/files.ts
CHANGED
|
@@ -5,21 +5,20 @@
|
|
|
5
5
|
* service 实例(registerLsp 创建的闭包变量),与 claude-code/files.ts 共享
|
|
6
6
|
* read-snapshot state 的方式一致;不再用模块级全局缓存。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - edit
|
|
13
|
-
* 写后等待文档诊断,ERROR 级追加到输出。
|
|
8
|
+
* 对齐官方 v1(packages/opencode/src/tool/{read,edit,write}.ts):
|
|
9
|
+
* - read:流式分行(LF / CRLF / CR)、每行 `N: ` 行号前缀、单行 2000
|
|
10
|
+
* 字符截断、1 起始 offset、目录排序;读取后后台 LSP warm-up。
|
|
11
|
+
* 不接 PDF、不接 <system-reminder>;图片 magic 检测保留。
|
|
12
|
+
* - edit:匹配引擎 + 把 old/new 转到文件换行后再替换;写后等待文档诊断。
|
|
14
13
|
* - write:BOM 保留(source.bom || next.bom);写后同 edit 的诊断输出。
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
* 复用。spawn-agent 子代理按工具名加载本文件(`--tools` allowlist 过滤)。
|
|
15
|
+
* 匹配引擎在 edit-engine.ts,也被 lib/write-guard 复用。
|
|
18
16
|
*/
|
|
19
17
|
|
|
20
|
-
import { constants } from "node:fs";
|
|
18
|
+
import { constants, createReadStream } from "node:fs";
|
|
21
19
|
import { access, mkdir, open, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
22
20
|
import { basename, dirname, extname, isAbsolute, resolve as resolvePath, sep } from "node:path";
|
|
21
|
+
import { createInterface } from "node:readline";
|
|
23
22
|
|
|
24
23
|
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
|
25
24
|
import {
|
|
@@ -32,13 +31,7 @@ import { Type } from "typebox";
|
|
|
32
31
|
|
|
33
32
|
import { type LspService, registerLsp } from "../lib/lsp/lsp.js";
|
|
34
33
|
import { guardWriteAccess } from "../lib/write-guard.js";
|
|
35
|
-
import {
|
|
36
|
-
detectLineEnding,
|
|
37
|
-
normalizeToLF,
|
|
38
|
-
replace,
|
|
39
|
-
restoreLineEndings,
|
|
40
|
-
stripBom,
|
|
41
|
-
} from "./edit-engine.js";
|
|
34
|
+
import { applyEdit, normalizeToLF, stripBom } from "./edit-engine.js";
|
|
42
35
|
|
|
43
36
|
// ── read 工具 ────────────────────────────────────────────────────────────────
|
|
44
37
|
|
|
@@ -138,21 +131,25 @@ function startsWith(buffer: Uint8Array, bytes: Uint8Array): boolean {
|
|
|
138
131
|
return bytes.every((b, i) => buffer[i] === b);
|
|
139
132
|
}
|
|
140
133
|
|
|
141
|
-
async function
|
|
134
|
+
async function readSample(filePath: string): Promise<Uint8Array> {
|
|
142
135
|
try {
|
|
143
136
|
const fileHandle = await open(filePath, "r");
|
|
144
137
|
try {
|
|
145
138
|
const buf = Buffer.alloc(SAMPLE_BYTES);
|
|
146
139
|
const { bytesRead } = await fileHandle.read(buf, 0, SAMPLE_BYTES, 0);
|
|
147
|
-
return
|
|
140
|
+
return buf.subarray(0, bytesRead);
|
|
148
141
|
} finally {
|
|
149
142
|
await fileHandle.close();
|
|
150
143
|
}
|
|
151
144
|
} catch {
|
|
152
|
-
return
|
|
145
|
+
return new Uint8Array();
|
|
153
146
|
}
|
|
154
147
|
}
|
|
155
148
|
|
|
149
|
+
async function detectImageMimeTypeFromFile(filePath: string): Promise<string | null> {
|
|
150
|
+
return detectImageMimeType(await readSample(filePath));
|
|
151
|
+
}
|
|
152
|
+
|
|
156
153
|
function isBinaryExtension(filePath: string): boolean {
|
|
157
154
|
return BINARY_EXTENSIONS.has(extname(filePath).toLowerCase());
|
|
158
155
|
}
|
|
@@ -172,68 +169,79 @@ export interface TruncationResult {
|
|
|
172
169
|
truncated: boolean;
|
|
173
170
|
truncatedBy: "lines" | "bytes" | null;
|
|
174
171
|
totalLines: number;
|
|
175
|
-
totalBytes: number;
|
|
176
172
|
outputLines: number;
|
|
177
173
|
outputBytes: number;
|
|
178
|
-
|
|
179
|
-
|
|
174
|
+
offset: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface LinePage {
|
|
178
|
+
raw: string[];
|
|
179
|
+
count: number;
|
|
180
|
+
cut: boolean;
|
|
181
|
+
more: boolean;
|
|
182
|
+
offset: number;
|
|
180
183
|
}
|
|
181
184
|
|
|
182
185
|
/**
|
|
183
|
-
*
|
|
184
|
-
* -
|
|
185
|
-
* - line
|
|
186
|
-
* -
|
|
186
|
+
* Stream a text file the way opencode v1 ReadTool.lines does:
|
|
187
|
+
* - readline with crlfDelay: Infinity (LF / CRLF / CR as one break)
|
|
188
|
+
* - per-line truncation to MAX_LINE_LENGTH
|
|
189
|
+
* - line cap via maxLines (more): keep scanning so count is the file total
|
|
190
|
+
* - byte cap via maxBytes (cut): stop immediately
|
|
187
191
|
*/
|
|
188
|
-
export function
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
?
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
192
|
+
export async function readLines(
|
|
193
|
+
filePath: string,
|
|
194
|
+
opts: { offset: number; limit: number; maxBytes?: number },
|
|
195
|
+
): Promise<LinePage> {
|
|
196
|
+
const start = opts.offset - 1;
|
|
197
|
+
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
198
|
+
const raw: string[] = [];
|
|
199
|
+
let count = 0;
|
|
200
|
+
let bytes = 0;
|
|
201
|
+
let cut = false;
|
|
202
|
+
let more = false;
|
|
203
|
+
|
|
204
|
+
const stream = createReadStream(filePath, { encoding: "utf8" });
|
|
205
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
206
|
+
try {
|
|
207
|
+
for await (const text of rl) {
|
|
208
|
+
count += 1;
|
|
209
|
+
if (count <= start) continue;
|
|
210
|
+
|
|
211
|
+
if (raw.length >= opts.limit) {
|
|
212
|
+
more = true;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const line =
|
|
217
|
+
text.length > MAX_LINE_LENGTH ? text.slice(0, MAX_LINE_LENGTH) + MAX_LINE_SUFFIX : text;
|
|
218
|
+
const size = Buffer.byteLength(line, "utf8") + (raw.length > 0 ? 1 : 0);
|
|
219
|
+
if (bytes + size > maxBytes) {
|
|
220
|
+
cut = true;
|
|
221
|
+
more = true;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
raw.push(line);
|
|
225
|
+
bytes += size;
|
|
221
226
|
}
|
|
222
|
-
|
|
223
|
-
|
|
227
|
+
} finally {
|
|
228
|
+
rl.close();
|
|
229
|
+
stream.destroy();
|
|
224
230
|
}
|
|
225
231
|
|
|
226
|
-
|
|
232
|
+
return { raw, count, cut, more, offset: opts.offset };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function truncationFromPage(page: LinePage): TruncationResult {
|
|
236
|
+
const content = page.raw.join("\n");
|
|
227
237
|
return {
|
|
228
|
-
content
|
|
229
|
-
truncated,
|
|
230
|
-
truncatedBy,
|
|
231
|
-
totalLines,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
maxLines,
|
|
236
|
-
maxBytes,
|
|
238
|
+
content,
|
|
239
|
+
truncated: page.more || page.cut,
|
|
240
|
+
truncatedBy: page.cut ? "bytes" : page.more ? "lines" : null,
|
|
241
|
+
totalLines: page.count,
|
|
242
|
+
outputLines: page.raw.length,
|
|
243
|
+
outputBytes: Buffer.byteLength(content, "utf8"),
|
|
244
|
+
offset: page.offset,
|
|
237
245
|
};
|
|
238
246
|
}
|
|
239
247
|
|
|
@@ -379,65 +387,41 @@ function registerReadTool(pi: ExtensionAPI, service: LspService): void {
|
|
|
379
387
|
return { content, details: undefined };
|
|
380
388
|
}
|
|
381
389
|
|
|
382
|
-
|
|
383
|
-
const buffer = await readFile(absolutePath);
|
|
384
|
-
const sample = buffer.subarray(0, SAMPLE_BYTES);
|
|
385
|
-
|
|
386
|
-
// Binary file detection
|
|
387
|
-
if (isBinaryExtension(absolutePath) || isBinaryFileBySample(sample)) {
|
|
390
|
+
if (isBinaryExtension(absolutePath) || isBinaryFileBySample(await readSample(absolutePath))) {
|
|
388
391
|
return {
|
|
389
392
|
content: [{ type: "text", text: `Cannot read binary file: ${absolutePath}` }],
|
|
390
393
|
details: undefined,
|
|
391
394
|
};
|
|
392
395
|
}
|
|
393
396
|
|
|
394
|
-
const textContent = buffer.toString("utf8");
|
|
395
|
-
// opencode 用 Stream.splitLines,不含末尾换行产生的空行
|
|
396
|
-
const allLines = textContent.split("\n");
|
|
397
|
-
if (textContent.endsWith("\n")) allLines.pop();
|
|
398
|
-
const totalFileLines = allLines.length;
|
|
399
|
-
|
|
400
|
-
// opencode: offset 1 起始,offset=0 视为 1(params.offset || 1)
|
|
401
397
|
const effectiveOffset = offset || 1;
|
|
402
|
-
const
|
|
398
|
+
const page = await readLines(absolutePath, {
|
|
399
|
+
offset: effectiveOffset,
|
|
400
|
+
limit: limit ?? DEFAULT_MAX_LINES,
|
|
401
|
+
});
|
|
403
402
|
|
|
404
|
-
|
|
405
|
-
if (totalFileLines < effectiveOffset && !(totalFileLines === 0 && effectiveOffset === 1)) {
|
|
403
|
+
if (page.count < page.offset && !(page.count === 0 && page.offset === 1)) {
|
|
406
404
|
throw new Error(
|
|
407
|
-
`Offset ${
|
|
405
|
+
`Offset ${page.offset} is out of range for this file (${page.count} lines)`,
|
|
408
406
|
);
|
|
409
407
|
}
|
|
410
408
|
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const selectedContent = allLines.slice(startLine).join("\n");
|
|
415
|
-
const truncation = truncateHead(selectedContent, limit ?? DEFAULT_MAX_LINES);
|
|
416
|
-
let outputText: string;
|
|
417
|
-
|
|
418
|
-
const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
|
|
409
|
+
const last = page.offset + page.raw.length - 1;
|
|
410
|
+
const next = last + 1;
|
|
411
|
+
const numbered = page.raw.map((line, i) => `${i + page.offset}: ${line}`).join("\n");
|
|
419
412
|
const header = `<path>${absolutePath}</path>\n<type>file</type>\n<content>\n`;
|
|
420
413
|
const footer = "\n</content>";
|
|
421
|
-
// opencode: 每行 `${i + offset}: ${line}` 行号前缀
|
|
422
|
-
const numbered =
|
|
423
|
-
truncation.content === ""
|
|
424
|
-
? ""
|
|
425
|
-
: truncation.content
|
|
426
|
-
.split("\n")
|
|
427
|
-
.map((line, i) => `${startLineDisplay + i}: ${line}`)
|
|
428
|
-
.join("\n");
|
|
429
414
|
|
|
415
|
+
let outputText: string;
|
|
430
416
|
let details: { truncation?: TruncationResult } | undefined;
|
|
431
|
-
if (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
details = { truncation };
|
|
417
|
+
if (page.cut) {
|
|
418
|
+
outputText = `${header}${numbered}\n\n(Output capped at ${MAX_BYTES_LABEL}. Showing lines ${page.offset}-${last}. Use offset=${next} to continue.)${footer}`;
|
|
419
|
+
details = { truncation: truncationFromPage(page) };
|
|
420
|
+
} else if (page.more) {
|
|
421
|
+
outputText = `${header}${numbered}\n\n(Showing lines ${page.offset}-${last} of ${page.count}. Use offset=${next} to continue.)${footer}`;
|
|
422
|
+
details = { truncation: truncationFromPage(page) };
|
|
439
423
|
} else {
|
|
440
|
-
outputText = `${header}${numbered}\n\n(End of file - total ${
|
|
424
|
+
outputText = `${header}${numbered}\n\n(End of file - total ${page.count} lines)${footer}`;
|
|
441
425
|
}
|
|
442
426
|
|
|
443
427
|
content = [{ type: "text", text: outputText }];
|
|
@@ -535,6 +519,15 @@ function registerEditTool(pi: ExtensionAPI, service: LspService): void {
|
|
|
535
519
|
] as const;
|
|
536
520
|
}
|
|
537
521
|
|
|
522
|
+
let fileStat: Awaited<ReturnType<typeof stat>>;
|
|
523
|
+
try {
|
|
524
|
+
fileStat = await stat(absolutePath);
|
|
525
|
+
} catch {
|
|
526
|
+
throw new Error(`File ${absolutePath} not found`);
|
|
527
|
+
}
|
|
528
|
+
if (fileStat.isDirectory()) {
|
|
529
|
+
throw new Error(`Path is a directory, not a file: ${absolutePath}`);
|
|
530
|
+
}
|
|
538
531
|
try {
|
|
539
532
|
await access(absolutePath, constants.R_OK | constants.W_OK);
|
|
540
533
|
} catch (error: unknown) {
|
|
@@ -545,23 +538,15 @@ function registerEditTool(pi: ExtensionAPI, service: LspService): void {
|
|
|
545
538
|
throw new Error(`Could not edit file: ${filePath}. ${msg}.`, { cause: error });
|
|
546
539
|
}
|
|
547
540
|
|
|
548
|
-
const
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
// Strip BOM then normalize line endings to LF.
|
|
552
|
-
// The opencode replacers split on \n and expect only LF.
|
|
553
|
-
const { bom, text: content } = stripBom(rawContent);
|
|
554
|
-
const originalEnding = detectLineEnding(content);
|
|
555
|
-
const normalizedContent = normalizeToLF(content);
|
|
556
|
-
|
|
557
|
-
const newContent = replace(normalizedContent, oldString, newString, replaceAll);
|
|
541
|
+
const rawContent = await readFile(absolutePath, "utf8");
|
|
542
|
+
const applied = applyEdit(rawContent, oldString, newString, replaceAll);
|
|
558
543
|
signal?.throwIfAborted();
|
|
544
|
+
await writeFile(absolutePath, applied.finalContent, "utf8");
|
|
559
545
|
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
const patch = generateUnifiedPatch(filePath, normalizedContent, newContent);
|
|
546
|
+
const diffOld = normalizeToLF(applied.contentOld);
|
|
547
|
+
const diffNew = normalizeToLF(applied.contentNew);
|
|
548
|
+
const diffResult = generateDiffString(diffOld, diffNew);
|
|
549
|
+
const patch = generateUnifiedPatch(filePath, diffOld, diffNew);
|
|
565
550
|
const diagnosticText = await service.lspDiagnosticsForFile(absolutePath, ctx.cwd, {
|
|
566
551
|
signal,
|
|
567
552
|
});
|
package/src/opencode/index.ts
CHANGED
|
@@ -21,6 +21,8 @@ import opencodeTodo from "./todo.js";
|
|
|
21
21
|
|
|
22
22
|
export { default as opencodeBash } from "./bash.js";
|
|
23
23
|
export {
|
|
24
|
+
applyEdit,
|
|
25
|
+
convertToLineEnding,
|
|
24
26
|
detectLineEnding,
|
|
25
27
|
normalizeForEdit,
|
|
26
28
|
normalizeToLF,
|
|
@@ -30,8 +32,8 @@ export {
|
|
|
30
32
|
} from "./edit-engine.js";
|
|
31
33
|
export {
|
|
32
34
|
default as opencodeFileTools,
|
|
35
|
+
readLines,
|
|
33
36
|
resolveBom,
|
|
34
|
-
truncateHead,
|
|
35
37
|
type TruncationResult,
|
|
36
38
|
} from "./files.js";
|
|
37
39
|
export { default as opencodeQuestion } from "./question.js";
|