@bendyline/docblocks 1.1.1 → 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 +108 -17
  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 +179 -42
  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 +118 -35
  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 +201 -45
  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-LORJUBON.js +0 -848
  268. package/dist/chunk-LORJUBON.js.map +0 -1
  269. package/dist/chunk-ME76RUMR.js.map +0 -1
  270. package/dist/index-Bunj8Kb_.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;
@@ -139,11 +172,32 @@ function baseName(p: string): string {
139
172
  return idx === -1 ? p : p.slice(idx + 1);
140
173
  }
141
174
 
175
+ function toWritableBinary(data: ArrayBuffer | Uint8Array): ArrayBuffer {
176
+ if (data instanceof ArrayBuffer) return data;
177
+ const copy = new Uint8Array(data.byteLength);
178
+ copy.set(data);
179
+ return copy.buffer;
180
+ }
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
+
142
195
  // ── Implementation ─────────────────────────────────────────────────
143
196
 
144
197
  export class NativeFileSystemProvider implements FileSystemProvider {
145
198
  readonly id: string;
146
199
  readonly label: string;
200
+ readonly v2: NativeFileSystemProviderV2;
147
201
 
148
202
  private root: FileSystemDirectoryHandle;
149
203
 
@@ -151,11 +205,17 @@ export class NativeFileSystemProvider implements FileSystemProvider {
151
205
  this.id = id;
152
206
  this.label = root.name;
153
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);
154
213
  }
155
214
 
156
- private async copyDirectory(oldDirPath: string, newDirPath: string): Promise<boolean> {
215
+ private async copyDirectory(oldDirPath: string, newDirPath: string): Promise<void> {
157
216
  const source = await resolveDir(this.root, oldDirPath);
158
- if (!source) return false;
217
+ if (!source)
218
+ throw new DOMException(`Source directory does not exist: ${oldDirPath}`, 'NotFoundError');
159
219
  await resolveDirCreate(this.root, newDirPath);
160
220
 
161
221
  for await (const [name, handle] of source as unknown as AsyncIterable<
@@ -168,11 +228,9 @@ export class NativeFileSystemProvider implements FileSystemProvider {
168
228
  } else {
169
229
  const fileHandle = await source.getFileHandle(name);
170
230
  const file = await fileHandle.getFile();
171
- await this.writeBinary(newChild, await file.arrayBuffer());
231
+ await this.writeBinaryUnlocked(newChild, await file.arrayBuffer());
172
232
  }
173
233
  }
174
-
175
- return true;
176
234
  }
177
235
 
178
236
  async readFile(path: string): Promise<string | null> {
@@ -183,30 +241,70 @@ export class NativeFileSystemProvider implements FileSystemProvider {
183
241
  const fileHandle = await dir.getFileHandle(baseName(p));
184
242
  const file = await fileHandle.getFile();
185
243
  return file.text();
186
- } catch {
187
- return null;
244
+ } catch (error: unknown) {
245
+ if (isMissingEntryError(error)) return null;
246
+ throw error;
188
247
  }
189
248
  }
190
249
 
191
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> {
192
255
  const p = normalisePath(path);
193
256
  const dir = await resolveDirCreate(this.root, parentDir(p));
194
257
  const fileHandle = await dir.getFileHandle(baseName(p), { create: true });
195
258
  const writable = await fileHandle.createWritable();
196
- await writable.write(content);
197
- 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
+ });
198
282
  }
199
283
 
200
284
  async delete(path: string): Promise<void> {
285
+ return this.withMutationLock(() => this.deleteUnlocked(path));
286
+ }
287
+
288
+ private async deleteUnlocked(path: string): Promise<void> {
201
289
  const p = normalisePath(path);
290
+ if (!p) throw new Error('Cannot delete the filesystem root');
202
291
  const parent = parentDir(p);
203
292
  const name = baseName(p);
204
293
  const dir = await resolveDir(this.root, parent);
205
294
  if (!dir) return;
206
- 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
+ }
207
301
  }
208
302
 
209
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> {
210
308
  const op = normalisePath(oldPath);
211
309
  const np = normalisePath(newPath);
212
310
  if (op === np) return;
@@ -216,25 +314,72 @@ export class NativeFileSystemProvider implements FileSystemProvider {
216
314
  if (np.startsWith(op + '/')) {
217
315
  throw new Error('Cannot move a directory into itself');
218
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
+ }
219
323
 
220
324
  // The File System Access API doesn't have a native rename.
221
325
  // Copy → delete.
222
326
  const oldParent = await resolveDir(this.root, parentDir(op));
223
- if (!oldParent) return;
327
+ if (!oldParent) {
328
+ throw new DOMException(`Source does not exist: ${oldPath}`, 'NotFoundError');
329
+ }
224
330
 
331
+ let fileHandle: FileSystemFileHandle | null = null;
225
332
  try {
226
- 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) {
227
339
  const file = await fileHandle.getFile();
228
- await this.writeBinary(np, await file.arrayBuffer());
229
- 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
+ }
230
354
  return;
231
- } catch {
232
- // Not a file; try directory below.
233
355
  }
234
356
 
235
- const copied = await this.copyDirectory(op, np);
236
- if (copied) {
237
- 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;
238
383
  }
239
384
  }
240
385
 
@@ -274,19 +419,23 @@ export class NativeFileSystemProvider implements FileSystemProvider {
274
419
  try {
275
420
  await dir.getFileHandle(name);
276
421
  return true;
277
- } catch {
422
+ } catch (fileError: unknown) {
423
+ if (!isMissingEntryError(fileError)) throw fileError;
278
424
  try {
279
425
  await dir.getDirectoryHandle(name);
280
426
  return true;
281
- } catch {
427
+ } catch (directoryError: unknown) {
428
+ if (!isMissingEntryError(directoryError)) throw directoryError;
282
429
  return false;
283
430
  }
284
431
  }
285
432
  }
286
433
 
287
434
  async createDirectory(path: string): Promise<void> {
288
- const p = normalisePath(path);
289
- await resolveDirCreate(this.root, p);
435
+ return this.withMutationLock(async () => {
436
+ const p = normalisePath(path);
437
+ await resolveDirCreate(this.root, p);
438
+ });
290
439
  }
291
440
 
292
441
  async stat(path: string): Promise<FileMeta | null> {
@@ -303,8 +452,9 @@ export class NativeFileSystemProvider implements FileSystemProvider {
303
452
  size: file.size,
304
453
  lastModified: new Date(file.lastModified).toISOString(),
305
454
  };
306
- } catch {
307
- return null;
455
+ } catch (error: unknown) {
456
+ if (isMissingEntryError(error)) return null;
457
+ throw error;
308
458
  }
309
459
  }
310
460
 
@@ -316,22 +466,22 @@ export class NativeFileSystemProvider implements FileSystemProvider {
316
466
  const fileHandle = await dir.getFileHandle(baseName(p));
317
467
  const file = await fileHandle.getFile();
318
468
  return file.arrayBuffer();
319
- } catch {
320
- return null;
469
+ } catch (error: unknown) {
470
+ if (isMissingEntryError(error)) return null;
471
+ throw error;
321
472
  }
322
473
  }
323
474
 
324
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> {
325
480
  const p = normalisePath(path);
326
481
  const dir = await resolveDirCreate(this.root, parentDir(p));
327
482
  const fileHandle = await dir.getFileHandle(baseName(p), { create: true });
328
483
  const writable = await fileHandle.createWritable();
329
- if (data instanceof ArrayBuffer) {
330
- await writable.write(data);
331
- } else {
332
- await writable.write(data.buffer as ArrayBuffer);
333
- }
334
- await writable.close();
484
+ await writeAndClose(writable, toWritableBinary(data));
335
485
  }
336
486
  }
337
487
 
@@ -346,8 +496,10 @@ export async function openNativeFolder(): Promise<NativeFileSystemProvider> {
346
496
  }
347
497
 
348
498
  const handle = await (
349
- globalThis as unknown as { showDirectoryPicker: () => Promise<FileSystemDirectoryHandle> }
350
- ).showDirectoryPicker();
499
+ globalThis as unknown as {
500
+ showDirectoryPicker: (options: { mode: 'readwrite' }) => Promise<FileSystemDirectoryHandle>;
501
+ }
502
+ ).showDirectoryPicker({ mode: 'readwrite' });
351
503
  const id = `native-${handle.name}-${Date.now()}`;
352
504
  await storeDirectoryHandle(id, handle);
353
505
  return new NativeFileSystemProvider(id, handle);
@@ -370,11 +522,15 @@ export async function restoreNativeFolder(
370
522
  queryPermission(desc: { mode: string }): Promise<string>;
371
523
  requestPermission(desc: { mode: string }): Promise<string>;
372
524
  };
373
- if ((await h.queryPermission(opts)) === 'granted') {
374
- return new NativeFileSystemProvider(workspaceId, handle);
375
- }
376
- if ((await h.requestPermission(opts)) === 'granted') {
377
- 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;
378
534
  }
379
535
 
380
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
+ }