@bendyline/docblocks 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/{chunk-2D7Y4IR3.js → chunk-4437YWLE.js} +123 -67
- package/dist/{chunk-5MYAICRE.js → chunk-45T5AFCN.js} +44 -57
- package/dist/{chunk-PGR7LK6D.js → chunk-6L3JRDE7.js} +14 -6
- package/dist/{chunk-RVQHY3XU.js → chunk-6XHOOBYF.js} +0 -1
- package/dist/chunk-73OOROXH.js +8 -0
- package/dist/{chunk-SI2LU2RP.js → chunk-AYWKIZLD.js} +1 -1
- package/dist/{chunk-GCPBEMMH.js → chunk-BVY7GGNV.js} +10 -21
- package/dist/{chunk-AEHCVWU5.js → chunk-DZB666V7.js} +0 -1
- package/dist/{chunk-75UUF6C4.js → chunk-HI7QEWL6.js} +18 -10
- package/dist/chunk-KQ6756AJ.js +28 -0
- package/dist/chunk-LMMOFRGQ.js +148 -0
- package/dist/{chunk-FNW556FN.js → chunk-M2PUNKHS.js} +0 -1
- package/dist/chunk-M5ZNQGR5.js +19 -0
- package/dist/chunk-M62ZGJ5I.js +51 -0
- package/dist/chunk-OQCPBOQA.js +168 -0
- package/dist/{chunk-EGNDADXB.js → chunk-QI6YKTQV.js} +2 -3
- package/dist/chunk-SJBB5OD7.js +227 -0
- package/dist/{chunk-EZKJZUNJ.js → chunk-XAMILHQP.js} +0 -1
- package/dist/{chunk-J32GU3F3.js → chunk-YM7O3PWO.js} +249 -222
- package/dist/{chunk-RQJRQF46.js → chunk-ZO7G53QP.js} +41 -49
- package/dist/document/index.d.ts +358 -6
- package/dist/document/index.js +24 -4
- package/dist/filesystem/electron.d.ts +73 -4
- package/dist/filesystem/electron.js +16 -4
- package/dist/filesystem/index.d.ts +228 -22
- package/dist/filesystem/index.js +133 -20
- package/dist/filesystem/indexeddb.d.ts +284 -5
- package/dist/filesystem/indexeddb.js +22 -5
- package/dist/filesystem/memory.d.ts +152 -4
- package/dist/filesystem/memory.js +12 -4
- package/dist/filesystem/native.d.ts +136 -4
- package/dist/filesystem/native.js +27 -4
- package/dist/host/index.d.ts +32 -9
- package/dist/host/index.js +26 -30
- package/dist/index.d.ts +14 -10
- package/dist/index.js +204 -10
- package/dist/mcp/index.d.ts +33 -4
- package/dist/mcp/index.js +31 -4
- package/dist/mcp/zod.d.ts +645 -643
- package/dist/mcp/zod.js +700 -885
- package/dist/share/index.d.ts +43 -0
- package/dist/share/index.js +12 -0
- package/dist/{types-CsX6EN2t.d.ts → types-BEg5SQ1_.d.ts} +1 -1
- package/dist/{types-eOlt9J7n.d.ts → types-CueK0tvU.d.ts} +34 -11
- package/dist/vscode/index.d.ts +225 -3
- package/dist/vscode/index.js +389 -3
- package/dist/workspace/index.d.ts +141 -4
- package/dist/workspace/index.js +28 -3
- package/package.json +10 -5
- package/dist/chunk-2D7Y4IR3.js.map +0 -1
- package/dist/chunk-5MYAICRE.js.map +0 -1
- package/dist/chunk-75UUF6C4.js.map +0 -1
- package/dist/chunk-AEHCVWU5.js.map +0 -1
- package/dist/chunk-AYZ4HN5M.js +0 -20
- package/dist/chunk-AYZ4HN5M.js.map +0 -1
- package/dist/chunk-EGNDADXB.js.map +0 -1
- package/dist/chunk-EZKJZUNJ.js.map +0 -1
- package/dist/chunk-FNW556FN.js.map +0 -1
- package/dist/chunk-GCPBEMMH.js.map +0 -1
- package/dist/chunk-J32GU3F3.js.map +0 -1
- package/dist/chunk-PGR7LK6D.js.map +0 -1
- package/dist/chunk-RQJRQF46.js.map +0 -1
- package/dist/chunk-RVQHY3XU.js.map +0 -1
- package/dist/chunk-SI2LU2RP.js.map +0 -1
- package/dist/chunk-W5UUUII5.js +0 -142
- package/dist/chunk-W5UUUII5.js.map +0 -1
- package/dist/document/document-session.d.ts +0 -127
- package/dist/document/document-session.d.ts.map +0 -1
- package/dist/document/document-session.js +0 -749
- package/dist/document/document-session.js.map +0 -1
- package/dist/document/filesystem-target.d.ts +0 -17
- package/dist/document/filesystem-target.d.ts.map +0 -1
- package/dist/document/filesystem-target.js +0 -70
- package/dist/document/filesystem-target.js.map +0 -1
- package/dist/document/index.d.ts.map +0 -1
- package/dist/document/index.js.map +0 -1
- package/dist/document/recovery-journal.d.ts +0 -125
- package/dist/document/recovery-journal.d.ts.map +0 -1
- package/dist/document/recovery-journal.js +0 -385
- package/dist/document/recovery-journal.js.map +0 -1
- package/dist/document/types.d.ts +0 -89
- package/dist/document/types.d.ts.map +0 -1
- package/dist/document/types.js +0 -9
- package/dist/document/types.js.map +0 -1
- package/dist/filesystem/commit-lock.d.ts +0 -3
- package/dist/filesystem/commit-lock.d.ts.map +0 -1
- package/dist/filesystem/commit-lock.js +0 -20
- package/dist/filesystem/commit-lock.js.map +0 -1
- package/dist/filesystem/dbk-workspace.d.ts +0 -34
- package/dist/filesystem/dbk-workspace.d.ts.map +0 -1
- package/dist/filesystem/dbk-workspace.js +0 -85
- package/dist/filesystem/dbk-workspace.js.map +0 -1
- package/dist/filesystem/electron-provider-v2.d.ts +0 -36
- package/dist/filesystem/electron-provider-v2.d.ts.map +0 -1
- package/dist/filesystem/electron-provider-v2.js +0 -503
- package/dist/filesystem/electron-provider-v2.js.map +0 -1
- package/dist/filesystem/electron-provider.d.ts +0 -35
- package/dist/filesystem/electron-provider.d.ts.map +0 -1
- package/dist/filesystem/electron-provider.js +0 -69
- package/dist/filesystem/electron-provider.js.map +0 -1
- package/dist/filesystem/electron.d.ts.map +0 -1
- package/dist/filesystem/electron.js.map +0 -1
- package/dist/filesystem/file-media-provider.d.ts +0 -23
- package/dist/filesystem/file-media-provider.d.ts.map +0 -1
- package/dist/filesystem/file-media-provider.js +0 -85
- package/dist/filesystem/file-media-provider.js.map +0 -1
- package/dist/filesystem/filesystem-content-container.d.ts +0 -24
- package/dist/filesystem/filesystem-content-container.d.ts.map +0 -1
- package/dist/filesystem/filesystem-content-container.js +0 -153
- package/dist/filesystem/filesystem-content-container.js.map +0 -1
- package/dist/filesystem/fs-error.d.ts +0 -46
- package/dist/filesystem/fs-error.d.ts.map +0 -1
- package/dist/filesystem/fs-error.js +0 -170
- package/dist/filesystem/fs-error.js.map +0 -1
- package/dist/filesystem/index.d.ts.map +0 -1
- package/dist/filesystem/index.js.map +0 -1
- package/dist/filesystem/indexeddb-content-container.d.ts +0 -22
- package/dist/filesystem/indexeddb-content-container.d.ts.map +0 -1
- package/dist/filesystem/indexeddb-content-container.js +0 -100
- package/dist/filesystem/indexeddb-content-container.js.map +0 -1
- package/dist/filesystem/indexeddb-provider-v2.d.ts +0 -96
- package/dist/filesystem/indexeddb-provider-v2.d.ts.map +0 -1
- package/dist/filesystem/indexeddb-provider-v2.js +0 -1241
- package/dist/filesystem/indexeddb-provider-v2.js.map +0 -1
- package/dist/filesystem/indexeddb-provider.d.ts +0 -39
- package/dist/filesystem/indexeddb-provider.d.ts.map +0 -1
- package/dist/filesystem/indexeddb-provider.js +0 -145
- package/dist/filesystem/indexeddb-provider.js.map +0 -1
- package/dist/filesystem/indexeddb-store.d.ts +0 -34
- package/dist/filesystem/indexeddb-store.d.ts.map +0 -1
- package/dist/filesystem/indexeddb-store.js +0 -165
- package/dist/filesystem/indexeddb-store.js.map +0 -1
- package/dist/filesystem/indexeddb.d.ts.map +0 -1
- package/dist/filesystem/indexeddb.js.map +0 -1
- package/dist/filesystem/legacy-v2-adapter.d.ts +0 -62
- package/dist/filesystem/legacy-v2-adapter.d.ts.map +0 -1
- package/dist/filesystem/legacy-v2-adapter.js +0 -499
- package/dist/filesystem/legacy-v2-adapter.js.map +0 -1
- package/dist/filesystem/memory-provider-v2.d.ts +0 -92
- package/dist/filesystem/memory-provider-v2.d.ts.map +0 -1
- package/dist/filesystem/memory-provider-v2.js +0 -615
- package/dist/filesystem/memory-provider-v2.js.map +0 -1
- package/dist/filesystem/memory-provider.d.ts +0 -54
- package/dist/filesystem/memory-provider.d.ts.map +0 -1
- package/dist/filesystem/memory-provider.js +0 -132
- package/dist/filesystem/memory-provider.js.map +0 -1
- package/dist/filesystem/memory.d.ts.map +0 -1
- package/dist/filesystem/memory.js.map +0 -1
- package/dist/filesystem/move-entry.d.ts +0 -9
- package/dist/filesystem/move-entry.d.ts.map +0 -1
- package/dist/filesystem/move-entry.js +0 -103
- package/dist/filesystem/move-entry.js.map +0 -1
- package/dist/filesystem/move-error.d.ts +0 -53
- package/dist/filesystem/move-error.d.ts.map +0 -1
- package/dist/filesystem/move-error.js +0 -99
- package/dist/filesystem/move-error.js.map +0 -1
- package/dist/filesystem/native-provider-v2.d.ts +0 -75
- package/dist/filesystem/native-provider-v2.d.ts.map +0 -1
- package/dist/filesystem/native-provider-v2.js +0 -849
- package/dist/filesystem/native-provider-v2.js.map +0 -1
- package/dist/filesystem/native-provider.d.ts +0 -53
- package/dist/filesystem/native-provider.d.ts.map +0 -1
- package/dist/filesystem/native-provider.js +0 -471
- package/dist/filesystem/native-provider.js.map +0 -1
- package/dist/filesystem/native.d.ts.map +0 -1
- package/dist/filesystem/native.js.map +0 -1
- package/dist/filesystem/types.d.ts +0 -83
- package/dist/filesystem/types.d.ts.map +0 -1
- package/dist/filesystem/types.js +0 -16
- package/dist/filesystem/types.js.map +0 -1
- package/dist/filesystem/v2.d.ts +0 -172
- package/dist/filesystem/v2.d.ts.map +0 -1
- package/dist/filesystem/v2.js +0 -16
- package/dist/filesystem/v2.js.map +0 -1
- package/dist/filesystem/workspace-path.d.ts +0 -21
- package/dist/filesystem/workspace-path.d.ts.map +0 -1
- package/dist/filesystem/workspace-path.js +0 -80
- package/dist/filesystem/workspace-path.js.map +0 -1
- package/dist/host/filesystem-v2.d.ts +0 -45
- package/dist/host/filesystem-v2.d.ts.map +0 -1
- package/dist/host/filesystem-v2.js +0 -13
- package/dist/host/filesystem-v2.js.map +0 -1
- package/dist/host/git.d.ts +0 -200
- package/dist/host/git.d.ts.map +0 -1
- package/dist/host/git.js +0 -15
- package/dist/host/git.js.map +0 -1
- package/dist/host/index.d.ts.map +0 -1
- package/dist/host/index.js.map +0 -1
- package/dist/host/types.d.ts +0 -232
- package/dist/host/types.d.ts.map +0 -1
- package/dist/host/types.js +0 -10
- package/dist/host/types.js.map +0 -1
- package/dist/host/wire-policy.d.ts +0 -22
- package/dist/host/wire-policy.d.ts.map +0 -1
- package/dist/host/wire-policy.js +0 -92
- package/dist/host/wire-policy.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/index.js.map +0 -1
- package/dist/mcp/types.d.ts +0 -466
- package/dist/mcp/types.d.ts.map +0 -1
- package/dist/mcp/types.js +0 -25
- package/dist/mcp/types.js.map +0 -1
- package/dist/mcp/wire.d.ts +0 -29
- package/dist/mcp/wire.d.ts.map +0 -1
- package/dist/mcp/wire.js +0 -1046
- package/dist/mcp/wire.js.map +0 -1
- package/dist/mcp/zod.d.ts.map +0 -1
- package/dist/mcp/zod.js.map +0 -1
- package/dist/vscode/index.d.ts.map +0 -1
- package/dist/vscode/index.js.map +0 -1
- package/dist/vscode/messages.d.ts +0 -204
- package/dist/vscode/messages.d.ts.map +0 -1
- package/dist/vscode/messages.js +0 -494
- package/dist/vscode/messages.js.map +0 -1
- package/dist/workspace/index.d.ts.map +0 -1
- package/dist/workspace/index.js.map +0 -1
- package/dist/workspace/reconcile-electron.d.ts +0 -19
- package/dist/workspace/reconcile-electron.d.ts.map +0 -1
- package/dist/workspace/reconcile-electron.js +0 -42
- package/dist/workspace/reconcile-electron.js.map +0 -1
- package/dist/workspace/types.d.ts +0 -69
- package/dist/workspace/types.d.ts.map +0 -1
- package/dist/workspace/types.js +0 -2
- package/dist/workspace/types.js.map +0 -1
- package/dist/workspace/workspace-manager.d.ts +0 -47
- package/dist/workspace/workspace-manager.d.ts.map +0 -1
- package/dist/workspace/workspace-manager.js +0 -126
- package/dist/workspace/workspace-manager.js.map +0 -1
- package/src/document/document-session.ts +0 -834
- package/src/document/filesystem-target.ts +0 -109
- package/src/document/index.ts +0 -41
- package/src/document/recovery-journal.ts +0 -536
- package/src/document/types.ts +0 -116
- package/src/filesystem/commit-lock.ts +0 -29
- package/src/filesystem/dbk-workspace.ts +0 -135
- package/src/filesystem/electron-provider-v2.ts +0 -630
- package/src/filesystem/electron-provider.ts +0 -99
- package/src/filesystem/electron.ts +0 -3
- package/src/filesystem/file-media-provider.ts +0 -103
- package/src/filesystem/filesystem-content-container.ts +0 -164
- package/src/filesystem/fs-error.ts +0 -234
- package/src/filesystem/index.ts +0 -143
- package/src/filesystem/indexeddb-content-container.ts +0 -112
- package/src/filesystem/indexeddb-provider-v2.ts +0 -1740
- package/src/filesystem/indexeddb-provider.ts +0 -171
- package/src/filesystem/indexeddb-store.ts +0 -207
- package/src/filesystem/indexeddb.ts +0 -12
- package/src/filesystem/legacy-v2-adapter.ts +0 -779
- package/src/filesystem/memory-provider-v2.ts +0 -896
- package/src/filesystem/memory-provider.ts +0 -180
- package/src/filesystem/memory.ts +0 -15
- package/src/filesystem/move-entry.ts +0 -142
- package/src/filesystem/move-error.ts +0 -159
- package/src/filesystem/native-provider-v2.ts +0 -1165
- package/src/filesystem/native-provider.ts +0 -537
- package/src/filesystem/native.ts +0 -16
- package/src/filesystem/types.ts +0 -114
- package/src/filesystem/v2.ts +0 -231
- package/src/filesystem/workspace-path.ts +0 -92
- package/src/host/filesystem-v2.ts +0 -104
- package/src/host/git.ts +0 -246
- package/src/host/index.ts +0 -93
- package/src/host/types.ts +0 -274
- package/src/host/wire-policy.ts +0 -103
- package/src/index.ts +0 -10
- package/src/mcp/index.ts +0 -3
- package/src/mcp/types.ts +0 -594
- package/src/mcp/wire.ts +0 -1280
- package/src/mcp/zod.ts +0 -1000
- package/src/vscode/index.ts +0 -2
- package/src/vscode/messages.ts +0 -704
- package/src/workspace/index.ts +0 -17
- package/src/workspace/reconcile-electron.ts +0 -65
- package/src/workspace/types.ts +0 -67
- package/src/workspace/workspace-manager.ts +0 -151
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ElectronFileSystemProvider — implements FileSystemProvider by delegating
|
|
3
|
-
* to the Electron desktop host's fs IPC bridge. Every operation is scoped
|
|
4
|
-
* to an opaque workspace id that the main process resolves against its
|
|
5
|
-
* registered workspace authority.
|
|
6
|
-
*
|
|
7
|
-
* This file has no Electron dependency — it is a pure IPC client that
|
|
8
|
-
* relies on the `docBlocksHost` global installed by the preload script.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { FileCommitResult, FileSystemProvider, FileSystemEntry, FileMeta } from './types.js';
|
|
12
|
-
import { maybeGetDocBlocksHost } from '../host/index.js';
|
|
13
|
-
import type { DocBlocksHostFsAPI } from '../host/types.js';
|
|
14
|
-
import { ElectronFileSystemProviderV2 } from './electron-provider-v2.js';
|
|
15
|
-
|
|
16
|
-
export { isElectronHost } from '../host/index.js';
|
|
17
|
-
|
|
18
|
-
function getHostFs(): DocBlocksHostFsAPI {
|
|
19
|
-
const host = maybeGetDocBlocksHost();
|
|
20
|
-
if (!host) {
|
|
21
|
-
throw new Error(
|
|
22
|
-
'ElectronFileSystemProvider: docBlocksHost is not available — not running under Electron?',
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
return host.fs;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class ElectronFileSystemProvider implements FileSystemProvider {
|
|
29
|
-
readonly id: string;
|
|
30
|
-
readonly label: string;
|
|
31
|
-
readonly v2: ElectronFileSystemProviderV2;
|
|
32
|
-
|
|
33
|
-
private readonly rootPath: string;
|
|
34
|
-
|
|
35
|
-
constructor(id: string, label: string, rootPath: string) {
|
|
36
|
-
this.id = id;
|
|
37
|
-
this.label = label;
|
|
38
|
-
this.rootPath = rootPath;
|
|
39
|
-
this.v2 = new ElectronFileSystemProviderV2(id, label, rootPath);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Absolute path this provider is rooted at. */
|
|
43
|
-
getRootPath(): string {
|
|
44
|
-
return this.rootPath;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
readFile(path: string): Promise<string | null> {
|
|
48
|
-
return getHostFs().readFile(this.id, path);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
writeFile(path: string, content: string): Promise<void> {
|
|
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);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
delete(path: string): Promise<void> {
|
|
64
|
-
return getHostFs().delete(this.id, path);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
rename(oldPath: string, newPath: string): Promise<void> {
|
|
68
|
-
return getHostFs().rename(this.id, oldPath, newPath);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
readDirectory(path: string): Promise<FileSystemEntry[]> {
|
|
72
|
-
return getHostFs().readDirectory(this.id, path);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
exists(path: string): Promise<boolean> {
|
|
76
|
-
return getHostFs().exists(this.id, path);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
createDirectory(path: string): Promise<void> {
|
|
80
|
-
return getHostFs().createDirectory(this.id, path);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
stat(path: string): Promise<FileMeta | null> {
|
|
84
|
-
return getHostFs().stat(this.id, path);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
readBinary(path: string): Promise<ArrayBuffer | null> {
|
|
88
|
-
return getHostFs().readBinary(this.id, path);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
writeBinary(path: string, data: ArrayBuffer | Uint8Array): Promise<void> {
|
|
92
|
-
return getHostFs().writeBinary(this.id, path, data);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Subscribe to external change notifications under this root. */
|
|
96
|
-
watch(onChange: (changedPath: string) => void): () => void {
|
|
97
|
-
return getHostFs().watch(this.id, onChange);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* createFileMediaProvider — per-file media storage following the pandoc /
|
|
3
|
-
* Word convention: a markdown file `notes.md` gets a sibling folder
|
|
4
|
-
* `notes_files/` that holds its images, audio, and video.
|
|
5
|
-
*
|
|
6
|
-
* Given:
|
|
7
|
-
* • `container` — a ContentContainer scoped to the markdown file's
|
|
8
|
-
* parent directory (so `readFile('notes_files/image.png')` maps to the
|
|
9
|
-
* parent-relative path)
|
|
10
|
-
* • `markdownBasename` — e.g. `"notes.md"`
|
|
11
|
-
*
|
|
12
|
-
* Returns a MediaProvider that:
|
|
13
|
-
* • Writes new media under `{basename}_files/{name}` in the parent dir
|
|
14
|
-
* • Returns the folder-qualified path (`notes_files/image.png`) from
|
|
15
|
-
* addMedia so the markdown stays portable outside DocBlocks
|
|
16
|
-
* • Resolves both bare (`image.png`) and folder-qualified
|
|
17
|
-
* (`notes_files/image.png`) references — so legacy markdown and
|
|
18
|
-
* exports from other tools both work
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import type { MediaProvider, MediaEntry } from '@bendyline/squisq/schemas';
|
|
22
|
-
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
23
|
-
|
|
24
|
-
function stripExt(name: string): string {
|
|
25
|
-
return name.replace(/\.[^.]+$/, '');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function createFileMediaProvider(
|
|
29
|
-
container: ContentContainer,
|
|
30
|
-
markdownBasename: string,
|
|
31
|
-
): MediaProvider {
|
|
32
|
-
const folder = stripExt(markdownBasename) + '_files';
|
|
33
|
-
const prefix = folder + '/';
|
|
34
|
-
const blobUrlCache = new Map<string, string>();
|
|
35
|
-
|
|
36
|
-
function toKey(ref: string): string {
|
|
37
|
-
const clean = ref.replace(/^\/+/, '');
|
|
38
|
-
return clean.startsWith(prefix) ? clean : prefix + clean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
async resolveUrl(ref: string): Promise<string> {
|
|
43
|
-
const key = toKey(ref);
|
|
44
|
-
const cached = blobUrlCache.get(key);
|
|
45
|
-
if (cached) return cached;
|
|
46
|
-
|
|
47
|
-
const data = await container.readFile(key);
|
|
48
|
-
if (!data) return ref;
|
|
49
|
-
|
|
50
|
-
const entries = await container.listFiles();
|
|
51
|
-
const entry = entries.find((e) => e.path === key);
|
|
52
|
-
const mimeType = entry?.mimeType ?? 'application/octet-stream';
|
|
53
|
-
|
|
54
|
-
const url = URL.createObjectURL(new Blob([data], { type: mimeType }));
|
|
55
|
-
blobUrlCache.set(key, url);
|
|
56
|
-
return url;
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
async listMedia(): Promise<MediaEntry[]> {
|
|
60
|
-
const entries = await container.listFiles(prefix);
|
|
61
|
-
return entries
|
|
62
|
-
.filter((e) => !e.path.toLowerCase().endsWith('.md'))
|
|
63
|
-
.map((e) => ({
|
|
64
|
-
name: e.path,
|
|
65
|
-
mimeType: e.mimeType,
|
|
66
|
-
size: e.size,
|
|
67
|
-
}));
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
async addMedia(
|
|
71
|
-
name: string,
|
|
72
|
-
data: ArrayBuffer | Blob | Uint8Array,
|
|
73
|
-
mimeType: string,
|
|
74
|
-
): Promise<string> {
|
|
75
|
-
const key = toKey(name);
|
|
76
|
-
const cached = blobUrlCache.get(key);
|
|
77
|
-
if (cached) {
|
|
78
|
-
URL.revokeObjectURL(cached);
|
|
79
|
-
blobUrlCache.delete(key);
|
|
80
|
-
}
|
|
81
|
-
const buffer = data instanceof Blob ? new Uint8Array(await data.arrayBuffer()) : data;
|
|
82
|
-
await container.writeFile(key, buffer, mimeType);
|
|
83
|
-
return key;
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
async removeMedia(ref: string): Promise<void> {
|
|
87
|
-
const key = toKey(ref);
|
|
88
|
-
const cached = blobUrlCache.get(key);
|
|
89
|
-
if (cached) {
|
|
90
|
-
URL.revokeObjectURL(cached);
|
|
91
|
-
blobUrlCache.delete(key);
|
|
92
|
-
}
|
|
93
|
-
await container.removeFile(key);
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
dispose(): void {
|
|
97
|
-
for (const url of blobUrlCache.values()) {
|
|
98
|
-
URL.revokeObjectURL(url);
|
|
99
|
-
}
|
|
100
|
-
blobUrlCache.clear();
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
}
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FileSystemContentContainer — a ContentContainer backed by any
|
|
3
|
-
* FileSystemProvider, scoped to a sub-path (e.g., ".docblocks/media/").
|
|
4
|
-
*
|
|
5
|
-
* Used by the Electron desktop app so media lives inside the workspace
|
|
6
|
-
* folder (visible as regular files) rather than in a separate IndexedDB
|
|
7
|
-
* origin that only the app can see.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { ContentContainer, ContentEntry } from '@bendyline/squisq/storage';
|
|
11
|
-
import { findDocumentPath } from '@bendyline/squisq/storage';
|
|
12
|
-
import { FsError } from './fs-error.js';
|
|
13
|
-
import { getFileSystemProviderV2, type FileSystemProvider } from './types.js';
|
|
14
|
-
import { parseWorkspacePath, workspacePathContains, workspacePathJoin } from './workspace-path.js';
|
|
15
|
-
|
|
16
|
-
const EXTENSION_MIME_MAP: Record<string, string> = {
|
|
17
|
-
'.md': 'text/markdown',
|
|
18
|
-
'.txt': 'text/plain',
|
|
19
|
-
'.json': 'application/json',
|
|
20
|
-
'.jpg': 'image/jpeg',
|
|
21
|
-
'.jpeg': 'image/jpeg',
|
|
22
|
-
'.png': 'image/png',
|
|
23
|
-
'.gif': 'image/gif',
|
|
24
|
-
'.svg': 'image/svg+xml',
|
|
25
|
-
'.webp': 'image/webp',
|
|
26
|
-
'.avif': 'image/avif',
|
|
27
|
-
'.mp4': 'video/mp4',
|
|
28
|
-
'.webm': 'video/webm',
|
|
29
|
-
'.mp3': 'audio/mpeg',
|
|
30
|
-
'.wav': 'audio/wav',
|
|
31
|
-
'.ogg': 'audio/ogg',
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
function guessMimeType(path: string): string {
|
|
35
|
-
const dot = path.lastIndexOf('.');
|
|
36
|
-
if (dot === -1) return 'application/octet-stream';
|
|
37
|
-
const ext = path.slice(dot).toLowerCase();
|
|
38
|
-
return EXTENSION_MIME_MAP[ext] ?? 'application/octet-stream';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function joinPrefix(prefix: string, p: string): string {
|
|
42
|
-
return workspacePathJoin(parseWorkspacePath(prefix), p);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export class FileSystemContentContainer implements ContentContainer {
|
|
46
|
-
private readonly prefix: string;
|
|
47
|
-
|
|
48
|
-
constructor(
|
|
49
|
-
private readonly provider: FileSystemProvider,
|
|
50
|
-
prefix = '.docblocks/media',
|
|
51
|
-
) {
|
|
52
|
-
this.prefix = parseWorkspacePath(prefix);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async readFile(path: string): Promise<ArrayBuffer | null> {
|
|
56
|
-
const full = joinPrefix(this.prefix, path);
|
|
57
|
-
const providerV2 = getFileSystemProviderV2(this.provider);
|
|
58
|
-
if (providerV2) return (await providerV2.readFile(parseWorkspacePath(full)))?.data ?? null;
|
|
59
|
-
const binary = await this.provider.readBinary(full);
|
|
60
|
-
if (binary) return binary;
|
|
61
|
-
// IndexedDB-backed workspaces store text via `writeFile(string)` and
|
|
62
|
-
// binary via `writeBinary(ArrayBuffer)` under separate keys, so a
|
|
63
|
-
// markdown file written through the text API is invisible to
|
|
64
|
-
// `readBinary`. Fall back to text-then-UTF-8 so consumers like the
|
|
65
|
-
// recursive HTML export can resolve sibling `.md` links uniformly
|
|
66
|
-
// across browser and native workspaces.
|
|
67
|
-
const text = await this.provider.readFile(full);
|
|
68
|
-
if (text === null) return null;
|
|
69
|
-
return new TextEncoder().encode(text).buffer as ArrayBuffer;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async writeFile(path: string, data: ArrayBuffer | Uint8Array, _mimeType?: string): Promise<void> {
|
|
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);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async removeFile(path: string): Promise<void> {
|
|
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);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async listFiles(prefix?: string): Promise<ContentEntry[]> {
|
|
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
|
-
};
|
|
104
|
-
const walk = async (dir: string) => {
|
|
105
|
-
let children: readonly ListedEntry[];
|
|
106
|
-
try {
|
|
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;
|
|
116
|
-
}
|
|
117
|
-
for (const child of children) {
|
|
118
|
-
if (child.kind === 'directory') {
|
|
119
|
-
await walk(child.path);
|
|
120
|
-
} else {
|
|
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);
|
|
125
|
-
if (prefix && !rel.startsWith(prefix)) continue;
|
|
126
|
-
const size = child.size ?? (await this.provider.stat(child.path))?.size ?? 0;
|
|
127
|
-
entries.push({
|
|
128
|
-
path: rel,
|
|
129
|
-
mimeType: guessMimeType(rel),
|
|
130
|
-
size,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
await walk(root);
|
|
136
|
-
return entries;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async exists(path: string): Promise<boolean> {
|
|
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);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async getDocumentPath(): Promise<string | null> {
|
|
148
|
-
return findDocumentPath(await this.listFiles());
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async readDocument(): Promise<string | null> {
|
|
152
|
-
const docPath = await this.getDocumentPath();
|
|
153
|
-
if (!docPath) return null;
|
|
154
|
-
const data = await this.readFile(docPath);
|
|
155
|
-
if (!data) return null;
|
|
156
|
-
return new TextDecoder().decode(data);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async writeDocument(markdown: string, filename?: string): Promise<void> {
|
|
160
|
-
const name = filename ?? 'index.md';
|
|
161
|
-
const data = new TextEncoder().encode(markdown);
|
|
162
|
-
await this.writeFile(name, data, 'text/markdown');
|
|
163
|
-
}
|
|
164
|
-
}
|
|
@@ -1,234 +0,0 @@
|
|
|
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
|
-
}
|