@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
@@ -1,16 +1,17 @@
1
1
  /**
2
2
  * ElectronFileSystemProvider — implements FileSystemProvider by delegating
3
3
  * to the Electron desktop host's fs IPC bridge. Every operation is scoped
4
- * to an absolute root path that the main process validates against a
5
- * whitelist of registered workspace roots.
4
+ * to an opaque workspace id that the main process resolves against its
5
+ * registered workspace authority.
6
6
  *
7
7
  * This file has no Electron dependency — it is a pure IPC client that
8
8
  * relies on the `docBlocksHost` global installed by the preload script.
9
9
  */
10
10
 
11
- import type { FileSystemProvider, FileSystemEntry, FileMeta } from './types.js';
11
+ import type { FileCommitResult, FileSystemProvider, FileSystemEntry, FileMeta } from './types.js';
12
12
  import { maybeGetDocBlocksHost } from '../host/index.js';
13
13
  import type { DocBlocksHostFsAPI } from '../host/types.js';
14
+ import { ElectronFileSystemProviderV2 } from './electron-provider-v2.js';
14
15
 
15
16
  export { isElectronHost } from '../host/index.js';
16
17
 
@@ -27,6 +28,7 @@ function getHostFs(): DocBlocksHostFsAPI {
27
28
  export class ElectronFileSystemProvider implements FileSystemProvider {
28
29
  readonly id: string;
29
30
  readonly label: string;
31
+ readonly v2: ElectronFileSystemProviderV2;
30
32
 
31
33
  private readonly rootPath: string;
32
34
 
@@ -34,6 +36,7 @@ export class ElectronFileSystemProvider implements FileSystemProvider {
34
36
  this.id = id;
35
37
  this.label = label;
36
38
  this.rootPath = rootPath;
39
+ this.v2 = new ElectronFileSystemProviderV2(id, label, rootPath);
37
40
  }
38
41
 
39
42
  /** Absolute path this provider is rooted at. */
@@ -42,47 +45,55 @@ export class ElectronFileSystemProvider implements FileSystemProvider {
42
45
  }
43
46
 
44
47
  readFile(path: string): Promise<string | null> {
45
- return getHostFs().readFile(this.rootPath, path);
48
+ return getHostFs().readFile(this.id, path);
46
49
  }
47
50
 
48
51
  writeFile(path: string, content: string): Promise<void> {
49
- return getHostFs().writeFile(this.rootPath, path, content);
52
+ return getHostFs().writeFile(this.id, path, content);
53
+ }
54
+
55
+ commitFile(
56
+ path: string,
57
+ content: string,
58
+ expectedContent: string | null,
59
+ ): Promise<FileCommitResult> {
60
+ return getHostFs().commitFile(this.id, path, content, expectedContent);
50
61
  }
51
62
 
52
63
  delete(path: string): Promise<void> {
53
- return getHostFs().delete(this.rootPath, path);
64
+ return getHostFs().delete(this.id, path);
54
65
  }
55
66
 
56
67
  rename(oldPath: string, newPath: string): Promise<void> {
57
- return getHostFs().rename(this.rootPath, oldPath, newPath);
68
+ return getHostFs().rename(this.id, oldPath, newPath);
58
69
  }
59
70
 
60
71
  readDirectory(path: string): Promise<FileSystemEntry[]> {
61
- return getHostFs().readDirectory(this.rootPath, path);
72
+ return getHostFs().readDirectory(this.id, path);
62
73
  }
63
74
 
64
75
  exists(path: string): Promise<boolean> {
65
- return getHostFs().exists(this.rootPath, path);
76
+ return getHostFs().exists(this.id, path);
66
77
  }
67
78
 
68
79
  createDirectory(path: string): Promise<void> {
69
- return getHostFs().createDirectory(this.rootPath, path);
80
+ return getHostFs().createDirectory(this.id, path);
70
81
  }
71
82
 
72
83
  stat(path: string): Promise<FileMeta | null> {
73
- return getHostFs().stat(this.rootPath, path);
84
+ return getHostFs().stat(this.id, path);
74
85
  }
75
86
 
76
87
  readBinary(path: string): Promise<ArrayBuffer | null> {
77
- return getHostFs().readBinary(this.rootPath, path);
88
+ return getHostFs().readBinary(this.id, path);
78
89
  }
79
90
 
80
91
  writeBinary(path: string, data: ArrayBuffer | Uint8Array): Promise<void> {
81
- return getHostFs().writeBinary(this.rootPath, path, data);
92
+ return getHostFs().writeBinary(this.id, path, data);
82
93
  }
83
94
 
84
95
  /** Subscribe to external change notifications under this root. */
85
96
  watch(onChange: (changedPath: string) => void): () => void {
86
- return getHostFs().watch(this.rootPath, onChange);
97
+ return getHostFs().watch(this.id, onChange);
87
98
  }
88
99
  }
@@ -0,0 +1,3 @@
1
+ /** Electron renderer filesystem provider entry point. */
2
+ export { ElectronFileSystemProvider, isElectronHost } from './electron-provider.js';
3
+ export { ElectronFileSystemProviderV2 } from './electron-provider-v2.js';
@@ -9,7 +9,9 @@
9
9
 
10
10
  import type { ContentContainer, ContentEntry } from '@bendyline/squisq/storage';
11
11
  import { findDocumentPath } from '@bendyline/squisq/storage';
12
- import type { FileSystemProvider } from './types.js';
12
+ import { FsError } from './fs-error.js';
13
+ import { getFileSystemProviderV2, type FileSystemProvider } from './types.js';
14
+ import { parseWorkspacePath, workspacePathContains, workspacePathJoin } from './workspace-path.js';
13
15
 
14
16
  const EXTENSION_MIME_MAP: Record<string, string> = {
15
17
  '.md': 'text/markdown',
@@ -37,8 +39,7 @@ function guessMimeType(path: string): string {
37
39
  }
38
40
 
39
41
  function joinPrefix(prefix: string, p: string): string {
40
- const clean = p.replace(/^\/+/, '');
41
- return prefix.replace(/\/+$/, '') + '/' + clean;
42
+ return workspacePathJoin(parseWorkspacePath(prefix), p);
42
43
  }
43
44
 
44
45
  export class FileSystemContentContainer implements ContentContainer {
@@ -48,11 +49,13 @@ export class FileSystemContentContainer implements ContentContainer {
48
49
  private readonly provider: FileSystemProvider,
49
50
  prefix = '.docblocks/media',
50
51
  ) {
51
- this.prefix = prefix.replace(/^\/+/, '').replace(/\/+$/, '');
52
+ this.prefix = parseWorkspacePath(prefix);
52
53
  }
53
54
 
54
55
  async readFile(path: string): Promise<ArrayBuffer | null> {
55
56
  const full = joinPrefix(this.prefix, path);
57
+ const providerV2 = getFileSystemProviderV2(this.provider);
58
+ if (providerV2) return (await providerV2.readFile(parseWorkspacePath(full)))?.data ?? null;
56
59
  const binary = await this.provider.readBinary(full);
57
60
  if (binary) return binary;
58
61
  // IndexedDB-backed workspaces store text via `writeFile(string)` and
@@ -67,43 +70,78 @@ export class FileSystemContentContainer implements ContentContainer {
67
70
  }
68
71
 
69
72
  async writeFile(path: string, data: ArrayBuffer | Uint8Array, _mimeType?: string): Promise<void> {
70
- await this.provider.writeBinary(joinPrefix(this.prefix, path), data);
73
+ const full = joinPrefix(this.prefix, path);
74
+ const providerV2 = getFileSystemProviderV2(this.provider);
75
+ if (providerV2) {
76
+ await providerV2.writeFile(parseWorkspacePath(full), data, {
77
+ mode: 'upsert',
78
+ createParents: true,
79
+ });
80
+ return;
81
+ }
82
+ await this.provider.writeBinary(full, data);
71
83
  }
72
84
 
73
85
  async removeFile(path: string): Promise<void> {
74
- await this.provider.delete(joinPrefix(this.prefix, path));
86
+ const full = joinPrefix(this.prefix, path);
87
+ const providerV2 = getFileSystemProviderV2(this.provider);
88
+ if (providerV2) {
89
+ await providerV2.remove(parseWorkspacePath(full), { missing: 'ignore' });
90
+ return;
91
+ }
92
+ await this.provider.delete(full);
75
93
  }
76
94
 
77
95
  async listFiles(prefix?: string): Promise<ContentEntry[]> {
78
96
  const entries: ContentEntry[] = [];
97
+ const providerV2 = getFileSystemProviderV2(this.provider);
98
+ const root = parseWorkspacePath(this.prefix);
99
+ type ListedEntry = {
100
+ readonly kind: 'file' | 'directory';
101
+ readonly path: string;
102
+ readonly size?: number | null;
103
+ };
79
104
  const walk = async (dir: string) => {
80
- let children;
105
+ let children: readonly ListedEntry[];
81
106
  try {
82
- children = await this.provider.readDirectory(dir);
83
- } catch {
84
- return;
107
+ children = providerV2
108
+ ? await providerV2.readDirectory(parseWorkspacePath(dir))
109
+ : await this.provider.readDirectory(dir);
110
+ } catch (error: unknown) {
111
+ // An absent container root means an empty container. Every other
112
+ // storage fault (including a subtree disappearing mid-snapshot)
113
+ // remains observable to backup/export callers.
114
+ if (dir === root && error instanceof FsError && error.code === 'not-found') return;
115
+ throw error;
85
116
  }
86
117
  for (const child of children) {
87
118
  if (child.kind === 'directory') {
88
119
  await walk(child.path);
89
120
  } else {
90
- const rel = child.path.replace(new RegExp('^/?' + this.prefix + '/?'), '');
121
+ const childPath = parseWorkspacePath(child.path);
122
+ const prefixPath = parseWorkspacePath(this.prefix);
123
+ if (!workspacePathContains(prefixPath, childPath)) continue;
124
+ const rel = childPath === prefixPath ? '' : childPath.slice(prefixPath.length + 1);
91
125
  if (prefix && !rel.startsWith(prefix)) continue;
92
- const meta = await this.provider.stat(child.path);
126
+ const size = child.size ?? (await this.provider.stat(child.path))?.size ?? 0;
93
127
  entries.push({
94
128
  path: rel,
95
129
  mimeType: guessMimeType(rel),
96
- size: meta?.size ?? 0,
130
+ size,
97
131
  });
98
132
  }
99
133
  }
100
134
  };
101
- await walk('/' + this.prefix);
135
+ await walk(root);
102
136
  return entries;
103
137
  }
104
138
 
105
139
  async exists(path: string): Promise<boolean> {
106
- return this.provider.exists(joinPrefix(this.prefix, path));
140
+ const full = joinPrefix(this.prefix, path);
141
+ const providerV2 = getFileSystemProviderV2(this.provider);
142
+ return providerV2
143
+ ? (await providerV2.stat(parseWorkspacePath(full))) !== null
144
+ : this.provider.exists(full);
107
145
  }
108
146
 
109
147
  async getDocumentPath(): Promise<string | null> {
@@ -0,0 +1,234 @@
1
+ /** Stable error codes shared by every filesystem transport and backend. */
2
+ export type FsErrorCode =
3
+ | 'invalid-path'
4
+ | 'path-escape'
5
+ | 'not-found'
6
+ | 'already-exists'
7
+ | 'type-mismatch'
8
+ | 'not-empty'
9
+ | 'permission-denied'
10
+ | 'conflict'
11
+ | 'quota-exceeded'
12
+ | 'not-supported'
13
+ | 'busy'
14
+ | 'disposed'
15
+ | 'closed'
16
+ | 'aborted'
17
+ | 'io';
18
+
19
+ export type FsOperation =
20
+ | 'parse-path'
21
+ | 'read'
22
+ | 'stat'
23
+ | 'list'
24
+ | 'write'
25
+ | 'create-directory'
26
+ | 'remove'
27
+ | 'move'
28
+ | 'snapshot'
29
+ | 'watch'
30
+ | 'dispose';
31
+
32
+ /** Plain-data form safe to send over IPC, postMessage, or JSON. */
33
+ export interface SerializedFsError {
34
+ readonly name: 'FsError';
35
+ readonly code: FsErrorCode;
36
+ readonly message: string;
37
+ readonly operation: FsOperation | null;
38
+ readonly path: string | null;
39
+ readonly destinationPath: string | null;
40
+ readonly retryable: boolean;
41
+ }
42
+
43
+ export interface FsErrorContext {
44
+ readonly operation?: FsOperation;
45
+ readonly path?: string;
46
+ readonly destinationPath?: string;
47
+ readonly retryable?: boolean;
48
+ readonly defaultCode?: FsErrorCode;
49
+ }
50
+
51
+ const FS_ERROR_CODES: ReadonlySet<string> = new Set<FsErrorCode>([
52
+ 'invalid-path',
53
+ 'path-escape',
54
+ 'not-found',
55
+ 'already-exists',
56
+ 'type-mismatch',
57
+ 'not-empty',
58
+ 'permission-denied',
59
+ 'conflict',
60
+ 'quota-exceeded',
61
+ 'not-supported',
62
+ 'busy',
63
+ 'disposed',
64
+ 'closed',
65
+ 'aborted',
66
+ 'io',
67
+ ]);
68
+
69
+ const FS_OPERATIONS: ReadonlySet<string> = new Set<FsOperation>([
70
+ 'parse-path',
71
+ 'read',
72
+ 'stat',
73
+ 'list',
74
+ 'write',
75
+ 'create-directory',
76
+ 'remove',
77
+ 'move',
78
+ 'snapshot',
79
+ 'watch',
80
+ 'dispose',
81
+ ]);
82
+
83
+ /** Error with a stable code and a deliberately serializable public shape. */
84
+ export class FsError extends Error {
85
+ public readonly code: FsErrorCode;
86
+ public readonly operation: FsOperation | null;
87
+ public readonly path: string | null;
88
+ public readonly destinationPath: string | null;
89
+ public readonly retryable: boolean;
90
+
91
+ public constructor(code: FsErrorCode, message: string, context: FsErrorContext = {}) {
92
+ super(message);
93
+ this.name = 'FsError';
94
+ this.code = code;
95
+ this.operation = context.operation ?? null;
96
+ this.path = context.path ?? null;
97
+ this.destinationPath = context.destinationPath ?? null;
98
+ this.retryable = context.retryable ?? (code === 'io' || code === 'aborted' || code === 'busy');
99
+ }
100
+
101
+ public toJSON(): SerializedFsError {
102
+ return {
103
+ name: 'FsError',
104
+ code: this.code,
105
+ message: this.message,
106
+ operation: this.operation,
107
+ path: this.path,
108
+ destinationPath: this.destinationPath,
109
+ retryable: this.retryable,
110
+ };
111
+ }
112
+ }
113
+
114
+ export function mapDomExceptionToFsErrorCode(name: string): FsErrorCode {
115
+ switch (name) {
116
+ case 'NotFoundError':
117
+ return 'not-found';
118
+ case 'ConstraintError':
119
+ return 'already-exists';
120
+ case 'TypeMismatchError':
121
+ return 'type-mismatch';
122
+ case 'InvalidModificationError':
123
+ return 'not-empty';
124
+ case 'NotAllowedError':
125
+ case 'SecurityError':
126
+ case 'NoModificationAllowedError':
127
+ return 'permission-denied';
128
+ case 'QuotaExceededError':
129
+ return 'quota-exceeded';
130
+ case 'NotSupportedError':
131
+ return 'not-supported';
132
+ case 'InvalidStateError':
133
+ return 'disposed';
134
+ case 'AbortError':
135
+ return 'aborted';
136
+ default:
137
+ return 'io';
138
+ }
139
+ }
140
+
141
+ export function mapNodeErrorCodeToFsErrorCode(code: string): FsErrorCode {
142
+ switch (code) {
143
+ case 'ENOENT':
144
+ return 'not-found';
145
+ case 'ENOTDIR':
146
+ return 'type-mismatch';
147
+ case 'EEXIST':
148
+ return 'already-exists';
149
+ case 'EISDIR':
150
+ return 'type-mismatch';
151
+ case 'ENOTEMPTY':
152
+ return 'not-empty';
153
+ case 'EACCES':
154
+ case 'EPERM':
155
+ return 'permission-denied';
156
+ case 'ENOSPC':
157
+ case 'EDQUOT':
158
+ return 'quota-exceeded';
159
+ case 'EBUSY':
160
+ return 'busy';
161
+ case 'EXDEV':
162
+ case 'ENOTSUP':
163
+ case 'EOPNOTSUPP':
164
+ return 'not-supported';
165
+ default:
166
+ return 'io';
167
+ }
168
+ }
169
+
170
+ export function isSerializedFsError(value: unknown): value is SerializedFsError {
171
+ if (!value || typeof value !== 'object') return false;
172
+ const candidate = value as Partial<Record<keyof SerializedFsError, unknown>>;
173
+ return (
174
+ candidate.name === 'FsError' &&
175
+ typeof candidate.code === 'string' &&
176
+ FS_ERROR_CODES.has(candidate.code) &&
177
+ typeof candidate.message === 'string' &&
178
+ (candidate.operation === null ||
179
+ (typeof candidate.operation === 'string' && FS_OPERATIONS.has(candidate.operation))) &&
180
+ (candidate.path === null || typeof candidate.path === 'string') &&
181
+ (candidate.destinationPath === null || typeof candidate.destinationPath === 'string') &&
182
+ typeof candidate.retryable === 'boolean'
183
+ );
184
+ }
185
+
186
+ export function deserializeFsError(value: SerializedFsError): FsError {
187
+ return new FsError(value.code, value.message, {
188
+ operation: value.operation ?? undefined,
189
+ path: value.path ?? undefined,
190
+ destinationPath: value.destinationPath ?? undefined,
191
+ retryable: value.retryable,
192
+ });
193
+ }
194
+
195
+ export function fsErrorFromUnknown(error: unknown, context: FsErrorContext = {}): FsError {
196
+ if (error instanceof FsError) return error;
197
+ if (isSerializedFsError(error)) return deserializeFsError(error);
198
+
199
+ const name = getErrorStringField(error, 'name');
200
+ const nodeCode = getErrorStringField(error, 'code');
201
+ const message = getErrorStringField(error, 'message') ?? String(error);
202
+ const code =
203
+ context.defaultCode ??
204
+ (nodeCode
205
+ ? mapNodeErrorCodeToFsErrorCode(nodeCode)
206
+ : name
207
+ ? mapDomExceptionToFsErrorCode(name)
208
+ : 'io');
209
+ return new FsError(code, message, context);
210
+ }
211
+
212
+ export function serializeFsError(error: unknown, context: FsErrorContext = {}): SerializedFsError {
213
+ return fsErrorFromUnknown(error, context).toJSON();
214
+ }
215
+
216
+ /**
217
+ * True when a storage backend rejected an operation because the origin or
218
+ * device is out of space. Covers typed `FsError`s (`'quota-exceeded'`, from
219
+ * DOMException `QuotaExceededError` or Node `ENOSPC`/`EDQUOT`), serialized
220
+ * transport copies, and raw DOMExceptions from backends that do not wrap
221
+ * their errors (e.g. the File System Access API) — including cross-realm
222
+ * instances where `instanceof DOMException` would fail.
223
+ */
224
+ export function isQuotaExceededError(error: unknown): boolean {
225
+ if (error instanceof FsError) return error.code === 'quota-exceeded';
226
+ if (isSerializedFsError(error)) return error.code === 'quota-exceeded';
227
+ return getErrorStringField(error, 'name') === 'QuotaExceededError';
228
+ }
229
+
230
+ function getErrorStringField(error: unknown, key: 'name' | 'message' | 'code'): string | null {
231
+ if (!error || typeof error !== 'object') return null;
232
+ const value = (error as Record<string, unknown>)[key];
233
+ return typeof value === 'string' ? value : null;
234
+ }
@@ -1,15 +1,127 @@
1
1
  export type {
2
2
  FileSystemProvider,
3
+ FileSystemProviderWithV2,
3
4
  FileSystemEntry,
4
5
  FileEntry,
5
6
  FolderEntry,
6
7
  FileMeta,
8
+ FileCommitResult,
7
9
  } from './types.js';
10
+ export { getFileSystemProviderV2, hasFileSystemProviderV2 } from './types.js';
11
+
12
+ export {
13
+ WORKSPACE_ROOT,
14
+ parseWorkspacePath,
15
+ tryParseWorkspacePath,
16
+ workspacePathToLegacy,
17
+ workspacePathJoin,
18
+ workspacePathDirname,
19
+ workspacePathBasename,
20
+ workspacePathContains,
21
+ type WorkspacePath,
22
+ } from './workspace-path.js';
23
+ export {
24
+ FsError,
25
+ mapDomExceptionToFsErrorCode,
26
+ mapNodeErrorCodeToFsErrorCode,
27
+ isSerializedFsError,
28
+ isQuotaExceededError,
29
+ serializeFsError,
30
+ deserializeFsError,
31
+ fsErrorFromUnknown,
32
+ type FsErrorCode,
33
+ type FsOperation,
34
+ type SerializedFsError,
35
+ type FsErrorContext,
36
+ } from './fs-error.js';
37
+ export type {
38
+ FileSystemProviderV2,
39
+ FileSystemProviderCapabilities,
40
+ FileSystemVersion,
41
+ FileSystemConditionalWriteStrength,
42
+ FileSystemSymlinkPolicy,
43
+ FileSystemAtomicity,
44
+ FileSystemCaseSensitivity,
45
+ FileSystemDurability,
46
+ FileSystemEntrySnapshot,
47
+ FileSystemFileSnapshot,
48
+ FileSystemDirectorySnapshot,
49
+ FileSystemFileRead,
50
+ FileSystemSnapshot,
51
+ FileSystemSnapshotEntry,
52
+ FileSystemSnapshotFile,
53
+ FileSystemWriteMode,
54
+ FileSystemWriteOptions,
55
+ FileSystemCreateDirectoryMode,
56
+ FileSystemCreateDirectoryOptions,
57
+ FileSystemRemoveOptions,
58
+ FileSystemRemoveResult,
59
+ FileSystemMoveOptions,
60
+ FileSystemWatchEventType,
61
+ FileSystemWatchEventOrigin,
62
+ FileSystemWatchEvent,
63
+ FileSystemEntryWatchEvent,
64
+ FileSystemOverflowWatchEvent,
65
+ FileSystemWatchListener,
66
+ FileSystemWatchErrorListener,
67
+ FileSystemWatchOptions,
68
+ FileSystemWatchSubscription,
69
+ } from './v2.js';
70
+ export { parseFileSystemVersion, tryParseFileSystemVersion } from './v2.js';
71
+ export {
72
+ MemoryFileSystemProviderV2,
73
+ type MemoryFilePayloadKind,
74
+ type MemoryFileSystemV2CompatibilityFile,
75
+ type MemoryFileSystemV2Replacement,
76
+ type MemoryFileSystemV2ReplacementFile,
77
+ type MemoryFileSystemV2State,
78
+ type MemoryFileSystemV2StateFile,
79
+ } from './memory-provider-v2.js';
80
+ export {
81
+ IndexedDBFileSystemProviderV2,
82
+ type IndexedDBFileSystemProviderV2Options,
83
+ type IndexedDBLegacyConflictReason,
84
+ type IndexedDBLegacyConflictResolution,
85
+ type IndexedDBLegacyMigrationConflict,
86
+ type IndexedDBLegacyRecoveryCandidate,
87
+ type IndexedDBLegacyRecoveryKind,
88
+ } from './indexeddb-provider-v2.js';
89
+ export {
90
+ LegacyFileSystemProviderV2Adapter,
91
+ type LegacyFilePayloadModel,
92
+ type LegacyFileSystemProviderV2AdapterOptions,
93
+ } from './legacy-v2-adapter.js';
8
94
 
9
95
  export { IndexedDBFileSystemProvider } from './indexeddb-provider.js';
96
+ export {
97
+ MemoryFileSystemProvider,
98
+ type MemoryFileSystemSnapshot,
99
+ type MemoryFileSystemSnapshotFile,
100
+ } from './memory-provider.js';
101
+ export {
102
+ createDbkWorkspaceSnapshot,
103
+ replaceMemoryWorkspaceFromDbk,
104
+ type DbkWorkspaceAssetLayout,
105
+ type DbkWorkspaceSnapshot,
106
+ type DbkWorkspaceSnapshotOptions,
107
+ } from './dbk-workspace.js';
10
108
  export { IndexedDBContentContainer } from './indexeddb-content-container.js';
11
109
  export { FileSystemContentContainer } from './filesystem-content-container.js';
12
110
  export { createFileMediaProvider } from './file-media-provider.js';
111
+ export { documentCompanionPath, moveFileSystemEntry } from './move-entry.js';
112
+ export {
113
+ FileSystemMoveRecoveryError,
114
+ FileSystemPartialMoveError,
115
+ isFileSystemMoveRecoveryError,
116
+ isFileSystemMoveStateError,
117
+ isFileSystemPartialMoveError,
118
+ describeEntryMove,
119
+ describePartialMove,
120
+ type FileSystemEntryMoveState,
121
+ type FileSystemMoveLocation,
122
+ type FileSystemPartialMoveState,
123
+ type FileSystemPathPresence,
124
+ } from './move-error.js';
13
125
 
14
126
  export {
15
127
  NativeFileSystemProvider,
@@ -20,5 +132,12 @@ export {
20
132
  loadDirectoryHandle,
21
133
  removeDirectoryHandle,
22
134
  } from './native-provider.js';
135
+ export {
136
+ NativeFileSystemProviderV2,
137
+ NativeFileSystemMoveRecoveryError,
138
+ type NativeFileSystemMovePathState,
139
+ type NativeFileSystemMoveRecoveryState,
140
+ } from './native-provider-v2.js';
23
141
 
24
142
  export { ElectronFileSystemProvider, isElectronHost } from './electron-provider.js';
143
+ export { ElectronFileSystemProviderV2 } from './electron-provider-v2.js';
@@ -7,7 +7,8 @@
7
7
 
8
8
  import type { ContentContainer, ContentEntry } from '@bendyline/squisq/storage';
9
9
  import { findDocumentPath } from '@bendyline/squisq/storage';
10
- import { IndexedDBFileSystemProvider } from './indexeddb-provider.js';
10
+ import { IndexedDBFileSystemProviderV2 } from './indexeddb-provider-v2.js';
11
+ import { parseWorkspacePath, WORKSPACE_ROOT } from './workspace-path.js';
11
12
 
12
13
  // ── MIME type guessing ─────────────────────────────────────────────
13
14
 
@@ -39,49 +40,51 @@ function guessMimeType(path: string): string {
39
40
  // ── Implementation ─────────────────────────────────────────────────
40
41
 
41
42
  export class IndexedDBContentContainer implements ContentContainer {
42
- private provider: IndexedDBFileSystemProvider;
43
+ private readonly provider: IndexedDBFileSystemProviderV2;
43
44
 
44
45
  constructor(workspaceId: string) {
45
- this.provider = new IndexedDBFileSystemProvider(`${workspaceId}-media`, 'Media Storage');
46
+ this.provider = new IndexedDBFileSystemProviderV2(`${workspaceId}-media`, 'Media Storage');
46
47
  }
47
48
 
48
49
  async readFile(path: string): Promise<ArrayBuffer | null> {
49
- return this.provider.readBinary(path);
50
+ return (await this.provider.readFile(parseWorkspacePath(path)))?.data ?? null;
50
51
  }
51
52
 
52
53
  async writeFile(path: string, data: ArrayBuffer | Uint8Array, _mimeType?: string): Promise<void> {
53
- await this.provider.writeBinary(path, data);
54
+ await this.provider.writeFile(parseWorkspacePath(path), data, {
55
+ mode: 'upsert',
56
+ createParents: true,
57
+ });
54
58
  }
55
59
 
56
60
  async removeFile(path: string): Promise<void> {
57
- await this.provider.delete(path);
61
+ await this.provider.remove(parseWorkspacePath(path), { missing: 'ignore' });
58
62
  }
59
63
 
60
64
  async listFiles(prefix?: string): Promise<ContentEntry[]> {
61
65
  const entries: ContentEntry[] = [];
62
66
  const walk = async (dir: string) => {
63
- const children = await this.provider.readDirectory(dir);
67
+ const children = await this.provider.readDirectory(parseWorkspacePath(dir));
64
68
  for (const child of children) {
65
69
  if (child.kind === 'directory') {
66
70
  await walk(child.path);
67
71
  } else {
68
- const filePath = child.path.replace(/^\//, '');
72
+ const filePath = child.path;
69
73
  if (prefix && !filePath.startsWith(prefix)) continue;
70
- const meta = await this.provider.stat(child.path);
71
74
  entries.push({
72
75
  path: filePath,
73
76
  mimeType: guessMimeType(filePath),
74
- size: meta?.size ?? 0,
77
+ size: child.size,
75
78
  });
76
79
  }
77
80
  }
78
81
  };
79
- await walk('/');
82
+ await walk(WORKSPACE_ROOT);
80
83
  return entries;
81
84
  }
82
85
 
83
86
  async exists(path: string): Promise<boolean> {
84
- return this.provider.exists(path);
87
+ return (await this.provider.stat(parseWorkspacePath(path))) !== null;
85
88
  }
86
89
 
87
90
  async getDocumentPath(): Promise<string | null> {
@@ -101,4 +104,9 @@ export class IndexedDBContentContainer implements ContentContainer {
101
104
  const data = new TextEncoder().encode(markdown);
102
105
  await this.writeFile(name, data, 'text/markdown');
103
106
  }
107
+
108
+ /** Release the IndexedDB connection when a long-lived container is retired. */
109
+ async dispose(): Promise<void> {
110
+ await this.provider.dispose();
111
+ }
104
112
  }