collabmd 0.1.19 → 0.1.21

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 (53) hide show
  1. package/README.md +13 -11
  2. package/docker-compose.yml +1 -1
  3. package/package.json +1 -1
  4. package/public/assets/css/style.css +1 -1
  5. package/public/assets/js/chunks/chunk-GFDM7YCF.js +1 -0
  6. package/public/assets/js/chunks/editor-session-EAUBJCHH.js +22 -0
  7. package/public/assets/js/chunks/{preview-render-compiler-UZ4SZDQQ.js → preview-render-compiler-6Y7TKXFJ.js} +1 -1
  8. package/public/assets/js/chunks/{quick-switcher-controller-J7I3CUET.js → quick-switcher-controller-A6SKH5HI.js} +1 -1
  9. package/public/assets/js/{excalidraw-editor-ZDYKMZOL.js → excalidraw-editor-TCIH6GJL.js} +1 -1
  10. package/public/assets/js/excalidraw-editor.js +1 -1
  11. package/public/assets/js/main.js +102 -75
  12. package/public/assets/js/preview-render-worker.js +15 -15
  13. package/public/index.html +14 -4
  14. package/src/client/application/app-shell/git-feature.js +86 -19
  15. package/src/client/application/app-shell/ui-feature.js +4 -0
  16. package/src/client/application/app-shell-elements.js +1 -0
  17. package/src/client/bootstrap/collabmd-app-shell.js +16 -0
  18. package/src/client/domain/vault-utils.js +2 -2
  19. package/src/client/excalidraw-editor.js +2 -1
  20. package/src/client/infrastructure/editor-session.js +4 -0
  21. package/src/client/infrastructure/editor-view-adapter.js +245 -7
  22. package/src/client/infrastructure/workspace-sync-client.js +324 -0
  23. package/src/client/presentation/backlinks-panel.js +157 -101
  24. package/src/client/presentation/comment-ui-controller.js +233 -10
  25. package/src/client/presentation/file-explorer-controller.js +15 -3
  26. package/src/client/presentation/file-explorer-view.js +152 -11
  27. package/src/client/presentation/git-panel-controller.js +73 -0
  28. package/src/client/presentation/outline-controller.js +25 -0
  29. package/src/client/styles/style.css +184 -9
  30. package/src/domain/wiki-link-resolver.js +16 -5
  31. package/src/domain/workspace-change.js +68 -0
  32. package/src/domain/workspace-room.js +3 -0
  33. package/src/server/create-app-server.js +41 -10
  34. package/src/server/domain/backlink-index.js +94 -1
  35. package/src/server/domain/collaboration/collaboration-room.js +191 -22
  36. package/src/server/domain/collaboration/room-registry.js +64 -10
  37. package/src/server/infrastructure/git/errors.js +4 -1
  38. package/src/server/infrastructure/git/git-service.js +215 -1
  39. package/src/server/infrastructure/git/responses.js +12 -19
  40. package/src/server/infrastructure/http/create-git-api-command-handler.js +58 -43
  41. package/src/server/infrastructure/http/create-git-api-handler.js +2 -0
  42. package/src/server/infrastructure/http/create-git-api-query-handler.js +16 -1
  43. package/src/server/infrastructure/http/create-request-handler.js +7 -0
  44. package/src/server/infrastructure/http/create-vault-api-command-handler.js +58 -14
  45. package/src/server/infrastructure/http/create-vault-api-handler.js +3 -0
  46. package/src/server/infrastructure/http/create-vault-api-query-handler.js +3 -1
  47. package/src/server/infrastructure/http/http-response.js +65 -28
  48. package/src/server/infrastructure/persistence/pull-backup-store.js +283 -0
  49. package/src/server/infrastructure/persistence/vault-file-store.js +179 -52
  50. package/src/server/infrastructure/workspace/file-system-sync-service.js +488 -0
  51. package/src/server/infrastructure/workspace/workspace-mutation-coordinator.js +551 -0
  52. package/public/assets/js/chunks/chunk-R3DDMJHH.js +0 -1
  53. package/public/assets/js/chunks/editor-session-AH6Z3MXW.js +0 -22
@@ -60,6 +60,19 @@ async function pathExists(pathValue) {
60
60
  }
61
61
  }
62
62
 
63
+ function createWorkspaceEntry(relativePath, type) {
64
+ return {
65
+ fileKind: type === 'directory' ? null : getVaultTreeNodeType(relativePath),
66
+ name: basename(relativePath),
67
+ nodeType: type,
68
+ parentPath: dirname(relativePath).replace(/\\/g, '/') === '.'
69
+ ? ''
70
+ : dirname(relativePath).replace(/\\/g, '/'),
71
+ path: relativePath,
72
+ type: type === 'directory' ? 'directory' : getVaultTreeNodeType(relativePath),
73
+ };
74
+ }
75
+
63
76
  async function cleanupPaths(paths = []) {
64
77
  await Promise.allSettled(paths.filter(Boolean).map((pathValue) => rm(pathValue, { force: true })));
65
78
  }
@@ -150,6 +163,19 @@ export class VaultFileStore {
150
163
  constructor({ vaultDir }) {
151
164
  this.vaultDir = resolve(vaultDir);
152
165
  this.sidecarStore = new SidecarStore({ vaultDir: this.vaultDir });
166
+ this.managedWriteTracker = null;
167
+ }
168
+
169
+ setManagedWriteTracker(tracker) {
170
+ this.managedWriteTracker = tracker ?? null;
171
+ }
172
+
173
+ async runManagedWrite(paths, operation) {
174
+ if (!this.managedWriteTracker?.runManagedWrite) {
175
+ return operation();
176
+ }
177
+
178
+ return this.managedWriteTracker.runManagedWrite(paths, operation);
153
179
  }
154
180
 
155
181
  async tree() {
@@ -251,11 +277,13 @@ export class VaultFileStore {
251
277
  }
252
278
 
253
279
  try {
254
- await mkdir(dirname(resolved.absolute), { recursive: true });
255
- await writeFile(resolved.absolute, content, 'utf-8');
256
- if (invalidateCollaborationSnapshot) {
257
- await this.deleteCollaborationSnapshot(filePath);
258
- }
280
+ await this.runManagedWrite([filePath], async () => {
281
+ await mkdir(dirname(resolved.absolute), { recursive: true });
282
+ await writeFile(resolved.absolute, content, 'utf-8');
283
+ if (invalidateCollaborationSnapshot) {
284
+ await this.deleteCollaborationSnapshot(filePath);
285
+ }
286
+ });
259
287
  return { ok: true };
260
288
  } catch (error) {
261
289
  return { ok: false, error: error.message };
@@ -356,8 +384,10 @@ export class VaultFileStore {
356
384
  }
357
385
 
358
386
  try {
359
- await mkdir(dirname(absolutePath), { recursive: true });
360
- await writeFile(absolutePath, content);
387
+ await this.runManagedWrite([storedPath], async () => {
388
+ await mkdir(dirname(absolutePath), { recursive: true });
389
+ await writeFile(absolutePath, content);
390
+ });
361
391
  } catch (error) {
362
392
  return { ok: false, error: error.message };
363
393
  }
@@ -412,49 +442,51 @@ export class VaultFileStore {
412
442
  const committedOperations = [];
413
443
 
414
444
  try {
415
- for (const operation of operations) {
416
- if (operation.kind !== 'write') {
417
- continue;
418
- }
445
+ await this.runManagedWrite([filePath], async () => {
446
+ for (const operation of operations) {
447
+ if (operation.kind !== 'write') {
448
+ continue;
449
+ }
419
450
 
420
- const tempPath = createTransactionalPath(operation.targetPath, 'tmp');
421
- await mkdir(dirname(operation.targetPath), { recursive: true });
422
- await writeFile(tempPath, operation.value, operation.writeOptions);
423
- operation.tempPath = tempPath;
424
- stagedWrites.push(tempPath);
425
- }
451
+ const tempPath = createTransactionalPath(operation.targetPath, 'tmp');
452
+ await mkdir(dirname(operation.targetPath), { recursive: true });
453
+ await writeFile(tempPath, operation.value, operation.writeOptions);
454
+ operation.tempPath = tempPath;
455
+ stagedWrites.push(tempPath);
456
+ }
426
457
 
427
- for (const operation of operations) {
428
- const hadExistingTarget = await pathExists(operation.targetPath);
429
- const backupPath = hadExistingTarget
430
- ? createTransactionalPath(operation.targetPath, 'bak')
431
- : null;
458
+ for (const operation of operations) {
459
+ const hadExistingTarget = await pathExists(operation.targetPath);
460
+ const backupPath = hadExistingTarget
461
+ ? createTransactionalPath(operation.targetPath, 'bak')
462
+ : null;
432
463
 
433
- if (backupPath) {
434
- await rename(operation.targetPath, backupPath);
435
- }
464
+ if (backupPath) {
465
+ await rename(operation.targetPath, backupPath);
466
+ }
436
467
 
437
- try {
438
- if (operation.kind === 'write') {
439
- await rename(operation.tempPath, operation.targetPath);
440
- const stagedIndex = stagedWrites.indexOf(operation.tempPath);
441
- if (stagedIndex >= 0) {
442
- stagedWrites.splice(stagedIndex, 1);
468
+ try {
469
+ if (operation.kind === 'write') {
470
+ await rename(operation.tempPath, operation.targetPath);
471
+ const stagedIndex = stagedWrites.indexOf(operation.tempPath);
472
+ if (stagedIndex >= 0) {
473
+ stagedWrites.splice(stagedIndex, 1);
474
+ }
443
475
  }
476
+ } catch (error) {
477
+ if (backupPath) {
478
+ await rename(backupPath, operation.targetPath);
479
+ }
480
+ throw error;
444
481
  }
445
- } catch (error) {
446
- if (backupPath) {
447
- await rename(backupPath, operation.targetPath);
448
- }
449
- throw error;
450
- }
451
482
 
452
- committedOperations.push({
453
- backupPath,
454
- kind: operation.kind,
455
- targetPath: operation.targetPath,
456
- });
457
- }
483
+ committedOperations.push({
484
+ backupPath,
485
+ kind: operation.kind,
486
+ targetPath: operation.targetPath,
487
+ });
488
+ }
489
+ });
458
490
 
459
491
  await cleanupPaths(committedOperations.map((operation) => operation.backupPath));
460
492
  return { ok: true };
@@ -510,9 +542,11 @@ export class VaultFileStore {
510
542
  }
511
543
  }
512
544
 
513
- await mkdir(dirname(absolute), { recursive: true });
514
- await writeFile(absolute, content, 'utf-8');
515
- await this.deleteCollaborationSnapshot(filePath);
545
+ await this.runManagedWrite([filePath], async () => {
546
+ await mkdir(dirname(absolute), { recursive: true });
547
+ await writeFile(absolute, content, 'utf-8');
548
+ await this.deleteCollaborationSnapshot(filePath);
549
+ });
516
550
  return { ok: true };
517
551
  }
518
552
 
@@ -523,8 +557,10 @@ export class VaultFileStore {
523
557
  }
524
558
 
525
559
  try {
526
- await rm(absolute, { force: true });
527
- await this.sidecarStore.deleteAllForFile(filePath);
560
+ await this.runManagedWrite([filePath], async () => {
561
+ await rm(absolute, { force: true });
562
+ await this.sidecarStore.deleteAllForFile(filePath);
563
+ });
528
564
  return { ok: true };
529
565
  } catch (error) {
530
566
  return { ok: false, error: error.message };
@@ -538,9 +574,11 @@ export class VaultFileStore {
538
574
  }
539
575
 
540
576
  try {
541
- await mkdir(dirname(absoluteNew), { recursive: true });
542
- await rename(absoluteOld, absoluteNew);
543
- await this.sidecarStore.renameAllForFile(oldPath, newPath);
577
+ await this.runManagedWrite([oldPath, newPath], async () => {
578
+ await mkdir(dirname(absoluteNew), { recursive: true });
579
+ await rename(absoluteOld, absoluteNew);
580
+ await this.sidecarStore.renameAllForFile(oldPath, newPath);
581
+ });
544
582
  return { ok: true };
545
583
  } catch (error) {
546
584
  return { ok: false, error: error.message };
@@ -554,7 +592,7 @@ export class VaultFileStore {
554
592
  }
555
593
 
556
594
  try {
557
- await mkdir(absolute, { recursive: true });
595
+ await this.runManagedWrite([dirPath], () => mkdir(absolute, { recursive: true }));
558
596
  return { ok: true };
559
597
  } catch (error) {
560
598
  return { ok: false, error: error.message };
@@ -582,6 +620,22 @@ export class VaultFileStore {
582
620
  ]);
583
621
  }
584
622
 
623
+ async reconcileCollaborationSnapshots({
624
+ changedPaths = [],
625
+ deletedPaths = [],
626
+ renamedPaths = [],
627
+ } = {}) {
628
+ const affectedPaths = new Set([
629
+ ...(changedPaths ?? []).filter(Boolean),
630
+ ...(deletedPaths ?? []).filter(Boolean),
631
+ ...((renamedPaths ?? []).flatMap((entry) => [entry?.oldPath, entry?.newPath]).filter(Boolean)),
632
+ ]);
633
+
634
+ await Promise.allSettled(
635
+ Array.from(affectedPaths, (filePath) => this.deleteCollaborationSnapshot(filePath)),
636
+ );
637
+ }
638
+
585
639
  async countFilesInDir(dirPath) {
586
640
  let count = 0;
587
641
 
@@ -617,4 +671,77 @@ export class VaultFileStore {
617
671
 
618
672
  return toVaultRelativePath(this.vaultDir, absolute);
619
673
  }
674
+
675
+ async scanWorkspaceState() {
676
+ const entries = new Map();
677
+ const metadata = new Map();
678
+
679
+ const visitDirectory = async (dirPath) => {
680
+ let dirEntries;
681
+ try {
682
+ dirEntries = await readdir(dirPath, { withFileTypes: true });
683
+ } catch {
684
+ return;
685
+ }
686
+
687
+ const sorted = dirEntries.sort((left, right) => {
688
+ if (left.isDirectory() && !right.isDirectory()) return -1;
689
+ if (!left.isDirectory() && right.isDirectory()) return 1;
690
+ return left.name.localeCompare(right.name, undefined, { sensitivity: 'base' });
691
+ });
692
+
693
+ for (const entry of sorted) {
694
+ if (isIgnoredVaultEntry(entry.name)) {
695
+ continue;
696
+ }
697
+
698
+ const fullPath = join(dirPath, entry.name);
699
+ const relativePath = toVaultRelativePath(this.vaultDir, fullPath).replace(/\\/g, '/');
700
+
701
+ if (entry.isDirectory()) {
702
+ entries.set(relativePath, createWorkspaceEntry(relativePath, 'directory'));
703
+ try {
704
+ const info = await stat(fullPath);
705
+ metadata.set(relativePath, {
706
+ inode: Number(info.ino || 0),
707
+ mtimeMs: Number(info.mtimeMs || 0),
708
+ path: relativePath,
709
+ size: 0,
710
+ type: 'directory',
711
+ });
712
+ } catch {
713
+ // Ignore transient directories that disappear during scans.
714
+ }
715
+ await visitDirectory(fullPath);
716
+ continue;
717
+ }
718
+
719
+ if (!isVaultFilePath(entry.name)) {
720
+ continue;
721
+ }
722
+
723
+ entries.set(relativePath, createWorkspaceEntry(relativePath, 'file'));
724
+ try {
725
+ const info = await stat(fullPath);
726
+ metadata.set(relativePath, {
727
+ inode: Number(info.ino || 0),
728
+ mtimeMs: Number(info.mtimeMs || 0),
729
+ path: relativePath,
730
+ size: Number(info.size || 0),
731
+ type: 'file',
732
+ });
733
+ } catch {
734
+ // Ignore transient files that disappear during scans.
735
+ }
736
+ }
737
+ };
738
+
739
+ await visitDirectory(this.vaultDir);
740
+
741
+ return {
742
+ entries,
743
+ metadata,
744
+ scannedAt: Date.now(),
745
+ };
746
+ }
620
747
  }