@deepseek-ai/dsh-fs-local 0.1.5-rc.2 → 0.1.6-alpha.2
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.i18n.yaml +2 -2
- package/README.md +5 -5
- package/README.zh.md +7 -7
- package/lib/index.js +25 -9
- package/lib/types/fsio.d.ts +7 -0
- package/package.json +5 -5
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/fs/fs-local/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: e781abd2032868a3768a41eb01ce5aac52f8c835
|
|
6
|
+
README.zh.md: bf21f92a48c7c9d804908ee8bc25b2be8daa8967
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
|
|
9
9
|
|
|
10
10
|
## Summary
|
|
11
11
|
|
|
12
|
-
Use `dsh-fs-local` to read, list, atomically write, and edit files on the host filesystem. Relative paths resolve from a configurable base directory, while absolute paths and parent traversal remain unrestricted. Paths and symlinks that reach the same file share one identity. Writes preserve file permissions, and optional version guards reject stale overwrites. Choose this package for direct host access; use `fs-sandbox` for confined mutations
|
|
12
|
+
Use `dsh-fs-local` to read, list, atomically write, and edit files on the host filesystem. Relative paths resolve from a configurable base directory, while absolute paths and parent traversal remain unrestricted. Paths and symlinks that reach the same file share one identity. Writes preserve file permissions, and optional version guards reject stale overwrites. Choose this package for direct host access; use `fs-sandbox` for confined mutations.
|
|
13
13
|
|
|
14
14
|
## Table of Contents
|
|
15
15
|
|
|
@@ -29,11 +29,11 @@ Mount this backend when a composition needs `ctx.fs` backed by the real host fil
|
|
|
29
29
|
|
|
30
30
|
### When to choose it
|
|
31
31
|
|
|
32
|
-
Choose `fs-local` for ordinary host-file access in a single process. Choose [`fs-sandbox`](../fs-sandbox/README.md) when a session's writes and edits must be confined to its workspace and temp roots — it extends this backend and adds only the mode fence.
|
|
32
|
+
Choose `fs-local` for ordinary host-file access in a single process. Choose [`fs-sandbox`](../fs-sandbox/README.md) when a session's writes and edits must be confined to its workspace and temp roots — it extends this backend and adds only the mode fence. `config.cwd` is a resolution default, not a containment boundary: absolute paths and `..` escape it.
|
|
33
33
|
|
|
34
34
|
### Minimal configuration
|
|
35
35
|
|
|
36
|
-
Load the backend with a base directory; relative paths resolve against it, and absolute paths ignore it.
|
|
36
|
+
Load the backend with a base directory; relative paths resolve against it, and absolute paths ignore it. A relative base is anchored to the provider process working directory, and display paths remain absolute. On POSIX, resolution follows filesystem semantics before lexical normalization: `symlink/..` reaches the parent of the link target, including when the final file does not exist yet. Directory listings preserve the same physical traversal in displayed child paths. Windows retains native drive-relative normalization.
|
|
37
37
|
|
|
38
38
|
```yaml
|
|
39
39
|
- name: '@deepseek-ai/dsh-fs-local'
|
|
@@ -50,9 +50,9 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
|
|
|
50
50
|
|
|
51
51
|
### What you can do
|
|
52
52
|
|
|
53
|
-
Read any regular UTF-8 text file whole or as a stream, read raw bytes up to a cap you choose or
|
|
53
|
+
Read any regular UTF-8 text file whole or as a stream, read raw bytes up to a cap you choose or in a byte window, and list one directory level in stable name order. Create or replace a file atomically, and apply a literal text edit atomically; both mutations serialize per file, so concurrent writers never interleave. The version guard is optional: omit it for unconditional create-or-overwrite, or supply it to fail when the file changed since you last observed it.
|
|
54
54
|
|
|
55
|
-
Failures are typed `FsError`s with stable codes — `FS_NOT_FOUND`, `FS_NOT_TEXT` (binary content), `FS_STALE_VERSION` (changed since observation), `FS_EDIT_NOT_FOUND` or `FS_AMBIGUOUS_EDIT` (no unique literal match), and others — so callers branch on the code, never on message text. A missing target on
|
|
55
|
+
Failures are typed `FsError`s with stable codes — `FS_NOT_FOUND`, `FS_NOT_TEXT` (binary content), `FS_STALE_VERSION` (changed since observation), `FS_EDIT_NOT_FOUND` or `FS_AMBIGUOUS_EDIT` (no unique literal match), and others — so callers branch on the code, never on message text. A missing target on an edit reports `FS_STALE_VERSION` whether or not the version guard is supplied.
|
|
56
56
|
|
|
57
57
|
-----
|
|
58
58
|
|
package/README.zh.md
CHANGED
|
@@ -9,7 +9,7 @@ kind: "package-reference"
|
|
|
9
9
|
|
|
10
10
|
## 概述
|
|
11
11
|
|
|
12
|
-
使用 `dsh-fs-local` 可在宿主文件系统上读取、列出、原子写入和编辑文件。相对路径从可配置的基准目录解析,而绝对路径和父目录遍历不受限制。到达同一文件的路径和符号链接共享一个身份。写入保留文件权限,可选版本防护会拒绝陈旧覆盖。直接访问宿主文件时选择本包;需要约束变更时使用 `fs-sandbox
|
|
12
|
+
使用 `dsh-fs-local` 可在宿主文件系统上读取、列出、原子写入和编辑文件。相对路径从可配置的基准目录解析,而绝对路径和父目录遍历不受限制。到达同一文件的路径和符号链接共享一个身份。写入保留文件权限,可选版本防护会拒绝陈旧覆盖。直接访问宿主文件时选择本包;需要约束变更时使用 `fs-sandbox`。
|
|
13
13
|
|
|
14
14
|
## 目录
|
|
15
15
|
|
|
@@ -29,11 +29,11 @@ kind: "package-reference"
|
|
|
29
29
|
|
|
30
30
|
### 何时选择
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
在单个进程中进行普通宿主文件访问时,请选择 `fs-local`。会话的写入与编辑必须限制在工作区与临时根目录内时,选择 [`fs-sandbox`](../fs-sandbox/README.zh.md)——它扩展此后端,只增加模式围栏。`config.cwd` 只是解析默认值,不是约束边界:绝对路径与 `..` 都可以逃逸它。
|
|
33
33
|
|
|
34
34
|
### 最小配置
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
用一个基础目录加载后端;相对路径基于它解析,绝对路径则忽略它。相对基础目录以提供方进程工作目录为起点,展示路径始终保持绝对路径。在 POSIX 上,解析先遵循文件系统语义,再进行词法规范化:`symlink/..` 到达链接目标的父目录,即使最终文件尚不存在也如此。目录列表中的子项展示路径保留同样的物理遍历语义。Windows 保留原生的驱动器相对路径规范化行为。
|
|
37
37
|
|
|
38
38
|
```yaml
|
|
39
39
|
- name: '@deepseek-ai/dsh-fs-local'
|
|
@@ -46,13 +46,13 @@ kind: "package-reference"
|
|
|
46
46
|
| `cwd` | `process.cwd()` | 相对路径的基准目录 |
|
|
47
47
|
| `diffBasisMaxBytes` | `10 MiB` | 每次覆写 diff 一侧的 UTF-8 字节上限;更大的覆写返回 `before: null` |
|
|
48
48
|
|
|
49
|
-
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-fs-local)
|
|
49
|
+
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-fs-local)完整列出了所有受支持字段及其 JSDoc。
|
|
50
50
|
|
|
51
51
|
### 你能做什么
|
|
52
52
|
|
|
53
53
|
完整或流式读取任意普通 UTF-8 文本文件,按你选择的上限或按字节窗口读取原始字节,并按稳定名称顺序列出一层目录。原子地创建或替换文件,并原子地应用字面量文本编辑;两个变更操作都按文件串行化,并发写入方绝不会交错。版本防护是可选的:省略它即无条件创建或覆盖,提供它则在文件自上次观察以来发生变化时失败。
|
|
54
54
|
|
|
55
|
-
失败是携带稳定错误码的类型化 `FsError`——`FS_NOT_FOUND`、`FS_NOT_TEXT`(二进制内容)、`FS_STALE_VERSION`(自观察以来已变化)、`FS_EDIT_NOT_FOUND` 或 `FS_AMBIGUOUS_EDIT
|
|
55
|
+
失败是携带稳定错误码的类型化 `FsError`——`FS_NOT_FOUND`、`FS_NOT_TEXT`(二进制内容)、`FS_STALE_VERSION`(自观察以来已变化)、`FS_EDIT_NOT_FOUND` 或 `FS_AMBIGUOUS_EDIT`(无唯一字面量匹配)等——因此调用方依据错误码分支,绝不解析消息文本。编辑遇到缺失目标时,无论是否提供版本防护,都报告 `FS_STALE_VERSION`。
|
|
56
56
|
|
|
57
57
|
-----
|
|
58
58
|
|
|
@@ -69,8 +69,8 @@ kind: "package-reference"
|
|
|
69
69
|
后端建立在三个想法之上:
|
|
70
70
|
|
|
71
71
|
- **Realpath 身份。** `targetKey` 是文件的 `realpath`,因此经符号链接到达同一文件的两个输入路径共享一个身份,写入落在链接目标上,同时保留链接。
|
|
72
|
-
- **原子发布。** 写入先写入目标旁私有暂存目录内的独占临时文件,执行 fsync 后发布;现有文件的 mode 会保留,Windows 上的 DACL
|
|
73
|
-
- **单一变更临界区。** 每目标 FIFO
|
|
72
|
+
- **原子发布。** 写入先写入目标旁私有暂存目录内的独占临时文件,执行 fsync 后发布;现有文件的 mode 会保留,Windows 上的 DACL 在替换后也会保留。
|
|
73
|
+
- **单一变更临界区。** 每目标 FIFO 锁串行化读取→防护→写入窗口,并发写入与编辑因此被确定性排序——一方胜出,其余操作看到新版本后因版本陈旧而被拒绝。
|
|
74
74
|
|
|
75
75
|
### 源码地图
|
|
76
76
|
|
package/lib/index.js
CHANGED
|
@@ -4,9 +4,9 @@ import { pathToFileURL } from "node:url";
|
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { FileSystem, FsError, FsTargetKey, FsVersion } from "@deepseek-ai/dsh-fs";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { createReadStream } from "node:fs";
|
|
8
|
-
import { chmod, link, lstat, mkdir, open, readFile, readdir,
|
|
9
|
-
import { TextDecoder } from "node:util";
|
|
7
|
+
import { createReadStream, realpath } from "node:fs";
|
|
8
|
+
import { chmod, link, lstat, mkdir, open, readFile, readdir, rename, rm, stat } from "node:fs/promises";
|
|
9
|
+
import { TextDecoder, promisify } from "node:util";
|
|
10
10
|
//#region lib/types/win32.js
|
|
11
11
|
/**
|
|
12
12
|
* Windows security-descriptor helpers for atomic local-file replacement. Koffi loads lazily so
|
|
@@ -93,6 +93,7 @@ async function replaceFileWin32(replaced, replacement) {
|
|
|
93
93
|
* @module @deepseek-ai/dsh-fs-local/fsio
|
|
94
94
|
*/
|
|
95
95
|
const BINARY_SAMPLE_BYTES = 8192;
|
|
96
|
+
const realpath$1 = promisify(realpath.native);
|
|
96
97
|
const DIFF_BASIS_READ_CHUNK_BYTES = 64 * 1024;
|
|
97
98
|
function isENOENT(error) {
|
|
98
99
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
@@ -143,6 +144,19 @@ function versionOf(info) {
|
|
|
143
144
|
return FsVersion(`${info.dev}:${info.ino}:${info.size}:${info.mtimeNs}:${info.ctimeNs}`);
|
|
144
145
|
}
|
|
145
146
|
/**
|
|
147
|
+
* Anchor a path using native drive semantics and POSIX physical parent traversal.
|
|
148
|
+
* @param cwd - provider base directory for relative paths.
|
|
149
|
+
* @param path - non-empty requested path.
|
|
150
|
+
* @returns absolute display spelling shared by target resolution and no-follow metadata.
|
|
151
|
+
*/
|
|
152
|
+
function localDisplayPath(cwd, path) {
|
|
153
|
+
const absoluteCwd = isAbsolute(cwd) ? cwd : `${process.cwd()}${sep}${cwd}`;
|
|
154
|
+
const raw = isAbsolute(path) ? path : `${absoluteCwd}${sep}${path}`;
|
|
155
|
+
const physicalSpelling = /(?:^|[\\/])\.\.(?:[\\/]|$)/u.test(raw) ? raw : resolve(cwd, path);
|
|
156
|
+
/* v8 ignore next -- Native Windows tests cover DOS drive-relative resolution; POSIX preserves physical traversal. */
|
|
157
|
+
return process.platform === "win32" ? resolve(cwd, path) : physicalSpelling;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
146
160
|
* Resolve a path to its absolute display path and realpath identity. For a missing target,
|
|
147
161
|
* realpath the nearest existing ancestor and append the missing suffix, preserving identity
|
|
148
162
|
* across symlinked ancestors before and after creation.
|
|
@@ -152,11 +166,11 @@ function versionOf(info) {
|
|
|
152
166
|
*/
|
|
153
167
|
async function resolveLocalTarget(cwd, path) {
|
|
154
168
|
if (path.trim().length === 0) throw new FsError("file_path must be a non-empty string", "FS_NOT_FOUND");
|
|
155
|
-
const displayPath =
|
|
169
|
+
const displayPath = localDisplayPath(cwd, path);
|
|
156
170
|
try {
|
|
157
171
|
return {
|
|
158
172
|
displayPath,
|
|
159
|
-
targetKey: FsTargetKey(await realpath(displayPath))
|
|
173
|
+
targetKey: FsTargetKey(await realpath$1(displayPath))
|
|
160
174
|
};
|
|
161
175
|
} catch (error) {
|
|
162
176
|
/* v8 ignore next -- Windows reports this case as ENOENT and repairs it in the ancestor walk below. */
|
|
@@ -167,7 +181,9 @@ async function resolveLocalTarget(cwd, path) {
|
|
|
167
181
|
const missing = [basename(displayPath)];
|
|
168
182
|
let ancestor = dirname(displayPath);
|
|
169
183
|
while (true) try {
|
|
170
|
-
const realAncestor = await realpath(ancestor);
|
|
184
|
+
const realAncestor = await realpath$1(ancestor);
|
|
185
|
+
/* v8 ignore next -- POSIX rejects this traversal; Windows normalizes parent segments before filesystem lookup. */
|
|
186
|
+
if (missing.includes("..")) throw new FsError(`cannot resolve "${displayPath}": parent traversal crosses a missing directory`, "FS_NOT_FOUND");
|
|
171
187
|
/* v8 ignore start -- native Windows coverage exercises this repair; POSIX reports ENOTDIR before this point. */
|
|
172
188
|
if (process.platform === "win32") {
|
|
173
189
|
if (!(await stat(realAncestor)).isDirectory()) throw new FsError(`cannot resolve "${displayPath}": a parent path segment is not a directory`, "FS_NOT_FOUND");
|
|
@@ -254,7 +270,7 @@ function listingIoError(displayPath, error) {
|
|
|
254
270
|
async function resolveListedChildTarget(parent, name) {
|
|
255
271
|
const identity = await resolveLocalTarget(parent.targetKey, name);
|
|
256
272
|
return {
|
|
257
|
-
displayPath:
|
|
273
|
+
displayPath: localDisplayPath(parent.displayPath, name),
|
|
258
274
|
targetKey: identity.targetKey
|
|
259
275
|
};
|
|
260
276
|
}
|
|
@@ -301,7 +317,7 @@ async function listDirectory(target, signal) {
|
|
|
301
317
|
...childInfo?.type === "file" ? { size: childInfo.size } : {}
|
|
302
318
|
});
|
|
303
319
|
} catch (error) {
|
|
304
|
-
throw listingIoError(
|
|
320
|
+
throw listingIoError(localDisplayPath(target.displayPath, entry.name), error);
|
|
305
321
|
}
|
|
306
322
|
throwIfAborted(signal, "list");
|
|
307
323
|
}
|
|
@@ -770,7 +786,7 @@ var LocalFileSystem = class extends FileSystem {
|
|
|
770
786
|
async lstat(path, opts, signal) {
|
|
771
787
|
if (signal?.aborted) throw new FsError("lstat aborted", "FS_ABORTED");
|
|
772
788
|
if (path.trim().length === 0) throw new FsError("file_path must be a non-empty string", "FS_NOT_FOUND");
|
|
773
|
-
const info = await probeNoFollow(
|
|
789
|
+
const info = await probeNoFollow(localDisplayPath(opts?.cwd ?? this.config.cwd, path));
|
|
774
790
|
if (signal?.aborted) throw new FsError("lstat aborted", "FS_ABORTED");
|
|
775
791
|
if (!info) return void 0;
|
|
776
792
|
return {
|
package/lib/types/fsio.d.ts
CHANGED
|
@@ -64,6 +64,13 @@ export interface LocalDirEntry {
|
|
|
64
64
|
version?: FsVersion;
|
|
65
65
|
size?: number;
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Anchor a path using native drive semantics and POSIX physical parent traversal.
|
|
69
|
+
* @param cwd - provider base directory for relative paths.
|
|
70
|
+
* @param path - non-empty requested path.
|
|
71
|
+
* @returns absolute display spelling shared by target resolution and no-follow metadata.
|
|
72
|
+
*/
|
|
73
|
+
export declare function localDisplayPath(cwd: string, path: string): string;
|
|
67
74
|
/**
|
|
68
75
|
* Resolve a path to its absolute display path and realpath identity. For a missing target,
|
|
69
76
|
* realpath the nearest existing ancestor and append the missing suffix, preserving identity
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-fs-local",
|
|
3
3
|
"description": "Local-filesystem implementation of the DeepSeek Harness filesystem seam (ctx.fs)",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.6-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@deepseek-ai/dsh-fs": "^0.1.
|
|
30
|
+
"@deepseek-ai/dsh-fs": "^0.1.6-alpha.2",
|
|
31
31
|
"@deepseek-ai/cordis": "^4.0.2"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"@deepseek-ai/schemastery": "^3.18.2"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@deepseek-ai/dsh-fs": "^0.1.
|
|
39
|
-
"@deepseek-ai/
|
|
40
|
-
"@deepseek-ai/
|
|
38
|
+
"@deepseek-ai/dsh-fs": "^0.1.6-alpha.2",
|
|
39
|
+
"@deepseek-ai/dsh-llm": "^0.1.6-alpha.2",
|
|
40
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
41
41
|
}
|
|
42
42
|
}
|