pi-hashline-edit-pro 4.3.5 → 4.3.7
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 +2 -1
- package/package.json +3 -4
- package/src/auto-read-all.ts +17 -0
- package/src/batch.ts +27 -9
- package/src/commit.ts +5 -2
- package/src/hashline/alphabet.ts +0 -16
- package/src/hashline/apply.ts +0 -1
- package/src/replace-render.ts +2 -2
- package/src/replace-response.ts +1 -1
- package/src/replace-undo.ts +1 -1
- package/src/replace.ts +2 -2
- package/src/utils.ts +9 -4
package/README.md
CHANGED
|
@@ -176,7 +176,7 @@ All five tools return machine-readable metadata in `details` alongside the model
|
|
|
176
176
|
| Tool | `details` |
|
|
177
177
|
| --- | --- |
|
|
178
178
|
| `read` | `truncation` (set when output was truncated), `snapshotId` (a `v2\|path\|ino\|mtime\|ctime\|size` fingerprint), `nextOffset` (use as the next `offset`), and `metrics` with `truncated` and `next_offset`. |
|
|
179
|
-
| `replace`, `insert` | `diff` (post-edit diff, capped, with current anchors on `+HASH│` and ` HASH│` rows; a same-message batch reports the combined diff on its last call and an empty diff on earlier calls), `patch` (a standard unified patch for external tools, capped like the diff), `patchTruncated` (true when the patch was cut or skipped for a pair over 1MB and can no longer be applied as-is), `firstChangedLine`, `snapshotId`, `classification` (`"noop"` when nothing changed), `batch` (`{ id, size, last, total }` marking same-message batch membership), and `metrics`: `edits_attempted`, `edits_noop`, `warnings`, `classification` (`"applied"` or `"noop"`), `changed_lines` (`{ first, last }`), `added_lines`, `removed_lines`. |
|
|
179
|
+
| `replace`, `insert` | `diff` (post-edit diff, capped, with current anchors on `+HASH│` and ` HASH│` rows; a same-message batch reports the combined diff on its last call and an empty diff on earlier calls), `patch` (a standard unified patch for external tools, capped like the diff), `patchTruncated` (true when the patch was cut or skipped for a pair over 1MB and can no longer be applied as-is), `firstChangedLine`, `snapshotId`, `classification` (`"noop"` when nothing changed), `batch` (`{ id, size, last, total }` marking same-message batch membership; earlier members also carry `aborted: true` and `abortMessage` after a batch abort), and `metrics`: `edits_attempted`, `edits_noop`, `warnings`, `classification` (`"applied"` or `"noop"`), `changed_lines` (`{ first, last }`), `added_lines`, `removed_lines`. |
|
|
180
180
|
| `undo_last_change` | `diff` (the undo diff with restored anchors), `patch`, `patchTruncated`, and `metrics` in the same shape as `replace`. |
|
|
181
181
|
| `anchor_grep` | `metrics` with `matches` (capped at `limit`), `files`, and `truncated`; `truncation` (the standard pi truncation report) when output was cut; and `linesTruncated` (true when long lines were shown as fragments). |
|
|
182
182
|
|
|
@@ -261,6 +261,7 @@ Codes starting with `E_` are errors: nothing was written — except `File was wr
|
|
|
261
261
|
- Stale anchors. `[E_STALE_ANCHOR]` means an anchor is not owned in this session: it was never shown to you, or its line was edited or the file was rewritten since. Call `read` for fresh anchors and retry.
|
|
262
262
|
- Range changed on disk. `[E_RANGE_STALE]` means a line inside the replaced range changed after it was last shown to you (or was never shown). Nothing was modified; the error carries the current range with fresh anchors, so retry with those without a `read`.
|
|
263
263
|
- Multi-conversation hosts. Anchors, served records, and ownership logs are resolved per calling session, so a tool call in one conversation is never answered by another conversation's registry; a foreign anchor fails with `[E_STALE_ANCHOR]`. Interactive previews are the one exception: pi does not pass the session into render callbacks, so when one process serves several conversations at once a preview can fall back to the most recently active session and show a stale or wrong-file diff. Previews never write files or claim anchors; run the call for the authoritative result.
|
|
264
|
+
- Undo scope. `undo_last_change` records are keyed by file path, not by session, so in a multi-conversation host any conversation that names the file can revert its most recent `replace` or `insert`, even one made by another conversation. Anchor ownership remains session-scoped; only undo is shared.
|
|
264
265
|
- Reset the anchor state. Anchors live in `~/.config/pi-hashline-edit-pro/hash-store.sqlite` (with `-wal`/`-shm` sidecars) and in per-session ownership logs under `~/.config/pi-hashline-edit-pro/sessions/`. Quit pi, delete those files, and everything is rebuilt on the next session. Anchor history is lost, but no project files are touched.
|
|
265
266
|
- Corrupt store. If the store fails its health check it is renamed to `hash-store.sqlite.corrupt-<timestamp>` and rebuilt automatically.
|
|
266
267
|
- Config directory moved. If `XDG_CONFIG_HOME` is set on a non-Windows platform, the config directory (and the anchor state inside it) lives at `$XDG_CONFIG_HOME/pi-hashline-edit-pro` instead of `~/.config/pi-hashline-edit-pro`. An existing store is not migrated automatically. To keep anchor and undo history, move the old `hash-store.sqlite` files (plus `-wal`/`-shm` sidecars) into the new directory before the first run.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hash-anchored read/replace/insert/grep tools for pi-coding-agent. Every line gets a unique 4-char tokenizer-friendly anchor that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -57,11 +57,10 @@
|
|
|
57
57
|
"test:watch": "vitest",
|
|
58
58
|
"test:coverage": "vitest run --coverage --coverage.thresholds.lines=90 --coverage.thresholds.statements=90 --coverage.thresholds.functions=85 --coverage.thresholds.branches=80",
|
|
59
59
|
"lint": "eslint \"src/**/*.ts\" \"index.ts\" \"test/**/*.ts\"",
|
|
60
|
-
"typecheck": "tsc --noEmit"
|
|
61
|
-
"prepublishOnly": "npm run typecheck && npm run lint && npm test"
|
|
60
|
+
"typecheck": "tsc --noEmit"
|
|
62
61
|
},
|
|
63
62
|
"devDependencies": {
|
|
64
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
63
|
+
"@earendil-works/pi-coding-agent": "^0.87.0",
|
|
65
64
|
"@eslint/js": "^10.0.1",
|
|
66
65
|
"@types/node": "^24",
|
|
67
66
|
"@vitest/coverage-v8": "^4.1.11",
|
package/src/auto-read-all.ts
CHANGED
|
@@ -388,6 +388,16 @@ export function chunkAutoReadAllSections(sections: string[], maxBytes: number =
|
|
|
388
388
|
if (current.length > 0) chunks.push(current.join("\n\n"));
|
|
389
389
|
return chunks;
|
|
390
390
|
}
|
|
391
|
+
|
|
392
|
+
async function candidateFileBytes(cwd: string, file: string): Promise<number | undefined> {
|
|
393
|
+
try {
|
|
394
|
+
const stats = await lstat(resolve(cwd, file));
|
|
395
|
+
return stats.isFile() ? stats.size : undefined;
|
|
396
|
+
} catch {
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
391
401
|
async function renderFile(file: string, cwd: string): Promise<AutoReadAllSection | undefined> {
|
|
392
402
|
try {
|
|
393
403
|
const { normalized, fileHashes, absolutePath } = await readNormFile(file, cwd, { maxLines: MAX_HASH_LINES });
|
|
@@ -427,6 +437,13 @@ export async function buildAutoReadAllInjection(cwd: string, budgetBytes: number
|
|
|
427
437
|
let bytes = 0;
|
|
428
438
|
let completeFiles = 0;
|
|
429
439
|
for (const file of discovery.files) {
|
|
440
|
+
if (sections.length > 0) {
|
|
441
|
+
const size = await candidateFileBytes(cwd, file);
|
|
442
|
+
if (size !== undefined && bytes + size > budgetBytes) {
|
|
443
|
+
omitted.push(file);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
430
447
|
const section = await renderFile(file, cwd);
|
|
431
448
|
if (section === undefined) {
|
|
432
449
|
omitted.push(file);
|
package/src/batch.ts
CHANGED
|
@@ -23,11 +23,11 @@ import { assertInsertReq, assertReq, normReq } from "./payload-contract";
|
|
|
23
23
|
import { saveUndo } from "./replace-undo";
|
|
24
24
|
import { buildChanged, buildNoop, type RMetrics, type TResult } from "./replace-response";
|
|
25
25
|
import { serveRows, servedHashesFromDiff } from "./served";
|
|
26
|
-
import { abortIf, assertLineLimit, errCode, isRec, splitLines } from "./utils";
|
|
27
|
-
import { MAX_BYTES } from "./constants";
|
|
26
|
+
import { abortIf, assertByteLimit, assertLineLimit, errCode, isRec, splitLines } from "./utils";
|
|
28
27
|
|
|
29
28
|
export interface PlannedMember {
|
|
30
29
|
batchKey: number;
|
|
30
|
+
id: string;
|
|
31
31
|
display: number;
|
|
32
32
|
total: number;
|
|
33
33
|
target: string;
|
|
@@ -126,12 +126,18 @@ const batches = new Map<number, BatchState>();
|
|
|
126
126
|
let nextBatchKey = 1;
|
|
127
127
|
const abortedMembers = new Map<string, { display: number; message: string }>();
|
|
128
128
|
const ABORTED_MEMBERS_LIMIT = 1024;
|
|
129
|
+
const placeholderResults = new Map<string, TResult>();
|
|
129
130
|
|
|
130
131
|
function markBatchMembersAborted(runtime: BatchState): void {
|
|
131
132
|
const message = abortedBatchMessage(runtime);
|
|
132
133
|
for (const id of runtime.memberIds) {
|
|
133
134
|
abortedMembers.delete(id);
|
|
134
135
|
abortedMembers.set(id, { display: runtime.display, message });
|
|
136
|
+
const result = placeholderResults.get(id);
|
|
137
|
+
if (result?.details.batch) {
|
|
138
|
+
result.details.batch.aborted = true;
|
|
139
|
+
result.details.batch.abortMessage = message;
|
|
140
|
+
}
|
|
135
141
|
}
|
|
136
142
|
while (abortedMembers.size > ABORTED_MEMBERS_LIMIT) {
|
|
137
143
|
const oldest = abortedMembers.keys().next().value;
|
|
@@ -157,6 +163,7 @@ export function resetBatchStateForTests(): void {
|
|
|
157
163
|
plan.clear();
|
|
158
164
|
batches.clear();
|
|
159
165
|
abortedMembers.clear();
|
|
166
|
+
placeholderResults.clear();
|
|
160
167
|
nextBatchKey = 1;
|
|
161
168
|
}
|
|
162
169
|
|
|
@@ -203,7 +210,12 @@ function enforceCap(): void {
|
|
|
203
210
|
if (oldest === undefined) return;
|
|
204
211
|
const state = batches.get(oldest);
|
|
205
212
|
batches.delete(oldest);
|
|
206
|
-
if (state)
|
|
213
|
+
if (state) {
|
|
214
|
+
for (const id of state.memberIds) {
|
|
215
|
+
plan.delete(id);
|
|
216
|
+
placeholderResults.delete(id);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
207
219
|
}
|
|
208
220
|
}
|
|
209
221
|
|
|
@@ -274,6 +286,7 @@ export async function planAssistantMessage(message: unknown, cwd: string): Promi
|
|
|
274
286
|
group.forEach((item, index) => {
|
|
275
287
|
plan.set(item.id, {
|
|
276
288
|
batchKey: key,
|
|
289
|
+
id: item.id,
|
|
277
290
|
display,
|
|
278
291
|
total: finalGroups.length,
|
|
279
292
|
target: item.target,
|
|
@@ -521,7 +534,11 @@ export async function executeBatchMember(input: BatchMemberInput): Promise<TResu
|
|
|
521
534
|
if (noop) runtime.noops += 1;
|
|
522
535
|
else runtime.applied += 1;
|
|
523
536
|
runtime.warnings.push(...piece.warnings);
|
|
524
|
-
if (!input.member.last)
|
|
537
|
+
if (!input.member.last) {
|
|
538
|
+
const placeholder = batchPlaceholder(input.member, piece, base.snapshotId);
|
|
539
|
+
placeholderResults.set(input.member.id, placeholder);
|
|
540
|
+
return placeholder;
|
|
541
|
+
}
|
|
525
542
|
return finishBatch(input.member, input.signal);
|
|
526
543
|
}
|
|
527
544
|
|
|
@@ -633,9 +650,7 @@ async function finishBatch(member: PlannedMember, signal?: AbortSignal): Promise
|
|
|
633
650
|
assertNotEmpty(base.content, composed);
|
|
634
651
|
assertLineLimit(composed, paths.displayPath, MAX_HASH_LINES);
|
|
635
652
|
const finalBytes = base.bom + restoreEndings(composed, base.ending);
|
|
636
|
-
|
|
637
|
-
throw new Error(`[E_FILE_TOO_LARGE] File is too large: ${paths.displayPath} (exceeds the ${MAX_BYTES / (1024 * 1024)}MB size limit). For very large files, use write.`);
|
|
638
|
-
}
|
|
653
|
+
assertByteLimit(finalBytes, paths.displayPath);
|
|
639
654
|
} catch (error) {
|
|
640
655
|
discardBatchState(runtime);
|
|
641
656
|
if (error instanceof Error) error.message = withAbortSuffix(error.message, runtime.display);
|
|
@@ -739,7 +754,7 @@ async function finishBatch(member: PlannedMember, signal?: AbortSignal): Promise
|
|
|
739
754
|
await getDiffContextLines(),
|
|
740
755
|
);
|
|
741
756
|
changed.details.diff = `${header}\n${changed.details.diff}`;
|
|
742
|
-
changed.details.diffLineNumbers?.unshift(
|
|
757
|
+
changed.details.diffLineNumbers?.unshift(null);
|
|
743
758
|
try {
|
|
744
759
|
serveRows(runtime.target, resultHashes, splitLines(composed), servedHashesFromDiff(changed.details.diff));
|
|
745
760
|
} catch (error) {
|
|
@@ -786,7 +801,10 @@ export async function finalizeTurn(toolCallIds: string[]): Promise<void> {
|
|
|
786
801
|
for (const key of keys) {
|
|
787
802
|
const runtime = batches.get(key);
|
|
788
803
|
if (!runtime) continue;
|
|
789
|
-
for (const id of runtime.memberIds)
|
|
804
|
+
for (const id of runtime.memberIds) {
|
|
805
|
+
plan.delete(id);
|
|
806
|
+
placeholderResults.delete(id);
|
|
807
|
+
}
|
|
790
808
|
batches.delete(key);
|
|
791
809
|
}
|
|
792
810
|
}
|
package/src/commit.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import type { PipelineResult } from "./replace";
|
|
3
|
-
import { abortIf, errCode, splitLines } from "./utils";
|
|
3
|
+
import { abortIf, assertByteLimit, errCode, splitLines } from "./utils";
|
|
4
4
|
import { DEDUP_ANCHOR } from "./constants";
|
|
5
5
|
import { HASH_SEP } from "./hashline";
|
|
6
6
|
import { buildChanged, buildNoop, type RMeta, type TResult } from "./replace-response";
|
|
@@ -70,6 +70,9 @@ export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promis
|
|
|
70
70
|
);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
const finalFileBytes = pipe.bom + restoreEndings(pipe.result, pipe.originalEnding);
|
|
74
|
+
assertByteLimit(finalFileBytes, path);
|
|
75
|
+
|
|
73
76
|
if (pipe.hadUtf8DecodeErrors) {
|
|
74
77
|
warnings.push(
|
|
75
78
|
"Non-UTF-8 bytes were shown as U+FFFD; this edit rewrote the file as UTF-8.",
|
|
@@ -112,7 +115,7 @@ export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promis
|
|
|
112
115
|
abortIf(signal);
|
|
113
116
|
await writeAtomic(
|
|
114
117
|
absolutePath,
|
|
115
|
-
|
|
118
|
+
finalFileBytes,
|
|
116
119
|
pipe.identity,
|
|
117
120
|
);
|
|
118
121
|
} catch (error) {
|
package/src/hashline/alphabet.ts
CHANGED
|
@@ -16,22 +16,6 @@ export const HASH_RUN = `[${LETTERS}]{${HASH_LEN},${HASH_LEN + 1}}`;
|
|
|
16
16
|
|
|
17
17
|
export const HASH_RE = new RegExp(`^${HASH_CLASS}$`);
|
|
18
18
|
|
|
19
|
-
let indexByAnchor: Map<string, number> | undefined;
|
|
20
|
-
|
|
21
|
-
function reverseIndex(): Map<string, number> {
|
|
22
|
-
if (!indexByAnchor) {
|
|
23
|
-
indexByAnchor = new Map();
|
|
24
|
-
for (let i = 0; i < ANCHOR_COUNT; i++) {
|
|
25
|
-
indexByAnchor.set(TABLE.slice(i * HASH_LEN, i * HASH_LEN + HASH_LEN), i);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return indexByAnchor;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
19
|
export function anchorAt(idx: number): string {
|
|
32
20
|
return TABLE.slice(idx * HASH_LEN, idx * HASH_LEN + HASH_LEN);
|
|
33
21
|
}
|
|
34
|
-
|
|
35
|
-
export function anchorIndex(hash: string): number {
|
|
36
|
-
return reverseIndex().get(hash) ?? -1;
|
|
37
|
-
}
|
package/src/hashline/apply.ts
CHANGED
package/src/replace-render.ts
CHANGED
|
@@ -42,7 +42,7 @@ export function colorLines(lines: string[], theme: FgT): string[] {
|
|
|
42
42
|
return theme.fg("dim", line);
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
|
-
export function toNumberedDiff(diff: string, lineNumbers: (number|undefined)[]): string {
|
|
45
|
+
export function toNumberedDiff(diff: string, lineNumbers: (number | null | undefined)[]): string {
|
|
46
46
|
return withLineNumbers(diff, lineNumbers);
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -162,7 +162,7 @@ function extractSummary(text: string | undefined): string | undefined {
|
|
|
162
162
|
|
|
163
163
|
const BATCH_HEADER_RE = /^batch \d+:\n/;
|
|
164
164
|
|
|
165
|
-
function splitBatchHeader(diff: string, lineNumbers: (number | undefined)[] | undefined): { header: string | undefined; body: string; lineNumbers: (number | undefined)[] | undefined } {
|
|
165
|
+
function splitBatchHeader(diff: string, lineNumbers: (number | null | undefined)[] | undefined): { header: string | undefined; body: string; lineNumbers: (number | null | undefined)[] | undefined } {
|
|
166
166
|
const match = diff.match(BATCH_HEADER_RE);
|
|
167
167
|
if (!match) return { header: undefined, body: diff, lineNumbers };
|
|
168
168
|
return { header: match[0]!.slice(0, -1), body: diff.slice(match[0].length), lineNumbers: lineNumbers?.slice(1) };
|
package/src/replace-response.ts
CHANGED
|
@@ -240,7 +240,7 @@ export function buildChanged(input: SuccessInput, verb = "replaced", diffContext
|
|
|
240
240
|
editMeta.firstChangedLine ?? baseDiff.firstChangedLine,
|
|
241
241
|
snapshotId,
|
|
242
242
|
metrics,
|
|
243
|
-
diffLineNumbers: diffResult.lineNumbers,
|
|
243
|
+
diffLineNumbers: diffResult.lineNumbers.map((line) => line ?? null),
|
|
244
244
|
...(warnings?.length ? { warnings: [...warnings] } : {}),
|
|
245
245
|
},
|
|
246
246
|
};
|
package/src/replace-undo.ts
CHANGED
|
@@ -248,7 +248,7 @@ export function regUndo(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
248
248
|
],
|
|
249
249
|
details: {
|
|
250
250
|
diff: undoDiff,
|
|
251
|
-
diffLineNumbers: undoDiffResult.lineNumbers,
|
|
251
|
+
diffLineNumbers: undoDiffResult.lineNumbers.map((line) => line ?? null),
|
|
252
252
|
patch: patchResult.patch,
|
|
253
253
|
...(patchResult.truncated ? { patchTruncated: true as const } : {}),
|
|
254
254
|
metrics: buildMetrics({
|
package/src/replace.ts
CHANGED
|
@@ -45,9 +45,9 @@ export type ReplaceDetails = {
|
|
|
45
45
|
snapshotId?: string;
|
|
46
46
|
classification?: "noop";
|
|
47
47
|
metrics?: RMetrics;
|
|
48
|
-
diffLineNumbers?: (number|
|
|
48
|
+
diffLineNumbers?: (number | null)[];
|
|
49
49
|
warnings?: string[];
|
|
50
|
-
batch?: { id: number; size: number; last: boolean; total: number };
|
|
50
|
+
batch?: { id: number; size: number; last: boolean; total: number; aborted?: boolean; abortMessage?: string };
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
export interface PipelineResult {
|
package/src/utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NUL_CONTENT_MSG } from "./constants";
|
|
1
|
+
import { NUL_CONTENT_MSG, MAX_BYTES } from "./constants";
|
|
2
2
|
import { HASH_CLASS } from "./hashline/alphabet";
|
|
3
3
|
|
|
4
4
|
export function isRec(value: unknown): value is Record<string, unknown> {
|
|
@@ -173,14 +173,14 @@ export function numberedRead(text: string, offset: number): string {
|
|
|
173
173
|
}).join("\n");
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
export function withLineNumbers(text: string, numbers: (number|undefined)[]): string {
|
|
176
|
+
export function withLineNumbers(text: string, numbers: (number | null | undefined)[]): string {
|
|
177
177
|
const lines = text.split("\n");
|
|
178
178
|
const nums = numbers ?? [];
|
|
179
|
-
const max = nums.reduce<number>((m, n) => n !== undefined && n > m ? n : m, 0);
|
|
179
|
+
const max = nums.reduce<number>((m, n) => n !== undefined && n !== null && n > m ? n : m, 0);
|
|
180
180
|
const width = gutterWidth(max, lines.length);
|
|
181
181
|
return lines.map((line, i) => {
|
|
182
182
|
const n = nums[i];
|
|
183
|
-
const prefix = n !== undefined ? formatGutter(n, width) : blankGutter(width);
|
|
183
|
+
const prefix = n !== undefined && n !== null ? formatGutter(n, width) : blankGutter(width);
|
|
184
184
|
return prefix + line;
|
|
185
185
|
}).join("\n");
|
|
186
186
|
}
|
|
@@ -192,6 +192,11 @@ export function assertLineLimit(content: string, displayPath: string, limit: num
|
|
|
192
192
|
const count = splitLines(content).length;
|
|
193
193
|
if (count > limit) throw new Error(formatLineLimit(displayPath, limit, count));
|
|
194
194
|
}
|
|
195
|
+
export function assertByteLimit(content: string, displayPath: string, limit = MAX_BYTES): void {
|
|
196
|
+
if (Buffer.byteLength(content, "utf-8") > limit) {
|
|
197
|
+
throw new Error(`[E_FILE_TOO_LARGE] File is too large: ${displayPath} (exceeds the ${limit / (1024 * 1024)}MB size limit). For very large files, use write.`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
195
200
|
export function lineLimitMoreThanMessage(displayPath: string, limit: number): string {
|
|
196
201
|
return formatLineLimit(displayPath, limit, undefined);
|
|
197
202
|
}
|