nano-git 0.2.0 → 0.2.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.
Files changed (40) hide show
  1. package/dist/core/errors.d.mts +71 -1
  2. package/dist/core/errors.mjs +99 -1
  3. package/dist/errors.d.mts +2 -2
  4. package/dist/errors.mjs +2 -2
  5. package/dist/log/index.d.mts +3 -0
  6. package/dist/log/index.mjs +2 -0
  7. package/dist/log/types.d.mts +71 -0
  8. package/dist/log/walk.d.mts +37 -0
  9. package/dist/log/walk.mjs +274 -0
  10. package/dist/workdir/change-log.d.mts +25 -0
  11. package/dist/workdir/change-log.mjs +69 -0
  12. package/dist/workdir/core.d.mts +188 -0
  13. package/dist/workdir/core.mjs +2 -0
  14. package/dist/workdir/file-backend.d.mts +22 -0
  15. package/dist/workdir/file-backend.mjs +366 -0
  16. package/dist/workdir/file.d.mts +2 -0
  17. package/dist/workdir/file.mjs +2 -0
  18. package/dist/workdir/ids.d.mts +7 -0
  19. package/dist/workdir/ids.mjs +20 -0
  20. package/dist/workdir/memory-backend.d.mts +17 -0
  21. package/dist/workdir/memory-backend.mjs +159 -0
  22. package/dist/workdir/memory.d.mts +3 -0
  23. package/dist/workdir/memory.mjs +3 -0
  24. package/dist/workdir/nodes.d.mts +57 -0
  25. package/dist/workdir/nodes.mjs +112 -0
  26. package/dist/workdir/origin.mjs +55 -0
  27. package/dist/workdir/overlay.d.mts +14 -0
  28. package/dist/workdir/overlay.mjs +95 -0
  29. package/dist/workdir/path.mjs +70 -0
  30. package/dist/workdir/session-id.mjs +18 -0
  31. package/dist/workdir/session-internal.mjs +141 -0
  32. package/dist/workdir/session.d.mts +30 -0
  33. package/dist/workdir/session.mjs +407 -0
  34. package/dist/workdir/sqlite-backend.d.mts +30 -0
  35. package/dist/workdir/sqlite-backend.mjs +363 -0
  36. package/dist/workdir/sqlite.d.mts +2 -0
  37. package/dist/workdir/sqlite.mjs +2 -0
  38. package/dist/workdir/state-store.d.mts +44 -0
  39. package/dist/workdir/write-tree.mjs +97 -0
  40. package/package.json +13 -3
@@ -0,0 +1,188 @@
1
+ import { SHA1 } from "../core/types.mjs";
2
+ import { ObjectDatabase } from "../core/types/odb.mjs";
3
+ import { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError } from "../core/errors.mjs";
4
+
5
+ //#region src/workdir/core.d.ts
6
+ /**
7
+ * 虚拟工作目录条目种类
8
+ *
9
+ * - `"blob"`: 普通文件或可执行文件(mode 100644 / 100755)
10
+ * - `"tree"`: 目录(mode 40000)
11
+ * - `"symlink"`: 符号链接(mode 120000)
12
+ */
13
+ type VirtualEntryKind = "blob" | "tree" | "symlink";
14
+ /**
15
+ * 虚拟路径状态信息
16
+ *
17
+ * 由 `stat()` 返回,描述路径对应的节点属性。
18
+ */
19
+ interface VirtualEntryStat {
20
+ /** 条目种类 */
21
+ readonly kind: VirtualEntryKind;
22
+ /** Git 文件模式(如 "100644"、"100755"、"40000"、"120000") */
23
+ readonly mode: string;
24
+ /** 文件大小(对 blob 和 symlink 有效;目录返回 0) */
25
+ readonly size: number;
26
+ /** 内容哈希(对 repo-backed 节点返回 origin 哈希;CoW 节点可能为 null) */
27
+ readonly hash: SHA1 | null;
28
+ }
29
+ /**
30
+ * 目录条目
31
+ *
32
+ * 由 `readdir()` 返回,描述目录下的子条目。
33
+ */
34
+ interface VirtualDirEntry {
35
+ /** 条目名称(不含路径前缀) */
36
+ readonly name: string;
37
+ /** 条目种类 */
38
+ readonly kind: VirtualEntryKind;
39
+ /** Git 文件模式 */
40
+ readonly mode: string;
41
+ }
42
+ /**
43
+ * 变更操作类型
44
+ *
45
+ * 用于 `listChanges()` 返回的变更记录。
46
+ */
47
+ type VirtualChangeType = "add" | "modify" | "delete" | "rename" | "copy";
48
+ /**
49
+ * 单条变更记录
50
+ *
51
+ * 由 `listChanges()` 返回,描述 session 内的单次操作。
52
+ * 变更记录是会话内调试/测试辅助,不保证是最小 diff。
53
+ */
54
+ interface VirtualChange {
55
+ /** 操作路径 */
56
+ readonly path: string;
57
+ /** 变更操作类型 */
58
+ readonly type: VirtualChangeType;
59
+ /** rename/copy 操作的源路径(其他操作为 undefined) */
60
+ readonly oldPath?: string;
61
+ }
62
+ /**
63
+ * 创建 VirtualWorkdirSession 的选项
64
+ */
65
+ interface CreateVirtualWorkdirSessionOptions {
66
+ /** 基线 tree 的 SHA-1 哈希 */
67
+ readonly baseTree: SHA1;
68
+ }
69
+ /**
70
+ * Virtual Workdir session 标识
71
+ */
72
+ type VirtualWorkdirSessionId = string & {
73
+ readonly __brand: "VirtualWorkdirSessionId";
74
+ };
75
+ /**
76
+ * VirtualWorkdirSession(虚拟工作目录会话)
77
+ *
78
+ * 提供独立生命周期的可变 tree 视图,基于 `baseTree + CoW overlay` 模型。
79
+ * 不绑定 commit,不涉及 Git index / 真实工作目录。
80
+ *
81
+ * 当前 session 对 origin 仓库对象采用弱保证:
82
+ * 如果 base tree / origin blob 在后续被移除、损坏或不可读取,
83
+ * 相关读取、`revert()`、`writeTree()` 等操作会抛出 `VirtualOriginUnavailableError`。
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * import { createMemoryRepository } from "nano-git/repository/memory";
88
+ * import { createVirtualWorkdirSession } from "nano-git/workdir/memory";
89
+ *
90
+ * const repo = createMemoryRepository();
91
+ * const tree = repo.writeTree(); // 初始空 tree
92
+ * const session = createVirtualWorkdirSession(repo, { baseTree: tree });
93
+ *
94
+ * session.writeFile("hello.txt", Buffer.from("world"));
95
+ * const newTree = session.writeTree();
96
+ * ```
97
+ */
98
+ interface VirtualWorkdirSession {
99
+ /** 当前基线 tree 的 SHA-1 哈希 */
100
+ readonly baseTree: SHA1;
101
+ /** 路径是否存在 */
102
+ exists(path: string): boolean;
103
+ /** 获取路径状态信息,不存在时返回 null */
104
+ stat(path: string): VirtualEntryStat | null;
105
+ /** 读取目录内容,根目录为 "" */
106
+ readdir(path?: string): VirtualDirEntry[];
107
+ /** 读取文件内容 */
108
+ readFile(path: string): Buffer;
109
+ /** 读取符号链接目标 */
110
+ readLink(path: string): string;
111
+ /** 写入文件(新建或覆盖) */
112
+ writeFile(path: string, content: Buffer, options?: {
113
+ readonly mode?: "100644" | "100755";
114
+ }): void;
115
+ /** 写入符号链接(新建或覆盖) */
116
+ writeLink(path: string, target: string): void;
117
+ /** 创建目录(含必要父目录) */
118
+ mkdir(path: string): void;
119
+ /** 删除路径(文件、目录或符号链接) */
120
+ delete(path: string): void;
121
+ /**
122
+ * 重命名路径
123
+ *
124
+ * 只做路径重绑定,不退化为 delete + write。
125
+ * 目录重命名后,子项保持懒加载。
126
+ */
127
+ rename(from: string, to: string): void;
128
+ /**
129
+ * 复制路径
130
+ *
131
+ * 新建 session node,共享 origin,不共享 node 身份。
132
+ * 目录复制为浅复制,子项保持懒加载。
133
+ */
134
+ copy(from: string, to: string): void;
135
+ /**
136
+ * 恢复路径到其 origin
137
+ *
138
+ * 仅对当前 CoW 节点恢复其 origin(repo-backed 版本)。
139
+ * 对纯新建节点抛出 VirtualRevertNotSupportedError。
140
+ */
141
+ revert(path: string): void;
142
+ /**
143
+ * 导出当前 overlay 为新 tree
144
+ *
145
+ * 只重新合成受影响目录,复用未修改节点的哈希。
146
+ * 不自动推进 baseTree。
147
+ */
148
+ writeTree(): SHA1;
149
+ /**
150
+ * 重置 session 到指定基线 tree
151
+ *
152
+ * 丢弃全部 overlay 与变更历史。
153
+ */
154
+ reset(baseTree: SHA1): void;
155
+ /**
156
+ * 列出会话内的变更记录
157
+ *
158
+ * 是会话内调试/测试辅助,不保证是最小 diff 引擎。
159
+ * 输出稳定、测试可断言即可。
160
+ */
161
+ listChanges(): VirtualChange[];
162
+ }
163
+ /**
164
+ * VirtualWorkdirBackend
165
+ *
166
+ * session 内部状态存储的抽象接口。
167
+ * memory / file / sqlite 后端通过实现此接口来提供不同的持久化策略。
168
+ *
169
+ * `file` / `sqlite` 持久化 backend 当前都按单进程、单写者场景收口;
170
+ * 不承诺跨进程并发写安全,也不提供多写者协调协议。
171
+ *
172
+ * 本接口在后续 Phase 中会逐步补充完整方法签名。
173
+ * 当前仅为命名冻结与角色声明。
174
+ */
175
+ interface VirtualWorkdirBackend {
176
+ /** 后端类型标识 */
177
+ readonly kind: "memory" | "file" | "sqlite";
178
+ /** 创建新 session 并返回其标识 */
179
+ createSession(options: CreateVirtualWorkdirSessionOptions): VirtualWorkdirSessionId;
180
+ /** 打开已存在的 session */
181
+ openSession(source: ObjectDatabase, sessionId: VirtualWorkdirSessionId): VirtualWorkdirSession;
182
+ /** 删除 session */
183
+ deleteSession(sessionId: VirtualWorkdirSessionId): void;
184
+ /** 列出当前后端中可完整恢复的 session */
185
+ listSessions(): VirtualWorkdirSessionId[];
186
+ }
187
+ //#endregion
188
+ export { CreateVirtualWorkdirSessionOptions, VirtualChange, VirtualChangeType, VirtualDirEntry, VirtualEntryKind, VirtualEntryStat, VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError, VirtualWorkdirBackend, VirtualWorkdirSession, VirtualWorkdirSessionId };
@@ -0,0 +1,2 @@
1
+ import { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError } from "../core/errors.mjs";
2
+ export { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError };
@@ -0,0 +1,22 @@
1
+ import { VirtualWorkdirBackend } from "./core.mjs";
2
+
3
+ //#region src/workdir/file-backend.d.ts
4
+ /** 创建文件系统 Virtual Workdir backend 的可选参数 */
5
+ interface CreateFileVirtualWorkdirBackendOptions {
6
+ /** session 根目录名,默认 `sessions` */
7
+ readonly sessionsDirName?: string;
8
+ }
9
+ /**
10
+ * 创建基于文件系统目录的 Virtual Workdir backend
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const backend = createFileVirtualWorkdirBackend("/tmp/workdirs");
15
+ * const sessionId = backend.createSession({ baseTree: tree });
16
+ * const session = backend.openSession(repo.objects, sessionId);
17
+ * expect(session.baseTree).toBe(tree);
18
+ * ```
19
+ */
20
+ declare function createFileVirtualWorkdirBackend(rootDir: string, options?: CreateFileVirtualWorkdirBackendOptions): VirtualWorkdirBackend;
21
+ //#endregion
22
+ export { CreateFileVirtualWorkdirBackendOptions, createFileVirtualWorkdirBackend };
@@ -0,0 +1,366 @@
1
+ import { createRootDirectoryNode } from "./nodes.mjs";
2
+ import { createVirtualWorkdirSessionId } from "./session-id.mjs";
3
+ import { openVirtualWorkdirSession } from "./session.mjs";
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ //#region src/workdir/file-backend.ts
7
+ /**
8
+ * Virtual Workdir 文件系统 backend
9
+ */
10
+ const FILE_WORKDIR_MANIFEST_VERSION = 1;
11
+ const FILE_WORKDIR_TRANSACTION_SNAPSHOT_SUFFIX = ".txn-snapshot";
12
+ /**
13
+ * 创建基于文件系统目录的 Virtual Workdir backend
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const backend = createFileVirtualWorkdirBackend("/tmp/workdirs");
18
+ * const sessionId = backend.createSession({ baseTree: tree });
19
+ * const session = backend.openSession(repo.objects, sessionId);
20
+ * expect(session.baseTree).toBe(tree);
21
+ * ```
22
+ */
23
+ function createFileVirtualWorkdirBackend(rootDir, options = {}) {
24
+ const sessionsRoot = join(rootDir, options.sessionsDirName ?? "sessions");
25
+ mkdirSync(sessionsRoot, { recursive: true });
26
+ return {
27
+ kind: "file",
28
+ createSession(options) {
29
+ const sessionId = createVirtualWorkdirSessionId();
30
+ createFileVirtualWorkdirStateStore(sessionsRoot, sessionId).reset(options.baseTree);
31
+ return sessionId;
32
+ },
33
+ openSession(source, sessionId) {
34
+ if (!hasSession(sessionsRoot, sessionId)) throw new Error(`Virtual workdir session not found: ${sessionId}`);
35
+ validateSessionIntegrity(sessionsRoot, sessionId);
36
+ return openVirtualWorkdirSession(source, createFileVirtualWorkdirStateStore(sessionsRoot, sessionId));
37
+ },
38
+ deleteSession(sessionId) {
39
+ if (!hasSession(sessionsRoot, sessionId)) throw new Error(`Virtual workdir session not found: ${sessionId}`);
40
+ rmSync(getSessionDir(sessionsRoot, sessionId), {
41
+ recursive: true,
42
+ force: true
43
+ });
44
+ },
45
+ listSessions() {
46
+ if (!existsSync(sessionsRoot)) return [];
47
+ return readdirSync(sessionsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => !entry.name.endsWith(FILE_WORKDIR_TRANSACTION_SNAPSHOT_SUFFIX)).filter((entry) => existsSync(getManifestPath(join(sessionsRoot, entry.name)))).map((entry) => decodePathToken(entry.name)).filter((sessionId) => {
48
+ try {
49
+ validateSessionIntegrity(sessionsRoot, sessionId);
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }).sort();
55
+ }
56
+ };
57
+ }
58
+ /**
59
+ * 创建单个 session 的文件系统状态存储
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const store = createFileVirtualWorkdirStateStore("/tmp/workdirs/sessions", sessionId);
64
+ * expect(store.kind).toBe("file");
65
+ * ```
66
+ */
67
+ function createFileVirtualWorkdirStateStore(sessionsRoot, sessionId) {
68
+ const sessionDir = getSessionDir(sessionsRoot, sessionId);
69
+ const manifestPath = getManifestPath(sessionDir);
70
+ const contentDir = getContentDir(sessionDir);
71
+ return {
72
+ kind: "file",
73
+ transact(fn) {
74
+ const snapshotDir = `${sessionDir}${FILE_WORKDIR_TRANSACTION_SNAPSHOT_SUFFIX}`;
75
+ rmSync(snapshotDir, {
76
+ recursive: true,
77
+ force: true
78
+ });
79
+ if (existsSync(sessionDir)) copyDirectoryRecursive(sessionDir, snapshotDir);
80
+ try {
81
+ const result = fn();
82
+ rmSync(snapshotDir, {
83
+ recursive: true,
84
+ force: true
85
+ });
86
+ return result;
87
+ } catch (error) {
88
+ rmSync(sessionDir, {
89
+ recursive: true,
90
+ force: true
91
+ });
92
+ if (existsSync(snapshotDir)) renameSync(snapshotDir, sessionDir);
93
+ throw error;
94
+ }
95
+ },
96
+ readBaseTree() {
97
+ return readManifest(manifestPath).baseTree;
98
+ },
99
+ writeBaseTree(baseTree) {
100
+ updateManifest(sessionDir, (manifest) => ({
101
+ ...manifest,
102
+ baseTree
103
+ }));
104
+ },
105
+ getNode(id) {
106
+ const record = readManifest(manifestPath).nodes[id];
107
+ if (record === void 0) return null;
108
+ return restoreNode(record, contentDir);
109
+ },
110
+ setNode(node) {
111
+ updateManifest(sessionDir, (manifest) => {
112
+ const record = persistNode(contentDir, node);
113
+ return {
114
+ ...manifest,
115
+ nodes: {
116
+ ...manifest.nodes,
117
+ [node.id]: record
118
+ }
119
+ };
120
+ });
121
+ },
122
+ deleteNode(id) {
123
+ updateManifest(sessionDir, (manifest) => {
124
+ if (manifest.nodes[id] === void 0) return manifest;
125
+ const { [id]: _deleted, ...rest } = manifest.nodes;
126
+ return {
127
+ ...manifest,
128
+ nodes: rest
129
+ };
130
+ });
131
+ },
132
+ appendChange(record) {
133
+ updateManifest(sessionDir, (manifest) => ({
134
+ ...manifest,
135
+ changes: [...manifest.changes, record]
136
+ }));
137
+ },
138
+ listChangeRecords() {
139
+ return readManifest(manifestPath).changes;
140
+ },
141
+ clearChanges() {
142
+ updateManifest(sessionDir, (manifest) => ({
143
+ ...manifest,
144
+ changes: []
145
+ }));
146
+ },
147
+ reset(baseTree) {
148
+ rmSync(sessionDir, {
149
+ recursive: true,
150
+ force: true
151
+ });
152
+ ensureSessionDirs(sessionDir, contentDir);
153
+ writeManifestAtomic(manifestPath, createManifest(baseTree, { [createRootDirectoryNode(baseTree).id]: serializeDirectoryNode(createRootDirectoryNode(baseTree)) }));
154
+ }
155
+ };
156
+ }
157
+ function hasSession(sessionsRoot, sessionId) {
158
+ return existsSync(getManifestPath(getSessionDir(sessionsRoot, sessionId)));
159
+ }
160
+ function validateSessionIntegrity(sessionsRoot, sessionId) {
161
+ const sessionDir = getSessionDir(sessionsRoot, sessionId);
162
+ const manifest = readManifest(getManifestPath(sessionDir));
163
+ const root = manifest.nodes.root;
164
+ if (root === void 0) throw new Error(`Virtual workdir session is corrupted: missing root node for ${sessionId}`);
165
+ if (restoreNode(root, getContentDir(sessionDir)).state.kind !== "directory") throw new Error(`Virtual workdir session is corrupted: root node is not a directory for ${sessionId}`);
166
+ for (const record of Object.values(manifest.nodes)) restoreNode(record, getContentDir(sessionDir));
167
+ }
168
+ function getSessionDir(sessionsRoot, sessionId) {
169
+ return join(sessionsRoot, encodePathToken(sessionId));
170
+ }
171
+ function getManifestPath(sessionDir) {
172
+ return join(sessionDir, "manifest.json");
173
+ }
174
+ function getContentDir(sessionDir) {
175
+ return join(sessionDir, "content");
176
+ }
177
+ function getContentPath(contentDir, payloadRef) {
178
+ return join(contentDir, `${encodePathToken(payloadRef)}.bin`);
179
+ }
180
+ function ensureSessionDirs(sessionDir, contentDir) {
181
+ mkdirSync(sessionDir, { recursive: true });
182
+ mkdirSync(contentDir, { recursive: true });
183
+ }
184
+ function copyDirectoryRecursive(sourceDir, targetDir) {
185
+ mkdirSync(targetDir, { recursive: true });
186
+ for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
187
+ const sourcePath = join(sourceDir, entry.name);
188
+ const targetPath = join(targetDir, entry.name);
189
+ if (entry.isDirectory()) {
190
+ copyDirectoryRecursive(sourcePath, targetPath);
191
+ continue;
192
+ }
193
+ writeFileSync(targetPath, readFileSync(sourcePath));
194
+ }
195
+ }
196
+ function createManifest(baseTree, nodes) {
197
+ return {
198
+ formatVersion: FILE_WORKDIR_MANIFEST_VERSION,
199
+ baseTree,
200
+ changes: [],
201
+ nodes
202
+ };
203
+ }
204
+ function updateManifest(sessionDir, updater) {
205
+ const manifestPath = getManifestPath(sessionDir);
206
+ ensureSessionDirs(sessionDir, getContentDir(sessionDir));
207
+ writeManifestAtomic(manifestPath, updater(readManifest(manifestPath)));
208
+ }
209
+ function writeManifestAtomic(path, manifest) {
210
+ writeJsonAtomic(path, manifest);
211
+ }
212
+ function readManifest(manifestPath) {
213
+ if (!existsSync(manifestPath)) throw new Error(`Virtual workdir session manifest not found: ${manifestPath}`);
214
+ const manifest = readJson(manifestPath);
215
+ validateManifest(manifest);
216
+ return manifest;
217
+ }
218
+ function validateManifest(manifest) {
219
+ if (typeof manifest.baseTree !== "string" || manifest.baseTree.length === 0) throw new Error("Invalid virtual workdir manifest baseTree");
220
+ if (!Array.isArray(manifest.changes)) throw new Error("Invalid virtual workdir manifest changes");
221
+ if (typeof manifest.nodes !== "object" || manifest.nodes === null || Array.isArray(manifest.nodes)) throw new Error("Invalid virtual workdir manifest nodes");
222
+ if (manifest.formatVersion !== FILE_WORKDIR_MANIFEST_VERSION) throw new Error(`Unsupported virtual workdir file manifest version: expected ${FILE_WORKDIR_MANIFEST_VERSION}, got ${manifest.formatVersion}`);
223
+ }
224
+ function persistNode(contentDir, node) {
225
+ if (node.state.kind === "directory") return serializeDirectoryNode(node);
226
+ if (node.state.kind === "file") {
227
+ const contentRef = node.state.content === void 0 ? null : persistPayload(contentDir, node.id, node.state.content);
228
+ return {
229
+ id: node.id,
230
+ origin: node.origin,
231
+ state: {
232
+ kind: "file",
233
+ mode: node.state.mode,
234
+ contentRef
235
+ }
236
+ };
237
+ }
238
+ const targetRef = node.state.target === void 0 ? null : persistPayload(contentDir, node.id, node.state.target);
239
+ return {
240
+ id: node.id,
241
+ origin: node.origin,
242
+ state: {
243
+ kind: "symlink",
244
+ mode: "120000",
245
+ targetRef
246
+ }
247
+ };
248
+ }
249
+ function serializeDirectoryNode(node) {
250
+ if (node.state.kind !== "directory") throw new Error("serializeDirectoryNode: node is not a directory");
251
+ return {
252
+ id: node.id,
253
+ origin: node.origin,
254
+ state: {
255
+ kind: "directory",
256
+ overlay: {
257
+ addedEntries: Array.from(node.state.overlay.addedEntries.entries()),
258
+ deletedNames: Array.from(node.state.overlay.deletedNames.values())
259
+ }
260
+ }
261
+ };
262
+ }
263
+ function persistPayload(contentDir, nodeId, payload) {
264
+ mkdirSync(contentDir, { recursive: true });
265
+ const payloadRef = `${nodeId}:${Date.now()}:${Math.random().toString(36).slice(2, 10)}`;
266
+ writeBufferAtomic(getContentPath(contentDir, payloadRef), payload);
267
+ return payloadRef;
268
+ }
269
+ function restoreNode(record, contentDir) {
270
+ const origin = restoreOrigin(record.origin);
271
+ if (record.state.kind === "directory") return {
272
+ id: record.id,
273
+ origin,
274
+ state: {
275
+ kind: "directory",
276
+ overlay: readDirectoryOverlayRecord(record.state.overlay)
277
+ }
278
+ };
279
+ const rawState = record.state;
280
+ if (rawState.kind === "file") {
281
+ const mode = rawState.mode;
282
+ if (mode !== "100644" && mode !== "100755") throw new Error(`Invalid file workdir node state mode: ${String(mode)}`);
283
+ const contentRef = rawState.contentRef;
284
+ if (contentRef !== void 0 && contentRef !== null && typeof contentRef !== "string") throw new Error("Invalid file workdir node content ref");
285
+ return {
286
+ id: record.id,
287
+ origin,
288
+ state: {
289
+ kind: "file",
290
+ mode,
291
+ content: contentRef === void 0 || contentRef === null ? void 0 : readPayload(contentDir, contentRef)
292
+ }
293
+ };
294
+ }
295
+ if (rawState.kind !== "symlink") throw new Error(`Invalid file workdir node state kind: ${String(rawState.kind)}`);
296
+ const targetRef = rawState.targetRef;
297
+ if (targetRef !== void 0 && targetRef !== null && typeof targetRef !== "string") throw new Error("Invalid file workdir node target ref");
298
+ return {
299
+ id: record.id,
300
+ origin,
301
+ state: {
302
+ kind: "symlink",
303
+ mode: "120000",
304
+ target: targetRef === void 0 || targetRef === null ? void 0 : readPayload(contentDir, targetRef)
305
+ }
306
+ };
307
+ }
308
+ function readDirectoryOverlayRecord(value) {
309
+ if (!isFileDirectoryOverlayPayload(value)) throw new Error("Invalid file workdir directory overlay payload");
310
+ return {
311
+ addedEntries: new Map(value.addedEntries.map(([name, nodeId]) => [name, nodeId])),
312
+ deletedNames: new Set(value.deletedNames)
313
+ };
314
+ }
315
+ function isFileDirectoryOverlayPayload(value) {
316
+ if (typeof value !== "object" || value === null) return false;
317
+ const maybe = value;
318
+ if (!Array.isArray(maybe.addedEntries) || !Array.isArray(maybe.deletedNames)) return false;
319
+ const hasValidAddedEntries = maybe.addedEntries.every((entry) => Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string" && typeof entry[1] === "string");
320
+ const hasValidDeletedNames = maybe.deletedNames.every((name) => typeof name === "string");
321
+ return hasValidAddedEntries && hasValidDeletedNames;
322
+ }
323
+ function restoreOrigin(record) {
324
+ const origin = record;
325
+ if (origin.kind === "none") return { kind: "none" };
326
+ if (origin.kind === "repo-tree") {
327
+ if (typeof origin.hash !== "string" || origin.hash.length === 0) throw new Error("Invalid file workdir node: repo-tree origin is missing hash");
328
+ return {
329
+ kind: "repo-tree",
330
+ hash: origin.hash
331
+ };
332
+ }
333
+ if (origin.kind !== "repo-blob") throw new Error(`Invalid file workdir node origin kind: ${String(origin.kind)}`);
334
+ if (typeof origin.hash !== "string" || origin.hash.length === 0) throw new Error("Invalid file workdir node: repo-blob origin is missing hash");
335
+ if (origin.mode !== "100644" && origin.mode !== "100755" && origin.mode !== "120000") throw new Error(`Invalid file workdir node origin mode: ${String(origin.mode)}`);
336
+ return {
337
+ kind: "repo-blob",
338
+ mode: origin.mode,
339
+ hash: origin.hash
340
+ };
341
+ }
342
+ function readPayload(contentDir, payloadRef) {
343
+ const path = getContentPath(contentDir, payloadRef);
344
+ if (!existsSync(path)) throw new Error(`Virtual workdir payload not found: ${payloadRef}`);
345
+ return readFileSync(path);
346
+ }
347
+ function readJson(path) {
348
+ return JSON.parse(readFileSync(path, "utf-8"));
349
+ }
350
+ function writeJsonAtomic(path, value) {
351
+ writeBufferAtomic(path, Buffer.from(JSON.stringify(value), "utf-8"));
352
+ }
353
+ function writeBufferAtomic(path, value) {
354
+ mkdirSync(dirname(path), { recursive: true });
355
+ const tempPath = `${path}.tmp`;
356
+ writeFileSync(tempPath, value);
357
+ renameSync(tempPath, path);
358
+ }
359
+ function encodePathToken(value) {
360
+ return encodeURIComponent(value);
361
+ }
362
+ function decodePathToken(value) {
363
+ return decodeURIComponent(value);
364
+ }
365
+ //#endregion
366
+ export { createFileVirtualWorkdirBackend };
@@ -0,0 +1,2 @@
1
+ import { CreateFileVirtualWorkdirBackendOptions, createFileVirtualWorkdirBackend } from "./file-backend.mjs";
2
+ export { type CreateFileVirtualWorkdirBackendOptions, createFileVirtualWorkdirBackend };
@@ -0,0 +1,2 @@
1
+ import { createFileVirtualWorkdirBackend } from "./file-backend.mjs";
2
+ export { createFileVirtualWorkdirBackend };
@@ -0,0 +1,7 @@
1
+ //#region src/workdir/ids.d.ts
2
+ /** 会话内节点的稳定身份 */
3
+ type NodeId = string & {
4
+ readonly __brand: "NodeId";
5
+ };
6
+ //#endregion
7
+ export { NodeId };
@@ -0,0 +1,20 @@
1
+ //#region src/workdir/ids.ts
2
+ /** 根目录节点的固定 ID(每个 session 一致) */
3
+ const VIRTUAL_ROOT_NODE_ID = "root";
4
+ let nextNodeCounter = 1;
5
+ /**
6
+ * 分配新的 session 节点 ID
7
+ */
8
+ function createNodeId() {
9
+ const id = `node:${nextNodeCounter}`;
10
+ nextNodeCounter += 1;
11
+ return id;
12
+ }
13
+ /**
14
+ * 为仓库对象哈希派生稳定的 session 节点 ID(懒加载、未 copy 前复用)
15
+ */
16
+ function originBackedNodeId(hash) {
17
+ return `origin:${hash}`;
18
+ }
19
+ //#endregion
20
+ export { VIRTUAL_ROOT_NODE_ID, createNodeId, originBackedNodeId };
@@ -0,0 +1,17 @@
1
+ import { VirtualWorkdirBackend } from "./core.mjs";
2
+
3
+ //#region src/workdir/memory-backend.d.ts
4
+ /**
5
+ * 创建内存版 Virtual Workdir backend
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const backend = createMemoryVirtualWorkdirBackend();
10
+ * const sessionId = backend.createSession({ baseTree: tree });
11
+ * const session = backend.openSession(repo.objects, sessionId);
12
+ * expect(session.baseTree).toBe(tree);
13
+ * ```
14
+ */
15
+ declare function createMemoryVirtualWorkdirBackend(): VirtualWorkdirBackend;
16
+ //#endregion
17
+ export { createMemoryVirtualWorkdirBackend };