Merge remote-tracking branch 'origin/master' into codex/project-instruction-files
# Conflicts: # AGENTS.md # docs/architecture.md # docs/event-producer-consumer.md # docs/module-graph.md # docs/rfc/INDEX.md # packages/bash/bash-local/tests/executor.spec.ts # packages/bash/bash-local/tests/run.spec.ts # packages/core/agent-core/src/index.ts # packages/core/agent-core/tests/agent-core.spec.ts # packages/fs/fs-local/src/fsio.ts # packages/ui/acp-agent/package.json # packages/ui/acp-agent/src/index.ts # packages/ui/acp-agent/tsconfig.json # packages/ui/stdio-agent/package.json # packages/ui/stdio-agent/tsconfig.json # packages/util/README.md # pnpm-lock.yaml # tsconfig.build.json # tsconfig.json
This commit is contained in:
@@ -137,6 +137,9 @@ export interface LocalDirEntry {
|
||||
* and intermediate directories are created by the write. Two input paths
|
||||
* reaching the same file via symlinks share one key. Falls back to the absolute
|
||||
* path only when no ancestor (not even the filesystem root) can be resolved.
|
||||
* @param cwd - base directory a relative `path` resolves against.
|
||||
* @param path - absolute or relative path; empty/whitespace-only throws `FS_NOT_FOUND`.
|
||||
* @returns the absolute display path plus the realpath-derived stable target key.
|
||||
*/
|
||||
export async function resolveLocalTarget(cwd: string, path: string): Promise<LocalTarget> {
|
||||
if (path.trim().length === 0) throw new FsError('file_path must be a non-empty string', 'FS_NOT_FOUND')
|
||||
@@ -197,7 +200,11 @@ async function probeStats(absolutePath: string, readStats: (path: string) => Pro
|
||||
}
|
||||
}
|
||||
|
||||
/** Probe a path for its version, mode, type, and size. Null if absent. */
|
||||
/**
|
||||
* Probe a path for its version, mode, type, and size. Null if absent.
|
||||
* @param absolutePath - the path to stat (typically a target key; symlinks are followed).
|
||||
* @returns the metadata, or null when the path — or a parent segment — does not exist.
|
||||
*/
|
||||
export async function probe(absolutePath: string): Promise<PathInfo | null> {
|
||||
const info = await probeStats(absolutePath, stat)
|
||||
if (!info) return null
|
||||
@@ -231,6 +238,9 @@ async function resolveListedChildTarget(parent: LocalTarget, name: string): Prom
|
||||
* List direct children of a directory in stable name order. Each child includes
|
||||
* a resolved target plus stat metadata when still available; file contents are
|
||||
* never read.
|
||||
* @param target - the resolved directory to list; a missing or non-directory target throws.
|
||||
* @param signal - aborts the listing, checked between children (`FS_ABORTED`).
|
||||
* @returns one entry per direct child, sorted by name.
|
||||
*/
|
||||
export async function listDirectory(target: LocalTarget, signal?: AbortSignal): Promise<LocalDirEntry[]> {
|
||||
throwIfAborted(signal, 'list')
|
||||
@@ -321,6 +331,9 @@ async function statRegularFile(target: LocalTarget, verb: 'read', signal?: Abort
|
||||
/**
|
||||
* Read a whole regular UTF-8 text file into a single decoded string. Rejects
|
||||
* non-regular files, invalid UTF-8, and NUL-byte binary samples.
|
||||
* @param target - the resolved file to read.
|
||||
* @param signal - aborts the read (`FS_ABORTED`).
|
||||
* @returns the full decoded text, byte-for-byte (no normalization).
|
||||
*/
|
||||
export async function readWholeText(target: LocalTarget, signal?: AbortSignal): Promise<string> {
|
||||
await statRegularFile(target, 'read', signal)
|
||||
@@ -336,6 +349,9 @@ export async function readWholeText(target: LocalTarget, signal?: AbortSignal):
|
||||
* Stream a whole regular UTF-8 text file as decoded text chunks. Same text
|
||||
* semantics as {@link readWholeText} (regular-file check, binary/NUL rejection,
|
||||
* cross-chunk UTF-8 decoding), but never holds the whole file in memory.
|
||||
* @param target - the resolved file to stream.
|
||||
* @param signal - aborts the stream, including between chunks (`FS_ABORTED`).
|
||||
* @returns decoded text chunks in file order; chunk boundaries carry no meaning.
|
||||
*/
|
||||
export async function* streamWholeText(target: LocalTarget, signal?: AbortSignal): AsyncIterable<string> {
|
||||
await statRegularFile(target, 'read', signal)
|
||||
@@ -383,6 +399,11 @@ async function removeStagingDirOrThrow(stagingDir: string, originalError: unknow
|
||||
* (`0o700`) staging directory, fsync, optionally chmod to the final mode while
|
||||
* still private, then rename over the target. `mode` (when given) preserves an
|
||||
* existing file's permissions across the replace.
|
||||
* @param absolutePath - the final destination (typically a target key); missing parent dirs are created.
|
||||
* @param content - the full UTF-8 text to write.
|
||||
* @param mode - final file mode applied before the rename (an existing file's, to preserve permissions); undefined leaves `0o600`.
|
||||
* @param signal - aborts the write (`FS_ABORTED`); checked before the rename, so the target is never left torn.
|
||||
* @param internals - test seam for pinning temp names and observing the staged file.
|
||||
*/
|
||||
export async function writeFileAtomic(
|
||||
absolutePath: string,
|
||||
@@ -440,6 +461,12 @@ export async function writeFileAtomic(
|
||||
/** Line ending style detected before LF normalization. */
|
||||
export type LineEndings = 'LF' | 'CRLF'
|
||||
|
||||
/**
|
||||
* Collapse CRLF to LF — the canonical in-memory form every edit/diff basis
|
||||
* uses. Lone `\r` bytes (not followed by `\n`) are left untouched.
|
||||
* @param content - decoded text in whatever line-ending style the file had.
|
||||
* @returns the text with every `\r\n` pair replaced by `\n`.
|
||||
*/
|
||||
function normalizeLineEndings(content: string): string {
|
||||
return content.replaceAll('\r\n', '\n')
|
||||
}
|
||||
@@ -451,6 +478,14 @@ function detectLineEndings(raw: string): LineEndings {
|
||||
return crlfCount > lfCount ? 'CRLF' : 'LF'
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert LF-normalized content back to the line-ending style detected at read
|
||||
* time, for write-back. `LF` returns the content unchanged; `CRLF` re-normalizes
|
||||
* first so an already-CRLF sequence is never doubled to `\r\r\n`.
|
||||
* @param content - the LF-normalized (edited) text.
|
||||
* @param lineEndings - the original file's style, as detected by {@link readForEdit}.
|
||||
* @returns the text in the original file's line-ending style.
|
||||
*/
|
||||
function restoreLineEndings(content: string, lineEndings: LineEndings): string {
|
||||
return lineEndings === 'LF' ? content : normalizeLineEndings(content).split('\n').join('\r\n')
|
||||
}
|
||||
@@ -469,6 +504,10 @@ function countOccurrences(content: string, needle: string): number {
|
||||
/**
|
||||
* Read and decode a file for editing: rejects binaries, returns LF-normalized
|
||||
* content plus the original line-ending style for write-back.
|
||||
* @param absolutePath - the file to read (typically a target key).
|
||||
* @param displayPath - the caller-facing path used in error messages.
|
||||
* @param signal - aborts the read (`FS_ABORTED`).
|
||||
* @returns the LF-normalized content and the detected style to restore on write-back.
|
||||
*/
|
||||
export async function readForEdit(
|
||||
absolutePath: string,
|
||||
@@ -490,6 +529,9 @@ export async function readForEdit(
|
||||
* prior bytes, so an undiffable prior file simply yields no contextual-hunk basis
|
||||
* (the caller treats `null` the same as an absent file: the result renders a
|
||||
* whole-file diff rather than an applied hunk).
|
||||
* @param absolutePath - the file to read (typically a target key); it must exist.
|
||||
* @param signal - aborts the read (`FS_ABORTED`).
|
||||
* @returns the LF-normalized text, or null for a binary or non-UTF-8 file.
|
||||
*/
|
||||
export async function readTextForDiff(absolutePath: string, signal?: AbortSignal): Promise<string | null> {
|
||||
const buffer = await readFileAbortable(absolutePath, 'read', signal)
|
||||
@@ -508,6 +550,12 @@ export async function readTextForDiff(absolutePath: string, signal?: AbortSignal
|
||||
* `FS_EDIT_NOT_FOUND` on empty `oldString` or zero matches and
|
||||
* `FS_AMBIGUOUS_EDIT` on multiple matches when `replaceAll` is false. Returns
|
||||
* the edited content (still LF-normalized) and the replacement count.
|
||||
* @param content - the current file content, already LF-normalized.
|
||||
* @param oldString - literal text to find; CRLF inside it is normalized to LF before matching.
|
||||
* @param newString - literal replacement text, normalized the same way.
|
||||
* @param replaceAll - replace every match instead of requiring exactly one.
|
||||
* @param displayPath - the caller-facing path used in error messages.
|
||||
* @returns the edited LF-normalized content plus how many occurrences were replaced.
|
||||
*/
|
||||
export function applyLiteralEdit(
|
||||
content: string,
|
||||
|
||||
@@ -77,6 +77,7 @@ export class LocalFileSystem extends FileSystem {
|
||||
cwd: z.string().default(process.cwd()),
|
||||
})
|
||||
|
||||
/** Validated config (schemastery applied the defaults before construction). */
|
||||
readonly config: ResolvedConfig
|
||||
/** Test seam forwarded to fsio (force streaming path, pin temp names). */
|
||||
internals: FsIoInternals = {}
|
||||
|
||||
Reference in New Issue
Block a user