@bendyline/docblocks 1.1.2 → 2.0.0

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 (270) hide show
  1. package/README.md +76 -11
  2. package/dist/chunk-2D7Y4IR3.js +1293 -0
  3. package/dist/chunk-2D7Y4IR3.js.map +1 -0
  4. package/dist/chunk-5MYAICRE.js +957 -0
  5. package/dist/chunk-5MYAICRE.js.map +1 -0
  6. package/dist/chunk-75UUF6C4.js +802 -0
  7. package/dist/chunk-75UUF6C4.js.map +1 -0
  8. package/dist/chunk-AEHCVWU5.js +25 -0
  9. package/dist/chunk-AEHCVWU5.js.map +1 -0
  10. package/dist/chunk-AYZ4HN5M.js +20 -0
  11. package/dist/chunk-AYZ4HN5M.js.map +1 -0
  12. package/dist/chunk-EGNDADXB.js +111 -0
  13. package/dist/chunk-EGNDADXB.js.map +1 -0
  14. package/dist/chunk-EZKJZUNJ.js +13 -0
  15. package/dist/chunk-EZKJZUNJ.js.map +1 -0
  16. package/dist/chunk-FNW556FN.js +75 -0
  17. package/dist/chunk-FNW556FN.js.map +1 -0
  18. package/dist/chunk-GCPBEMMH.js +563 -0
  19. package/dist/chunk-GCPBEMMH.js.map +1 -0
  20. package/dist/chunk-J32GU3F3.js +1664 -0
  21. package/dist/chunk-J32GU3F3.js.map +1 -0
  22. package/dist/chunk-PGR7LK6D.js +1062 -0
  23. package/dist/chunk-PGR7LK6D.js.map +1 -0
  24. package/dist/chunk-RQJRQF46.js +787 -0
  25. package/dist/chunk-RQJRQF46.js.map +1 -0
  26. package/dist/{chunk-ME76RUMR.js → chunk-RVQHY3XU.js} +15 -1
  27. package/dist/chunk-RVQHY3XU.js.map +1 -0
  28. package/dist/chunk-SI2LU2RP.js +224 -0
  29. package/dist/chunk-SI2LU2RP.js.map +1 -0
  30. package/dist/chunk-W5UUUII5.js +142 -0
  31. package/dist/chunk-W5UUUII5.js.map +1 -0
  32. package/dist/document/document-session.d.ts +127 -0
  33. package/dist/document/document-session.d.ts.map +1 -0
  34. package/dist/document/document-session.js +749 -0
  35. package/dist/document/document-session.js.map +1 -0
  36. package/dist/document/filesystem-target.d.ts +17 -0
  37. package/dist/document/filesystem-target.d.ts.map +1 -0
  38. package/dist/document/filesystem-target.js +70 -0
  39. package/dist/document/filesystem-target.js.map +1 -0
  40. package/dist/document/index.d.ts +6 -0
  41. package/dist/document/index.d.ts.map +1 -0
  42. package/dist/document/index.js +4 -0
  43. package/dist/document/index.js.map +1 -0
  44. package/dist/document/recovery-journal.d.ts +125 -0
  45. package/dist/document/recovery-journal.d.ts.map +1 -0
  46. package/dist/document/recovery-journal.js +385 -0
  47. package/dist/document/recovery-journal.js.map +1 -0
  48. package/dist/document/types.d.ts +89 -0
  49. package/dist/document/types.d.ts.map +1 -0
  50. package/dist/document/types.js +9 -0
  51. package/dist/document/types.js.map +1 -0
  52. package/dist/filesystem/commit-lock.d.ts +3 -0
  53. package/dist/filesystem/commit-lock.d.ts.map +1 -0
  54. package/dist/filesystem/commit-lock.js +20 -0
  55. package/dist/filesystem/commit-lock.js.map +1 -0
  56. package/dist/filesystem/dbk-workspace.d.ts +34 -0
  57. package/dist/filesystem/dbk-workspace.d.ts.map +1 -0
  58. package/dist/filesystem/dbk-workspace.js +85 -0
  59. package/dist/filesystem/dbk-workspace.js.map +1 -0
  60. package/dist/filesystem/electron-provider-v2.d.ts +36 -0
  61. package/dist/filesystem/electron-provider-v2.d.ts.map +1 -0
  62. package/dist/filesystem/electron-provider-v2.js +503 -0
  63. package/dist/filesystem/electron-provider-v2.js.map +1 -0
  64. package/dist/filesystem/electron-provider.d.ts +6 -3
  65. package/dist/filesystem/electron-provider.d.ts.map +1 -1
  66. package/dist/filesystem/electron-provider.js +18 -13
  67. package/dist/filesystem/electron-provider.js.map +1 -1
  68. package/dist/filesystem/electron.d.ts +4 -0
  69. package/dist/filesystem/electron.d.ts.map +1 -0
  70. package/dist/filesystem/electron.js +4 -0
  71. package/dist/filesystem/electron.js.map +1 -0
  72. package/dist/filesystem/filesystem-content-container.d.ts +1 -1
  73. package/dist/filesystem/filesystem-content-container.d.ts.map +1 -1
  74. package/dist/filesystem/filesystem-content-container.js +50 -13
  75. package/dist/filesystem/filesystem-content-container.js.map +1 -1
  76. package/dist/filesystem/fs-error.d.ts +46 -0
  77. package/dist/filesystem/fs-error.d.ts.map +1 -0
  78. package/dist/filesystem/fs-error.js +170 -0
  79. package/dist/filesystem/fs-error.js.map +1 -0
  80. package/dist/filesystem/index.d.ts +15 -1
  81. package/dist/filesystem/index.d.ts.map +1 -1
  82. package/dist/filesystem/index.js +13 -0
  83. package/dist/filesystem/index.js.map +1 -1
  84. package/dist/filesystem/indexeddb-content-container.d.ts +3 -1
  85. package/dist/filesystem/indexeddb-content-container.d.ts.map +1 -1
  86. package/dist/filesystem/indexeddb-content-container.js +18 -11
  87. package/dist/filesystem/indexeddb-content-container.js.map +1 -1
  88. package/dist/filesystem/indexeddb-provider-v2.d.ts +96 -0
  89. package/dist/filesystem/indexeddb-provider-v2.d.ts.map +1 -0
  90. package/dist/filesystem/indexeddb-provider-v2.js +1241 -0
  91. package/dist/filesystem/indexeddb-provider-v2.js.map +1 -0
  92. package/dist/filesystem/indexeddb-provider.d.ts +21 -14
  93. package/dist/filesystem/indexeddb-provider.d.ts.map +1 -1
  94. package/dist/filesystem/indexeddb-provider.js +111 -261
  95. package/dist/filesystem/indexeddb-provider.js.map +1 -1
  96. package/dist/filesystem/indexeddb-store.d.ts +34 -0
  97. package/dist/filesystem/indexeddb-store.d.ts.map +1 -0
  98. package/dist/filesystem/indexeddb-store.js +165 -0
  99. package/dist/filesystem/indexeddb-store.js.map +1 -0
  100. package/dist/filesystem/indexeddb.d.ts +5 -0
  101. package/dist/filesystem/indexeddb.d.ts.map +1 -0
  102. package/dist/filesystem/indexeddb.js +5 -0
  103. package/dist/filesystem/indexeddb.js.map +1 -0
  104. package/dist/filesystem/legacy-v2-adapter.d.ts +62 -0
  105. package/dist/filesystem/legacy-v2-adapter.d.ts.map +1 -0
  106. package/dist/filesystem/legacy-v2-adapter.js +499 -0
  107. package/dist/filesystem/legacy-v2-adapter.js.map +1 -0
  108. package/dist/filesystem/memory-provider-v2.d.ts +92 -0
  109. package/dist/filesystem/memory-provider-v2.d.ts.map +1 -0
  110. package/dist/filesystem/memory-provider-v2.js +615 -0
  111. package/dist/filesystem/memory-provider-v2.js.map +1 -0
  112. package/dist/filesystem/memory-provider.d.ts +54 -0
  113. package/dist/filesystem/memory-provider.d.ts.map +1 -0
  114. package/dist/filesystem/memory-provider.js +132 -0
  115. package/dist/filesystem/memory-provider.js.map +1 -0
  116. package/dist/filesystem/memory.d.ts +4 -0
  117. package/dist/filesystem/memory.d.ts.map +1 -0
  118. package/dist/filesystem/memory.js +4 -0
  119. package/dist/filesystem/memory.js.map +1 -0
  120. package/dist/filesystem/move-entry.d.ts +9 -0
  121. package/dist/filesystem/move-entry.d.ts.map +1 -0
  122. package/dist/filesystem/move-entry.js +103 -0
  123. package/dist/filesystem/move-entry.js.map +1 -0
  124. package/dist/filesystem/move-error.d.ts +53 -0
  125. package/dist/filesystem/move-error.d.ts.map +1 -0
  126. package/dist/filesystem/move-error.js +99 -0
  127. package/dist/filesystem/move-error.js.map +1 -0
  128. package/dist/filesystem/native-provider-v2.d.ts +75 -0
  129. package/dist/filesystem/native-provider-v2.d.ts.map +1 -0
  130. package/dist/filesystem/native-provider-v2.js +849 -0
  131. package/dist/filesystem/native-provider-v2.js.map +1 -0
  132. package/dist/filesystem/native-provider.d.ts +9 -1
  133. package/dist/filesystem/native-provider.d.ts.map +1 -1
  134. package/dist/filesystem/native-provider.js +172 -37
  135. package/dist/filesystem/native-provider.js.map +1 -1
  136. package/dist/filesystem/native.d.ts +4 -0
  137. package/dist/filesystem/native.d.ts.map +1 -0
  138. package/dist/filesystem/native.js +4 -0
  139. package/dist/filesystem/native.js.map +1 -0
  140. package/dist/filesystem/types.d.ts +37 -4
  141. package/dist/filesystem/types.d.ts.map +1 -1
  142. package/dist/filesystem/types.js +11 -3
  143. package/dist/filesystem/types.js.map +1 -1
  144. package/dist/filesystem/v2.d.ts +172 -0
  145. package/dist/filesystem/v2.d.ts.map +1 -0
  146. package/dist/filesystem/v2.js +16 -0
  147. package/dist/filesystem/v2.js.map +1 -0
  148. package/dist/filesystem/workspace-path.d.ts +21 -0
  149. package/dist/filesystem/workspace-path.d.ts.map +1 -0
  150. package/dist/filesystem/workspace-path.js +80 -0
  151. package/dist/filesystem/workspace-path.js.map +1 -0
  152. package/dist/host/filesystem-v2.d.ts +45 -0
  153. package/dist/host/filesystem-v2.d.ts.map +1 -0
  154. package/dist/host/filesystem-v2.js +13 -0
  155. package/dist/host/filesystem-v2.js.map +1 -0
  156. package/dist/host/git.d.ts +200 -0
  157. package/dist/host/git.d.ts.map +1 -0
  158. package/dist/host/git.js +15 -0
  159. package/dist/host/git.js.map +1 -0
  160. package/dist/host/index.d.ts +4 -1
  161. package/dist/host/index.d.ts.map +1 -1
  162. package/dist/host/index.js +2 -0
  163. package/dist/host/index.js.map +1 -1
  164. package/dist/host/types.d.ts +114 -31
  165. package/dist/host/types.d.ts.map +1 -1
  166. package/dist/host/wire-policy.d.ts +22 -0
  167. package/dist/host/wire-policy.d.ts.map +1 -0
  168. package/dist/host/wire-policy.js +92 -0
  169. package/dist/host/wire-policy.js.map +1 -0
  170. package/dist/index.d.ts +2 -1
  171. package/dist/index.d.ts.map +1 -1
  172. package/dist/index.js +2 -1
  173. package/dist/index.js.map +1 -1
  174. package/dist/mcp/index.d.ts +4 -0
  175. package/dist/mcp/index.d.ts.map +1 -0
  176. package/dist/mcp/index.js +4 -0
  177. package/dist/mcp/index.js.map +1 -0
  178. package/dist/mcp/types.d.ts +466 -0
  179. package/dist/mcp/types.d.ts.map +1 -0
  180. package/dist/mcp/types.js +25 -0
  181. package/dist/mcp/types.js.map +1 -0
  182. package/dist/mcp/wire.d.ts +29 -0
  183. package/dist/mcp/wire.d.ts.map +1 -0
  184. package/dist/mcp/wire.js +1046 -0
  185. package/dist/mcp/wire.js.map +1 -0
  186. package/dist/mcp/zod.d.ts +5763 -0
  187. package/dist/mcp/zod.d.ts.map +1 -0
  188. package/dist/mcp/zod.js +908 -0
  189. package/dist/mcp/zod.js.map +1 -0
  190. package/dist/types-CX893hgH.d.ts +468 -0
  191. package/dist/types-CsX6EN2t.d.ts +301 -0
  192. package/dist/types-eOlt9J7n.d.ts +478 -0
  193. package/dist/vscode/index.d.ts +3 -0
  194. package/dist/vscode/index.d.ts.map +1 -0
  195. package/dist/vscode/index.js +3 -0
  196. package/dist/vscode/index.js.map +1 -0
  197. package/dist/vscode/messages.d.ts +204 -0
  198. package/dist/vscode/messages.d.ts.map +1 -0
  199. package/dist/vscode/messages.js +494 -0
  200. package/dist/vscode/messages.js.map +1 -0
  201. package/dist/workspace/index.d.ts +3 -2
  202. package/dist/workspace/index.d.ts.map +1 -1
  203. package/dist/workspace/index.js +2 -1
  204. package/dist/workspace/index.js.map +1 -1
  205. package/dist/workspace/reconcile-electron.d.ts +19 -0
  206. package/dist/workspace/reconcile-electron.d.ts.map +1 -0
  207. package/dist/workspace/reconcile-electron.js +42 -0
  208. package/dist/workspace/reconcile-electron.js.map +1 -0
  209. package/dist/workspace/types.d.ts +41 -1
  210. package/dist/workspace/types.d.ts.map +1 -1
  211. package/dist/workspace/types.js +0 -4
  212. package/dist/workspace/types.js.map +1 -1
  213. package/dist/workspace/workspace-manager.d.ts +17 -5
  214. package/dist/workspace/workspace-manager.d.ts.map +1 -1
  215. package/dist/workspace/workspace-manager.js +55 -11
  216. package/dist/workspace/workspace-manager.js.map +1 -1
  217. package/dist/workspace-path-CWVrcuPL.d.ts +21 -0
  218. package/package.json +43 -2
  219. package/src/document/document-session.ts +834 -0
  220. package/src/document/filesystem-target.ts +109 -0
  221. package/src/document/index.ts +41 -0
  222. package/src/document/recovery-journal.ts +536 -0
  223. package/src/document/types.ts +116 -0
  224. package/src/filesystem/commit-lock.ts +29 -0
  225. package/src/filesystem/dbk-workspace.ts +135 -0
  226. package/src/filesystem/electron-provider-v2.ts +630 -0
  227. package/src/filesystem/electron-provider.ts +25 -14
  228. package/src/filesystem/electron.ts +3 -0
  229. package/src/filesystem/filesystem-content-container.ts +53 -15
  230. package/src/filesystem/fs-error.ts +234 -0
  231. package/src/filesystem/index.ts +119 -0
  232. package/src/filesystem/indexeddb-content-container.ts +20 -12
  233. package/src/filesystem/indexeddb-provider-v2.ts +1740 -0
  234. package/src/filesystem/indexeddb-provider.ts +126 -300
  235. package/src/filesystem/indexeddb-store.ts +207 -0
  236. package/src/filesystem/indexeddb.ts +12 -0
  237. package/src/filesystem/legacy-v2-adapter.ts +779 -0
  238. package/src/filesystem/memory-provider-v2.ts +896 -0
  239. package/src/filesystem/memory-provider.ts +180 -0
  240. package/src/filesystem/memory.ts +15 -0
  241. package/src/filesystem/move-entry.ts +142 -0
  242. package/src/filesystem/move-error.ts +159 -0
  243. package/src/filesystem/native-provider-v2.ts +1165 -0
  244. package/src/filesystem/native-provider.ts +194 -41
  245. package/src/filesystem/native.ts +16 -0
  246. package/src/filesystem/types.ts +49 -4
  247. package/src/filesystem/v2.ts +231 -0
  248. package/src/filesystem/workspace-path.ts +92 -0
  249. package/src/host/filesystem-v2.ts +104 -0
  250. package/src/host/git.ts +246 -0
  251. package/src/host/index.ts +46 -0
  252. package/src/host/types.ts +155 -34
  253. package/src/host/wire-policy.ts +103 -0
  254. package/src/index.ts +2 -1
  255. package/src/mcp/index.ts +3 -0
  256. package/src/mcp/types.ts +594 -0
  257. package/src/mcp/wire.ts +1280 -0
  258. package/src/mcp/zod.ts +1000 -0
  259. package/src/vscode/index.ts +2 -0
  260. package/src/vscode/messages.ts +704 -0
  261. package/src/workspace/index.ts +8 -1
  262. package/src/workspace/reconcile-electron.ts +65 -0
  263. package/src/workspace/types.ts +39 -1
  264. package/src/workspace/workspace-manager.ts +71 -10
  265. package/dist/chunk-GUNM43XZ.js +0 -63
  266. package/dist/chunk-GUNM43XZ.js.map +0 -1
  267. package/dist/chunk-IM7LCQJ6.js +0 -850
  268. package/dist/chunk-IM7LCQJ6.js.map +0 -1
  269. package/dist/chunk-ME76RUMR.js.map +0 -1
  270. package/dist/index-pgBHBQ9G.d.ts +0 -213
@@ -6,12 +6,18 @@
6
6
  * the API (Chrome, Edge). Feature-detect with `isNativeFileSystemSupported()`.
7
7
  */
8
8
 
9
- import type { FileSystemProvider, FileSystemEntry, FileMeta } from './types.js';
9
+ import type { FileCommitResult, FileSystemProvider, FileSystemEntry, FileMeta } from './types.js';
10
+ import { withFileCommitLock } from './commit-lock.js';
11
+ import { NativeFileSystemProviderV2 } from './native-provider-v2.js';
12
+ import { parseWorkspacePath } from './workspace-path.js';
10
13
 
11
14
  // ── Feature detection ──────────────────────────────────────────────
12
15
 
13
16
  export function isNativeFileSystemSupported(): boolean {
14
- return typeof globalThis !== 'undefined' && 'showDirectoryPicker' in globalThis;
17
+ return (
18
+ typeof globalThis !== 'undefined' &&
19
+ typeof (globalThis as { showDirectoryPicker?: unknown }).showDirectoryPicker === 'function'
20
+ );
15
21
  }
16
22
 
17
23
  // ── Handle persistence (IndexedDB, structured clone) ───────────────
@@ -89,7 +95,33 @@ export async function removeDirectoryHandle(workspaceId: string): Promise<void>
89
95
  // ── Helpers ────────────────────────────────────────────────────────
90
96
 
91
97
  function normalisePath(p: string): string {
92
- return p.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\//, '').replace(/\/$/, '');
98
+ return parseWorkspacePath(p);
99
+ }
100
+
101
+ function errorName(error: unknown): string | undefined {
102
+ return typeof error === 'object' && error !== null && 'name' in error
103
+ ? String((error as { name?: unknown }).name)
104
+ : undefined;
105
+ }
106
+
107
+ function isMissingEntryError(error: unknown): boolean {
108
+ const name = errorName(error);
109
+ return name === 'NotFoundError' || name === 'TypeMismatchError';
110
+ }
111
+
112
+ function isPermissionError(error: unknown): boolean {
113
+ const name = errorName(error);
114
+ return name === 'NotAllowedError' || name === 'SecurityError';
115
+ }
116
+
117
+ class NativeMoveRecoveryError extends Error {
118
+ readonly causes: readonly unknown[];
119
+
120
+ constructor(message: string, causes: readonly unknown[]) {
121
+ super(message);
122
+ this.name = 'NativeMoveRecoveryError';
123
+ this.causes = causes;
124
+ }
93
125
  }
94
126
 
95
127
  /**
@@ -106,8 +138,9 @@ async function resolveDir(
106
138
  for (const part of parts) {
107
139
  try {
108
140
  current = await current.getDirectoryHandle(part);
109
- } catch {
110
- return null;
141
+ } catch (error: unknown) {
142
+ if (isMissingEntryError(error)) return null;
143
+ throw error;
111
144
  }
112
145
  }
113
146
  return current;
@@ -146,11 +179,25 @@ function toWritableBinary(data: ArrayBuffer | Uint8Array): ArrayBuffer {
146
179
  return copy.buffer;
147
180
  }
148
181
 
182
+ async function writeAndClose(
183
+ writable: FileSystemWritableFileStream,
184
+ data: string | ArrayBuffer,
185
+ ): Promise<void> {
186
+ try {
187
+ await writable.write(data);
188
+ await writable.close();
189
+ } catch (error: unknown) {
190
+ await writable.abort(error).catch(() => undefined);
191
+ throw error;
192
+ }
193
+ }
194
+
149
195
  // ── Implementation ─────────────────────────────────────────────────
150
196
 
151
197
  export class NativeFileSystemProvider implements FileSystemProvider {
152
198
  readonly id: string;
153
199
  readonly label: string;
200
+ readonly v2: NativeFileSystemProviderV2;
154
201
 
155
202
  private root: FileSystemDirectoryHandle;
156
203
 
@@ -158,11 +205,17 @@ export class NativeFileSystemProvider implements FileSystemProvider {
158
205
  this.id = id;
159
206
  this.label = root.name;
160
207
  this.root = root;
208
+ this.v2 = new NativeFileSystemProviderV2(id, root);
209
+ }
210
+
211
+ private withMutationLock<T>(operation: () => Promise<T>): Promise<T> {
212
+ return withFileCommitLock(`native:${this.id}:workspace`, operation);
161
213
  }
162
214
 
163
- private async copyDirectory(oldDirPath: string, newDirPath: string): Promise<boolean> {
215
+ private async copyDirectory(oldDirPath: string, newDirPath: string): Promise<void> {
164
216
  const source = await resolveDir(this.root, oldDirPath);
165
- if (!source) return false;
217
+ if (!source)
218
+ throw new DOMException(`Source directory does not exist: ${oldDirPath}`, 'NotFoundError');
166
219
  await resolveDirCreate(this.root, newDirPath);
167
220
 
168
221
  for await (const [name, handle] of source as unknown as AsyncIterable<
@@ -175,11 +228,9 @@ export class NativeFileSystemProvider implements FileSystemProvider {
175
228
  } else {
176
229
  const fileHandle = await source.getFileHandle(name);
177
230
  const file = await fileHandle.getFile();
178
- await this.writeBinary(newChild, await file.arrayBuffer());
231
+ await this.writeBinaryUnlocked(newChild, await file.arrayBuffer());
179
232
  }
180
233
  }
181
-
182
- return true;
183
234
  }
184
235
 
185
236
  async readFile(path: string): Promise<string | null> {
@@ -190,30 +241,70 @@ export class NativeFileSystemProvider implements FileSystemProvider {
190
241
  const fileHandle = await dir.getFileHandle(baseName(p));
191
242
  const file = await fileHandle.getFile();
192
243
  return file.text();
193
- } catch {
194
- return null;
244
+ } catch (error: unknown) {
245
+ if (isMissingEntryError(error)) return null;
246
+ throw error;
195
247
  }
196
248
  }
197
249
 
198
250
  async writeFile(path: string, content: string): Promise<void> {
251
+ return this.withMutationLock(() => this.writeFileUnlocked(path, content));
252
+ }
253
+
254
+ private async writeFileUnlocked(path: string, content: string): Promise<void> {
199
255
  const p = normalisePath(path);
200
256
  const dir = await resolveDirCreate(this.root, parentDir(p));
201
257
  const fileHandle = await dir.getFileHandle(baseName(p), { create: true });
202
258
  const writable = await fileHandle.createWritable();
203
- await writable.write(content);
204
- await writable.close();
259
+ await writeAndClose(writable, content);
260
+ }
261
+
262
+ async commitFile(
263
+ path: string,
264
+ content: string,
265
+ expectedContent: string | null,
266
+ ): Promise<FileCommitResult> {
267
+ const p = normalisePath(path);
268
+ return this.withMutationLock(async () => {
269
+ const current = await this.readFile(p);
270
+ if (current !== expectedContent && current !== content) {
271
+ const meta = await this.stat(p);
272
+ return {
273
+ status: 'conflict',
274
+ content: current,
275
+ version: meta?.lastModified ?? null,
276
+ };
277
+ }
278
+ await this.writeFileUnlocked(p, content);
279
+ const meta = await this.stat(p);
280
+ return { status: 'committed', version: meta?.lastModified ?? null };
281
+ });
205
282
  }
206
283
 
207
284
  async delete(path: string): Promise<void> {
285
+ return this.withMutationLock(() => this.deleteUnlocked(path));
286
+ }
287
+
288
+ private async deleteUnlocked(path: string): Promise<void> {
208
289
  const p = normalisePath(path);
290
+ if (!p) throw new Error('Cannot delete the filesystem root');
209
291
  const parent = parentDir(p);
210
292
  const name = baseName(p);
211
293
  const dir = await resolveDir(this.root, parent);
212
294
  if (!dir) return;
213
- await dir.removeEntry(name, { recursive: true });
295
+ try {
296
+ await dir.removeEntry(name, { recursive: true });
297
+ } catch (error: unknown) {
298
+ if (isMissingEntryError(error)) return;
299
+ throw error;
300
+ }
214
301
  }
215
302
 
216
303
  async rename(oldPath: string, newPath: string): Promise<void> {
304
+ return this.withMutationLock(() => this.renameUnlocked(oldPath, newPath));
305
+ }
306
+
307
+ private async renameUnlocked(oldPath: string, newPath: string): Promise<void> {
217
308
  const op = normalisePath(oldPath);
218
309
  const np = normalisePath(newPath);
219
310
  if (op === np) return;
@@ -223,25 +314,72 @@ export class NativeFileSystemProvider implements FileSystemProvider {
223
314
  if (np.startsWith(op + '/')) {
224
315
  throw new Error('Cannot move a directory into itself');
225
316
  }
317
+ if (!(await this.exists(op))) {
318
+ throw new Error(`Source does not exist: ${oldPath}`);
319
+ }
320
+ if (await this.exists(np)) {
321
+ throw new Error(`Destination exists: ${newPath}`);
322
+ }
226
323
 
227
324
  // The File System Access API doesn't have a native rename.
228
325
  // Copy → delete.
229
326
  const oldParent = await resolveDir(this.root, parentDir(op));
230
- if (!oldParent) return;
327
+ if (!oldParent) {
328
+ throw new DOMException(`Source does not exist: ${oldPath}`, 'NotFoundError');
329
+ }
231
330
 
331
+ let fileHandle: FileSystemFileHandle | null = null;
232
332
  try {
233
- const fileHandle = await oldParent.getFileHandle(baseName(op));
333
+ fileHandle = await oldParent.getFileHandle(baseName(op));
334
+ } catch (error: unknown) {
335
+ if (!isMissingEntryError(error)) throw error;
336
+ }
337
+
338
+ if (fileHandle) {
234
339
  const file = await fileHandle.getFile();
235
- await this.writeBinary(np, await file.arrayBuffer());
236
- await this.delete(op);
340
+ await this.writeBinaryUnlocked(np, await file.arrayBuffer());
341
+ try {
342
+ await this.deleteUnlocked(op);
343
+ } catch (error: unknown) {
344
+ try {
345
+ await this.deleteUnlocked(np);
346
+ } catch (rollbackError: unknown) {
347
+ throw new NativeMoveRecoveryError(
348
+ `Move partially completed and rollback failed: ${oldPath} -> ${newPath}`,
349
+ [error, rollbackError],
350
+ );
351
+ }
352
+ throw error;
353
+ }
237
354
  return;
238
- } catch {
239
- // Not a file; try directory below.
240
355
  }
241
356
 
242
- const copied = await this.copyDirectory(op, np);
243
- if (copied) {
244
- await this.delete(op);
357
+ try {
358
+ await this.copyDirectory(op, np);
359
+ } catch (error: unknown) {
360
+ try {
361
+ await this.deleteUnlocked(np);
362
+ } catch (rollbackError: unknown) {
363
+ throw new NativeMoveRecoveryError(
364
+ `Move failed and destination cleanup failed: ${oldPath} -> ${newPath}`,
365
+ [error, rollbackError],
366
+ );
367
+ }
368
+ throw error;
369
+ }
370
+
371
+ try {
372
+ await this.deleteUnlocked(op);
373
+ } catch (error: unknown) {
374
+ try {
375
+ await this.deleteUnlocked(np);
376
+ } catch (rollbackError: unknown) {
377
+ throw new NativeMoveRecoveryError(
378
+ `Move partially completed and rollback failed: ${oldPath} -> ${newPath}`,
379
+ [error, rollbackError],
380
+ );
381
+ }
382
+ throw error;
245
383
  }
246
384
  }
247
385
 
@@ -281,19 +419,23 @@ export class NativeFileSystemProvider implements FileSystemProvider {
281
419
  try {
282
420
  await dir.getFileHandle(name);
283
421
  return true;
284
- } catch {
422
+ } catch (fileError: unknown) {
423
+ if (!isMissingEntryError(fileError)) throw fileError;
285
424
  try {
286
425
  await dir.getDirectoryHandle(name);
287
426
  return true;
288
- } catch {
427
+ } catch (directoryError: unknown) {
428
+ if (!isMissingEntryError(directoryError)) throw directoryError;
289
429
  return false;
290
430
  }
291
431
  }
292
432
  }
293
433
 
294
434
  async createDirectory(path: string): Promise<void> {
295
- const p = normalisePath(path);
296
- await resolveDirCreate(this.root, p);
435
+ return this.withMutationLock(async () => {
436
+ const p = normalisePath(path);
437
+ await resolveDirCreate(this.root, p);
438
+ });
297
439
  }
298
440
 
299
441
  async stat(path: string): Promise<FileMeta | null> {
@@ -310,8 +452,9 @@ export class NativeFileSystemProvider implements FileSystemProvider {
310
452
  size: file.size,
311
453
  lastModified: new Date(file.lastModified).toISOString(),
312
454
  };
313
- } catch {
314
- return null;
455
+ } catch (error: unknown) {
456
+ if (isMissingEntryError(error)) return null;
457
+ throw error;
315
458
  }
316
459
  }
317
460
 
@@ -323,18 +466,22 @@ export class NativeFileSystemProvider implements FileSystemProvider {
323
466
  const fileHandle = await dir.getFileHandle(baseName(p));
324
467
  const file = await fileHandle.getFile();
325
468
  return file.arrayBuffer();
326
- } catch {
327
- return null;
469
+ } catch (error: unknown) {
470
+ if (isMissingEntryError(error)) return null;
471
+ throw error;
328
472
  }
329
473
  }
330
474
 
331
475
  async writeBinary(path: string, data: ArrayBuffer | Uint8Array): Promise<void> {
476
+ return this.withMutationLock(() => this.writeBinaryUnlocked(path, data));
477
+ }
478
+
479
+ private async writeBinaryUnlocked(path: string, data: ArrayBuffer | Uint8Array): Promise<void> {
332
480
  const p = normalisePath(path);
333
481
  const dir = await resolveDirCreate(this.root, parentDir(p));
334
482
  const fileHandle = await dir.getFileHandle(baseName(p), { create: true });
335
483
  const writable = await fileHandle.createWritable();
336
- await writable.write(toWritableBinary(data));
337
- await writable.close();
484
+ await writeAndClose(writable, toWritableBinary(data));
338
485
  }
339
486
  }
340
487
 
@@ -349,8 +496,10 @@ export async function openNativeFolder(): Promise<NativeFileSystemProvider> {
349
496
  }
350
497
 
351
498
  const handle = await (
352
- globalThis as unknown as { showDirectoryPicker: () => Promise<FileSystemDirectoryHandle> }
353
- ).showDirectoryPicker();
499
+ globalThis as unknown as {
500
+ showDirectoryPicker: (options: { mode: 'readwrite' }) => Promise<FileSystemDirectoryHandle>;
501
+ }
502
+ ).showDirectoryPicker({ mode: 'readwrite' });
354
503
  const id = `native-${handle.name}-${Date.now()}`;
355
504
  await storeDirectoryHandle(id, handle);
356
505
  return new NativeFileSystemProvider(id, handle);
@@ -373,11 +522,15 @@ export async function restoreNativeFolder(
373
522
  queryPermission(desc: { mode: string }): Promise<string>;
374
523
  requestPermission(desc: { mode: string }): Promise<string>;
375
524
  };
376
- if ((await h.queryPermission(opts)) === 'granted') {
377
- return new NativeFileSystemProvider(workspaceId, handle);
378
- }
379
- if ((await h.requestPermission(opts)) === 'granted') {
380
- return new NativeFileSystemProvider(workspaceId, handle);
525
+ try {
526
+ if ((await h.queryPermission(opts)) === 'granted') {
527
+ return new NativeFileSystemProvider(workspaceId, handle);
528
+ }
529
+ if ((await h.requestPermission(opts)) === 'granted') {
530
+ return new NativeFileSystemProvider(workspaceId, handle);
531
+ }
532
+ } catch (error: unknown) {
533
+ if (!isPermissionError(error)) throw error;
381
534
  }
382
535
 
383
536
  return null;
@@ -0,0 +1,16 @@
1
+ /** Browser File System Access API provider entry point. */
2
+ export {
3
+ NativeFileSystemProvider,
4
+ isNativeFileSystemSupported,
5
+ openNativeFolder,
6
+ restoreNativeFolder,
7
+ storeDirectoryHandle,
8
+ loadDirectoryHandle,
9
+ removeDirectoryHandle,
10
+ } from './native-provider.js';
11
+ export {
12
+ NativeFileSystemProviderV2,
13
+ NativeFileSystemMoveRecoveryError,
14
+ type NativeFileSystemMovePathState,
15
+ type NativeFileSystemMoveRecoveryState,
16
+ } from './native-provider-v2.js';
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * FileSystemProvider — abstract interface for a virtual filesystem.
3
3
  *
4
- * Implementations back onto IndexedDB (for browser-local storage) or
5
- * the File System Access API (for native folder access).
4
+ * Legacy text-oriented facade retained while consumers migrate to
5
+ * FileSystemProviderV2. New persistence code must use the provider's `v2`
6
+ * contract so paths, errors, operation modes, and capabilities are explicit.
6
7
  */
7
8
 
8
9
  // ── Entry types ────────────────────────────────────────────────────
9
10
 
11
+ import type { FileSystemProviderV2 } from './v2.js';
12
+
10
13
  export interface FileEntry {
11
14
  kind: 'file';
12
15
  name: string;
@@ -28,8 +31,17 @@ export interface FileMeta {
28
31
  lastModified: string;
29
32
  }
30
33
 
34
+ export type FileCommitResult =
35
+ | { status: 'committed'; version: string | null }
36
+ | { status: 'conflict'; content: string | null; version: string | null };
37
+
31
38
  // ── Provider interface ─────────────────────────────────────────────
32
39
 
40
+ /**
41
+ * @deprecated Compatibility facade for external providers and older callers.
42
+ * Implement new backends with `FileSystemProviderV2`; built-in facades expose
43
+ * their authoritative implementation through `v2`.
44
+ */
33
45
  export interface FileSystemProvider {
34
46
  /** Unique identifier for this provider instance. */
35
47
  readonly id: string;
@@ -37,16 +49,33 @@ export interface FileSystemProvider {
37
49
  /** Human-readable label (e.g., folder name or "Browser Storage"). */
38
50
  readonly label: string;
39
51
 
52
+ /** Optional correctness-first implementation exposed during the v1-to-v2 migration. */
53
+ readonly v2?: FileSystemProviderV2;
54
+
40
55
  /** Read the text content of a file. Returns null if the file doesn't exist. */
41
56
  readFile(path: string): Promise<string | null>;
42
57
 
43
58
  /** Write text content to a file, creating it (and parent dirs) if needed. */
44
59
  writeFile(path: string, content: string): Promise<void>;
45
60
 
46
- /** Delete a file or empty directory. */
61
+ /**
62
+ * Conditionally replace a text file when it still matches the caller's
63
+ * persisted baseline. Production providers implement this at their
64
+ * strongest available serialization boundary.
65
+ */
66
+ commitFile?(
67
+ path: string,
68
+ content: string,
69
+ expectedContent: string | null,
70
+ ): Promise<FileCommitResult>;
71
+
72
+ /**
73
+ * Legacy recursive removal of a file or directory subtree. Workspace root
74
+ * deletion is forbidden. Prefer v2 `remove({ recursive })`.
75
+ */
47
76
  delete(path: string): Promise<void>;
48
77
 
49
- /** Rename or move an entry. */
78
+ /** Rename or move an entry. Rejects missing sources and existing destinations. */
50
79
  rename(oldPath: string, newPath: string): Promise<void>;
51
80
 
52
81
  /** List immediate children of a directory. */
@@ -67,3 +96,19 @@ export interface FileSystemProvider {
67
96
  /** Write raw binary content. */
68
97
  writeBinary(path: string, data: ArrayBuffer | Uint8Array): Promise<void>;
69
98
  }
99
+
100
+ export type FileSystemProviderWithV2 = FileSystemProvider & {
101
+ readonly v2: FileSystemProviderV2;
102
+ };
103
+
104
+ /** Discover a provider-owned v2 implementation without coupling consumers to a concrete class. */
105
+ export function hasFileSystemProviderV2(
106
+ provider: FileSystemProvider,
107
+ ): provider is FileSystemProviderWithV2 {
108
+ return provider.v2 !== undefined && provider.v2 !== null;
109
+ }
110
+
111
+ /** Return the provider-owned v2 implementation when migration support is available. */
112
+ export function getFileSystemProviderV2(provider: FileSystemProvider): FileSystemProviderV2 | null {
113
+ return provider.v2 ?? null;
114
+ }