@stackstackstack/dsh-fs-local 0.1.5

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.
@@ -0,0 +1,23 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@stackstackstack/dsh-fs-local`.
4
+ * @module @stackstackstack/dsh-fs-local/invariant
5
+ */
6
+ const PACKAGE_NAME = "@stackstackstack/dsh-fs-local";
7
+ /** Cordis companion plugin name. */
8
+ const name = "fs-local-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: this package exposes no independent event sequence or mutable data relation
13
+ * beyond contracts enforced at its owning seam.
14
+ */
15
+ const install = () => {};
16
+ /**
17
+ * Register this package's invariant companion.
18
+ * @param ctx - Cordis context carrying the invariant service.
19
+ * @returns the installed registration's disposer after setup succeeds.
20
+ */
21
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
+ //#endregion
23
+ export { apply, inject, name };
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Cordis-free local filesystem mechanics. This provider layer returns validated UTF-8 text,
3
+ * streams large files, and rejects binary data; line windows belong to `dsh-tool-fs`. Writes
4
+ * stage an exclusive owner-only file in a private sibling directory and atomically publish it.
5
+ * @module @stackstackstack/dsh-fs-local/fsio
6
+ */
7
+ import type { BigIntStats } from 'node:fs';
8
+ import { FsTargetKey, FsVersion } from '@stackstackstack/dsh-fs';
9
+ /**
10
+ * Test hook: lets specs pin the atomic-write temp names (to prove exclusive-open behavior without
11
+ * a name race), override native boundaries, and observe the staged temp file before publication.
12
+ */
13
+ export interface FsIoInternals {
14
+ /** Override the host platform for native-publication unit coverage. */
15
+ platform?: NodeJS.Platform;
16
+ /** Override the generated private staging-dir name (relative to the target dir). */
17
+ tempDirName?: (writePath: string) => string;
18
+ /** Override the generated temp-file name (relative to the private staging dir). */
19
+ tempName?: (writePath: string) => string;
20
+ /** Override the Win32 DACL copy boundary. */
21
+ copyFileDacl?: (source: string, destination: string) => Promise<void>;
22
+ /** Override the Win32 security-preserving replacement boundary. */
23
+ replaceFile?: (replaced: string, replacement: string) => Promise<void>;
24
+ /** Override the hard-link no-replace publication boundary. */
25
+ linkFile?: (existingPath: string, newPath: string) => Promise<void>;
26
+ /** Override target inspection after guarded publication fails. */
27
+ inspectPublicationTarget?: (path: string) => Promise<BigIntStats>;
28
+ /** Override staging-directory removal for commit-point failure coverage. */
29
+ removeStagingDir?: (stagingDir: string) => Promise<void>;
30
+ /** Test hook after the temp file is written/synced but before final chmod+publication. */
31
+ inspectTemp?: (paths: {
32
+ stagingDir: string;
33
+ tempPath: string;
34
+ }) => void | Promise<void>;
35
+ /** Test hook after raw-read stat preflight and before bounded content I/O. */
36
+ inspectReadBytesAfterStat?: (target: LocalTarget) => void | Promise<void>;
37
+ }
38
+ /** A resolved local path: the absolute path shown to callers and its realpath identity. */
39
+ export interface LocalTarget {
40
+ /** Absolute path (symlinks not resolved) — used for display. */
41
+ displayPath: string;
42
+ /** Realpath identity — used as the stable target key and the I/O path. */
43
+ targetKey: FsTargetKey;
44
+ }
45
+ /** Result of probing a path: null when it does not exist. */
46
+ export interface PathInfo {
47
+ version: FsVersion;
48
+ mode: number;
49
+ type: 'file' | 'directory' | 'other';
50
+ size: number;
51
+ }
52
+ /** Result of probing a path without following the final symlink component. */
53
+ export interface PathLinkInfo {
54
+ version: FsVersion;
55
+ mode: number;
56
+ type: 'file' | 'directory' | 'symlink' | 'other';
57
+ size: number;
58
+ }
59
+ /** One local directory child with a resolved target and cheap metadata. */
60
+ export interface LocalDirEntry {
61
+ name: string;
62
+ type: 'file' | 'directory' | 'other';
63
+ target: LocalTarget;
64
+ version?: FsVersion;
65
+ size?: number;
66
+ }
67
+ /**
68
+ * Resolve a path to its absolute display path and realpath identity. For a missing target,
69
+ * realpath the nearest existing ancestor and append the missing suffix, preserving identity
70
+ * across symlinked ancestors before and after creation.
71
+ * @param cwd - base directory a relative `path` resolves against.
72
+ * @param path - absolute or relative path; empty/whitespace-only throws `FS_NOT_FOUND`.
73
+ * @returns the absolute display path plus the realpath-derived stable target key.
74
+ */
75
+ export declare function resolveLocalTarget(cwd: string, path: string): Promise<LocalTarget>;
76
+ /**
77
+ * Probe a path for its version, mode, type, and size. Null if absent.
78
+ * @param absolutePath - the path to stat (typically a target key; symlinks are followed).
79
+ * @returns the metadata, or null when the path — or a parent segment — does not exist.
80
+ */
81
+ export declare function probe(absolutePath: string): Promise<PathInfo | null>;
82
+ /**
83
+ * Probe a path without following the final symlink component.
84
+ * @param absolutePath - the path entry to inspect with `lstat` semantics.
85
+ * @returns path-entry metadata, or null when the entry is absent.
86
+ */
87
+ export declare function probeNoFollow(absolutePath: string): Promise<PathLinkInfo | null>;
88
+ /**
89
+ * List direct children of a directory in stable name order. Each child includes
90
+ * a resolved target plus stat metadata when still available; file contents are
91
+ * never read.
92
+ * @param target - the resolved directory to list; a missing or non-directory target throws.
93
+ * @param signal - aborts the listing, checked between children (`FS_ABORTED`).
94
+ * @returns one entry per direct child, sorted by name.
95
+ */
96
+ export declare function listDirectory(target: LocalTarget, signal?: AbortSignal): Promise<LocalDirEntry[]>;
97
+ /**
98
+ * Read a whole regular UTF-8 text file into a single decoded string. Rejects
99
+ * non-regular files, invalid UTF-8, and NUL-byte binary samples.
100
+ * @param target - the resolved file to read.
101
+ * @param signal - aborts the read (`FS_ABORTED`).
102
+ * @returns the full decoded text, byte-for-byte (no normalization).
103
+ */
104
+ export declare function readWholeText(target: LocalTarget, signal?: AbortSignal): Promise<string>;
105
+ /**
106
+ * Read a whole regular file as raw bytes with no decoding or binary rejection.
107
+ * `maxBytes` bounds the complete content: the stat size short-circuits an
108
+ * oversized file before any content I/O, and the stream retains at most the
109
+ * cap while detecting post-stat growth.
110
+ * @param target - the resolved file to read.
111
+ * @param signal - aborts the read (`FS_ABORTED`).
112
+ * @param maxBytes - inclusive byte cap on the complete content (`FS_TOO_LARGE`).
113
+ * @param internals - test seam for a deterministic post-stat growth race.
114
+ * @returns the full raw content, at most `maxBytes` long.
115
+ */
116
+ export declare function readWholeBytes(target: LocalTarget, signal: AbortSignal | undefined, maxBytes: number, internals?: FsIoInternals): Promise<Uint8Array>;
117
+ /**
118
+ * Stream a whole regular UTF-8 text file as decoded text chunks. Same text
119
+ * semantics as {@link readWholeText} (regular-file check, binary/NUL rejection,
120
+ * cross-chunk UTF-8 decoding), but never holds the whole file in memory.
121
+ * @param target - the resolved file to stream.
122
+ * @param signal - aborts the stream, including between chunks (`FS_ABORTED`).
123
+ * @returns decoded text chunks in file order; chunk boundaries carry no meaning.
124
+ */
125
+ export declare function streamWholeText(target: LocalTarget, signal?: AbortSignal): AsyncIterable<string>;
126
+ /**
127
+ * Atomically replace a file through a private, synced staging file in the same directory.
128
+ * POSIX protects the staging directory and file with `0o700` and `0o600`. A new Windows file
129
+ * inherits the destination directory's DACL; a replacement copies the existing target's DACL
130
+ * onto the empty temp before writing and preserves the target descriptor at publication.
131
+ * @param absolutePath - destination; missing parent directories are created.
132
+ * @param content - the full UTF-8 text to write.
133
+ * @param mode - existing destination's POSIX mode to preserve, or `undefined` for a new file;
134
+ * inert as a mode on Windows but identifies replacement security semantics.
135
+ * @param signal - cancellation checked before final publication.
136
+ * @param internals - Test hook for pinning temp names and observing the staged file.
137
+ * @param createIfAbsent - when provided, publish with a hard-link no-replace
138
+ * primitive; a concurrent creator's file is preserved and this write is
139
+ * rejected with `FS_NOT_OBSERVED` using the supplied display path.
140
+ */
141
+ export declare function writeFileAtomic(absolutePath: string, content: string, mode: number | undefined, signal: AbortSignal | undefined, internals?: FsIoInternals, createIfAbsent?: {
142
+ displayPath: string;
143
+ }): Promise<void>;
144
+ /** Line ending style detected before LF normalization. */
145
+ export type LineEndings = 'LF' | 'CRLF';
146
+ /**
147
+ * Collapse CRLF to LF — the canonical in-memory form every edit/diff basis
148
+ * uses. Lone `\r` bytes (not followed by `\n`) are left untouched.
149
+ * @param content - decoded text in whatever line-ending style the file had.
150
+ * @returns the text with every `\r\n` pair replaced by `\n`.
151
+ */
152
+ declare function normalizeLineEndings(content: string): string;
153
+ /**
154
+ * Convert LF-normalized content back to the line-ending style detected at read
155
+ * time, for write-back. `LF` returns the content unchanged; `CRLF` re-normalizes
156
+ * first so an already-CRLF sequence is never doubled to `\r\r\n`.
157
+ * @param content - the LF-normalized (edited) text.
158
+ * @param lineEndings - the original file's style, as detected by {@link readForEdit}.
159
+ * @returns the text in the original file's line-ending style.
160
+ */
161
+ declare function restoreLineEndings(content: string, lineEndings: LineEndings): string;
162
+ /**
163
+ * Read and decode a file for editing: rejects binaries, returns LF-normalized
164
+ * content plus the original line-ending style for write-back.
165
+ * @param absolutePath - the file to read (typically a target key).
166
+ * @param displayPath - the caller-facing path used in error messages.
167
+ * @param signal - aborts the read (`FS_ABORTED`).
168
+ * @returns the LF-normalized content and the detected style to restore on write-back.
169
+ */
170
+ export declare function readForEdit(absolutePath: string, displayPath: string, signal?: AbortSignal): Promise<{
171
+ content: string;
172
+ lineEndings: LineEndings;
173
+ }>;
174
+ /**
175
+ * Best-effort overwrite diff basis. Binary, invalid UTF-8, a file at/above the byte limit,
176
+ * or a file deleted/made unreadable after the caller's preflight returns `null` so the write
177
+ * still succeeds and presentation falls back to a whole-file diff. The bound is enforced on
178
+ * the opened descriptor rather than a prior path stat, so concurrent external replacement or
179
+ * size changes cannot make this helper buffer more than `maxBytes`.
180
+ * @param absolutePath - the file to read (typically a target key).
181
+ * @param maxBytes - exclusive upper bound for bytes held as the contextual-diff basis.
182
+ * @param signal - aborts the read (`FS_ABORTED`); cancellation propagates, unlike I/O failure.
183
+ * @returns the LF-normalized text, or null for a non-regular, at/above-limit, binary, non-UTF-8,
184
+ * descriptor-size-changed, or unreadable file.
185
+ */
186
+ export declare function readTextForDiff(absolutePath: string, maxBytes: number, signal?: AbortSignal): Promise<string | null>;
187
+ /**
188
+ * Apply a literal replacement to LF-normalized content. Empty or missing search text throws
189
+ * `FS_EDIT_NOT_FOUND`; multiple matches throw `FS_AMBIGUOUS_EDIT` unless `replaceAll` is true.
190
+ * @param content - the current file content, already LF-normalized.
191
+ * @param oldString - literal text to find; CRLF inside it is normalized to LF before
192
+ * matching.
193
+ * @param newString - literal replacement text, normalized the same way.
194
+ * @param replaceAll - replace every match instead of requiring exactly one.
195
+ * @param displayPath - the caller-facing path used in error messages.
196
+ * @returns the edited LF-normalized content plus how many occurrences were replaced.
197
+ */
198
+ export declare function applyLiteralEdit(content: string, oldString: string, newString: string, replaceAll: boolean, displayPath: string): {
199
+ content: string;
200
+ replacements: number;
201
+ };
202
+ export { normalizeLineEndings, restoreLineEndings };
203
+ //# sourceMappingURL=fsio.d.ts.map
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Host-filesystem implementation of `ctx.fs`. Realpath-derived target identity makes aliases
3
+ * share stale guards, and writes through a symlink update its target without replacing the link.
4
+ * @module @stackstackstack/dsh-fs-local
5
+ */
6
+ import { Context } from '@deepseek-ai/cordis';
7
+ import z from '@deepseek-ai/schemastery';
8
+ import { FileSystem, FsVersion } from '@stackstackstack/dsh-fs';
9
+ import type { FsDirEntry, FsEditOutcome, FsEditRequest, FsInfo, FsPathInfo, FsTarget, FsWriteIntent, FsWriteOutcome } from '@stackstackstack/dsh-fs';
10
+ import type { FsIoInternals } from './fsio.ts';
11
+ /** Configuration for the local filesystem backend. */
12
+ export interface Config {
13
+ /** Base directory for relative paths. Defaults to `process.cwd()`. */
14
+ cwd?: string;
15
+ /**
16
+ * Exclusive UTF-8 byte limit on each overwrite-diff side, capped by the
17
+ * runtime's safe allocation/decode maximum. Defaults to 10 MiB.
18
+ */
19
+ diffBasisMaxBytes?: number;
20
+ }
21
+ type ResolvedConfig = Required<Config>;
22
+ /**
23
+ * The host-filesystem backend. Reads resolve relative paths from {@link Config.cwd}
24
+ * (a resolution default, NOT a containment boundary — see the filesystem
25
+ * capability-seam Agent Note); enforce
26
+ * containment with a stricter backend or a `tools/execute` permission plugin.
27
+ */
28
+ export declare class LocalFileSystem extends FileSystem {
29
+ static Config: z<Config>;
30
+ /** Validated config (schemastery applied the defaults before construction). */
31
+ readonly config: ResolvedConfig;
32
+ /** Test hook forwarded to fsio for atomic-publication boundaries. */
33
+ internals: FsIoInternals;
34
+ /** Per-targetKey tail promise: serializes mutating ops so the read→guard→write
35
+ * window can't interleave, making concurrent writes/edits deterministically
36
+ * ordered (one wins, the rest see the new version and reject as stale). */
37
+ private locks;
38
+ constructor(ctx: Context, config: Config);
39
+ /** Run `op` with exclusive access to `targetKey` (FIFO per key). */
40
+ private withLock;
41
+ resolve(path: string, opts?: {
42
+ cwd?: string;
43
+ signal?: AbortSignal;
44
+ }): Promise<FsTarget>;
45
+ processPath(target: FsTarget): string;
46
+ fileUrl(target: FsTarget): string;
47
+ contains(parent: FsTarget, child: FsTarget): boolean;
48
+ stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined>;
49
+ lstat(path: string, opts?: {
50
+ cwd?: string;
51
+ }, signal?: AbortSignal): Promise<FsPathInfo | undefined>;
52
+ readText(target: FsTarget, signal?: AbortSignal): Promise<string>;
53
+ streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>>;
54
+ readBytes(target: FsTarget, signal: AbortSignal | undefined, maxBytes: number): Promise<Uint8Array>;
55
+ listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]>;
56
+ writeText(target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal): Promise<FsWriteOutcome>;
57
+ editText(target: FsTarget, edit: FsEditRequest, expected?: {
58
+ version: FsVersion;
59
+ }, signal?: AbortSignal): Promise<FsEditOutcome>;
60
+ private versionAfterWrite;
61
+ }
62
+ export default LocalFileSystem;
63
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@stackstackstack/dsh-fs-local`.
3
+ * @module @stackstackstack/dsh-fs-local/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "fs-local-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Windows security-descriptor helpers for atomic local-file replacement. Koffi loads lazily so
3
+ * non-Windows processes never open Win32 libraries.
4
+ * @module @stackstackstack/dsh-fs-local/win32
5
+ */
6
+ /**
7
+ * Read a file's self-relative DACL security descriptor.
8
+ * @param path - existing file whose DACL is read.
9
+ * @returns a descriptor buffer accepted by `SetFileSecurityW`.
10
+ */
11
+ export declare function readFileDaclWin32(path: string): Promise<Buffer>;
12
+ /**
13
+ * Copy an existing file's DACL onto another file and protect it from staging-parent inheritance.
14
+ * The destination must still be empty when confidentiality depends on this call.
15
+ * @param source - existing file whose DACL is copied.
16
+ * @param destination - existing file that receives the protected DACL.
17
+ */
18
+ export declare function copyFileDaclWin32(source: string, destination: string): Promise<void>;
19
+ /**
20
+ * Replace a Windows file while preserving the replaced file's ACL and other replace metadata.
21
+ * @param replaced - existing destination file.
22
+ * @param replacement - closed staging file on the same volume.
23
+ */
24
+ export declare function replaceFileWin32(replaced: string, replacement: string): Promise<void>;
25
+ //# sourceMappingURL=win32.d.ts.map
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@stackstackstack/dsh-fs-local",
3
+ "description": "Local-filesystem implementation of the DeepSeek Harness filesystem seam (ctx.fs)",
4
+ "version": "0.1.5",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/fs/fs-local"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib/index.js",
30
+ "lib/invariant.js",
31
+ "lib/types/**/*.d.ts"
32
+ ],
33
+ "license": "MIT",
34
+ "peerDependencies": {
35
+ "@stackstackstack/dsh-fs": "^0.1.5",
36
+ "@stackstackstack/dsh-invariants": "^0.1.5",
37
+ "@deepseek-ai/cordis": "^4.0.1"
38
+ },
39
+ "dependencies": {
40
+ "koffi": "^3.1.0",
41
+ "@deepseek-ai/schemastery": "^3.18.1"
42
+ },
43
+ "devDependencies": {
44
+ "@stackstackstack/dsh-fs": "^0.1.5",
45
+ "@stackstackstack/dsh-invariants": "^0.1.5",
46
+ "@stackstackstack/dsh-llm": "^0.1.5",
47
+ "@deepseek-ai/cordis": "^4.0.1"
48
+ }
49
+ }