@arnilo/prism-coding-agent 0.0.3

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/dist/edit.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Edit tool: precise text replacement in an existing file via exact-then-fuzzy matching.
3
+ *
4
+ * Behavioral port of pi's core/tools/edit for @arnilo/prism-coding-agent, adapted to Prism's
5
+ * `ToolDefinition` contract. Faithfully ports the access → read → stripBom → line-ending-normalize →
6
+ * `applyEditsToNormalizedContent` (exact then fuzzy, with duplicate/overlap/empty/no-change guards) →
7
+ * write flow, serialized per-path via `withFileMutationQueue`. Also ports `prepareEditArguments`
8
+ * (tolerates models that send `edits` as a JSON string or as legacy top-level `oldText`/`newText`).
9
+ *
10
+ * Drops pi's TUI (`renderCall`/`renderResult`, live preview cache, theme/syntax-highlight).
11
+ *
12
+ * Deviations from pi (documented):
13
+ * - Abort + every failure (missing/unreadable file, no-match, duplicate, overlap, empty oldText,
14
+ * no-change) return a Prism `error` result (pi throws/rejects). On no-match the file is untouched
15
+ * because `applyEditsToNormalizedContent` throws before `writeFile`.
16
+ * - pi's per-tool `details: { diff, patch, firstChangedLine }` (TUI-facing) is surfaced as
17
+ * `ToolResult.metadata` (host-readable, keeps model context small — the model only sees the short
18
+ * `Successfully replaced N block(s)` confirmation).
19
+ * - The post-`writeFile` abort check is dropped (consistent with the write tool): if the write
20
+ * completed, the edit is real and is reported as success rather than a misleading "aborted".
21
+ */
22
+ import { Buffer } from "node:buffer";
23
+ import type { ToolDefinition } from "@arnilo/prism";
24
+ export interface Edit {
25
+ oldText: string;
26
+ newText: string;
27
+ }
28
+ /** Display/result details mirrored from pi's `EditToolDetails`, surfaced via `ToolResult.metadata`. */
29
+ export interface EditToolDetails {
30
+ /** Display-oriented diff of the changes made. */
31
+ diff: string;
32
+ /** Standard unified patch of the changes made. */
33
+ patch: string;
34
+ /** Line number of the first change in the new file (for editor navigation). */
35
+ firstChangedLine?: number;
36
+ }
37
+ /**
38
+ * Pluggable operations for the edit tool. Override to delegate file editing to remote systems
39
+ * (e.g. SSH) while keeping the tool's matching + per-path serialization behavior.
40
+ */
41
+ export interface EditOperations {
42
+ /** Read file contents as a Buffer. */
43
+ readFile: (absolutePath: string) => Promise<Buffer>;
44
+ /** Write content to a file. */
45
+ writeFile: (absolutePath: string, content: string) => Promise<void>;
46
+ /** Check the file is readable and writable (throw if not). */
47
+ access: (absolutePath: string) => Promise<void>;
48
+ }
49
+ export interface EditToolOptions {
50
+ /** Custom operations backend (default: local filesystem). */
51
+ operations?: EditOperations;
52
+ }
53
+ export declare function createEditTool(cwd: string, options?: EditToolOptions): ToolDefinition;
package/dist/edit.js ADDED
@@ -0,0 +1,159 @@
1
+ import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { resolveToCwd } from "./path-utils.js";
4
+ import { withFileMutationQueue } from "./file-mutation-queue.js";
5
+ import { applyEditsToNormalizedContent, detectLineEnding, generateDiffString, generateUnifiedPatch, normalizeToLF, restoreLineEndings, stripBom, } from "./edit-diff.js";
6
+ const defaultEditOperations = {
7
+ readFile: (path) => fsReadFile(path),
8
+ writeFile: (path, content) => fsWriteFile(path, content, "utf-8"),
9
+ access: (path) => fsAccess(path, constants.R_OK | constants.W_OK),
10
+ };
11
+ function errorResult(toolCallId, message) {
12
+ return {
13
+ toolCallId,
14
+ name: "edit",
15
+ content: [{ type: "text", text: message }],
16
+ error: { message },
17
+ };
18
+ }
19
+ /** Port of pi's `prepareEditArguments`: tolerate model quirks (edits as JSON string; legacy fields). */
20
+ function prepareEditArguments(input) {
21
+ let edits = input.edits;
22
+ // Some models send edits as a JSON string instead of an array.
23
+ if (typeof edits === "string") {
24
+ try {
25
+ const parsed = JSON.parse(edits);
26
+ if (Array.isArray(parsed))
27
+ edits = parsed;
28
+ }
29
+ catch {
30
+ /* leave as-is; validation will reject */
31
+ }
32
+ }
33
+ const path = typeof input.path === "string" ? input.path : "";
34
+ // Legacy: top-level oldText/newText instead of edits[].
35
+ if (typeof input.oldText === "string" && typeof input.newText === "string") {
36
+ const arr = Array.isArray(edits) ? [...edits] : [];
37
+ arr.push({ oldText: input.oldText, newText: input.newText });
38
+ edits = arr;
39
+ }
40
+ return { path, edits };
41
+ }
42
+ function validateEdits(edits) {
43
+ if (!Array.isArray(edits) || edits.length === 0) {
44
+ return "edits must contain at least one replacement.";
45
+ }
46
+ const out = [];
47
+ for (let i = 0; i < edits.length; i++) {
48
+ const e = edits[i];
49
+ if (typeof e?.oldText !== "string" || typeof e?.newText !== "string") {
50
+ return `edits[${i}] must have string oldText and newText.`;
51
+ }
52
+ out.push({ oldText: e.oldText, newText: e.newText });
53
+ }
54
+ return out;
55
+ }
56
+ export function createEditTool(cwd, options) {
57
+ const ops = options?.operations ?? defaultEditOperations;
58
+ return {
59
+ name: "edit",
60
+ description: "Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.",
61
+ parameters: {
62
+ type: "object",
63
+ properties: {
64
+ path: { type: "string", description: "Path to the file to edit (relative or absolute)" },
65
+ edits: {
66
+ type: "array",
67
+ description: "One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead.",
68
+ items: {
69
+ type: "object",
70
+ properties: {
71
+ oldText: {
72
+ type: "string",
73
+ description: "Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call.",
74
+ },
75
+ newText: { type: "string", description: "Replacement text for this targeted edit." },
76
+ },
77
+ required: ["oldText", "newText"],
78
+ additionalProperties: false,
79
+ },
80
+ },
81
+ },
82
+ required: ["path", "edits"],
83
+ additionalProperties: false,
84
+ },
85
+ async execute(args, context) {
86
+ const toolCallId = context.toolCallId;
87
+ const prepared = prepareEditArguments(args);
88
+ if (prepared.path.length === 0) {
89
+ return errorResult(toolCallId, "path is required and must be a non-empty string.");
90
+ }
91
+ const editsOrError = validateEdits(prepared.edits);
92
+ if (typeof editsOrError === "string") {
93
+ return errorResult(toolCallId, editsOrError);
94
+ }
95
+ const edits = editsOrError;
96
+ try {
97
+ const absolutePath = resolveToCwd(prepared.path, cwd);
98
+ return await withFileMutationQueue(absolutePath, async () => {
99
+ if (context.signal?.aborted)
100
+ return errorResult(toolCallId, "Operation aborted");
101
+ // Check the file is readable + writable.
102
+ try {
103
+ await ops.access(absolutePath);
104
+ }
105
+ catch (error) {
106
+ if (context.signal?.aborted)
107
+ return errorResult(toolCallId, "Operation aborted");
108
+ const err = error;
109
+ const errorMessage = error instanceof Error && "code" in error ? `Error code: ${err.code}` : String(error);
110
+ return errorResult(toolCallId, `Could not edit file: ${prepared.path}. ${errorMessage}.`);
111
+ }
112
+ if (context.signal?.aborted)
113
+ return errorResult(toolCallId, "Operation aborted");
114
+ const buffer = await ops.readFile(absolutePath);
115
+ if (context.signal?.aborted)
116
+ return errorResult(toolCallId, "Operation aborted");
117
+ const rawContent = buffer.toString("utf-8");
118
+ // The model will not include an invisible BOM in oldText.
119
+ const { bom, text: content } = stripBom(rawContent);
120
+ const originalEnding = detectLineEnding(content);
121
+ const normalizedContent = normalizeToLF(content);
122
+ // Apply exact-then-fuzzy matching. Throws on no-match / duplicate / overlap / empty / no-change.
123
+ let baseContent;
124
+ let newContent;
125
+ try {
126
+ ({ baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, prepared.path));
127
+ }
128
+ catch (error) {
129
+ const message = error instanceof Error ? error.message : String(error);
130
+ return errorResult(toolCallId, message);
131
+ }
132
+ if (context.signal?.aborted)
133
+ return errorResult(toolCallId, "Operation aborted");
134
+ const finalContent = bom + restoreLineEndings(newContent, originalEnding);
135
+ await ops.writeFile(absolutePath, finalContent);
136
+ const diffResult = generateDiffString(baseContent, newContent);
137
+ const patch = generateUnifiedPatch(prepared.path, baseContent, newContent);
138
+ return {
139
+ toolCallId,
140
+ name: "edit",
141
+ content: [
142
+ { type: "text", text: `Successfully replaced ${edits.length} block(s) in ${prepared.path}.` },
143
+ ],
144
+ metadata: {
145
+ diff: diffResult.diff,
146
+ patch,
147
+ firstChangedLine: diffResult.firstChangedLine,
148
+ },
149
+ };
150
+ });
151
+ }
152
+ catch (error) {
153
+ const message = error instanceof Error ? error.message : String(error);
154
+ return errorResult(toolCallId, message);
155
+ }
156
+ },
157
+ };
158
+ }
159
+ //# sourceMappingURL=edit.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Run `fn` exclusively for `filePath`: concurrent calls with the same path
3
+ * (or the same realpath) run one after another; calls for different paths run
4
+ * in parallel. Resolves/rejects with `fn`'s result and always releases the slot.
5
+ */
6
+ export declare function withFileMutationQueue<T>(filePath: string, fn: () => Promise<T>): Promise<T>;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Serialize file mutation operations targeting the same file.
3
+ * Operations for different files still run in parallel.
4
+ *
5
+ * Behavioral port of pi's core/tools/file-mutation-queue for @arnilo/prism-coding-agent.
6
+ * stdlib only (node:fs/promises realpath, node:path resolve).
7
+ *
8
+ * ponytail: global process-wide Map mutex keyed by realpath. Ceiling: all
9
+ * mutation queues share one process Map; if a host runs many concurrent
10
+ * sessions in one process they all share the namespace (intended — same file
11
+ * must serialize regardless of session). Upgrade path: scope the Map per
12
+ * ToolRegistry if isolation between registries is ever needed.
13
+ */
14
+ import { realpath } from "node:fs/promises";
15
+ import { resolve } from "node:path";
16
+ const fileMutationQueues = new Map();
17
+ let registrationQueue = Promise.resolve();
18
+ function isMissingPathError(error) {
19
+ return (typeof error === "object" &&
20
+ error !== null &&
21
+ "code" in error &&
22
+ (error.code === "ENOENT" ||
23
+ error.code === "ENOTDIR"));
24
+ }
25
+ async function getMutationQueueKey(filePath) {
26
+ const resolvedPath = resolve(filePath);
27
+ try {
28
+ return await realpath(resolvedPath);
29
+ }
30
+ catch (error) {
31
+ if (isMissingPathError(error)) {
32
+ return resolvedPath;
33
+ }
34
+ throw error;
35
+ }
36
+ }
37
+ /**
38
+ * Run `fn` exclusively for `filePath`: concurrent calls with the same path
39
+ * (or the same realpath) run one after another; calls for different paths run
40
+ * in parallel. Resolves/rejects with `fn`'s result and always releases the slot.
41
+ */
42
+ export async function withFileMutationQueue(filePath, fn) {
43
+ const registration = registrationQueue.then(async () => {
44
+ const key = await getMutationQueueKey(filePath);
45
+ const currentQueue = fileMutationQueues.get(key) ?? Promise.resolve();
46
+ let releaseNext;
47
+ const nextQueue = new Promise((resolveQueue) => {
48
+ releaseNext = resolveQueue;
49
+ });
50
+ const chainedQueue = currentQueue.then(() => nextQueue);
51
+ fileMutationQueues.set(key, chainedQueue);
52
+ return { key, currentQueue, chainedQueue, releaseNext };
53
+ });
54
+ registrationQueue = registration.then(() => undefined, () => undefined);
55
+ const { key, currentQueue, chainedQueue, releaseNext } = await registration;
56
+ await currentQueue;
57
+ try {
58
+ return await fn();
59
+ }
60
+ finally {
61
+ releaseNext();
62
+ if (fileMutationQueues.get(key) === chainedQueue) {
63
+ fileMutationQueues.delete(key);
64
+ }
65
+ }
66
+ }
67
+ //# sourceMappingURL=file-mutation-queue.js.map
@@ -0,0 +1,29 @@
1
+ export { createShellTool, createLocalBashOperations, getShellConfig, killProcessTree, waitForChildProcess, } from "./shell.js";
2
+ export type { ShellToolOptions, ShellConfig, BashOperations, BashExecOptions, BashSpawnContext, BashSpawnHook, } from "./shell.js";
3
+ export { createReadTool, detectSupportedImageMimeType, detectSupportedImageMimeTypeFromFile, } from "./read.js";
4
+ export type { ReadToolOptions, ReadOperations } from "./read.js";
5
+ export { createWriteTool } from "./write.js";
6
+ export type { WriteToolOptions, WriteOperations } from "./write.js";
7
+ export { createEditTool } from "./edit.js";
8
+ export type { EditToolOptions, EditOperations, EditToolDetails, Edit } from "./edit.js";
9
+ export { withFileMutationQueue } from "./file-mutation-queue.js";
10
+ import type { ToolDefinition } from "@arnilo/prism";
11
+ import type { ShellToolOptions } from "./shell.js";
12
+ import type { ReadToolOptions } from "./read.js";
13
+ import type { WriteToolOptions } from "./write.js";
14
+ import type { EditToolOptions } from "./edit.js";
15
+ /** Per-tool options combined for the aggregator factories. */
16
+ export interface ToolsOptions {
17
+ shell?: ShellToolOptions;
18
+ read?: ReadToolOptions;
19
+ write?: WriteToolOptions;
20
+ edit?: EditToolOptions;
21
+ }
22
+ /**
23
+ * The four coding tools: `shell`, `read`, `write`, `edit`. Register all of them for a coding agent.
24
+ */
25
+ export declare function createCodingTools(cwd: string, options?: ToolsOptions): readonly ToolDefinition[];
26
+ /** Read-only subset: `read` only (this package ships no grep/find/ls). */
27
+ export declare function createReadOnlyTools(cwd: string, options?: ToolsOptions): readonly ToolDefinition[];
28
+ /** Every tool this package provides — identical to {@link createCodingTools} for now. */
29
+ export declare function createAllTools(cwd: string, options?: ToolsOptions): readonly ToolDefinition[];
package/dist/index.js ADDED
@@ -0,0 +1,36 @@
1
+ // @arnilo/prism-coding-agent public barrel.
2
+ //
3
+ // First-party coding tools for the Prism agent harness. Factory functions return Prism
4
+ // `ToolDefinition`s that hosts register into a `ToolRegistry` (e.g.
5
+ // `createToolRegistry(createCodingTools(cwd))`). No tools are auto-registered — import what you need.
6
+ // --- per-tool factories & types ---
7
+ export { createShellTool, createLocalBashOperations, getShellConfig, killProcessTree, waitForChildProcess, } from "./shell.js";
8
+ export { createReadTool, detectSupportedImageMimeType, detectSupportedImageMimeTypeFromFile, } from "./read.js";
9
+ export { createWriteTool } from "./write.js";
10
+ export { createEditTool } from "./edit.js";
11
+ // --- generic primitives (re-exported for hosts that want them) ---
12
+ export { withFileMutationQueue } from "./file-mutation-queue.js";
13
+ import { createShellTool } from "./shell.js";
14
+ import { createReadTool } from "./read.js";
15
+ import { createWriteTool } from "./write.js";
16
+ import { createEditTool } from "./edit.js";
17
+ /**
18
+ * The four coding tools: `shell`, `read`, `write`, `edit`. Register all of them for a coding agent.
19
+ */
20
+ export function createCodingTools(cwd, options) {
21
+ return [
22
+ createShellTool(cwd, options?.shell),
23
+ createReadTool(cwd, options?.read),
24
+ createWriteTool(cwd, options?.write),
25
+ createEditTool(cwd, options?.edit),
26
+ ];
27
+ }
28
+ /** Read-only subset: `read` only (this package ships no grep/find/ls). */
29
+ export function createReadOnlyTools(cwd, options) {
30
+ return [createReadTool(cwd, options?.read)];
31
+ }
32
+ /** Every tool this package provides — identical to {@link createCodingTools} for now. */
33
+ export function createAllTools(cwd, options) {
34
+ return createCodingTools(cwd, options);
35
+ }
36
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,51 @@
1
+ import type { TruncationResult } from "./truncate.js";
2
+ export interface OutputAccumulatorOptions {
3
+ maxLines?: number;
4
+ maxBytes?: number;
5
+ tempFilePrefix?: string;
6
+ }
7
+ export interface OutputSnapshot {
8
+ content: string;
9
+ truncation: TruncationResult;
10
+ fullOutputPath?: string;
11
+ }
12
+ /**
13
+ * Incrementally tracks streaming output with bounded memory.
14
+ *
15
+ * Appends decode chunks with a streaming UTF-8 decoder, keeps only a decoded
16
+ * tail for display snapshots, and opens a temp file when the full output needs
17
+ * to be preserved.
18
+ */
19
+ export declare class OutputAccumulator {
20
+ private readonly maxLines;
21
+ private readonly maxBytes;
22
+ private readonly maxRollingBytes;
23
+ private readonly tempFilePrefix;
24
+ private readonly decoder;
25
+ private rawChunks;
26
+ private tailText;
27
+ private tailBytes;
28
+ private tailStartsAtLineBoundary;
29
+ private totalRawBytes;
30
+ private totalDecodedBytes;
31
+ private completedLines;
32
+ private totalLines;
33
+ private currentLineBytes;
34
+ private hasOpenLine;
35
+ private finished;
36
+ private tempFilePath?;
37
+ private tempFileStream?;
38
+ constructor(options?: OutputAccumulatorOptions);
39
+ append(data: Buffer): void;
40
+ finish(): void;
41
+ snapshot(options?: {
42
+ persistIfTruncated?: boolean;
43
+ }): OutputSnapshot;
44
+ closeTempFile(): Promise<void>;
45
+ getLastLineBytes(): number;
46
+ private appendDecodedText;
47
+ private trimTail;
48
+ private getSnapshotText;
49
+ private shouldUseTempFile;
50
+ private ensureTempFile;
51
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Streaming output accumulator with bounded memory.
3
+ *
4
+ * Behavioral port of pi's core/tools/output-accumulator for @arnilo/prism-coding-agent.
5
+ * Appends raw chunks through a streaming UTF-8 decoder, keeps only a decoded tail
6
+ * for display snapshots, and spills the full output to a temp file once limits are
7
+ * exceeded. stdlib only (node:crypto/fs/os).
8
+ *
9
+ * Single deviation from pi: the default temp-file prefix is `prism-output`
10
+ * (pi uses `pi-output`) — cosmetic, user-overridable via `tempFilePrefix`.
11
+ */
12
+ import { randomBytes } from "node:crypto";
13
+ import { createWriteStream } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, truncateTail } from "./truncate.js";
17
+ function defaultTempFilePath(prefix) {
18
+ const id = randomBytes(8).toString("hex");
19
+ return join(tmpdir(), `${prefix}-${id}.log`);
20
+ }
21
+ function byteLength(text) {
22
+ return Buffer.byteLength(text, "utf-8");
23
+ }
24
+ /**
25
+ * Incrementally tracks streaming output with bounded memory.
26
+ *
27
+ * Appends decode chunks with a streaming UTF-8 decoder, keeps only a decoded
28
+ * tail for display snapshots, and opens a temp file when the full output needs
29
+ * to be preserved.
30
+ */
31
+ export class OutputAccumulator {
32
+ maxLines;
33
+ maxBytes;
34
+ maxRollingBytes;
35
+ tempFilePrefix;
36
+ decoder = new TextDecoder();
37
+ rawChunks = [];
38
+ tailText = "";
39
+ tailBytes = 0;
40
+ tailStartsAtLineBoundary = true;
41
+ totalRawBytes = 0;
42
+ totalDecodedBytes = 0;
43
+ completedLines = 0;
44
+ totalLines = 0;
45
+ currentLineBytes = 0;
46
+ hasOpenLine = false;
47
+ finished = false;
48
+ tempFilePath;
49
+ tempFileStream;
50
+ constructor(options = {}) {
51
+ this.maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
52
+ this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
53
+ this.maxRollingBytes = Math.max(this.maxBytes * 2, 1);
54
+ this.tempFilePrefix = options.tempFilePrefix ?? "prism-output";
55
+ }
56
+ append(data) {
57
+ if (this.finished) {
58
+ throw new Error("Cannot append to a finished output accumulator");
59
+ }
60
+ this.totalRawBytes += data.length;
61
+ this.appendDecodedText(this.decoder.decode(data, { stream: true }));
62
+ if (this.tempFileStream || this.shouldUseTempFile()) {
63
+ this.ensureTempFile();
64
+ this.tempFileStream?.write(data);
65
+ }
66
+ else if (data.length > 0) {
67
+ this.rawChunks.push(data);
68
+ }
69
+ }
70
+ finish() {
71
+ if (this.finished) {
72
+ return;
73
+ }
74
+ this.finished = true;
75
+ this.appendDecodedText(this.decoder.decode());
76
+ if (this.shouldUseTempFile()) {
77
+ this.ensureTempFile();
78
+ }
79
+ }
80
+ snapshot(options = {}) {
81
+ const tailTruncation = truncateTail(this.getSnapshotText(), {
82
+ maxLines: this.maxLines,
83
+ maxBytes: this.maxBytes,
84
+ });
85
+ const truncated = this.totalLines > this.maxLines || this.totalDecodedBytes > this.maxBytes;
86
+ const truncatedBy = truncated
87
+ ? (tailTruncation.truncatedBy ?? (this.totalDecodedBytes > this.maxBytes ? "bytes" : "lines"))
88
+ : null;
89
+ const truncation = {
90
+ ...tailTruncation,
91
+ truncated,
92
+ truncatedBy,
93
+ totalLines: this.totalLines,
94
+ totalBytes: this.totalDecodedBytes,
95
+ maxLines: this.maxLines,
96
+ maxBytes: this.maxBytes,
97
+ };
98
+ if (options.persistIfTruncated && truncation.truncated) {
99
+ this.ensureTempFile();
100
+ }
101
+ return {
102
+ content: truncation.content,
103
+ truncation,
104
+ fullOutputPath: this.tempFilePath,
105
+ };
106
+ }
107
+ async closeTempFile() {
108
+ if (!this.tempFileStream) {
109
+ return;
110
+ }
111
+ const stream = this.tempFileStream;
112
+ this.tempFileStream = undefined;
113
+ await new Promise((resolve, reject) => {
114
+ const onError = (error) => {
115
+ stream.off("finish", onFinish);
116
+ reject(error);
117
+ };
118
+ const onFinish = () => {
119
+ stream.off("error", onError);
120
+ resolve();
121
+ };
122
+ stream.once("error", onError);
123
+ stream.once("finish", onFinish);
124
+ stream.end();
125
+ });
126
+ }
127
+ getLastLineBytes() {
128
+ return this.currentLineBytes;
129
+ }
130
+ appendDecodedText(text) {
131
+ if (text.length === 0) {
132
+ return;
133
+ }
134
+ const bytes = byteLength(text);
135
+ this.totalDecodedBytes += bytes;
136
+ this.tailText += text;
137
+ this.tailBytes += bytes;
138
+ if (this.tailBytes > this.maxRollingBytes * 2) {
139
+ this.trimTail();
140
+ }
141
+ let newlines = 0;
142
+ let lastNewline = -1;
143
+ for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) {
144
+ newlines++;
145
+ lastNewline = i;
146
+ }
147
+ if (newlines === 0) {
148
+ this.currentLineBytes += bytes;
149
+ this.hasOpenLine = true;
150
+ }
151
+ else {
152
+ this.completedLines += newlines;
153
+ const tail = text.slice(lastNewline + 1);
154
+ this.currentLineBytes = byteLength(tail);
155
+ this.hasOpenLine = tail.length > 0;
156
+ }
157
+ this.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);
158
+ }
159
+ trimTail() {
160
+ const buffer = Buffer.from(this.tailText, "utf-8");
161
+ if (buffer.length <= this.maxRollingBytes) {
162
+ this.tailBytes = buffer.length;
163
+ return;
164
+ }
165
+ let start = buffer.length - this.maxRollingBytes;
166
+ while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) {
167
+ start++;
168
+ }
169
+ this.tailStartsAtLineBoundary =
170
+ start === 0 ? this.tailStartsAtLineBoundary : buffer[start - 1] === 0x0a;
171
+ this.tailText = buffer.subarray(start).toString("utf-8");
172
+ this.tailBytes = byteLength(this.tailText);
173
+ }
174
+ getSnapshotText() {
175
+ if (this.tailStartsAtLineBoundary) {
176
+ return this.tailText;
177
+ }
178
+ const firstNewline = this.tailText.indexOf("\n");
179
+ return firstNewline === -1 ? this.tailText : this.tailText.slice(firstNewline + 1);
180
+ }
181
+ shouldUseTempFile() {
182
+ return (this.totalRawBytes > this.maxBytes ||
183
+ this.totalDecodedBytes > this.maxBytes ||
184
+ this.totalLines > this.maxLines);
185
+ }
186
+ ensureTempFile() {
187
+ if (this.tempFilePath) {
188
+ return;
189
+ }
190
+ this.tempFilePath = defaultTempFilePath(this.tempFilePrefix);
191
+ this.tempFileStream = createWriteStream(this.tempFilePath);
192
+ for (const chunk of this.rawChunks) {
193
+ this.tempFileStream.write(chunk);
194
+ }
195
+ this.rawChunks = [];
196
+ }
197
+ }
198
+ //# sourceMappingURL=output-accumulator.js.map
@@ -0,0 +1,8 @@
1
+ export declare function pathExists(filePath: string): Promise<boolean>;
2
+ export declare function expandPath(filePath: string): string;
3
+ /**
4
+ * Resolve a path relative to the given cwd. Handles ~ expansion and absolute paths.
5
+ */
6
+ export declare function resolveToCwd(filePath: string, cwd: string): string;
7
+ export declare function resolveReadPath(filePath: string, cwd: string): string;
8
+ export declare function resolveReadPathAsync(filePath: string, cwd: string): Promise<string>;