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.
- package/README.md +13 -11
- package/docker-compose.yml +1 -1
- package/package.json +1 -1
- package/public/assets/css/style.css +1 -1
- package/public/assets/js/chunks/chunk-GFDM7YCF.js +1 -0
- package/public/assets/js/chunks/editor-session-EAUBJCHH.js +22 -0
- package/public/assets/js/chunks/{preview-render-compiler-UZ4SZDQQ.js → preview-render-compiler-6Y7TKXFJ.js} +1 -1
- package/public/assets/js/chunks/{quick-switcher-controller-J7I3CUET.js → quick-switcher-controller-A6SKH5HI.js} +1 -1
- package/public/assets/js/{excalidraw-editor-ZDYKMZOL.js → excalidraw-editor-TCIH6GJL.js} +1 -1
- package/public/assets/js/excalidraw-editor.js +1 -1
- package/public/assets/js/main.js +102 -75
- package/public/assets/js/preview-render-worker.js +15 -15
- package/public/index.html +14 -4
- package/src/client/application/app-shell/git-feature.js +86 -19
- package/src/client/application/app-shell/ui-feature.js +4 -0
- package/src/client/application/app-shell-elements.js +1 -0
- package/src/client/bootstrap/collabmd-app-shell.js +16 -0
- package/src/client/domain/vault-utils.js +2 -2
- package/src/client/excalidraw-editor.js +2 -1
- package/src/client/infrastructure/editor-session.js +4 -0
- package/src/client/infrastructure/editor-view-adapter.js +245 -7
- package/src/client/infrastructure/workspace-sync-client.js +324 -0
- package/src/client/presentation/backlinks-panel.js +157 -101
- package/src/client/presentation/comment-ui-controller.js +233 -10
- package/src/client/presentation/file-explorer-controller.js +15 -3
- package/src/client/presentation/file-explorer-view.js +152 -11
- package/src/client/presentation/git-panel-controller.js +73 -0
- package/src/client/presentation/outline-controller.js +25 -0
- package/src/client/styles/style.css +184 -9
- package/src/domain/wiki-link-resolver.js +16 -5
- package/src/domain/workspace-change.js +68 -0
- package/src/domain/workspace-room.js +3 -0
- package/src/server/create-app-server.js +41 -10
- package/src/server/domain/backlink-index.js +94 -1
- package/src/server/domain/collaboration/collaboration-room.js +191 -22
- package/src/server/domain/collaboration/room-registry.js +64 -10
- package/src/server/infrastructure/git/errors.js +4 -1
- package/src/server/infrastructure/git/git-service.js +215 -1
- package/src/server/infrastructure/git/responses.js +12 -19
- package/src/server/infrastructure/http/create-git-api-command-handler.js +58 -43
- package/src/server/infrastructure/http/create-git-api-handler.js +2 -0
- package/src/server/infrastructure/http/create-git-api-query-handler.js +16 -1
- package/src/server/infrastructure/http/create-request-handler.js +7 -0
- package/src/server/infrastructure/http/create-vault-api-command-handler.js +58 -14
- package/src/server/infrastructure/http/create-vault-api-handler.js +3 -0
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +3 -1
- package/src/server/infrastructure/http/http-response.js +65 -28
- package/src/server/infrastructure/persistence/pull-backup-store.js +283 -0
- package/src/server/infrastructure/persistence/vault-file-store.js +179 -52
- package/src/server/infrastructure/workspace/file-system-sync-service.js +488 -0
- package/src/server/infrastructure/workspace/workspace-mutation-coordinator.js +551 -0
- package/public/assets/js/chunks/chunk-R3DDMJHH.js +0 -1
- 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
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
360
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
445
|
+
await this.runManagedWrite([filePath], async () => {
|
|
446
|
+
for (const operation of operations) {
|
|
447
|
+
if (operation.kind !== 'write') {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
419
450
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
464
|
+
if (backupPath) {
|
|
465
|
+
await rename(operation.targetPath, backupPath);
|
|
466
|
+
}
|
|
436
467
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
|
514
|
-
|
|
515
|
-
|
|
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
|
|
527
|
-
|
|
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
|
|
542
|
-
|
|
543
|
-
|
|
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
|
}
|