@bendyline/docblocks 0.1.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.
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/chunk-GUNM43XZ.js +63 -0
- package/dist/chunk-GUNM43XZ.js.map +1 -0
- package/dist/chunk-NSVTXALR.js +559 -0
- package/dist/chunk-NSVTXALR.js.map +1 -0
- package/dist/filesystem/index.d.ts +5 -0
- package/dist/filesystem/index.d.ts.map +1 -0
- package/dist/filesystem/index.js +4 -0
- package/dist/filesystem/index.js.map +1 -0
- package/dist/filesystem/indexeddb-content-container.d.ts +20 -0
- package/dist/filesystem/indexeddb-content-container.d.ts.map +1 -0
- package/dist/filesystem/indexeddb-content-container.js +93 -0
- package/dist/filesystem/indexeddb-content-container.js.map +1 -0
- package/dist/filesystem/indexeddb-provider.d.ts +32 -0
- package/dist/filesystem/indexeddb-provider.d.ts.map +1 -0
- package/dist/filesystem/indexeddb-provider.js +253 -0
- package/dist/filesystem/indexeddb-provider.js.map +1 -0
- package/dist/filesystem/native-provider.d.ts +44 -0
- package/dist/filesystem/native-provider.d.ts.map +1 -0
- package/dist/filesystem/native-provider.js +302 -0
- package/dist/filesystem/native-provider.js.map +1 -0
- package/dist/filesystem/types.d.ts +50 -0
- package/dist/filesystem/types.d.ts.map +1 -0
- package/dist/filesystem/types.js +8 -0
- package/dist/filesystem/types.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/workspace/index.d.ts +3 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +2 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/types.d.ts +15 -0
- package/dist/workspace/types.d.ts.map +1 -0
- package/dist/workspace/types.js +6 -0
- package/dist/workspace/types.js.map +1 -0
- package/dist/workspace/workspace-manager.d.ts +35 -0
- package/dist/workspace/workspace-manager.d.ts.map +1 -0
- package/dist/workspace/workspace-manager.js +82 -0
- package/dist/workspace/workspace-manager.js.map +1 -0
- package/package.json +59 -0
- package/src/filesystem/index.ts +20 -0
- package/src/filesystem/indexeddb-content-container.ts +104 -0
- package/src/filesystem/indexeddb-provider.ts +299 -0
- package/src/filesystem/native-provider.ts +348 -0
- package/src/filesystem/types.ts +69 -0
- package/src/index.ts +8 -0
- package/src/workspace/index.ts +10 -0
- package/src/workspace/types.ts +15 -0
- package/src/workspace/workspace-manager.ts +90 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeFileSystemProvider — wraps the File System Access API
|
|
3
|
+
* (window.showDirectoryPicker / FileSystemDirectoryHandle).
|
|
4
|
+
*
|
|
5
|
+
* Progressive enhancement: only available in browsers that support
|
|
6
|
+
* the API (Chrome, Edge). Feature-detect with `isNativeFileSystemSupported()`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { FileSystemProvider, FileSystemEntry, FileMeta } from './types.js';
|
|
10
|
+
|
|
11
|
+
// ── Feature detection ──────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export function isNativeFileSystemSupported(): boolean {
|
|
14
|
+
return typeof globalThis !== 'undefined' && 'showDirectoryPicker' in globalThis;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ── Handle persistence (IndexedDB, structured clone) ───────────────
|
|
18
|
+
|
|
19
|
+
const HANDLE_DB_NAME = 'docblocks-handles';
|
|
20
|
+
const HANDLE_STORE_NAME = 'directory-handles';
|
|
21
|
+
|
|
22
|
+
function openHandleDB(): Promise<IDBDatabase> {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const req = indexedDB.open(HANDLE_DB_NAME, 1);
|
|
25
|
+
req.onupgradeneeded = () => {
|
|
26
|
+
req.result.createObjectStore(HANDLE_STORE_NAME);
|
|
27
|
+
};
|
|
28
|
+
req.onsuccess = () => resolve(req.result);
|
|
29
|
+
req.onerror = () => reject(req.error);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Persist a FileSystemDirectoryHandle so it survives page reloads. */
|
|
34
|
+
export async function storeDirectoryHandle(
|
|
35
|
+
workspaceId: string,
|
|
36
|
+
handle: FileSystemDirectoryHandle,
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
const db = await openHandleDB();
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const tx = db.transaction(HANDLE_STORE_NAME, 'readwrite');
|
|
41
|
+
tx.objectStore(HANDLE_STORE_NAME).put(handle, workspaceId);
|
|
42
|
+
tx.oncomplete = () => {
|
|
43
|
+
db.close();
|
|
44
|
+
resolve();
|
|
45
|
+
};
|
|
46
|
+
tx.onerror = () => {
|
|
47
|
+
db.close();
|
|
48
|
+
reject(tx.error);
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Retrieve a previously stored handle. Returns null if not found. */
|
|
54
|
+
export async function loadDirectoryHandle(
|
|
55
|
+
workspaceId: string,
|
|
56
|
+
): Promise<FileSystemDirectoryHandle | null> {
|
|
57
|
+
const db = await openHandleDB();
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const tx = db.transaction(HANDLE_STORE_NAME, 'readonly');
|
|
60
|
+
const req = tx.objectStore(HANDLE_STORE_NAME).get(workspaceId);
|
|
61
|
+
req.onsuccess = () => {
|
|
62
|
+
db.close();
|
|
63
|
+
resolve(req.result ?? null);
|
|
64
|
+
};
|
|
65
|
+
req.onerror = () => {
|
|
66
|
+
db.close();
|
|
67
|
+
reject(req.error);
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Remove a stored handle (e.g. when deleting a workspace). */
|
|
73
|
+
export async function removeDirectoryHandle(workspaceId: string): Promise<void> {
|
|
74
|
+
const db = await openHandleDB();
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const tx = db.transaction(HANDLE_STORE_NAME, 'readwrite');
|
|
77
|
+
tx.objectStore(HANDLE_STORE_NAME).delete(workspaceId);
|
|
78
|
+
tx.oncomplete = () => {
|
|
79
|
+
db.close();
|
|
80
|
+
resolve();
|
|
81
|
+
};
|
|
82
|
+
tx.onerror = () => {
|
|
83
|
+
db.close();
|
|
84
|
+
reject(tx.error);
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function normalisePath(p: string): string {
|
|
92
|
+
return p.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\//, '').replace(/\/$/, '');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Walk a chain of path segments to reach a FileSystemDirectoryHandle.
|
|
97
|
+
* Returns null if any segment is missing.
|
|
98
|
+
*/
|
|
99
|
+
async function resolveDir(
|
|
100
|
+
root: FileSystemDirectoryHandle,
|
|
101
|
+
dirPath: string,
|
|
102
|
+
): Promise<FileSystemDirectoryHandle | null> {
|
|
103
|
+
if (!dirPath) return root;
|
|
104
|
+
const parts = dirPath.split('/');
|
|
105
|
+
let current = root;
|
|
106
|
+
for (const part of parts) {
|
|
107
|
+
try {
|
|
108
|
+
current = await current.getDirectoryHandle(part);
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return current;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Walk path segments, creating directories as needed.
|
|
118
|
+
*/
|
|
119
|
+
async function resolveDirCreate(
|
|
120
|
+
root: FileSystemDirectoryHandle,
|
|
121
|
+
dirPath: string,
|
|
122
|
+
): Promise<FileSystemDirectoryHandle> {
|
|
123
|
+
if (!dirPath) return root;
|
|
124
|
+
const parts = dirPath.split('/');
|
|
125
|
+
let current = root;
|
|
126
|
+
for (const part of parts) {
|
|
127
|
+
current = await current.getDirectoryHandle(part, { create: true });
|
|
128
|
+
}
|
|
129
|
+
return current;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parentDir(p: string): string {
|
|
133
|
+
const idx = p.lastIndexOf('/');
|
|
134
|
+
return idx === -1 ? '' : p.slice(0, idx);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function baseName(p: string): string {
|
|
138
|
+
const idx = p.lastIndexOf('/');
|
|
139
|
+
return idx === -1 ? p : p.slice(idx + 1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Implementation ─────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
export class NativeFileSystemProvider implements FileSystemProvider {
|
|
145
|
+
readonly id: string;
|
|
146
|
+
readonly label: string;
|
|
147
|
+
|
|
148
|
+
private root: FileSystemDirectoryHandle;
|
|
149
|
+
|
|
150
|
+
constructor(id: string, root: FileSystemDirectoryHandle) {
|
|
151
|
+
this.id = id;
|
|
152
|
+
this.label = root.name;
|
|
153
|
+
this.root = root;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async readFile(path: string): Promise<string | null> {
|
|
157
|
+
const p = normalisePath(path);
|
|
158
|
+
const dir = await resolveDir(this.root, parentDir(p));
|
|
159
|
+
if (!dir) return null;
|
|
160
|
+
try {
|
|
161
|
+
const fileHandle = await dir.getFileHandle(baseName(p));
|
|
162
|
+
const file = await fileHandle.getFile();
|
|
163
|
+
return file.text();
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async writeFile(path: string, content: string): Promise<void> {
|
|
170
|
+
const p = normalisePath(path);
|
|
171
|
+
const dir = await resolveDirCreate(this.root, parentDir(p));
|
|
172
|
+
const fileHandle = await dir.getFileHandle(baseName(p), { create: true });
|
|
173
|
+
const writable = await fileHandle.createWritable();
|
|
174
|
+
await writable.write(content);
|
|
175
|
+
await writable.close();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async delete(path: string): Promise<void> {
|
|
179
|
+
const p = normalisePath(path);
|
|
180
|
+
const parent = parentDir(p);
|
|
181
|
+
const name = baseName(p);
|
|
182
|
+
const dir = await resolveDir(this.root, parent);
|
|
183
|
+
if (!dir) return;
|
|
184
|
+
await dir.removeEntry(name, { recursive: true });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async rename(oldPath: string, newPath: string): Promise<void> {
|
|
188
|
+
const op = normalisePath(oldPath);
|
|
189
|
+
const np = normalisePath(newPath);
|
|
190
|
+
|
|
191
|
+
// The File System Access API doesn't have a native rename.
|
|
192
|
+
// Read → write → delete.
|
|
193
|
+
const content = await this.readFile(op);
|
|
194
|
+
if (content !== null) {
|
|
195
|
+
await this.writeFile(np, content);
|
|
196
|
+
await this.delete(op);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Try binary
|
|
201
|
+
const binary = await this.readBinary(op);
|
|
202
|
+
if (binary !== null) {
|
|
203
|
+
await this.writeBinary(np, binary);
|
|
204
|
+
await this.delete(op);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async readDirectory(path: string): Promise<FileSystemEntry[]> {
|
|
209
|
+
const p = normalisePath(path);
|
|
210
|
+
const dir = await resolveDir(this.root, p);
|
|
211
|
+
if (!dir) return [];
|
|
212
|
+
|
|
213
|
+
const entries: FileSystemEntry[] = [];
|
|
214
|
+
for await (const [name, handle] of dir as unknown as AsyncIterable<
|
|
215
|
+
[string, FileSystemHandle]
|
|
216
|
+
>) {
|
|
217
|
+
const entryPath = p ? `${p}/${name}` : name;
|
|
218
|
+
if (handle.kind === 'directory') {
|
|
219
|
+
entries.push({ kind: 'directory', name, path: entryPath });
|
|
220
|
+
} else {
|
|
221
|
+
entries.push({ kind: 'file', name, path: entryPath });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Sort: directories first, then alphabetical
|
|
226
|
+
entries.sort((a, b) => {
|
|
227
|
+
if (a.kind !== b.kind) return a.kind === 'directory' ? -1 : 1;
|
|
228
|
+
return a.name.localeCompare(b.name);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return entries;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async exists(path: string): Promise<boolean> {
|
|
235
|
+
const p = normalisePath(path);
|
|
236
|
+
const parent = parentDir(p);
|
|
237
|
+
const name = baseName(p);
|
|
238
|
+
const dir = await resolveDir(this.root, parent);
|
|
239
|
+
if (!dir) return false;
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
await dir.getFileHandle(name);
|
|
243
|
+
return true;
|
|
244
|
+
} catch {
|
|
245
|
+
try {
|
|
246
|
+
await dir.getDirectoryHandle(name);
|
|
247
|
+
return true;
|
|
248
|
+
} catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async createDirectory(path: string): Promise<void> {
|
|
255
|
+
const p = normalisePath(path);
|
|
256
|
+
await resolveDirCreate(this.root, p);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async stat(path: string): Promise<FileMeta | null> {
|
|
260
|
+
const p = normalisePath(path);
|
|
261
|
+
const dir = await resolveDir(this.root, parentDir(p));
|
|
262
|
+
if (!dir) return null;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const fileHandle = await dir.getFileHandle(baseName(p));
|
|
266
|
+
const file = await fileHandle.getFile();
|
|
267
|
+
return {
|
|
268
|
+
name: file.name,
|
|
269
|
+
path: p,
|
|
270
|
+
size: file.size,
|
|
271
|
+
lastModified: new Date(file.lastModified).toISOString(),
|
|
272
|
+
};
|
|
273
|
+
} catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async readBinary(path: string): Promise<ArrayBuffer | null> {
|
|
279
|
+
const p = normalisePath(path);
|
|
280
|
+
const dir = await resolveDir(this.root, parentDir(p));
|
|
281
|
+
if (!dir) return null;
|
|
282
|
+
try {
|
|
283
|
+
const fileHandle = await dir.getFileHandle(baseName(p));
|
|
284
|
+
const file = await fileHandle.getFile();
|
|
285
|
+
return file.arrayBuffer();
|
|
286
|
+
} catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async writeBinary(path: string, data: ArrayBuffer | Uint8Array): Promise<void> {
|
|
292
|
+
const p = normalisePath(path);
|
|
293
|
+
const dir = await resolveDirCreate(this.root, parentDir(p));
|
|
294
|
+
const fileHandle = await dir.getFileHandle(baseName(p), { create: true });
|
|
295
|
+
const writable = await fileHandle.createWritable();
|
|
296
|
+
if (data instanceof ArrayBuffer) {
|
|
297
|
+
await writable.write(data);
|
|
298
|
+
} else {
|
|
299
|
+
await writable.write(data.buffer as ArrayBuffer);
|
|
300
|
+
}
|
|
301
|
+
await writable.close();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Prompt the user to pick a local folder and return a NativeFileSystemProvider.
|
|
307
|
+
* The directory handle is persisted in IndexedDB so it can be restored later.
|
|
308
|
+
* Throws if the user cancels or the API is unsupported.
|
|
309
|
+
*/
|
|
310
|
+
export async function openNativeFolder(): Promise<NativeFileSystemProvider> {
|
|
311
|
+
if (!isNativeFileSystemSupported()) {
|
|
312
|
+
throw new Error('File System Access API is not supported in this browser');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const handle = await (
|
|
316
|
+
globalThis as unknown as { showDirectoryPicker: () => Promise<FileSystemDirectoryHandle> }
|
|
317
|
+
).showDirectoryPicker();
|
|
318
|
+
const id = `native-${handle.name}-${Date.now()}`;
|
|
319
|
+
await storeDirectoryHandle(id, handle);
|
|
320
|
+
return new NativeFileSystemProvider(id, handle);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Restore a previously opened native folder from a persisted handle.
|
|
325
|
+
* Re-requests read/write permission (browser will show a prompt).
|
|
326
|
+
* Returns null if the handle is not found or permission is denied.
|
|
327
|
+
*/
|
|
328
|
+
export async function restoreNativeFolder(
|
|
329
|
+
workspaceId: string,
|
|
330
|
+
): Promise<NativeFileSystemProvider | null> {
|
|
331
|
+
const handle = await loadDirectoryHandle(workspaceId);
|
|
332
|
+
if (!handle) return null;
|
|
333
|
+
|
|
334
|
+
// Verify/request permission
|
|
335
|
+
const opts = { mode: 'readwrite' as const };
|
|
336
|
+
const h = handle as FileSystemDirectoryHandle & {
|
|
337
|
+
queryPermission(desc: { mode: string }): Promise<string>;
|
|
338
|
+
requestPermission(desc: { mode: string }): Promise<string>;
|
|
339
|
+
};
|
|
340
|
+
if ((await h.queryPermission(opts)) === 'granted') {
|
|
341
|
+
return new NativeFileSystemProvider(workspaceId, handle);
|
|
342
|
+
}
|
|
343
|
+
if ((await h.requestPermission(opts)) === 'granted') {
|
|
344
|
+
return new NativeFileSystemProvider(workspaceId, handle);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileSystemProvider — abstract interface for a virtual filesystem.
|
|
3
|
+
*
|
|
4
|
+
* Implementations back onto IndexedDB (for browser-local storage) or
|
|
5
|
+
* the File System Access API (for native folder access).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── Entry types ────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface FileEntry {
|
|
11
|
+
kind: 'file';
|
|
12
|
+
name: string;
|
|
13
|
+
path: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FolderEntry {
|
|
17
|
+
kind: 'directory';
|
|
18
|
+
name: string;
|
|
19
|
+
path: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type FileSystemEntry = FileEntry | FolderEntry;
|
|
23
|
+
|
|
24
|
+
export interface FileMeta {
|
|
25
|
+
name: string;
|
|
26
|
+
path: string;
|
|
27
|
+
size: number;
|
|
28
|
+
lastModified: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Provider interface ─────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export interface FileSystemProvider {
|
|
34
|
+
/** Unique identifier for this provider instance. */
|
|
35
|
+
readonly id: string;
|
|
36
|
+
|
|
37
|
+
/** Human-readable label (e.g., folder name or "Browser Storage"). */
|
|
38
|
+
readonly label: string;
|
|
39
|
+
|
|
40
|
+
/** Read the text content of a file. Returns null if the file doesn't exist. */
|
|
41
|
+
readFile(path: string): Promise<string | null>;
|
|
42
|
+
|
|
43
|
+
/** Write text content to a file, creating it (and parent dirs) if needed. */
|
|
44
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
45
|
+
|
|
46
|
+
/** Delete a file or empty directory. */
|
|
47
|
+
delete(path: string): Promise<void>;
|
|
48
|
+
|
|
49
|
+
/** Rename or move an entry. */
|
|
50
|
+
rename(oldPath: string, newPath: string): Promise<void>;
|
|
51
|
+
|
|
52
|
+
/** List immediate children of a directory. */
|
|
53
|
+
readDirectory(path: string): Promise<FileSystemEntry[]>;
|
|
54
|
+
|
|
55
|
+
/** Check whether a path exists. */
|
|
56
|
+
exists(path: string): Promise<boolean>;
|
|
57
|
+
|
|
58
|
+
/** Create a directory (and parents if needed). */
|
|
59
|
+
createDirectory(path: string): Promise<void>;
|
|
60
|
+
|
|
61
|
+
/** Get metadata for a file. Returns null if not found. */
|
|
62
|
+
stat(path: string): Promise<FileMeta | null>;
|
|
63
|
+
|
|
64
|
+
/** Read raw binary content. Returns null if not found. */
|
|
65
|
+
readBinary(path: string): Promise<ArrayBuffer | null>;
|
|
66
|
+
|
|
67
|
+
/** Write raw binary content. */
|
|
68
|
+
writeBinary(path: string, data: ArrayBuffer | Uint8Array): Promise<void>;
|
|
69
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace — a named binding to a FileSystemProvider, representing
|
|
3
|
+
* the user's current working context (a folder of markdown files).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface WorkspaceDescriptor {
|
|
7
|
+
/** Unique stable identifier (persisted in IndexedDB). */
|
|
8
|
+
id: string;
|
|
9
|
+
/** User-visible label. */
|
|
10
|
+
name: string;
|
|
11
|
+
/** 'indexeddb' for browser-local storage, 'native' for File System Access API. */
|
|
12
|
+
type: 'indexeddb' | 'native';
|
|
13
|
+
/** ISO timestamp of last access. */
|
|
14
|
+
lastOpened: string;
|
|
15
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkspaceManager — manages the list of known workspaces in IndexedDB
|
|
3
|
+
* and provides helpers for creating / switching / removing them.
|
|
4
|
+
*
|
|
5
|
+
* The actual FileSystemProvider instances are created by the caller
|
|
6
|
+
* (since native providers need a DirectoryHandle from user interaction).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { LocalForageAdapter } from '@bendyline/squisq/storage';
|
|
10
|
+
import type { WorkspaceDescriptor } from './types.js';
|
|
11
|
+
|
|
12
|
+
const DB_NAME = 'docblocks-workspaces';
|
|
13
|
+
const STORE_NAME = 'workspaces';
|
|
14
|
+
const LIST_KEY = 'workspace-list';
|
|
15
|
+
const DEFAULT_WORKSPACE_ID = 'default';
|
|
16
|
+
|
|
17
|
+
const store = new LocalForageAdapter({
|
|
18
|
+
name: DB_NAME,
|
|
19
|
+
storeName: STORE_NAME,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get the list of all known workspace descriptors.
|
|
24
|
+
*/
|
|
25
|
+
export async function listWorkspaces(): Promise<WorkspaceDescriptor[]> {
|
|
26
|
+
const list = await store.get<WorkspaceDescriptor[]>(LIST_KEY);
|
|
27
|
+
return list ?? [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get a specific workspace descriptor by id.
|
|
32
|
+
*/
|
|
33
|
+
export async function getWorkspace(id: string): Promise<WorkspaceDescriptor | null> {
|
|
34
|
+
const list = await listWorkspaces();
|
|
35
|
+
return list.find((w) => w.id === id) ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Add or update a workspace descriptor. If a workspace with the same id
|
|
40
|
+
* already exists, it is replaced.
|
|
41
|
+
*/
|
|
42
|
+
export async function saveWorkspace(workspace: WorkspaceDescriptor): Promise<void> {
|
|
43
|
+
const list = await listWorkspaces();
|
|
44
|
+
const idx = list.findIndex((w) => w.id === workspace.id);
|
|
45
|
+
if (idx >= 0) {
|
|
46
|
+
list[idx] = workspace;
|
|
47
|
+
} else {
|
|
48
|
+
list.push(workspace);
|
|
49
|
+
}
|
|
50
|
+
await store.set(LIST_KEY, list);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Remove a workspace descriptor by id.
|
|
55
|
+
*/
|
|
56
|
+
export async function removeWorkspace(id: string): Promise<void> {
|
|
57
|
+
const list = await listWorkspaces();
|
|
58
|
+
const filtered = list.filter((w) => w.id !== id);
|
|
59
|
+
await store.set(LIST_KEY, filtered);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Touch the lastOpened timestamp on a workspace.
|
|
64
|
+
*/
|
|
65
|
+
export async function touchWorkspace(id: string): Promise<void> {
|
|
66
|
+
const list = await listWorkspaces();
|
|
67
|
+
const workspace = list.find((w) => w.id === id);
|
|
68
|
+
if (workspace) {
|
|
69
|
+
workspace.lastOpened = new Date().toISOString();
|
|
70
|
+
await store.set(LIST_KEY, list);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Ensure a default IndexedDB workspace exists. Called on app startup.
|
|
76
|
+
* Returns the descriptor.
|
|
77
|
+
*/
|
|
78
|
+
export async function ensureDefaultWorkspace(): Promise<WorkspaceDescriptor> {
|
|
79
|
+
const existing = await getWorkspace(DEFAULT_WORKSPACE_ID);
|
|
80
|
+
if (existing) return existing;
|
|
81
|
+
|
|
82
|
+
const descriptor: WorkspaceDescriptor = {
|
|
83
|
+
id: DEFAULT_WORKSPACE_ID,
|
|
84
|
+
name: 'My Documents',
|
|
85
|
+
type: 'indexeddb',
|
|
86
|
+
lastOpened: new Date().toISOString(),
|
|
87
|
+
};
|
|
88
|
+
await saveWorkspace(descriptor);
|
|
89
|
+
return descriptor;
|
|
90
|
+
}
|