@jmylchreest/aide-plugin 0.0.51 → 0.0.53
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/package.json +1 -1
- package/src/core/mcp-sync.ts +277 -143
package/package.json
CHANGED
package/src/core/mcp-sync.ts
CHANGED
|
@@ -64,15 +64,29 @@ export interface AideMcpConfig {
|
|
|
64
64
|
mcpServers: Record<string, Omit<CanonicalMcpServer, "name">>;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/**
|
|
67
|
+
/** Per-server, per-source state entry in the journal. */
|
|
68
|
+
export interface ServerSourceState {
|
|
69
|
+
/** Whether the server was present in this source at last check */
|
|
70
|
+
present: boolean;
|
|
71
|
+
/** Mtime of the source file when this state transition was detected */
|
|
72
|
+
mtime: number;
|
|
73
|
+
/** ISO timestamp for human readability */
|
|
74
|
+
ts: string;
|
|
75
|
+
/** Hash of the server config (to detect changes while present) */
|
|
76
|
+
configHash?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Journal v2: per-server, per-source state tracking.
|
|
81
|
+
*
|
|
82
|
+
* Tracks the last state transition (added/removed/changed) for each
|
|
83
|
+
* MCP server in each config source file, with the file's mtime at the
|
|
84
|
+
* time of that transition. Resolution uses per-server latest-mtime-wins.
|
|
85
|
+
*/
|
|
68
86
|
export interface McpSyncJournal {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
servers: string[];
|
|
73
|
-
}>;
|
|
74
|
-
/** Server names intentionally removed from aide canonical config */
|
|
75
|
-
removed: string[];
|
|
87
|
+
version: 2;
|
|
88
|
+
/** server name → source file path → state */
|
|
89
|
+
servers: Record<string, Record<string, ServerSourceState>>;
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
/** Result of a sync operation. */
|
|
@@ -445,24 +459,48 @@ function writeAssistantConfig(
|
|
|
445
459
|
}
|
|
446
460
|
|
|
447
461
|
// =============================================================================
|
|
448
|
-
// Journal
|
|
462
|
+
// Journal (v2: per-server, per-source state tracking)
|
|
449
463
|
// =============================================================================
|
|
450
464
|
|
|
465
|
+
/** Legacy journal format (v1) — only used for migration. */
|
|
466
|
+
interface McpSyncJournalV1 {
|
|
467
|
+
entries?: Array<{ ts: string; servers: string[] }>;
|
|
468
|
+
removed?: string[];
|
|
469
|
+
}
|
|
470
|
+
|
|
451
471
|
function readJournal(path: string): McpSyncJournal {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
472
|
+
const empty: McpSyncJournal = { version: 2, servers: {} };
|
|
473
|
+
if (!existsSync(path)) return empty;
|
|
455
474
|
|
|
456
475
|
try {
|
|
457
476
|
const parsed: unknown = JSON.parse(readFileSync(path, "utf-8"));
|
|
458
477
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
459
|
-
return
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
478
|
+
return empty;
|
|
479
|
+
|
|
480
|
+
const obj = parsed as Record<string, unknown>;
|
|
481
|
+
|
|
482
|
+
// v2 journal
|
|
483
|
+
if (obj.version === 2) return parsed as McpSyncJournal;
|
|
484
|
+
|
|
485
|
+
// v1 → v2 migration: preserve the removed list as synthetic removal entries.
|
|
486
|
+
// Use mtime=0 so any real source file with the server will override the
|
|
487
|
+
// migration entry. If no source has the server, absence resolves it as removed.
|
|
488
|
+
const v1 = parsed as McpSyncJournalV1;
|
|
489
|
+
const migrated: McpSyncJournal = { version: 2, servers: {} };
|
|
490
|
+
if (Array.isArray(v1.removed)) {
|
|
491
|
+
for (const name of v1.removed) {
|
|
492
|
+
migrated.servers[name] = {
|
|
493
|
+
"migrated-v1": {
|
|
494
|
+
present: false,
|
|
495
|
+
mtime: 0,
|
|
496
|
+
ts: new Date().toISOString(),
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return migrated;
|
|
464
502
|
} catch {
|
|
465
|
-
return
|
|
503
|
+
return empty;
|
|
466
504
|
}
|
|
467
505
|
}
|
|
468
506
|
|
|
@@ -472,66 +510,51 @@ function writeJournal(path: string, journal: McpSyncJournal): void {
|
|
|
472
510
|
mkdirSync(dir, { recursive: true });
|
|
473
511
|
}
|
|
474
512
|
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
513
|
+
// Prune server entries with no remaining sources
|
|
514
|
+
for (const [serverName, sourceMap] of Object.entries(journal.servers)) {
|
|
515
|
+
if (Object.keys(sourceMap).length === 0) {
|
|
516
|
+
delete journal.servers[serverName];
|
|
517
|
+
}
|
|
478
518
|
}
|
|
479
519
|
|
|
480
520
|
writeFileSync(path, JSON.stringify(journal, null, 2) + "\n");
|
|
481
521
|
}
|
|
482
522
|
|
|
483
|
-
/**
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
): string[] {
|
|
493
|
-
const journal = readJournal(jrnlPath);
|
|
494
|
-
const sorted = [...currentAideServers].sort();
|
|
495
|
-
|
|
496
|
-
// Get previous entry's server list
|
|
497
|
-
const prevEntry =
|
|
498
|
-
journal.entries.length > 0
|
|
499
|
-
? journal.entries[journal.entries.length - 1]
|
|
500
|
-
: null;
|
|
501
|
-
const prevServers = prevEntry
|
|
502
|
-
? new Set(prevEntry.servers)
|
|
503
|
-
: new Set<string>();
|
|
504
|
-
|
|
505
|
-
// Detect deletions: servers in previous entry but not in current aide config
|
|
506
|
-
const currentSet = new Set(sorted);
|
|
507
|
-
for (const prev of prevServers) {
|
|
508
|
-
if (!currentSet.has(prev)) {
|
|
509
|
-
// Server was removed from aide config since last run
|
|
510
|
-
if (!journal.removed.includes(prev)) {
|
|
511
|
-
journal.removed.push(prev);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
523
|
+
/** Recursively sort object keys for deterministic serialization. */
|
|
524
|
+
function sortDeep(v: unknown): unknown {
|
|
525
|
+
if (Array.isArray(v)) return v.map(sortDeep);
|
|
526
|
+
if (v && typeof v === "object") {
|
|
527
|
+
return Object.fromEntries(
|
|
528
|
+
Object.keys(v as object)
|
|
529
|
+
.sort()
|
|
530
|
+
.map((k) => [k, sortDeep((v as Record<string, unknown>)[k])]),
|
|
531
|
+
);
|
|
514
532
|
}
|
|
533
|
+
return v;
|
|
534
|
+
}
|
|
515
535
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
536
|
+
/**
|
|
537
|
+
* Deterministic hash of a server config for change detection.
|
|
538
|
+
* Normalizes away trivial differences (empty env, undefined fields)
|
|
539
|
+
* and sorts all keys recursively for stable ordering.
|
|
540
|
+
*/
|
|
541
|
+
function serverConfigHash(server: CanonicalMcpServer): string {
|
|
542
|
+
const { name: _, ...rest } = server;
|
|
543
|
+
const normalized: Record<string, unknown> = {};
|
|
544
|
+
const keys = Object.keys(rest).sort();
|
|
545
|
+
for (const k of keys) {
|
|
546
|
+
const v = (rest as Record<string, unknown>)[k];
|
|
547
|
+
if (v === undefined) continue;
|
|
548
|
+
if (
|
|
549
|
+
typeof v === "object" &&
|
|
550
|
+
v !== null &&
|
|
551
|
+
!Array.isArray(v) &&
|
|
552
|
+
Object.keys(v).length === 0
|
|
553
|
+
)
|
|
554
|
+
continue;
|
|
555
|
+
normalized[k] = v;
|
|
530
556
|
}
|
|
531
|
-
|
|
532
|
-
writeJournal(jrnlPath, journal);
|
|
533
|
-
|
|
534
|
-
return journal.removed;
|
|
557
|
+
return JSON.stringify(sortDeep(normalized));
|
|
535
558
|
}
|
|
536
559
|
|
|
537
560
|
// =============================================================================
|
|
@@ -546,77 +569,173 @@ function getFileMtime(path: string): number {
|
|
|
546
569
|
}
|
|
547
570
|
}
|
|
548
571
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
572
|
+
interface McpSource {
|
|
573
|
+
path: string;
|
|
574
|
+
servers: Record<string, CanonicalMcpServer>;
|
|
575
|
+
mtime: number;
|
|
576
|
+
}
|
|
552
577
|
|
|
553
578
|
/**
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
* Priority order (last-modified wins for conflicts):
|
|
557
|
-
* 1. Aide canonical config (always included as base)
|
|
558
|
-
* 2. Each assistant's config (sorted by file mtime, oldest first)
|
|
579
|
+
* Gather all MCP config source files for a given scope.
|
|
559
580
|
*/
|
|
560
|
-
function
|
|
561
|
-
scope: McpScope,
|
|
562
|
-
cwd: string,
|
|
563
|
-
): Record<string, CanonicalMcpServer> {
|
|
581
|
+
function gatherSources(scope: McpScope, cwd: string): McpSource[] {
|
|
564
582
|
const platforms: McpPlatform[] = ["claude-code", "opencode"];
|
|
583
|
+
const sources: McpSource[] = [];
|
|
565
584
|
|
|
566
|
-
// Collect all sources with their modification times
|
|
567
|
-
const sources: Array<{
|
|
568
|
-
label: string;
|
|
569
|
-
servers: Record<string, CanonicalMcpServer>;
|
|
570
|
-
mtime: number;
|
|
571
|
-
}> = [];
|
|
572
|
-
|
|
573
|
-
// Aide canonical config (always lowest priority as base)
|
|
574
585
|
const aidePath =
|
|
575
586
|
scope === "user" ? aideUserMcpPath() : aideProjectMcpPath(cwd);
|
|
576
587
|
sources.push({
|
|
577
|
-
|
|
588
|
+
path: aidePath,
|
|
578
589
|
servers: readAideConfig(aidePath),
|
|
579
590
|
mtime: getFileMtime(aidePath),
|
|
580
591
|
});
|
|
581
592
|
|
|
582
|
-
// Assistant configs
|
|
583
593
|
for (const platform of platforms) {
|
|
584
594
|
const paths = getAssistantReadPaths(platform, scope, cwd);
|
|
585
595
|
for (const p of paths) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
mtime: getFileMtime(p),
|
|
592
|
-
});
|
|
593
|
-
}
|
|
596
|
+
sources.push({
|
|
597
|
+
path: p,
|
|
598
|
+
servers: readAssistantConfig(platform, p),
|
|
599
|
+
mtime: getFileMtime(p),
|
|
600
|
+
});
|
|
594
601
|
}
|
|
595
602
|
}
|
|
596
603
|
|
|
597
|
-
|
|
598
|
-
|
|
604
|
+
return sources;
|
|
605
|
+
}
|
|
599
606
|
|
|
600
|
-
|
|
601
|
-
|
|
607
|
+
/**
|
|
608
|
+
* Update the journal with per-server state transitions and resolve the
|
|
609
|
+
* final server set using per-server latest-mtime-wins semantics.
|
|
610
|
+
*
|
|
611
|
+
* For each server in each source file, we track the last state transition
|
|
612
|
+
* (present→removed or absent→present or config changed). The mtime stored
|
|
613
|
+
* is the source file's mtime at the time of that transition. To resolve,
|
|
614
|
+
* for each server we pick the entry with the highest mtime: if present the
|
|
615
|
+
* server is included, if removed it is excluded.
|
|
616
|
+
*/
|
|
617
|
+
function updateJournalAndResolve(
|
|
618
|
+
jrnlPath: string,
|
|
619
|
+
sources: McpSource[],
|
|
620
|
+
): { resolved: Record<string, CanonicalMcpServer>; journal: McpSyncJournal } {
|
|
621
|
+
const journal = readJournal(jrnlPath);
|
|
622
|
+
const now = new Date().toISOString();
|
|
623
|
+
const activePaths = new Set(sources.map((s) => s.path));
|
|
624
|
+
|
|
625
|
+
// Detect state transitions per server per source
|
|
602
626
|
for (const source of sources) {
|
|
627
|
+
const currentNames = new Set(Object.keys(source.servers));
|
|
628
|
+
|
|
629
|
+
// Servers currently present in this source
|
|
603
630
|
for (const [name, server] of Object.entries(source.servers)) {
|
|
604
|
-
|
|
631
|
+
if (!journal.servers[name]) journal.servers[name] = {};
|
|
632
|
+
const existing = journal.servers[name][source.path];
|
|
633
|
+
const hash = serverConfigHash(server);
|
|
634
|
+
|
|
635
|
+
if (!existing || !existing.present || existing.configHash !== hash) {
|
|
636
|
+
// State transition: newly added, re-added, or config changed
|
|
637
|
+
journal.servers[name][source.path] = {
|
|
638
|
+
present: true,
|
|
639
|
+
mtime: source.mtime,
|
|
640
|
+
ts: now,
|
|
641
|
+
configHash: hash,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
// If already present with same config: no transition, keep existing mtime
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Servers previously tracked in this source but now gone
|
|
648
|
+
for (const [serverName, sourceMap] of Object.entries(journal.servers)) {
|
|
649
|
+
const entry = sourceMap[source.path];
|
|
650
|
+
if (entry?.present && !currentNames.has(serverName)) {
|
|
651
|
+
sourceMap[source.path] = {
|
|
652
|
+
present: false,
|
|
653
|
+
mtime: source.mtime,
|
|
654
|
+
ts: now,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
605
657
|
}
|
|
606
658
|
}
|
|
607
659
|
|
|
608
|
-
|
|
660
|
+
// Bootstrap: for servers found in some sources but absent from others,
|
|
661
|
+
// record "not present" entries for the missing sources. Without this,
|
|
662
|
+
// a server deleted from one file before the v2 journal existed would
|
|
663
|
+
// have no removal event to counterbalance presence in other files.
|
|
664
|
+
const allServerNames = new Set(Object.keys(journal.servers));
|
|
665
|
+
for (const serverName of allServerNames) {
|
|
666
|
+
const sourceMap = journal.servers[serverName];
|
|
667
|
+
for (const source of sources) {
|
|
668
|
+
if (!sourceMap[source.path]) {
|
|
669
|
+
// This source has never been tracked for this server.
|
|
670
|
+
// If the file exists (mtime > 0), record its absence.
|
|
671
|
+
if (source.mtime > 0) {
|
|
672
|
+
sourceMap[source.path] = {
|
|
673
|
+
present: false,
|
|
674
|
+
mtime: source.mtime,
|
|
675
|
+
ts: now,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Clean up journal entries for source files that no longer exist
|
|
683
|
+
for (const sourceMap of Object.values(journal.servers)) {
|
|
684
|
+
for (const sourcePath of Object.keys(sourceMap)) {
|
|
685
|
+
if (sourcePath !== "migrated-v1" && !activePaths.has(sourcePath)) {
|
|
686
|
+
delete sourceMap[sourcePath];
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Resolve: per server, latest mtime wins
|
|
692
|
+
const resolved: Record<string, CanonicalMcpServer> = {};
|
|
693
|
+
|
|
694
|
+
const allNames = new Set<string>();
|
|
695
|
+
for (const source of sources) {
|
|
696
|
+
for (const name of Object.keys(source.servers)) allNames.add(name);
|
|
697
|
+
}
|
|
698
|
+
for (const name of Object.keys(journal.servers)) allNames.add(name);
|
|
699
|
+
|
|
700
|
+
for (const serverName of allNames) {
|
|
701
|
+
const sourceMap = journal.servers[serverName];
|
|
702
|
+
if (!sourceMap) continue;
|
|
703
|
+
|
|
704
|
+
let bestMtime = -1;
|
|
705
|
+
let bestPresent = false;
|
|
706
|
+
let bestPath = "";
|
|
707
|
+
|
|
708
|
+
for (const [sourcePath, state] of Object.entries(sourceMap)) {
|
|
709
|
+
if (
|
|
710
|
+
state.mtime > bestMtime ||
|
|
711
|
+
(state.mtime === bestMtime && state.present && !bestPresent)
|
|
712
|
+
) {
|
|
713
|
+
bestMtime = state.mtime;
|
|
714
|
+
bestPresent = state.present;
|
|
715
|
+
bestPath = sourcePath;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (bestPresent) {
|
|
720
|
+
// Prefer the winning source; fall back to any source that has the server
|
|
721
|
+
const source = sources.find((s) => s.path === bestPath);
|
|
722
|
+
const server =
|
|
723
|
+
source?.servers[serverName] ??
|
|
724
|
+
sources.find((s) => s.servers[serverName])?.servers[serverName];
|
|
725
|
+
if (server) resolved[serverName] = server;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
writeJournal(jrnlPath, journal);
|
|
730
|
+
return { resolved, journal };
|
|
609
731
|
}
|
|
610
732
|
|
|
611
733
|
/**
|
|
612
734
|
* Run MCP sync for a specific scope level.
|
|
613
735
|
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
* 4. Filters out removed servers
|
|
618
|
-
* 5. Writes merged result to the aide canonical config
|
|
619
|
-
* 6. Writes the result (in assistant-native format) to the current assistant's config
|
|
736
|
+
* Gathers servers from all sources, updates the per-server journal to
|
|
737
|
+
* track state transitions, resolves using latest-mtime-wins per server,
|
|
738
|
+
* and writes the result to the aide canonical config and current assistant.
|
|
620
739
|
*/
|
|
621
740
|
function syncScope(
|
|
622
741
|
platform: McpPlatform,
|
|
@@ -631,56 +750,55 @@ function syncScope(
|
|
|
631
750
|
serverNames: [],
|
|
632
751
|
};
|
|
633
752
|
|
|
634
|
-
// Step 1: Read current aide canonical config
|
|
635
753
|
const aidePath =
|
|
636
754
|
scope === "user" ? aideUserMcpPath() : aideProjectMcpPath(cwd);
|
|
637
755
|
const aideServers = readAideConfig(aidePath);
|
|
638
|
-
const aideServerNames = Object.keys(aideServers);
|
|
639
756
|
|
|
640
|
-
// Step 2: Update journal and get removed list
|
|
641
757
|
const jrnlPath = scope === "user" ? userJournalPath() : journalPath(cwd);
|
|
642
|
-
const
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
// Step 4: Filter out removed servers
|
|
649
|
-
const finalServers: Record<string, CanonicalMcpServer> = {};
|
|
650
|
-
for (const [name, server] of Object.entries(allServers)) {
|
|
651
|
-
if (removedSet.has(name)) {
|
|
652
|
-
result.skipped++;
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
finalServers[name] = server;
|
|
656
|
-
}
|
|
758
|
+
const sources = gatherSources(scope, cwd);
|
|
759
|
+
const { resolved: finalServers, journal } = updateJournalAndResolve(
|
|
760
|
+
jrnlPath,
|
|
761
|
+
sources,
|
|
762
|
+
);
|
|
657
763
|
|
|
658
|
-
//
|
|
764
|
+
// Count imports (servers not previously in aide config)
|
|
659
765
|
for (const name of Object.keys(finalServers)) {
|
|
660
766
|
if (!aideServers[name]) {
|
|
661
767
|
result.imported++;
|
|
662
768
|
}
|
|
663
769
|
}
|
|
664
770
|
|
|
665
|
-
//
|
|
771
|
+
// Count skipped (servers whose latest journal state is a removal)
|
|
772
|
+
for (const [serverName, sourceMap] of Object.entries(journal.servers)) {
|
|
773
|
+
if (finalServers[serverName]) continue;
|
|
774
|
+
let bestMtime = -1;
|
|
775
|
+
let bestPresent = false;
|
|
776
|
+
for (const state of Object.values(sourceMap)) {
|
|
777
|
+
if (state.mtime > bestMtime) {
|
|
778
|
+
bestMtime = state.mtime;
|
|
779
|
+
bestPresent = state.present;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
if (!bestPresent) result.skipped++;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Write to aide canonical config (if changed)
|
|
786
|
+
const sortedStringify = (obj: object) =>
|
|
787
|
+
JSON.stringify(obj, Object.keys(obj).sort());
|
|
666
788
|
const aideChanged =
|
|
667
|
-
|
|
668
|
-
JSON.stringify(
|
|
669
|
-
Object.fromEntries(Object.entries(finalServers).map(([k, v]) => [k, v])),
|
|
670
|
-
);
|
|
789
|
+
sortedStringify(aideServers) !== sortedStringify(finalServers);
|
|
671
790
|
|
|
672
791
|
if (aideChanged) {
|
|
673
792
|
writeAideConfig(aidePath, finalServers);
|
|
674
793
|
result.modified = true;
|
|
675
794
|
}
|
|
676
795
|
|
|
677
|
-
//
|
|
796
|
+
// Write to current assistant's config
|
|
678
797
|
const assistantPaths = getAssistantWritePaths(platform, scope, cwd);
|
|
679
798
|
for (const p of assistantPaths) {
|
|
680
|
-
// Read existing assistant config to check if it needs updating
|
|
681
799
|
const existingAssistant = readAssistantConfig(platform, p);
|
|
682
800
|
const assistantChanged =
|
|
683
|
-
|
|
801
|
+
sortedStringify(existingAssistant) !== sortedStringify(finalServers);
|
|
684
802
|
|
|
685
803
|
if (assistantChanged) {
|
|
686
804
|
writeAssistantConfig(platform, p, finalServers);
|
|
@@ -738,16 +856,32 @@ export function listSyncedServers(cwd: string): {
|
|
|
738
856
|
|
|
739
857
|
/**
|
|
740
858
|
* Get the current removed (blocked) server names.
|
|
859
|
+
* Derived from the v2 journal: a server is "removed" if its latest
|
|
860
|
+
* mtime across all sources corresponds to a removal event.
|
|
741
861
|
*/
|
|
742
862
|
export function getRemovedServers(cwd: string): {
|
|
743
863
|
user: string[];
|
|
744
864
|
project: string[];
|
|
745
865
|
} {
|
|
746
|
-
|
|
747
|
-
|
|
866
|
+
function deriveRemoved(jrnlPath: string): string[] {
|
|
867
|
+
const journal = readJournal(jrnlPath);
|
|
868
|
+
const removed: string[] = [];
|
|
869
|
+
for (const [serverName, sourceMap] of Object.entries(journal.servers)) {
|
|
870
|
+
let bestMtime = -1;
|
|
871
|
+
let bestPresent = false;
|
|
872
|
+
for (const state of Object.values(sourceMap)) {
|
|
873
|
+
if (state.mtime > bestMtime) {
|
|
874
|
+
bestMtime = state.mtime;
|
|
875
|
+
bestPresent = state.present;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (!bestPresent) removed.push(serverName);
|
|
879
|
+
}
|
|
880
|
+
return removed;
|
|
881
|
+
}
|
|
748
882
|
|
|
749
883
|
return {
|
|
750
|
-
user:
|
|
751
|
-
project:
|
|
884
|
+
user: deriveRemoved(userJournalPath()),
|
|
885
|
+
project: deriveRemoved(journalPath(cwd)),
|
|
752
886
|
};
|
|
753
887
|
}
|