@ncukondo/reference-manager 0.1.0 → 0.4.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/README.md +278 -80
- package/dist/chunks/{detector-BF8Mcc72.js → file-watcher-B-SiUw5f.js} +469 -327
- package/dist/chunks/file-watcher-B-SiUw5f.js.map +1 -0
- package/dist/chunks/index-DLIGxQaB.js +29851 -0
- package/dist/chunks/index-DLIGxQaB.js.map +1 -0
- package/dist/chunks/loader-DuzyKV70.js +394 -0
- package/dist/chunks/loader-DuzyKV70.js.map +1 -0
- package/dist/cli/commands/add.d.ts +44 -16
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/cite.d.ts +49 -0
- package/dist/cli/commands/cite.d.ts.map +1 -0
- package/dist/cli/commands/fulltext.d.ts +72 -0
- package/dist/cli/commands/fulltext.d.ts.map +1 -0
- package/dist/cli/commands/index.d.ts +14 -10
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/list.d.ts +23 -6
- package/dist/cli/commands/list.d.ts.map +1 -1
- package/dist/cli/commands/mcp.d.ts +16 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/remove.d.ts +47 -12
- package/dist/cli/commands/remove.d.ts.map +1 -1
- package/dist/cli/commands/search.d.ts +24 -7
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/commands/server.d.ts +2 -0
- package/dist/cli/commands/server.d.ts.map +1 -1
- package/dist/cli/commands/update.d.ts +26 -13
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/execution-context.d.ts +47 -0
- package/dist/cli/execution-context.d.ts.map +1 -0
- package/dist/cli/helpers.d.ts +18 -0
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/server-client.d.ts +61 -14
- package/dist/cli/server-client.d.ts.map +1 -1
- package/dist/cli/server-detection.d.ts +1 -0
- package/dist/cli/server-detection.d.ts.map +1 -1
- package/dist/cli.js +21979 -564
- package/dist/cli.js.map +1 -1
- package/dist/config/csl-styles.d.ts +83 -0
- package/dist/config/csl-styles.d.ts.map +1 -0
- package/dist/config/defaults.d.ts +10 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/schema.d.ts +86 -3
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/core/csl-json/types.d.ts +18 -3
- package/dist/core/csl-json/types.d.ts.map +1 -1
- package/dist/core/library-interface.d.ts +100 -0
- package/dist/core/library-interface.d.ts.map +1 -0
- package/dist/core/library.d.ts +56 -13
- package/dist/core/library.d.ts.map +1 -1
- package/dist/features/format/bibtex.d.ts +6 -0
- package/dist/features/format/bibtex.d.ts.map +1 -0
- package/dist/features/format/citation-csl.d.ts +41 -0
- package/dist/features/format/citation-csl.d.ts.map +1 -0
- package/dist/features/format/citation-fallback.d.ts +24 -0
- package/dist/features/format/citation-fallback.d.ts.map +1 -0
- package/dist/features/format/index.d.ts +10 -0
- package/dist/features/format/index.d.ts.map +1 -0
- package/dist/features/format/json.d.ts +6 -0
- package/dist/features/format/json.d.ts.map +1 -0
- package/dist/features/format/pretty.d.ts +6 -0
- package/dist/features/format/pretty.d.ts.map +1 -0
- package/dist/features/fulltext/filename.d.ts +17 -0
- package/dist/features/fulltext/filename.d.ts.map +1 -0
- package/dist/features/fulltext/index.d.ts +7 -0
- package/dist/features/fulltext/index.d.ts.map +1 -0
- package/dist/features/fulltext/manager.d.ts +109 -0
- package/dist/features/fulltext/manager.d.ts.map +1 -0
- package/dist/features/fulltext/types.d.ts +12 -0
- package/dist/features/fulltext/types.d.ts.map +1 -0
- package/dist/features/import/cache.d.ts +37 -0
- package/dist/features/import/cache.d.ts.map +1 -0
- package/dist/features/import/detector.d.ts +42 -0
- package/dist/features/import/detector.d.ts.map +1 -0
- package/dist/features/import/fetcher.d.ts +49 -0
- package/dist/features/import/fetcher.d.ts.map +1 -0
- package/dist/features/import/importer.d.ts +61 -0
- package/dist/features/import/importer.d.ts.map +1 -0
- package/dist/features/import/index.d.ts +8 -0
- package/dist/features/import/index.d.ts.map +1 -0
- package/dist/features/import/normalizer.d.ts +15 -0
- package/dist/features/import/normalizer.d.ts.map +1 -0
- package/dist/features/import/parser.d.ts +33 -0
- package/dist/features/import/parser.d.ts.map +1 -0
- package/dist/features/import/rate-limiter.d.ts +45 -0
- package/dist/features/import/rate-limiter.d.ts.map +1 -0
- package/dist/features/operations/add.d.ts +65 -0
- package/dist/features/operations/add.d.ts.map +1 -0
- package/dist/features/operations/cite.d.ts +48 -0
- package/dist/features/operations/cite.d.ts.map +1 -0
- package/dist/features/operations/fulltext/attach.d.ts +47 -0
- package/dist/features/operations/fulltext/attach.d.ts.map +1 -0
- package/dist/features/operations/fulltext/detach.d.ts +38 -0
- package/dist/features/operations/fulltext/detach.d.ts.map +1 -0
- package/dist/features/operations/fulltext/get.d.ts +41 -0
- package/dist/features/operations/fulltext/get.d.ts.map +1 -0
- package/dist/features/operations/fulltext/index.d.ts +9 -0
- package/dist/features/operations/fulltext/index.d.ts.map +1 -0
- package/dist/features/operations/index.d.ts +15 -0
- package/dist/features/operations/index.d.ts.map +1 -0
- package/dist/features/operations/library-operations.d.ts +64 -0
- package/dist/features/operations/library-operations.d.ts.map +1 -0
- package/dist/features/operations/list.d.ts +28 -0
- package/dist/features/operations/list.d.ts.map +1 -0
- package/dist/features/operations/operations-library.d.ts +36 -0
- package/dist/features/operations/operations-library.d.ts.map +1 -0
- package/dist/features/operations/remove.d.ts +29 -0
- package/dist/features/operations/remove.d.ts.map +1 -0
- package/dist/features/operations/search.d.ts +30 -0
- package/dist/features/operations/search.d.ts.map +1 -0
- package/dist/features/operations/update.d.ts +39 -0
- package/dist/features/operations/update.d.ts.map +1 -0
- package/dist/features/search/matcher.d.ts.map +1 -1
- package/dist/features/search/normalizer.d.ts +12 -0
- package/dist/features/search/normalizer.d.ts.map +1 -1
- package/dist/features/search/tokenizer.d.ts.map +1 -1
- package/dist/features/search/types.d.ts +1 -1
- package/dist/features/search/types.d.ts.map +1 -1
- package/dist/features/search/uppercase.d.ts +41 -0
- package/dist/features/search/uppercase.d.ts.map +1 -0
- package/dist/index.js +21 -187
- package/dist/index.js.map +1 -1
- package/dist/mcp/context.d.ts +19 -0
- package/dist/mcp/context.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +20 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/resources/index.d.ts +10 -0
- package/dist/mcp/resources/index.d.ts.map +1 -0
- package/dist/mcp/resources/library.d.ts +26 -0
- package/dist/mcp/resources/library.d.ts.map +1 -0
- package/dist/mcp/tools/add.d.ts +17 -0
- package/dist/mcp/tools/add.d.ts.map +1 -0
- package/dist/mcp/tools/cite.d.ts +15 -0
- package/dist/mcp/tools/cite.d.ts.map +1 -0
- package/dist/mcp/tools/fulltext.d.ts +51 -0
- package/dist/mcp/tools/fulltext.d.ts.map +1 -0
- package/dist/mcp/tools/index.d.ts +12 -0
- package/dist/mcp/tools/index.d.ts.map +1 -0
- package/dist/mcp/tools/list.d.ts +13 -0
- package/dist/mcp/tools/list.d.ts.map +1 -0
- package/dist/mcp/tools/remove.d.ts +19 -0
- package/dist/mcp/tools/remove.d.ts.map +1 -0
- package/dist/mcp/tools/search.d.ts +13 -0
- package/dist/mcp/tools/search.d.ts.map +1 -0
- package/dist/server/index.d.ts +26 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/routes/add.d.ts +11 -0
- package/dist/server/routes/add.d.ts.map +1 -0
- package/dist/server/routes/cite.d.ts +9 -0
- package/dist/server/routes/cite.d.ts.map +1 -0
- package/dist/server/routes/list.d.ts +25 -0
- package/dist/server/routes/list.d.ts.map +1 -0
- package/dist/server/routes/references.d.ts.map +1 -1
- package/dist/server/routes/search.d.ts +26 -0
- package/dist/server/routes/search.d.ts.map +1 -0
- package/dist/server.js +5 -88
- package/dist/server.js.map +1 -1
- package/package.json +16 -4
- package/dist/chunks/detector-BF8Mcc72.js.map +0 -1
- package/dist/cli/output/bibtex.d.ts +0 -6
- package/dist/cli/output/bibtex.d.ts.map +0 -1
- package/dist/cli/output/index.d.ts +0 -7
- package/dist/cli/output/index.d.ts.map +0 -1
- package/dist/cli/output/json.d.ts +0 -6
- package/dist/cli/output/json.d.ts.map +0 -1
- package/dist/cli/output/pretty.d.ts +0 -6
- package/dist/cli/output/pretty.d.ts.map +0 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { randomUUID, createHash } from "node:crypto";
|
|
2
|
-
import { createReadStream
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
3
4
|
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
4
5
|
import { z } from "zod";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
import { EventEmitter } from "node:events";
|
|
9
|
+
import chokidar from "chokidar";
|
|
8
10
|
function normalizeText(text) {
|
|
9
11
|
return text.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_|_$/g, "");
|
|
10
12
|
}
|
|
@@ -310,12 +312,18 @@ const CslDateSchema = z.object({
|
|
|
310
312
|
circa: z.boolean().optional(),
|
|
311
313
|
literal: z.string().optional()
|
|
312
314
|
});
|
|
315
|
+
const CslFulltextSchema = z.object({
|
|
316
|
+
pdf: z.string().optional(),
|
|
317
|
+
markdown: z.string().optional()
|
|
318
|
+
});
|
|
313
319
|
const CslCustomSchema = z.object({
|
|
314
320
|
uuid: z.string(),
|
|
315
321
|
created_at: z.string(),
|
|
316
322
|
timestamp: z.string(),
|
|
317
|
-
additional_urls: z.array(z.string()).optional()
|
|
318
|
-
|
|
323
|
+
additional_urls: z.array(z.string()).optional(),
|
|
324
|
+
fulltext: CslFulltextSchema.optional(),
|
|
325
|
+
tags: z.array(z.string()).optional()
|
|
326
|
+
}).passthrough();
|
|
319
327
|
const CslItemSchema = z.object({
|
|
320
328
|
id: z.string(),
|
|
321
329
|
type: z.string(),
|
|
@@ -449,46 +457,83 @@ class Library {
|
|
|
449
457
|
await writeCslJson(this.filePath, items);
|
|
450
458
|
this.currentHash = await computeFileHash(this.filePath);
|
|
451
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* Reloads the library from file if it was modified externally.
|
|
462
|
+
* Self-writes (detected via hash comparison) are skipped.
|
|
463
|
+
* @returns true if reload occurred, false if skipped (self-write detected)
|
|
464
|
+
*/
|
|
465
|
+
async reload() {
|
|
466
|
+
const newHash = await computeFileHash(this.filePath);
|
|
467
|
+
if (newHash === this.currentHash) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
const items = await parseCslJson(this.filePath);
|
|
471
|
+
this.references = [];
|
|
472
|
+
this.uuidIndex.clear();
|
|
473
|
+
this.idIndex.clear();
|
|
474
|
+
this.doiIndex.clear();
|
|
475
|
+
this.pmidIndex.clear();
|
|
476
|
+
for (const item of items) {
|
|
477
|
+
const ref = new Reference(item);
|
|
478
|
+
this.references.push(ref);
|
|
479
|
+
this.addToIndices(ref);
|
|
480
|
+
}
|
|
481
|
+
this.currentHash = newHash;
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
452
484
|
/**
|
|
453
485
|
* Add a reference to the library
|
|
486
|
+
* @param item - The CSL item to add
|
|
487
|
+
* @returns The added CSL item (with generated ID and UUID)
|
|
454
488
|
*/
|
|
455
|
-
add(item) {
|
|
489
|
+
async add(item) {
|
|
456
490
|
const existingIds = new Set(this.references.map((ref2) => ref2.getId()));
|
|
457
491
|
const ref = Reference.create(item, { existingIds });
|
|
458
492
|
this.references.push(ref);
|
|
459
493
|
this.addToIndices(ref);
|
|
494
|
+
return ref.getItem();
|
|
460
495
|
}
|
|
461
496
|
/**
|
|
462
|
-
* Remove a reference by UUID
|
|
497
|
+
* Remove a reference by citation ID or UUID.
|
|
498
|
+
* @param identifier - The citation ID or UUID of the reference to remove
|
|
499
|
+
* @param options - Remove options (byUuid to use UUID lookup)
|
|
500
|
+
* @returns Remove result with removed status and the removed item
|
|
463
501
|
*/
|
|
464
|
-
|
|
465
|
-
const
|
|
502
|
+
async remove(identifier, options = {}) {
|
|
503
|
+
const { byUuid = false } = options;
|
|
504
|
+
const ref = byUuid ? this.uuidIndex.get(identifier) : this.idIndex.get(identifier);
|
|
466
505
|
if (!ref) {
|
|
467
|
-
return false;
|
|
506
|
+
return { removed: false };
|
|
468
507
|
}
|
|
469
|
-
|
|
508
|
+
const removedItem = ref.getItem();
|
|
509
|
+
const removed = this.removeReference(ref);
|
|
510
|
+
return { removed, removedItem };
|
|
470
511
|
}
|
|
471
512
|
/**
|
|
472
|
-
*
|
|
513
|
+
* Update a reference by citation ID or UUID.
|
|
514
|
+
* @param identifier - The citation ID or UUID of the reference to update
|
|
515
|
+
* @param updates - Partial updates to apply to the reference
|
|
516
|
+
* @param options - Update options (byUuid to use UUID lookup, onIdCollision for collision handling)
|
|
517
|
+
* @returns Update result with updated item, success status, and any ID changes
|
|
473
518
|
*/
|
|
474
|
-
|
|
475
|
-
const
|
|
519
|
+
async update(identifier, updates, options = {}) {
|
|
520
|
+
const { byUuid = false, ...updateOptions } = options;
|
|
521
|
+
const ref = byUuid ? this.uuidIndex.get(identifier) : this.idIndex.get(identifier);
|
|
476
522
|
if (!ref) {
|
|
477
|
-
return false;
|
|
523
|
+
return { updated: false };
|
|
478
524
|
}
|
|
479
|
-
return this.
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* Find a reference by UUID
|
|
483
|
-
*/
|
|
484
|
-
findByUuid(uuid) {
|
|
485
|
-
return this.uuidIndex.get(uuid);
|
|
525
|
+
return this.updateReference(ref, updates, updateOptions);
|
|
486
526
|
}
|
|
487
527
|
/**
|
|
488
|
-
* Find a reference by ID
|
|
528
|
+
* Find a reference by citation ID or UUID.
|
|
529
|
+
* @param identifier - The citation ID or UUID of the reference to find
|
|
530
|
+
* @param options - Find options (byUuid to use UUID lookup)
|
|
531
|
+
* @returns The CSL item if found, undefined otherwise
|
|
489
532
|
*/
|
|
490
|
-
|
|
491
|
-
|
|
533
|
+
async find(identifier, options = {}) {
|
|
534
|
+
const { byUuid = false } = options;
|
|
535
|
+
const ref = byUuid ? this.uuidIndex.get(identifier) : this.idIndex.get(identifier);
|
|
536
|
+
return ref?.getItem();
|
|
492
537
|
}
|
|
493
538
|
/**
|
|
494
539
|
* Find a reference by DOI
|
|
@@ -505,8 +550,8 @@ class Library {
|
|
|
505
550
|
/**
|
|
506
551
|
* Get all references
|
|
507
552
|
*/
|
|
508
|
-
getAll() {
|
|
509
|
-
return
|
|
553
|
+
async getAll() {
|
|
554
|
+
return this.references.map((ref) => ref.getItem());
|
|
510
555
|
}
|
|
511
556
|
/**
|
|
512
557
|
* Get the file path
|
|
@@ -545,281 +590,122 @@ class Library {
|
|
|
545
590
|
return false;
|
|
546
591
|
}
|
|
547
592
|
this.references.splice(index, 1);
|
|
548
|
-
this.
|
|
549
|
-
this.idIndex.delete(ref.getId());
|
|
550
|
-
const doi = ref.getDoi();
|
|
551
|
-
if (doi) {
|
|
552
|
-
this.doiIndex.delete(doi);
|
|
553
|
-
}
|
|
554
|
-
const pmid = ref.getPmid();
|
|
555
|
-
if (pmid) {
|
|
556
|
-
this.pmidIndex.delete(pmid);
|
|
557
|
-
}
|
|
593
|
+
this.removeFromIndices(ref);
|
|
558
594
|
return true;
|
|
559
595
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
enabled: z.boolean(),
|
|
569
|
-
debounceMs: z.number().int().nonnegative(),
|
|
570
|
-
pollIntervalMs: z.number().int().positive(),
|
|
571
|
-
retryIntervalMs: z.number().int().positive(),
|
|
572
|
-
maxRetries: z.number().int().nonnegative()
|
|
573
|
-
});
|
|
574
|
-
const serverConfigSchema = z.object({
|
|
575
|
-
autoStart: z.boolean(),
|
|
576
|
-
autoStopMinutes: z.number().int().nonnegative()
|
|
577
|
-
});
|
|
578
|
-
const configSchema = z.object({
|
|
579
|
-
library: z.string().min(1),
|
|
580
|
-
logLevel: logLevelSchema,
|
|
581
|
-
backup: backupConfigSchema,
|
|
582
|
-
watch: watchConfigSchema,
|
|
583
|
-
server: serverConfigSchema
|
|
584
|
-
});
|
|
585
|
-
const partialConfigSchema = z.object({
|
|
586
|
-
library: z.string().min(1).optional(),
|
|
587
|
-
logLevel: logLevelSchema.optional(),
|
|
588
|
-
log_level: logLevelSchema.optional(),
|
|
589
|
-
// snake_case support
|
|
590
|
-
backup: z.object({
|
|
591
|
-
maxGenerations: z.number().int().positive().optional(),
|
|
592
|
-
max_generations: z.number().int().positive().optional(),
|
|
593
|
-
maxAgeDays: z.number().int().positive().optional(),
|
|
594
|
-
max_age_days: z.number().int().positive().optional(),
|
|
595
|
-
directory: z.string().min(1).optional()
|
|
596
|
-
}).optional(),
|
|
597
|
-
watch: z.object({
|
|
598
|
-
enabled: z.boolean().optional(),
|
|
599
|
-
debounceMs: z.number().int().nonnegative().optional(),
|
|
600
|
-
debounce_ms: z.number().int().nonnegative().optional(),
|
|
601
|
-
pollIntervalMs: z.number().int().positive().optional(),
|
|
602
|
-
poll_interval_ms: z.number().int().positive().optional(),
|
|
603
|
-
retryIntervalMs: z.number().int().positive().optional(),
|
|
604
|
-
retry_interval_ms: z.number().int().positive().optional(),
|
|
605
|
-
maxRetries: z.number().int().nonnegative().optional(),
|
|
606
|
-
max_retries: z.number().int().nonnegative().optional()
|
|
607
|
-
}).optional(),
|
|
608
|
-
server: z.object({
|
|
609
|
-
autoStart: z.boolean().optional(),
|
|
610
|
-
auto_start: z.boolean().optional(),
|
|
611
|
-
autoStopMinutes: z.number().int().nonnegative().optional(),
|
|
612
|
-
auto_stop_minutes: z.number().int().nonnegative().optional()
|
|
613
|
-
}).optional()
|
|
614
|
-
}).passthrough();
|
|
615
|
-
function normalizeBackupConfig(backup) {
|
|
616
|
-
const normalized = {};
|
|
617
|
-
const maxGenerations = backup.maxGenerations ?? backup.max_generations;
|
|
618
|
-
if (maxGenerations !== void 0) {
|
|
619
|
-
normalized.maxGenerations = maxGenerations;
|
|
620
|
-
}
|
|
621
|
-
const maxAgeDays = backup.maxAgeDays ?? backup.max_age_days;
|
|
622
|
-
if (maxAgeDays !== void 0) {
|
|
623
|
-
normalized.maxAgeDays = maxAgeDays;
|
|
624
|
-
}
|
|
625
|
-
if (backup.directory !== void 0) {
|
|
626
|
-
normalized.directory = backup.directory;
|
|
627
|
-
}
|
|
628
|
-
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
629
|
-
}
|
|
630
|
-
function normalizeWatchConfig(watch) {
|
|
631
|
-
const normalized = {};
|
|
632
|
-
if (watch.enabled !== void 0) {
|
|
633
|
-
normalized.enabled = watch.enabled;
|
|
634
|
-
}
|
|
635
|
-
const debounceMs = watch.debounceMs ?? watch.debounce_ms;
|
|
636
|
-
if (debounceMs !== void 0) {
|
|
637
|
-
normalized.debounceMs = debounceMs;
|
|
638
|
-
}
|
|
639
|
-
const pollIntervalMs = watch.pollIntervalMs ?? watch.poll_interval_ms;
|
|
640
|
-
if (pollIntervalMs !== void 0) {
|
|
641
|
-
normalized.pollIntervalMs = pollIntervalMs;
|
|
642
|
-
}
|
|
643
|
-
const retryIntervalMs = watch.retryIntervalMs ?? watch.retry_interval_ms;
|
|
644
|
-
if (retryIntervalMs !== void 0) {
|
|
645
|
-
normalized.retryIntervalMs = retryIntervalMs;
|
|
646
|
-
}
|
|
647
|
-
const maxRetries = watch.maxRetries ?? watch.max_retries;
|
|
648
|
-
if (maxRetries !== void 0) {
|
|
649
|
-
normalized.maxRetries = maxRetries;
|
|
650
|
-
}
|
|
651
|
-
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
652
|
-
}
|
|
653
|
-
function normalizeServerConfig(server) {
|
|
654
|
-
const normalized = {};
|
|
655
|
-
const autoStart = server.autoStart ?? server.auto_start;
|
|
656
|
-
if (autoStart !== void 0) {
|
|
657
|
-
normalized.autoStart = autoStart;
|
|
658
|
-
}
|
|
659
|
-
const autoStopMinutes = server.autoStopMinutes ?? server.auto_stop_minutes;
|
|
660
|
-
if (autoStopMinutes !== void 0) {
|
|
661
|
-
normalized.autoStopMinutes = autoStopMinutes;
|
|
662
|
-
}
|
|
663
|
-
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
664
|
-
}
|
|
665
|
-
function normalizePartialConfig(partial) {
|
|
666
|
-
const normalized = {};
|
|
667
|
-
if (partial.library !== void 0) {
|
|
668
|
-
normalized.library = partial.library;
|
|
669
|
-
}
|
|
670
|
-
const logLevel = partial.logLevel ?? partial.log_level;
|
|
671
|
-
if (logLevel !== void 0) {
|
|
672
|
-
normalized.logLevel = logLevel;
|
|
673
|
-
}
|
|
674
|
-
if (partial.backup !== void 0) {
|
|
675
|
-
const backup = normalizeBackupConfig(
|
|
676
|
-
partial.backup
|
|
677
|
-
);
|
|
678
|
-
if (backup) {
|
|
679
|
-
normalized.backup = backup;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
if (partial.watch !== void 0) {
|
|
683
|
-
const watch = normalizeWatchConfig(partial.watch);
|
|
684
|
-
if (watch) {
|
|
685
|
-
normalized.watch = watch;
|
|
596
|
+
/**
|
|
597
|
+
* Update a reference with partial updates.
|
|
598
|
+
* Preserves uuid and created_at, updates timestamp.
|
|
599
|
+
*/
|
|
600
|
+
updateReference(ref, updates, options = {}) {
|
|
601
|
+
const index = this.references.indexOf(ref);
|
|
602
|
+
if (index === -1) {
|
|
603
|
+
return { updated: false };
|
|
686
604
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const
|
|
690
|
-
|
|
605
|
+
const existingItem = ref.getItem();
|
|
606
|
+
const currentId = ref.getId();
|
|
607
|
+
const { newId, idChanged, collision } = this.resolveNewId(
|
|
608
|
+
updates.id ?? existingItem.id,
|
|
609
|
+
currentId,
|
|
610
|
+
options
|
|
691
611
|
);
|
|
692
|
-
if (
|
|
693
|
-
|
|
612
|
+
if (collision) {
|
|
613
|
+
return { updated: false, idCollision: true };
|
|
694
614
|
}
|
|
615
|
+
const updatedItem = this.buildUpdatedItem(existingItem, updates, newId);
|
|
616
|
+
this.removeFromIndices(ref);
|
|
617
|
+
const newRef = new Reference(updatedItem);
|
|
618
|
+
this.references[index] = newRef;
|
|
619
|
+
this.addToIndices(newRef);
|
|
620
|
+
const result = { updated: true, item: newRef.getItem() };
|
|
621
|
+
if (idChanged) {
|
|
622
|
+
result.idChanged = true;
|
|
623
|
+
result.newId = newId;
|
|
624
|
+
}
|
|
625
|
+
return result;
|
|
695
626
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
return join(homedir(), ".reference-manager", "csl.library.json");
|
|
703
|
-
}
|
|
704
|
-
function getDefaultUserConfigPath() {
|
|
705
|
-
return join(homedir(), ".reference-manager", "config.toml");
|
|
706
|
-
}
|
|
707
|
-
function getDefaultCurrentDirConfigFilename() {
|
|
708
|
-
return ".reference-manager.config.toml";
|
|
709
|
-
}
|
|
710
|
-
const defaultConfig = {
|
|
711
|
-
library: getDefaultLibraryPath(),
|
|
712
|
-
logLevel: "info",
|
|
713
|
-
backup: {
|
|
714
|
-
maxGenerations: 50,
|
|
715
|
-
maxAgeDays: 365,
|
|
716
|
-
directory: getDefaultBackupDirectory()
|
|
717
|
-
},
|
|
718
|
-
watch: {
|
|
719
|
-
enabled: true,
|
|
720
|
-
debounceMs: 500,
|
|
721
|
-
pollIntervalMs: 5e3,
|
|
722
|
-
retryIntervalMs: 200,
|
|
723
|
-
maxRetries: 10
|
|
724
|
-
},
|
|
725
|
-
server: {
|
|
726
|
-
autoStart: false,
|
|
727
|
-
autoStopMinutes: 0
|
|
728
|
-
}
|
|
729
|
-
};
|
|
730
|
-
function loadTOMLFile(path) {
|
|
731
|
-
if (!existsSync(path)) {
|
|
732
|
-
return null;
|
|
733
|
-
}
|
|
734
|
-
try {
|
|
735
|
-
const content = readFileSync(path, "utf-8");
|
|
736
|
-
const parsed = parse(content);
|
|
737
|
-
const validated = partialConfigSchema.parse(parsed);
|
|
738
|
-
return validated;
|
|
739
|
-
} catch (error) {
|
|
740
|
-
throw new Error(
|
|
741
|
-
`Failed to load config from ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
function mergeConfigs(base, ...overrides) {
|
|
746
|
-
const result = { ...base };
|
|
747
|
-
for (const override of overrides) {
|
|
748
|
-
if (!override) continue;
|
|
749
|
-
if (override.library !== void 0) {
|
|
750
|
-
result.library = override.library;
|
|
627
|
+
/**
|
|
628
|
+
* Resolve the new ID, handling collisions based on options.
|
|
629
|
+
*/
|
|
630
|
+
resolveNewId(requestedId, currentId, options) {
|
|
631
|
+
if (requestedId === currentId) {
|
|
632
|
+
return { newId: requestedId, idChanged: false, collision: false };
|
|
751
633
|
}
|
|
752
|
-
|
|
753
|
-
|
|
634
|
+
const conflictingRef = this.idIndex.get(requestedId);
|
|
635
|
+
if (!conflictingRef) {
|
|
636
|
+
return { newId: requestedId, idChanged: false, collision: false };
|
|
754
637
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
...override.backup
|
|
759
|
-
};
|
|
638
|
+
const onIdCollision = options.onIdCollision ?? "fail";
|
|
639
|
+
if (onIdCollision === "fail") {
|
|
640
|
+
return { newId: requestedId, idChanged: false, collision: true };
|
|
760
641
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
642
|
+
const existingIds = new Set(this.references.map((r) => r.getId()));
|
|
643
|
+
existingIds.delete(currentId);
|
|
644
|
+
const resolvedId = this.resolveIdCollision(requestedId, existingIds);
|
|
645
|
+
return { newId: resolvedId, idChanged: true, collision: false };
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Build the updated CslItem, preserving uuid and created_at.
|
|
649
|
+
*/
|
|
650
|
+
buildUpdatedItem(existingItem, updates, newId) {
|
|
651
|
+
return {
|
|
652
|
+
...existingItem,
|
|
653
|
+
...updates,
|
|
654
|
+
id: newId,
|
|
655
|
+
type: updates.type ?? existingItem.type,
|
|
656
|
+
custom: {
|
|
657
|
+
...existingItem.custom || {},
|
|
658
|
+
...updates.custom || {},
|
|
659
|
+
uuid: existingItem.custom?.uuid || "",
|
|
660
|
+
created_at: existingItem.custom?.created_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
661
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Remove a reference from all indices.
|
|
667
|
+
*/
|
|
668
|
+
removeFromIndices(ref) {
|
|
669
|
+
this.uuidIndex.delete(ref.getUuid());
|
|
670
|
+
this.idIndex.delete(ref.getId());
|
|
671
|
+
const doi = ref.getDoi();
|
|
672
|
+
if (doi) {
|
|
673
|
+
this.doiIndex.delete(doi);
|
|
766
674
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
...override.server
|
|
771
|
-
};
|
|
675
|
+
const pmid = ref.getPmid();
|
|
676
|
+
if (pmid) {
|
|
677
|
+
this.pmidIndex.delete(pmid);
|
|
772
678
|
}
|
|
773
679
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
autoStopMinutes: partial.server?.autoStopMinutes ?? defaultConfig.server.autoStopMinutes
|
|
680
|
+
/**
|
|
681
|
+
* Generate an alphabetic suffix for ID collision resolution.
|
|
682
|
+
* 0 -> 'a', 1 -> 'b', ..., 25 -> 'z', 26 -> 'aa', etc.
|
|
683
|
+
*/
|
|
684
|
+
generateSuffix(index) {
|
|
685
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz";
|
|
686
|
+
let suffix = "";
|
|
687
|
+
let n = index;
|
|
688
|
+
do {
|
|
689
|
+
suffix = alphabet[n % 26] + suffix;
|
|
690
|
+
n = Math.floor(n / 26) - 1;
|
|
691
|
+
} while (n >= 0);
|
|
692
|
+
return suffix;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Resolve ID collision by appending alphabetic suffix.
|
|
696
|
+
*/
|
|
697
|
+
resolveIdCollision(baseId, existingIds) {
|
|
698
|
+
if (!existingIds.has(baseId)) {
|
|
699
|
+
return baseId;
|
|
795
700
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const currentConfigPath = join(cwd, getDefaultCurrentDirConfigFilename());
|
|
805
|
-
const currentConfig = loadTOMLFile(currentConfigPath);
|
|
806
|
-
const normalizedUser = userConfig ? normalizePartialConfig(userConfig) : null;
|
|
807
|
-
const normalizedEnv = envConfig ? normalizePartialConfig(envConfig) : null;
|
|
808
|
-
const normalizedCurrent = currentConfig ? normalizePartialConfig(currentConfig) : null;
|
|
809
|
-
const merged = mergeConfigs(
|
|
810
|
-
{},
|
|
811
|
-
normalizedUser,
|
|
812
|
-
normalizedEnv,
|
|
813
|
-
normalizedCurrent,
|
|
814
|
-
options.overrides
|
|
815
|
-
);
|
|
816
|
-
const config = fillDefaults(merged);
|
|
817
|
-
try {
|
|
818
|
-
return configSchema.parse(config);
|
|
819
|
-
} catch (error) {
|
|
820
|
-
throw new Error(
|
|
821
|
-
`Invalid configuration: ${error instanceof Error ? error.message : String(error)}`
|
|
822
|
-
);
|
|
701
|
+
let index = 0;
|
|
702
|
+
let newId;
|
|
703
|
+
do {
|
|
704
|
+
const suffix = this.generateSuffix(index);
|
|
705
|
+
newId = `${baseId}${suffix}`;
|
|
706
|
+
index++;
|
|
707
|
+
} while (existingIds.has(newId));
|
|
708
|
+
return newId;
|
|
823
709
|
}
|
|
824
710
|
}
|
|
825
711
|
const VALID_FIELDS = /* @__PURE__ */ new Set([
|
|
@@ -830,7 +716,8 @@ const VALID_FIELDS = /* @__PURE__ */ new Set([
|
|
|
830
716
|
"pmid",
|
|
831
717
|
"pmcid",
|
|
832
718
|
"url",
|
|
833
|
-
"keyword"
|
|
719
|
+
"keyword",
|
|
720
|
+
"tag"
|
|
834
721
|
]);
|
|
835
722
|
function isWhitespace(query, index) {
|
|
836
723
|
return /\s/.test(query.charAt(index));
|
|
@@ -993,6 +880,82 @@ function normalize(text) {
|
|
|
993
880
|
normalized = normalized.replace(/\s+/g, " ").trim();
|
|
994
881
|
return normalized;
|
|
995
882
|
}
|
|
883
|
+
function normalizePreservingCase(text) {
|
|
884
|
+
let normalized = text.normalize("NFKC");
|
|
885
|
+
normalized = normalized.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "");
|
|
886
|
+
normalized = normalized.replace(/\s+/g, " ").trim();
|
|
887
|
+
return normalized;
|
|
888
|
+
}
|
|
889
|
+
function hasConsecutiveUppercase(text) {
|
|
890
|
+
const pattern = /[A-Z]{2,}/;
|
|
891
|
+
return pattern.test(text);
|
|
892
|
+
}
|
|
893
|
+
function extractUppercaseSegments(text) {
|
|
894
|
+
const pattern = /[A-Z]{2,}/g;
|
|
895
|
+
const segments = [];
|
|
896
|
+
for (const match of text.matchAll(pattern)) {
|
|
897
|
+
segments.push({
|
|
898
|
+
segment: match[0],
|
|
899
|
+
start: match.index,
|
|
900
|
+
end: match.index + match[0].length
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
return segments;
|
|
904
|
+
}
|
|
905
|
+
function escapeRegex(str) {
|
|
906
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
907
|
+
}
|
|
908
|
+
function normalizeWhitespace(text) {
|
|
909
|
+
return text.replace(/\s+/g, " ").trim();
|
|
910
|
+
}
|
|
911
|
+
function allUppercaseSegmentsExist(segments, target) {
|
|
912
|
+
return segments.every((seg) => target.includes(seg.segment));
|
|
913
|
+
}
|
|
914
|
+
function buildMatchPattern(query, segments) {
|
|
915
|
+
const patternParts = [];
|
|
916
|
+
let lastEnd = 0;
|
|
917
|
+
for (const seg of segments) {
|
|
918
|
+
if (seg.start > lastEnd) {
|
|
919
|
+
const beforePart = query.slice(lastEnd, seg.start);
|
|
920
|
+
if (beforePart.trim()) {
|
|
921
|
+
patternParts.push(escapeRegex(beforePart));
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
patternParts.push(`(?:${escapeRegex(seg.segment)})`);
|
|
925
|
+
lastEnd = seg.end;
|
|
926
|
+
}
|
|
927
|
+
if (lastEnd < query.length) {
|
|
928
|
+
const afterPart = query.slice(lastEnd);
|
|
929
|
+
if (afterPart.trim()) {
|
|
930
|
+
patternParts.push(escapeRegex(afterPart));
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return patternParts.join(".*?");
|
|
934
|
+
}
|
|
935
|
+
function matchWithUppercaseSensitivity(query, target) {
|
|
936
|
+
if (query === "") {
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
if (target === "") {
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
const normalizedQuery = normalizeWhitespace(query);
|
|
943
|
+
const normalizedTarget = normalizeWhitespace(target);
|
|
944
|
+
if (!hasConsecutiveUppercase(normalizedQuery)) {
|
|
945
|
+
return normalizedTarget.toLowerCase().includes(normalizedQuery.toLowerCase());
|
|
946
|
+
}
|
|
947
|
+
const segments = extractUppercaseSegments(normalizedQuery);
|
|
948
|
+
if (!allUppercaseSegmentsExist(segments, target)) {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
const pattern = buildMatchPattern(normalizedQuery, segments);
|
|
952
|
+
try {
|
|
953
|
+
const regex = new RegExp(pattern, "i");
|
|
954
|
+
return regex.test(normalizedTarget);
|
|
955
|
+
} catch {
|
|
956
|
+
return normalizedTarget.toLowerCase().includes(normalizedQuery.toLowerCase());
|
|
957
|
+
}
|
|
958
|
+
}
|
|
996
959
|
const ID_FIELDS = /* @__PURE__ */ new Set(["DOI", "PMID", "PMCID", "URL"]);
|
|
997
960
|
function extractYear$2(reference) {
|
|
998
961
|
if (reference.issued?.["date-parts"]?.[0]?.[0]) {
|
|
@@ -1006,8 +969,8 @@ function extractAuthors(reference) {
|
|
|
1006
969
|
}
|
|
1007
970
|
return reference.author.map((author) => {
|
|
1008
971
|
const family = author.family || "";
|
|
1009
|
-
const
|
|
1010
|
-
return
|
|
972
|
+
const given = author.given || "";
|
|
973
|
+
return given ? `${family} ${given}` : family;
|
|
1011
974
|
}).join(" ");
|
|
1012
975
|
}
|
|
1013
976
|
function getFieldValue(reference, field) {
|
|
@@ -1056,11 +1019,11 @@ function matchKeyword(queryValue, reference) {
|
|
|
1056
1019
|
if (!reference.keyword || !Array.isArray(reference.keyword)) {
|
|
1057
1020
|
return null;
|
|
1058
1021
|
}
|
|
1059
|
-
const normalizedQuery =
|
|
1022
|
+
const normalizedQuery = normalizePreservingCase(queryValue);
|
|
1060
1023
|
for (const keyword of reference.keyword) {
|
|
1061
1024
|
if (typeof keyword === "string") {
|
|
1062
|
-
const normalizedKeyword =
|
|
1063
|
-
if (
|
|
1025
|
+
const normalizedKeyword = normalizePreservingCase(keyword);
|
|
1026
|
+
if (matchWithUppercaseSensitivity(normalizedQuery, normalizedKeyword)) {
|
|
1064
1027
|
return {
|
|
1065
1028
|
field: "keyword",
|
|
1066
1029
|
strength: "partial",
|
|
@@ -1071,6 +1034,25 @@ function matchKeyword(queryValue, reference) {
|
|
|
1071
1034
|
}
|
|
1072
1035
|
return null;
|
|
1073
1036
|
}
|
|
1037
|
+
function matchTag(queryValue, reference) {
|
|
1038
|
+
if (!reference.custom?.tags || !Array.isArray(reference.custom.tags)) {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
const normalizedQuery = normalizePreservingCase(queryValue);
|
|
1042
|
+
for (const tag of reference.custom.tags) {
|
|
1043
|
+
if (typeof tag === "string") {
|
|
1044
|
+
const normalizedTag = normalizePreservingCase(tag);
|
|
1045
|
+
if (matchWithUppercaseSensitivity(normalizedQuery, normalizedTag)) {
|
|
1046
|
+
return {
|
|
1047
|
+
field: "tag",
|
|
1048
|
+
strength: "partial",
|
|
1049
|
+
value: tag
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return null;
|
|
1055
|
+
}
|
|
1074
1056
|
const FIELD_MAP = {
|
|
1075
1057
|
author: "author",
|
|
1076
1058
|
title: "title",
|
|
@@ -1104,9 +1086,9 @@ function matchFieldValue(field, tokenValue, reference) {
|
|
|
1104
1086
|
}
|
|
1105
1087
|
return null;
|
|
1106
1088
|
}
|
|
1107
|
-
const normalizedFieldValue =
|
|
1108
|
-
const normalizedQuery =
|
|
1109
|
-
if (
|
|
1089
|
+
const normalizedFieldValue = normalizePreservingCase(fieldValue);
|
|
1090
|
+
const normalizedQuery = normalizePreservingCase(tokenValue);
|
|
1091
|
+
if (matchWithUppercaseSensitivity(normalizedQuery, normalizedFieldValue)) {
|
|
1110
1092
|
return {
|
|
1111
1093
|
field,
|
|
1112
1094
|
strength: "partial",
|
|
@@ -1133,6 +1115,11 @@ function matchSpecificField(token, reference) {
|
|
|
1133
1115
|
if (keywordMatch) matches.push(keywordMatch);
|
|
1134
1116
|
return matches;
|
|
1135
1117
|
}
|
|
1118
|
+
if (fieldToSearch === "tag") {
|
|
1119
|
+
const tagMatch = matchTag(token.value, reference);
|
|
1120
|
+
if (tagMatch) matches.push(tagMatch);
|
|
1121
|
+
return matches;
|
|
1122
|
+
}
|
|
1136
1123
|
const actualField = FIELD_MAP[fieldToSearch] || fieldToSearch;
|
|
1137
1124
|
const match = matchFieldValue(actualField, token.value, reference);
|
|
1138
1125
|
if (match) matches.push(match);
|
|
@@ -1158,11 +1145,14 @@ function matchSingleField(field, tokenValue, reference) {
|
|
|
1158
1145
|
if (field === "keyword") {
|
|
1159
1146
|
return matchKeyword(tokenValue, reference);
|
|
1160
1147
|
}
|
|
1148
|
+
if (field === "tag") {
|
|
1149
|
+
return matchTag(tokenValue, reference);
|
|
1150
|
+
}
|
|
1161
1151
|
return matchFieldValue(field, tokenValue, reference);
|
|
1162
1152
|
}
|
|
1163
1153
|
function matchAllFields(token, reference) {
|
|
1164
1154
|
const matches = [];
|
|
1165
|
-
const specialFields = ["year", "URL", "keyword"];
|
|
1155
|
+
const specialFields = ["year", "URL", "keyword", "tag"];
|
|
1166
1156
|
for (const field of specialFields) {
|
|
1167
1157
|
const match = matchSingleField(field, token.value, reference);
|
|
1168
1158
|
if (match) matches.push(match);
|
|
@@ -1377,39 +1367,191 @@ function detectDuplicate(item, existingReferences) {
|
|
|
1377
1367
|
matches
|
|
1378
1368
|
};
|
|
1379
1369
|
}
|
|
1370
|
+
const DEFAULT_DEBOUNCE_MS = 500;
|
|
1371
|
+
const DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
1372
|
+
const DEFAULT_RETRY_DELAY_MS = 200;
|
|
1373
|
+
const DEFAULT_MAX_RETRIES = 10;
|
|
1374
|
+
function shouldIgnore(filePath) {
|
|
1375
|
+
const basename = path.basename(filePath);
|
|
1376
|
+
if (basename.endsWith(".tmp")) return true;
|
|
1377
|
+
if (basename.endsWith(".bak")) return true;
|
|
1378
|
+
if (basename.includes(".conflict.")) return true;
|
|
1379
|
+
if (basename.endsWith(".lock")) return true;
|
|
1380
|
+
if (basename.startsWith(".") && basename.endsWith(".swp")) return true;
|
|
1381
|
+
if (basename.endsWith("~")) return true;
|
|
1382
|
+
return false;
|
|
1383
|
+
}
|
|
1384
|
+
class FileWatcher extends EventEmitter {
|
|
1385
|
+
watchPath;
|
|
1386
|
+
debounceMs;
|
|
1387
|
+
pollIntervalMs;
|
|
1388
|
+
usePolling;
|
|
1389
|
+
retryDelayMs;
|
|
1390
|
+
maxRetries;
|
|
1391
|
+
watcher = null;
|
|
1392
|
+
watching = false;
|
|
1393
|
+
debounceTimers = /* @__PURE__ */ new Map();
|
|
1394
|
+
constructor(watchPath, options) {
|
|
1395
|
+
super();
|
|
1396
|
+
this.watchPath = watchPath;
|
|
1397
|
+
this.debounceMs = options?.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
1398
|
+
this.pollIntervalMs = options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
1399
|
+
this.usePolling = options?.usePolling ?? false;
|
|
1400
|
+
this.retryDelayMs = options?.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
1401
|
+
this.maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Start watching for file changes
|
|
1405
|
+
*/
|
|
1406
|
+
async start() {
|
|
1407
|
+
if (this.watching) {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
return new Promise((resolve, reject) => {
|
|
1411
|
+
this.watcher = chokidar.watch(this.watchPath, {
|
|
1412
|
+
ignored: shouldIgnore,
|
|
1413
|
+
persistent: true,
|
|
1414
|
+
usePolling: this.usePolling,
|
|
1415
|
+
interval: this.pollIntervalMs,
|
|
1416
|
+
ignoreInitial: true,
|
|
1417
|
+
awaitWriteFinish: false
|
|
1418
|
+
});
|
|
1419
|
+
this.watcher.on("ready", () => {
|
|
1420
|
+
this.watching = true;
|
|
1421
|
+
this.emit("ready");
|
|
1422
|
+
resolve();
|
|
1423
|
+
});
|
|
1424
|
+
this.watcher.on("error", (error) => {
|
|
1425
|
+
this.emit("error", error);
|
|
1426
|
+
if (!this.watching) {
|
|
1427
|
+
reject(error);
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
this.watcher.on("change", (filePath) => {
|
|
1431
|
+
this.handleFileChange(filePath);
|
|
1432
|
+
});
|
|
1433
|
+
this.watcher.on("add", (filePath) => {
|
|
1434
|
+
this.handleFileChange(filePath);
|
|
1435
|
+
});
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Handle file change with debouncing
|
|
1440
|
+
*/
|
|
1441
|
+
handleFileChange(filePath) {
|
|
1442
|
+
const existingTimer = this.debounceTimers.get(filePath);
|
|
1443
|
+
if (existingTimer) {
|
|
1444
|
+
clearTimeout(existingTimer);
|
|
1445
|
+
}
|
|
1446
|
+
const timer = setTimeout(() => {
|
|
1447
|
+
this.debounceTimers.delete(filePath);
|
|
1448
|
+
this.emit("change", filePath);
|
|
1449
|
+
this.tryParseJsonFile(filePath);
|
|
1450
|
+
}, this.debounceMs);
|
|
1451
|
+
this.debounceTimers.set(filePath, timer);
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Try to parse JSON file with retries
|
|
1455
|
+
*/
|
|
1456
|
+
async tryParseJsonFile(filePath) {
|
|
1457
|
+
if (path.extname(filePath).toLowerCase() !== ".json") {
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
let lastError = null;
|
|
1461
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
1462
|
+
try {
|
|
1463
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
1464
|
+
const parsed = JSON.parse(content);
|
|
1465
|
+
this.emit("parsed", filePath, parsed);
|
|
1466
|
+
return;
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
lastError = error;
|
|
1469
|
+
if (attempt < this.maxRetries) {
|
|
1470
|
+
await this.delay(this.retryDelayMs);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
this.emit("parseError", filePath, lastError);
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Delay helper
|
|
1478
|
+
*/
|
|
1479
|
+
delay(ms) {
|
|
1480
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Stop watching for file changes
|
|
1484
|
+
*/
|
|
1485
|
+
close() {
|
|
1486
|
+
if (this.watcher) {
|
|
1487
|
+
this.watcher.close();
|
|
1488
|
+
this.watcher = null;
|
|
1489
|
+
}
|
|
1490
|
+
for (const timer of this.debounceTimers.values()) {
|
|
1491
|
+
clearTimeout(timer);
|
|
1492
|
+
}
|
|
1493
|
+
this.debounceTimers.clear();
|
|
1494
|
+
this.watching = false;
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Get the watched path
|
|
1498
|
+
*/
|
|
1499
|
+
getPath() {
|
|
1500
|
+
return this.watchPath;
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Check if the watcher is currently active
|
|
1504
|
+
*/
|
|
1505
|
+
isWatching() {
|
|
1506
|
+
return this.watching;
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Get the debounce time in milliseconds
|
|
1510
|
+
*/
|
|
1511
|
+
getDebounceMs() {
|
|
1512
|
+
return this.debounceMs;
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Get the poll interval in milliseconds
|
|
1516
|
+
*/
|
|
1517
|
+
getPollIntervalMs() {
|
|
1518
|
+
return this.pollIntervalMs;
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Get the retry delay in milliseconds
|
|
1522
|
+
*/
|
|
1523
|
+
getRetryDelayMs() {
|
|
1524
|
+
return this.retryDelayMs;
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Get the maximum number of retries
|
|
1528
|
+
*/
|
|
1529
|
+
getMaxRetries() {
|
|
1530
|
+
return this.maxRetries;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1380
1533
|
export {
|
|
1381
|
-
generateUuid as A,
|
|
1382
|
-
isValidUuid as B,
|
|
1383
1534
|
CslLibrarySchema as C,
|
|
1384
|
-
|
|
1385
|
-
extractUuidFromCustom as E,
|
|
1535
|
+
FileWatcher as F,
|
|
1386
1536
|
Library as L,
|
|
1387
1537
|
Reference as R,
|
|
1388
1538
|
computeHash as a,
|
|
1389
|
-
|
|
1539
|
+
sortResults as b,
|
|
1390
1540
|
computeFileHash as c,
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
partialConfigSchema as p,
|
|
1404
|
-
CslItemSchema as q,
|
|
1405
|
-
parseCslJson as r,
|
|
1541
|
+
detectDuplicate as d,
|
|
1542
|
+
CslItemSchema as e,
|
|
1543
|
+
serializeCslJson as f,
|
|
1544
|
+
generateId as g,
|
|
1545
|
+
generateIdWithCollisionCheck as h,
|
|
1546
|
+
normalizeText as i,
|
|
1547
|
+
generateUuid as j,
|
|
1548
|
+
isValidUuid as k,
|
|
1549
|
+
ensureCustomMetadata as l,
|
|
1550
|
+
extractUuidFromCustom as m,
|
|
1551
|
+
normalize as n,
|
|
1552
|
+
parseCslJson as p,
|
|
1406
1553
|
search as s,
|
|
1407
1554
|
tokenize as t,
|
|
1408
|
-
|
|
1409
|
-
writeCslJson as v,
|
|
1410
|
-
watchConfigSchema as w,
|
|
1411
|
-
generateId as x,
|
|
1412
|
-
generateIdWithCollisionCheck as y,
|
|
1413
|
-
normalizeText as z
|
|
1555
|
+
writeCslJson as w
|
|
1414
1556
|
};
|
|
1415
|
-
//# sourceMappingURL=
|
|
1557
|
+
//# sourceMappingURL=file-watcher-B-SiUw5f.js.map
|