@aprovan/patchwork 0.1.0 → 0.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.
Files changed (71) hide show
  1. package/.github/workflows/publish.yml +1 -1
  2. package/.vscode/launch.json +19 -0
  3. package/README.md +24 -0
  4. package/apps/chat/package.json +4 -4
  5. package/apps/chat/vite.config.ts +8 -8
  6. package/docs/specs/directory-sync.md +822 -0
  7. package/docs/specs/patchwork-vscode.md +625 -0
  8. package/package.json +2 -2
  9. package/packages/compiler/package.json +3 -2
  10. package/packages/compiler/src/index.ts +13 -14
  11. package/packages/compiler/src/vfs/backends/http.ts +139 -0
  12. package/packages/compiler/src/vfs/backends/indexeddb.ts +185 -24
  13. package/packages/compiler/src/vfs/backends/memory.ts +166 -0
  14. package/packages/compiler/src/vfs/core/index.ts +26 -0
  15. package/packages/compiler/src/vfs/core/types.ts +93 -0
  16. package/packages/compiler/src/vfs/core/utils.ts +42 -0
  17. package/packages/compiler/src/vfs/core/virtual-fs.ts +120 -0
  18. package/packages/compiler/src/vfs/index.ts +37 -5
  19. package/packages/compiler/src/vfs/project.ts +16 -16
  20. package/packages/compiler/src/vfs/store.ts +183 -19
  21. package/packages/compiler/src/vfs/sync/differ.ts +47 -0
  22. package/packages/compiler/src/vfs/sync/engine.ts +398 -0
  23. package/packages/compiler/src/vfs/sync/index.ts +3 -0
  24. package/packages/compiler/src/vfs/sync/resolver.ts +46 -0
  25. package/packages/compiler/src/vfs/types.ts +1 -8
  26. package/packages/compiler/tsup.config.ts +5 -5
  27. package/packages/editor/package.json +1 -1
  28. package/packages/editor/src/components/CodeBlockExtension.tsx +1 -1
  29. package/packages/editor/src/components/CodePreview.tsx +59 -1
  30. package/packages/editor/src/components/edit/CodeBlockView.tsx +72 -0
  31. package/packages/editor/src/components/edit/EditModal.tsx +169 -28
  32. package/packages/editor/src/components/edit/FileTree.tsx +67 -13
  33. package/packages/editor/src/components/edit/MediaPreview.tsx +106 -0
  34. package/packages/editor/src/components/edit/SaveConfirmDialog.tsx +60 -0
  35. package/packages/editor/src/components/edit/fileTypes.ts +125 -0
  36. package/packages/editor/src/components/edit/index.ts +4 -0
  37. package/packages/editor/src/components/edit/types.ts +3 -0
  38. package/packages/editor/src/components/edit/useEditSession.ts +22 -4
  39. package/packages/editor/src/index.ts +17 -0
  40. package/packages/editor/src/lib/diff.ts +2 -1
  41. package/packages/editor/src/lib/vfs.ts +28 -10
  42. package/packages/editor/tsup.config.ts +10 -5
  43. package/packages/stitchery/package.json +5 -3
  44. package/packages/stitchery/src/server/index.ts +57 -57
  45. package/packages/stitchery/src/server/vfs-routes.ts +246 -56
  46. package/packages/stitchery/tsup.config.ts +5 -5
  47. package/packages/utcp/package.json +3 -2
  48. package/packages/utcp/tsconfig.json +6 -2
  49. package/packages/utcp/tsup.config.ts +6 -6
  50. package/packages/vscode/README.md +31 -0
  51. package/packages/vscode/media/outline.png +0 -0
  52. package/packages/vscode/media/outline.svg +70 -0
  53. package/packages/vscode/media/patchwork.png +0 -0
  54. package/packages/vscode/media/patchwork.svg +72 -0
  55. package/packages/vscode/node_modules/.bin/jiti +17 -0
  56. package/packages/vscode/node_modules/.bin/tsc +17 -0
  57. package/packages/vscode/node_modules/.bin/tsserver +17 -0
  58. package/packages/vscode/node_modules/.bin/tsup +17 -0
  59. package/packages/vscode/node_modules/.bin/tsup-node +17 -0
  60. package/packages/vscode/node_modules/.bin/tsx +17 -0
  61. package/packages/vscode/package.json +136 -0
  62. package/packages/vscode/src/extension.ts +612 -0
  63. package/packages/vscode/src/providers/PatchworkFileSystemProvider.ts +205 -0
  64. package/packages/vscode/src/providers/PatchworkTreeProvider.ts +177 -0
  65. package/packages/vscode/src/providers/PreviewPanelProvider.ts +536 -0
  66. package/packages/vscode/src/services/EditService.ts +24 -0
  67. package/packages/vscode/src/services/EmbeddedStitchery.ts +82 -0
  68. package/packages/vscode/tsconfig.json +13 -0
  69. package/packages/vscode/tsup.config.ts +11 -0
  70. package/packages/compiler/src/vfs/backends/local-fs.ts +0 -41
  71. package/packages/compiler/src/vfs/backends/s3.ts +0 -60
@@ -0,0 +1,822 @@
1
+ # Patchwork Directory Sync
2
+
3
+ Patchwork now has a full virtual filesystem setup, where we can load files/directories and edit files individually, saving them to the remote store as-needed.
4
+
5
+ Extend this functionality, allowing for a generic implementation where we sync changes with remote sources (a local directory, later an S3 bucket...)
6
+
7
+ Generally, we want to maintain compatibility with Node-like FS operations, as we need a lot of functionality to run in the browser.
8
+
9
+ ---
10
+
11
+ ## Executive Summary
12
+
13
+ This document outlines a comprehensive plan to refactor Patchwork's virtual filesystem into a bidirectional sync system supporting multiple backends (local directories, S3, IndexedDB) with proper change tracking, conflict resolution, and event-based updates.
14
+
15
+ **Key Goals:**
16
+ - Unified `FSProvider` interface matching Node.js `fs/promises` semantics
17
+ - Bidirectional sync between in-memory state and remote backends
18
+ - Event-driven change propagation (watch capabilities)
19
+ - Clean separation between storage backends and sync logic
20
+ - Breaking changes acceptable; provide clear migration path
21
+
22
+ ---
23
+
24
+ ## Current Architecture
25
+
26
+ ### Package Structure
27
+
28
+ ```
29
+ packages/compiler/src/vfs/
30
+ ├── backends/
31
+ │ ├── indexeddb.ts # Browser storage
32
+ │ ├── local-fs.ts # HTTP → stitchery server
33
+ │ └── s3.ts # Direct S3 (no auth signing)
34
+ ├── project.ts # VirtualProject utilities
35
+ ├── store.ts # VFSStore class
36
+ └── types.ts # Core interfaces
37
+ ```
38
+
39
+ ### Existing Interfaces
40
+
41
+ ```typescript
42
+ // Current StorageBackend - flat key/value only
43
+ interface StorageBackend {
44
+ get(path: string): Promise<string | null>;
45
+ put(path: string, content: string): Promise<void>;
46
+ delete(path: string): Promise<void>;
47
+ list(prefix?: string): Promise<string[]>;
48
+ exists(path: string): Promise<boolean>;
49
+ }
50
+
51
+ // Current VirtualFile - minimal metadata
52
+ interface VirtualFile {
53
+ path: string;
54
+ content: string;
55
+ language?: string;
56
+ note?: string;
57
+ encoding?: 'utf8' | 'base64';
58
+ }
59
+
60
+ // Current VirtualProject - in-memory project state
61
+ interface VirtualProject {
62
+ id: string;
63
+ entry: string;
64
+ files: Map<string, VirtualFile>;
65
+ }
66
+ ```
67
+
68
+ ### Current Data Flow
69
+
70
+ ```
71
+ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
72
+ │ Browser │ ───► │ VFSStore │ ───► │ StorageBackend │
73
+ │ (React) │ │ │ │ │
74
+ └─────────────┘ └──────────────┘ └─────────────────┘
75
+
76
+ ┌─────────────────────────────┼─────────────────────────────┐
77
+ │ │ │
78
+ ┌──────▼──────┐ ┌────────▼────────┐ ┌────────▼────────┐
79
+ │ IndexedDB │ │ LocalFS │ │ S3 │
80
+ │ Backend │ │ Backend │ │ Backend │
81
+ └─────────────┘ └────────┬────────┘ └─────────────────┘
82
+ │ HTTP
83
+ ┌────────▼────────┐
84
+ │ Stitchery │
85
+ │ /vfs routes │
86
+ └────────┬────────┘
87
+
88
+ ┌────────▼────────┐
89
+ │ Local Disk │
90
+ │ (Node.js fs) │
91
+ └─────────────────┘
92
+ ```
93
+
94
+ ### Issues with Current Implementation
95
+
96
+ | Issue | Impact |
97
+ |-------|--------|
98
+ | Flat key-value semantics | No directory operations (mkdir, rmdir, readdir) |
99
+ | No change events | Cannot watch for external changes |
100
+ | Unidirectional sync | Only pushes; doesn't pull remote updates |
101
+ | No conflict resolution | Overwrites without checking |
102
+ | S3 backend incomplete | Missing AWS Signature V4 authentication |
103
+ | Metadata loss | File stats (mtime, size) not tracked |
104
+ | No atomic operations | Partial writes possible on failure |
105
+
106
+ ---
107
+
108
+ ## Proposed Architecture
109
+
110
+ ### New Package Structure
111
+
112
+ ```
113
+ packages/compiler/src/vfs/
114
+ ├── core/
115
+ │ ├── types.ts # FSProvider, SyncEngine interfaces
116
+ │ ├── fs-provider.ts # Base FSProvider implementation
117
+ │ └── virtual-fs.ts # In-memory FS with change tracking
118
+ ├── sync/
119
+ │ ├── engine.ts # Bidirectional sync orchestration
120
+ │ ├── differ.ts # Change detection and diff generation
121
+ │ └── resolver.ts # Conflict resolution strategies
122
+ ├── backends/
123
+ │ ├── memory.ts # In-memory (for tests/ephemeral)
124
+ │ ├── indexeddb.ts # Browser IndexedDB
125
+ │ ├── http.ts # Generic HTTP backend (replaces local-fs)
126
+ │ └── s3.ts # AWS S3 with proper signing
127
+ ├── project.ts # VirtualProject utilities (preserved)
128
+ └── index.ts # Public exports
129
+ ```
130
+
131
+ ### Core Interfaces
132
+
133
+ ```typescript
134
+ /**
135
+ * File statistics matching Node.js fs.Stats subset
136
+ */
137
+ interface FileStats {
138
+ size: number;
139
+ mtime: Date;
140
+ isFile(): boolean;
141
+ isDirectory(): boolean;
142
+ }
143
+
144
+ /**
145
+ * Directory entry matching Node.js fs.Dirent
146
+ */
147
+ interface DirEntry {
148
+ name: string;
149
+ isFile(): boolean;
150
+ isDirectory(): boolean;
151
+ }
152
+
153
+ /**
154
+ * FSProvider - Node.js fs/promises compatible interface
155
+ * All paths are relative to the provider's root.
156
+ */
157
+ interface FSProvider {
158
+ // File operations
159
+ readFile(path: string, encoding?: 'utf8' | 'base64'): Promise<string>;
160
+ writeFile(path: string, content: string, encoding?: 'utf8' | 'base64'): Promise<void>;
161
+ unlink(path: string): Promise<void>;
162
+
163
+ // Directory operations
164
+ mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
165
+ rmdir(path: string, options?: { recursive?: boolean }): Promise<void>;
166
+ readdir(path: string): Promise<DirEntry[]>;
167
+
168
+ // Metadata
169
+ stat(path: string): Promise<FileStats>;
170
+ exists(path: string): Promise<boolean>;
171
+
172
+ // Watch (optional - backends may not support)
173
+ watch?(path: string, callback: WatchCallback): () => void;
174
+ }
175
+
176
+ type WatchEventType = 'create' | 'update' | 'delete';
177
+ type WatchCallback = (event: WatchEventType, path: string) => void;
178
+
179
+ /**
180
+ * Change record for sync operations
181
+ */
182
+ interface ChangeRecord {
183
+ path: string;
184
+ type: 'create' | 'update' | 'delete';
185
+ content?: string;
186
+ mtime?: Date;
187
+ checksum?: string;
188
+ }
189
+
190
+ /**
191
+ * Conflict resolution strategy
192
+ */
193
+ type ConflictStrategy =
194
+ | 'local-wins' // Always use local version (default)
195
+ | 'remote-wins' // Always use remote version
196
+ | 'newest-wins' // Compare mtime
197
+ | 'manual'; // Queue for user resolution
198
+
199
+ /**
200
+ * SyncEngine - orchestrates bidirectional sync
201
+ */
202
+ interface SyncEngine {
203
+ /** Current sync status */
204
+ readonly status: 'idle' | 'syncing' | 'error';
205
+
206
+ /** Pending changes not yet synced */
207
+ readonly pendingChanges: ChangeRecord[];
208
+
209
+ /** Start continuous sync (if backend supports watch) */
210
+ start(): Promise<void>;
211
+
212
+ /** Stop continuous sync */
213
+ stop(): Promise<void>;
214
+
215
+ /** Force immediate sync */
216
+ sync(): Promise<SyncResult>;
217
+
218
+ /** Subscribe to sync events */
219
+ on(event: 'change' | 'conflict' | 'error', callback: Function): () => void;
220
+ }
221
+
222
+ interface SyncResult {
223
+ pushed: number;
224
+ pulled: number;
225
+ conflicts: ConflictRecord[];
226
+ }
227
+
228
+ interface ConflictRecord {
229
+ path: string;
230
+ local: ChangeRecord;
231
+ remote: ChangeRecord;
232
+ resolved?: 'local' | 'remote';
233
+ }
234
+ ```
235
+
236
+ ### Data Flow (Proposed)
237
+
238
+ ```
239
+ ┌─────────────────────────────────────────────────────────────────────────┐
240
+ │ Browser │
241
+ ├─────────────────────────────────────────────────────────────────────────┤
242
+ │ ┌───────────────┐ │
243
+ │ │ VFSStore │ (preserved API, uses VirtualFS internally) │
244
+ │ └───────┬───────┘ │
245
+ │ │ │
246
+ │ ┌───────▼───────┐ ┌──────────────┐ ┌────────────────┐ │
247
+ │ │ VirtualFS │◄────►│ SyncEngine │◄────►│ FSProvider │ │
248
+ │ │ (in-memory) │ │ │ │ (backend) │ │
249
+ │ └───────────────┘ └──────────────┘ └───────┬────────┘ │
250
+ │ │ │
251
+ └───────────────────────────────────────────────────────┼──────────────────┘
252
+
253
+ ┌───────────────────────────────────────────────┼───────────────────────────────────┐
254
+ │ │ │
255
+ ┌───────▼───────┐ ┌────────▼────────┐ ┌──────────▼──────────┐
256
+ │ IndexedDB │ │ HTTP Backend │ │ S3 Backend │
257
+ │ Backend │ │ │ │ (signed requests) │
258
+ └───────────────┘ └────────┬────────┘ └─────────────────────┘
259
+
260
+ ┌────────▼────────┐
261
+ │ Stitchery │
262
+ │ /vfs routes │
263
+ └────────┬────────┘
264
+
265
+ ┌────────▼────────┐
266
+ │ Local Disk │
267
+ └─────────────────┘
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Breaking Changes
273
+
274
+ ### Removed/Renamed
275
+
276
+ | Old | New | Migration |
277
+ |-----|-----|-----------|
278
+ | `StorageBackend` | `FSProvider` | Implement new interface methods |
279
+ | `LocalFSBackend` | `HttpBackend` | Rename import, update config |
280
+ | `VFSStore.getFile()` | `VFSStore.readFile()` | Update call sites |
281
+ | `VFSStore.putFile()` | `VFSStore.writeFile()` | Update call sites |
282
+
283
+ ### New Required Methods
284
+
285
+ Backends must implement these additional methods:
286
+
287
+ ```typescript
288
+ // Old interface (5 methods)
289
+ interface StorageBackend {
290
+ get, put, delete, list, exists
291
+ }
292
+
293
+ // New interface (10 methods)
294
+ interface FSProvider {
295
+ readFile, writeFile, unlink, // File ops
296
+ mkdir, rmdir, readdir, // Directory ops
297
+ stat, exists, // Metadata
298
+ watch? // Optional
299
+ }
300
+ ```
301
+
302
+ ### VFSStore API Changes
303
+
304
+ ```typescript
305
+ // BEFORE
306
+ const store = new VFSStore(backend);
307
+ const file = await store.getFile('path');
308
+ await store.putFile({ path: 'x', content: 'y' });
309
+
310
+ // AFTER
311
+ const store = new VFSStore(provider, { sync: true });
312
+ const content = await store.readFile('path');
313
+ await store.writeFile('x', 'y');
314
+
315
+ // New capabilities
316
+ store.on('change', (path, type) => console.log(path, type));
317
+ await store.sync(); // Force sync
318
+ ```
319
+
320
+ ---
321
+
322
+ ## Implementation Phases
323
+
324
+ ## Current Status (Feb 22, 2026)
325
+
326
+ - Phase 1 complete: `FSProvider` types, `VirtualFS`, and `MemoryBackend` are in place.
327
+ - Phase 2 complete: stitchery `/vfs` routes support `stat`, `mkdir`, `readdir`, `rmdir`, and `watch`; `HttpBackend` aligns with the new interface.
328
+ - Phase 3 complete: `SyncEngine` handles push/pull, mtime-based conflict detection, remote watch events, and checksum-based conflict resolution via `differ.ts` and `resolver.ts`.
329
+ - Phase 4 complete: IndexedDB backend supports directory semantics and `stat` metadata.
330
+ - Phase 5 not started: S3 backend/signing is removed and needs a full re-implementation.
331
+ - Phase 6 complete: chat editor VFS sync + change listeners wired; zolvery types deduplicated to use compiler exports.
332
+ - Tests: new unit/integration coverage for FSProvider and sync engine is still pending.
333
+
334
+ ### Phase 1: Core Interfaces & In-Memory Backend (1-2 days)
335
+
336
+ **Goal:** Establish new type system, implement VirtualFS with change tracking.
337
+
338
+ 1. Create `FSProvider` interface in `core/types.ts`
339
+ 2. Implement `VirtualFS` class (in-memory FSProvider with events)
340
+ 3. Add `ChangeTracker` for recording modifications
341
+ 4. Write tests for basic FS operations
342
+
343
+ **Deliverables:**
344
+ - `packages/compiler/src/vfs/core/types.ts`
345
+ - `packages/compiler/src/vfs/core/virtual-fs.ts`
346
+ - `packages/compiler/src/vfs/backends/memory.ts`
347
+
348
+ ### Phase 2: HTTP Backend Upgrade (1 day)
349
+
350
+ **Goal:** Upgrade stitchery VFS routes and HTTP backend to support full FSProvider interface.
351
+
352
+ 1. Add directory operations to stitchery `/vfs` routes:
353
+ - `POST /vfs?mkdir=path` - Create directory
354
+ - `DELETE /vfs/path?recursive=true` - Remove directory
355
+ - `GET /vfs/path?stat=true` - Get file stats
356
+ 2. Update `HttpBackend` to implement `FSProvider`
357
+ 3. Add SSE endpoint for watch: `GET /vfs?watch=path`
358
+
359
+ **Deliverables:**
360
+ - Updated `packages/stitchery/src/server/vfs-routes.ts`
361
+ - New `packages/compiler/src/vfs/backends/http.ts`
362
+
363
+ ### Phase 3: SyncEngine Implementation (2-3 days)
364
+
365
+ **Goal:** Build bidirectional sync with conflict handling.
366
+
367
+ 1. Implement `SyncEngine` class with:
368
+ - Change diffing (checksum-based)
369
+ - Push/pull orchestration
370
+ - Conflict detection
371
+ 2. Add conflict resolution strategies (`local-wins` default)
372
+ 3. Integrate with `VFSStore`
373
+
374
+ **Deliverables:**
375
+ - `packages/compiler/src/vfs/sync/engine.ts`
376
+ - `packages/compiler/src/vfs/sync/differ.ts`
377
+ - `packages/compiler/src/vfs/sync/resolver.ts`
378
+
379
+ ### Phase 4: IndexedDB Backend Upgrade (1 day)
380
+
381
+ **Goal:** Add directory semantics to browser storage.
382
+
383
+ 1. Implement virtual directory structure over flat IDB
384
+ 2. Add `stat` metadata storage
385
+ 3. Maintain backwards compatibility with existing stored data
386
+
387
+ **Deliverables:**
388
+ - Updated `packages/compiler/src/vfs/backends/indexeddb.ts`
389
+
390
+ ### Phase 5: S3 Backend with Auth (1-2 days)
391
+
392
+ **Goal:** Production-ready S3 storage with AWS Signature V4.
393
+
394
+ 1. Implement AWS Signature V4 signing
395
+ 2. Add presigned URL support for browser-direct uploads
396
+ 3. Implement `watch` via S3 Events → WebSocket (optional)
397
+
398
+ **Deliverables:**
399
+ - Updated `packages/compiler/src/vfs/backends/s3.ts`
400
+
401
+ ### Phase 6: Client Migration (1-2 days)
402
+
403
+ **Goal:** Update Patchwork apps (chat, zolvery) and document migration for other clients.
404
+
405
+ 1. Update `@aprovan/patchwork-editor` VFS utilities
406
+ 2. Update chat app to use new APIs
407
+ 3. Update zolvery app to use new APIs
408
+ 4. Write migration guide
409
+
410
+ ---
411
+
412
+ ## Chat App Migration Guide
413
+
414
+ ### Step 1: Update VFS Imports
415
+
416
+ ```typescript
417
+ // BEFORE (packages/editor/src/lib/vfs.ts)
418
+ import {
419
+ VFSStore,
420
+ LocalFSBackend,
421
+ type VirtualProject,
422
+ type VirtualFile
423
+ } from '@aprovan/patchwork-compiler';
424
+
425
+ // AFTER
426
+ import {
427
+ VFSStore,
428
+ HttpBackend,
429
+ type VirtualProject,
430
+ type VirtualFile
431
+ } from '@aprovan/patchwork-compiler';
432
+ ```
433
+
434
+ ### Step 2: Update Store Initialization
435
+
436
+ ```typescript
437
+ // BEFORE
438
+ export function getVFSStore(): VFSStore {
439
+ if (!storeInstance) {
440
+ const backend = new LocalFSBackend({ baseUrl: VFS_BASE_URL });
441
+ storeInstance = new VFSStore(backend);
442
+ }
443
+ return storeInstance;
444
+ }
445
+
446
+ // AFTER
447
+ export function getVFSStore(): VFSStore {
448
+ if (!storeInstance) {
449
+ const provider = new HttpBackend({ baseUrl: VFS_BASE_URL });
450
+ storeInstance = new VFSStore(provider, {
451
+ sync: true, // Enable auto-sync
452
+ conflictStrategy: 'local-wins'
453
+ });
454
+ }
455
+ return storeInstance;
456
+ }
457
+ ```
458
+
459
+ ### Step 3: Update File Operations
460
+
461
+ ```typescript
462
+ // BEFORE
463
+ export async function saveFile(file: VirtualFile): Promise<void> {
464
+ const store = getVFSStore();
465
+ await store.putFile(file);
466
+ }
467
+
468
+ // AFTER
469
+ export async function saveFile(path: string, content: string): Promise<void> {
470
+ const store = getVFSStore();
471
+ await store.writeFile(path, content);
472
+ }
473
+ ```
474
+
475
+ ### Step 4: Add Change Listeners (Optional)
476
+
477
+ ```typescript
478
+ // New capability - react to external changes
479
+ export function subscribeToChanges(
480
+ callback: (path: string, type: 'create' | 'update' | 'delete') => void
481
+ ): () => void {
482
+ const store = getVFSStore();
483
+ return store.on('change', callback);
484
+ }
485
+ ```
486
+
487
+ ### Step 5: Update CodePreview Save Logic
488
+
489
+ ```typescript
490
+ // In CodePreview.tsx
491
+ // BEFORE
492
+ const handleSave = useCallback(async () => {
493
+ setSaveStatus('saving');
494
+ try {
495
+ const project = createSingleFileProject(currentCode, entryFile, projectId);
496
+ await saveProject(project);
497
+ setSaveStatus('saved');
498
+ } catch { ... }
499
+ }, [...]);
500
+
501
+ // AFTER
502
+ const handleSave = useCallback(async () => {
503
+ setSaveStatus('saving');
504
+ try {
505
+ const path = `${projectId}/${entryFile}`;
506
+ await saveFile(path, currentCode);
507
+ setSaveStatus('saved');
508
+ } catch { ... }
509
+ }, [...]);
510
+ ```
511
+
512
+ ---
513
+
514
+ ## Zolvery App Migration Guide
515
+
516
+ ### Step 1: Update VFS Imports
517
+
518
+ ```typescript
519
+ // BEFORE
520
+ import {
521
+ VFSStore,
522
+ LocalFSBackend,
523
+ type VirtualProject
524
+ } from '@aprovan/patchwork-compiler';
525
+
526
+ // AFTER
527
+ import {
528
+ VFSStore,
529
+ HttpBackend,
530
+ type VirtualProject
531
+ } from '@aprovan/patchwork-compiler';
532
+ ```
533
+
534
+ ### Step 2: Initialize Store with Sync
535
+
536
+ ```typescript
537
+ // BEFORE
538
+ const backend = new LocalFSBackend({ baseUrl: VFS_BASE_URL });
539
+ const store = new VFSStore(backend);
540
+
541
+ // AFTER
542
+ const provider = new HttpBackend({ baseUrl: VFS_BASE_URL });
543
+ const store = new VFSStore(provider, {
544
+ sync: true,
545
+ conflictStrategy: 'local-wins'
546
+ });
547
+ ```
548
+
549
+ ### Step 3: Replace File APIs
550
+
551
+ ```typescript
552
+ // BEFORE
553
+ await store.putFile({ path, content });
554
+ const file = await store.getFile(path);
555
+
556
+ // AFTER
557
+ await store.writeFile(path, content);
558
+ const content = await store.readFile(path);
559
+ ```
560
+
561
+ ### Step 4: Update Project Save/Load Helpers
562
+
563
+ ```typescript
564
+ // BEFORE
565
+ export async function saveProject(project: VirtualProject) {
566
+ await store.putProject(project);
567
+ }
568
+
569
+ // AFTER
570
+ export async function saveProject(project: VirtualProject) {
571
+ await store.writeProject(project);
572
+ }
573
+ ```
574
+
575
+ ---
576
+
577
+ ## Stitchery Server Upgrade
578
+
579
+ ### New VFS Routes
580
+
581
+ ```typescript
582
+ // packages/stitchery/src/server/vfs-routes.ts
583
+
584
+ // Existing routes (unchanged)
585
+ GET /vfs → List all files
586
+ GET /vfs/:path → Read file content
587
+ PUT /vfs/:path → Write file content
588
+ DELETE /vfs/:path → Delete file
589
+
590
+ // New routes
591
+ GET /vfs/:path?stat=true → Get file/dir stats
592
+ POST /vfs/:path?mkdir=true → Create directory
593
+ DELETE /vfs/:path?recursive=true → Delete directory recursively
594
+ GET /vfs?watch=:path → SSE stream for file changes
595
+
596
+ // Stats response
597
+ {
598
+ "size": 1234,
599
+ "mtime": "2024-01-15T12:00:00Z",
600
+ "isFile": true,
601
+ "isDirectory": false
602
+ }
603
+
604
+ // Watch event format (SSE)
605
+ event: change
606
+ data: {"type":"update","path":"project/file.tsx","mtime":"..."}
607
+ ```
608
+
609
+ ### Route Behavior Details
610
+
611
+ - Paths are always relative to the VFS root. Normalize `.` and `..` and reject escaping the root.
612
+ - `readdir` returns only direct children, sorted lexicographically, excluding `.` and `..`.
613
+ - `stat` returns `isFile` and `isDirectory` based on the underlying FS entry, not inferred from content.
614
+ - `mkdir` supports `recursive=true` and is idempotent for existing directories.
615
+ - `rmdir` with `recursive=true` removes all descendants; without it, fails on non-empty directories.
616
+ - `unlink` returns 404 for missing paths and 409 when attempting to delete a directory without `recursive=true`.
617
+ - `watch` streams `create`, `update`, and `delete` events, with a root-relative `path` and RFC3339 `mtime`.
618
+
619
+ ### Error Mapping
620
+
621
+ - 400: Invalid query params or unsupported operation
622
+ - 404: Path not found
623
+ - 409: Directory not empty or type mismatch
624
+ - 500: Unhandled server error
625
+
626
+ ### Watch Implementation
627
+
628
+ ```typescript
629
+ // Simple watch using fs.watch (Node.js)
630
+ import { watch } from 'node:fs';
631
+
632
+ const watchers = new Map<string, Set<ServerResponse>>();
633
+
634
+ export function handleVFSWatch(
635
+ req: IncomingMessage,
636
+ res: ServerResponse,
637
+ ctx: VFSContext
638
+ ): void {
639
+ const query = new URL(req.url!, 'http://localhost').searchParams;
640
+ const watchPath = query.get('watch');
641
+
642
+ if (!watchPath) return;
643
+
644
+ res.setHeader('Content-Type', 'text/event-stream');
645
+ res.setHeader('Cache-Control', 'no-cache');
646
+ res.setHeader('Connection', 'keep-alive');
647
+
648
+ const fullPath = join(ctx.rootDir, watchPath);
649
+
650
+ const watcher = watch(fullPath, { recursive: true }, (event, filename) => {
651
+ const data = JSON.stringify({
652
+ type: event === 'rename' ? 'create' : 'update',
653
+ path: filename,
654
+ mtime: new Date().toISOString()
655
+ });
656
+ res.write(`event: change\ndata: ${data}\n\n`);
657
+ });
658
+
659
+ req.on('close', () => watcher.close());
660
+ }
661
+ ```
662
+
663
+ ---
664
+
665
+ ## Testing Strategy
666
+
667
+ ### Unit Tests
668
+
669
+ ```typescript
670
+ // Test FSProvider implementations
671
+ describe('FSProvider', () => {
672
+ const providers = [
673
+ ['Memory', () => new MemoryBackend()],
674
+ ['IndexedDB', () => new IndexedDBBackend()],
675
+ ['HTTP', () => new HttpBackend({ baseUrl: 'http://test' })]
676
+ ];
677
+
678
+ test.each(providers)('%s: readFile/writeFile', async (_, create) => {
679
+ const fs = create();
680
+ await fs.writeFile('test.txt', 'hello');
681
+ expect(await fs.readFile('test.txt')).toBe('hello');
682
+ });
683
+
684
+ test.each(providers)('%s: mkdir/readdir', async (_, create) => {
685
+ const fs = create();
686
+ await fs.mkdir('dir', { recursive: true });
687
+ await fs.writeFile('dir/file.txt', 'content');
688
+ const entries = await fs.readdir('dir');
689
+ expect(entries).toHaveLength(1);
690
+ expect(entries[0].name).toBe('file.txt');
691
+ });
692
+ });
693
+ ```
694
+
695
+ ### Integration Tests
696
+
697
+ ```typescript
698
+ // Test sync engine with mock backends
699
+ describe('SyncEngine', () => {
700
+ test('pushes local changes to remote', async () => {
701
+ const local = new MemoryBackend();
702
+ const remote = new MemoryBackend();
703
+ const sync = new SyncEngine(local, remote);
704
+
705
+ await local.writeFile('new.txt', 'content');
706
+ const result = await sync.sync();
707
+
708
+ expect(result.pushed).toBe(1);
709
+ expect(await remote.readFile('new.txt')).toBe('content');
710
+ });
711
+
712
+ test('pulls remote changes to local', async () => { ... });
713
+ test('detects conflicts', async () => { ... });
714
+ });
715
+ ```
716
+
717
+ ---
718
+
719
+ ## Open Questions
720
+
721
+ 1. **Conflict UI**: How should conflicts be displayed?
722
+ - *Recommendation:* Default to `local-wins` for simplicity. Patchwork is local-first.
723
+
724
+ 2. **Watch granularity**: Watch entire VFS root, or per-project?
725
+ - *Recommendation:* Per-project for performance; add `store.watch(projectId)`
726
+
727
+ 3. **Binary files**: Current system is text-only. Support binary?
728
+ - *Recommendation:* Treat binary files as remote references. Store locally for viewing, override on save (no conflict resolution for binary)
729
+
730
+ 4. **Offline support**: Queue changes when offline?
731
+ - *Recommendation:* Defer for now. Keep abstractions clean for future implementation, but no built-in support yet.
732
+
733
+ ---
734
+
735
+ ## Timeline Estimate
736
+
737
+ | Phase | Duration | Dependencies |
738
+ |-------|----------|--------------|
739
+ | 1. Core Interfaces | 1-2 days | None |
740
+ | 2. HTTP Backend | 1 day | Phase 1 |
741
+ | 3. SyncEngine | 2-3 days | Phase 1, 2 |
742
+ | 4. IndexedDB Upgrade | 1 day | Phase 1 |
743
+ | 5. S3 Backend | 1-2 days | Phase 1 |
744
+ | 6. Client Migration | 1-2 days | Phase 2, 3 |
745
+
746
+ **Total: 7-11 days**
747
+
748
+ ---
749
+
750
+ ## Appendix: Full Interface Definitions
751
+
752
+ ```typescript
753
+ // packages/compiler/src/vfs/core/types.ts
754
+
755
+ export interface FileStats {
756
+ size: number;
757
+ mtime: Date;
758
+ isFile(): boolean;
759
+ isDirectory(): boolean;
760
+ }
761
+
762
+ export interface DirEntry {
763
+ name: string;
764
+ isFile(): boolean;
765
+ isDirectory(): boolean;
766
+ }
767
+
768
+ export type WatchEventType = 'create' | 'update' | 'delete';
769
+ export type WatchCallback = (event: WatchEventType, path: string) => void;
770
+
771
+ export interface FSProvider {
772
+ readFile(path: string, encoding?: 'utf8' | 'base64'): Promise<string>;
773
+ writeFile(path: string, content: string, encoding?: 'utf8' | 'base64'): Promise<void>;
774
+ unlink(path: string): Promise<void>;
775
+ mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
776
+ rmdir(path: string, options?: { recursive?: boolean }): Promise<void>;
777
+ readdir(path: string): Promise<DirEntry[]>;
778
+ stat(path: string): Promise<FileStats>;
779
+ exists(path: string): Promise<boolean>;
780
+ watch?(path: string, callback: WatchCallback): () => void;
781
+ }
782
+
783
+ export interface ChangeRecord {
784
+ path: string;
785
+ type: WatchEventType;
786
+ content?: string;
787
+ mtime?: Date;
788
+ checksum?: string;
789
+ }
790
+
791
+ export type ConflictStrategy = 'local-wins' | 'remote-wins' | 'newest-wins' | 'manual';
792
+
793
+ export interface SyncOptions {
794
+ conflictStrategy?: ConflictStrategy;
795
+ autoSync?: boolean;
796
+ syncInterval?: number;
797
+ }
798
+
799
+ export interface SyncResult {
800
+ pushed: number;
801
+ pulled: number;
802
+ conflicts: ConflictRecord[];
803
+ }
804
+
805
+ export interface ConflictRecord {
806
+ path: string;
807
+ local: ChangeRecord;
808
+ remote: ChangeRecord;
809
+ resolved?: 'local' | 'remote';
810
+ }
811
+
812
+ export interface SyncEngine {
813
+ readonly status: 'idle' | 'syncing' | 'error';
814
+ readonly pendingChanges: ChangeRecord[];
815
+ start(): Promise<void>;
816
+ stop(): Promise<void>;
817
+ sync(): Promise<SyncResult>;
818
+ on(event: 'change', callback: (path: string, type: WatchEventType) => void): () => void;
819
+ on(event: 'conflict', callback: (conflict: ConflictRecord) => void): () => void;
820
+ on(event: 'error', callback: (error: Error) => void): () => void;
821
+ }
822
+ ```