@ncukondo/reference-manager 0.1.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 +167 -0
- package/bin/reference-manager.js +5 -0
- package/dist/chunks/detector-BF8Mcc72.js +1415 -0
- package/dist/chunks/detector-BF8Mcc72.js.map +1 -0
- package/dist/cli/commands/add.d.ts +22 -0
- package/dist/cli/commands/add.d.ts.map +1 -0
- package/dist/cli/commands/index.d.ts +16 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/list.d.ts +15 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/remove.d.ts +19 -0
- package/dist/cli/commands/remove.d.ts.map +1 -0
- package/dist/cli/commands/search.d.ts +16 -0
- package/dist/cli/commands/search.d.ts.map +1 -0
- package/dist/cli/commands/server.d.ts +32 -0
- package/dist/cli/commands/server.d.ts.map +1 -0
- package/dist/cli/commands/update.d.ts +20 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/helpers.d.ts +61 -0
- package/dist/cli/helpers.d.ts.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/output/bibtex.d.ts +6 -0
- package/dist/cli/output/bibtex.d.ts.map +1 -0
- package/dist/cli/output/index.d.ts +7 -0
- package/dist/cli/output/index.d.ts.map +1 -0
- package/dist/cli/output/json.d.ts +6 -0
- package/dist/cli/output/json.d.ts.map +1 -0
- package/dist/cli/output/pretty.d.ts +6 -0
- package/dist/cli/output/pretty.d.ts.map +1 -0
- package/dist/cli/server-client.d.ts +38 -0
- package/dist/cli/server-client.d.ts.map +1 -0
- package/dist/cli/server-detection.d.ts +27 -0
- package/dist/cli/server-detection.d.ts.map +1 -0
- package/dist/cli.js +981 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/defaults.d.ts +29 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/loader.d.ts +27 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/schema.d.ts +129 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/core/csl-json/parser.d.ts +9 -0
- package/dist/core/csl-json/parser.d.ts.map +1 -0
- package/dist/core/csl-json/serializer.d.ts +15 -0
- package/dist/core/csl-json/serializer.d.ts.map +1 -0
- package/dist/core/csl-json/types.d.ts +124 -0
- package/dist/core/csl-json/types.d.ts.map +1 -0
- package/dist/core/csl-json/validator.d.ts +19 -0
- package/dist/core/csl-json/validator.d.ts.map +1 -0
- package/dist/core/identifier/generator.d.ts +17 -0
- package/dist/core/identifier/generator.d.ts.map +1 -0
- package/dist/core/identifier/normalize.d.ts +20 -0
- package/dist/core/identifier/normalize.d.ts.map +1 -0
- package/dist/core/identifier/uuid.d.ts +24 -0
- package/dist/core/identifier/uuid.d.ts.map +1 -0
- package/dist/core/index.d.ts +15 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/library.d.ts +73 -0
- package/dist/core/library.d.ts.map +1 -0
- package/dist/core/reference.d.ts +86 -0
- package/dist/core/reference.d.ts.map +1 -0
- package/dist/features/duplicate/detector.d.ts +19 -0
- package/dist/features/duplicate/detector.d.ts.map +1 -0
- package/dist/features/duplicate/index.d.ts +6 -0
- package/dist/features/duplicate/index.d.ts.map +1 -0
- package/dist/features/duplicate/types.d.ts +45 -0
- package/dist/features/duplicate/types.d.ts.map +1 -0
- package/dist/features/file-watcher/file-watcher.d.ts +83 -0
- package/dist/features/file-watcher/file-watcher.d.ts.map +1 -0
- package/dist/features/file-watcher/index.d.ts +2 -0
- package/dist/features/file-watcher/index.d.ts.map +1 -0
- package/dist/features/merge/index.d.ts +8 -0
- package/dist/features/merge/index.d.ts.map +1 -0
- package/dist/features/merge/three-way.d.ts +16 -0
- package/dist/features/merge/three-way.d.ts.map +1 -0
- package/dist/features/merge/types.d.ts +74 -0
- package/dist/features/merge/types.d.ts.map +1 -0
- package/dist/features/search/index.d.ts +9 -0
- package/dist/features/search/index.d.ts.map +1 -0
- package/dist/features/search/matcher.d.ts +18 -0
- package/dist/features/search/matcher.d.ts.map +1 -0
- package/dist/features/search/normalizer.d.ts +12 -0
- package/dist/features/search/normalizer.d.ts.map +1 -0
- package/dist/features/search/sorter.d.ts +11 -0
- package/dist/features/search/sorter.d.ts.map +1 -0
- package/dist/features/search/tokenizer.d.ts +6 -0
- package/dist/features/search/tokenizer.d.ts.map +1 -0
- package/dist/features/search/types.d.ts +77 -0
- package/dist/features/search/types.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +559 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/portfile.d.ts +43 -0
- package/dist/server/portfile.d.ts.map +1 -0
- package/dist/server/routes/health.d.ts +7 -0
- package/dist/server/routes/health.d.ts.map +1 -0
- package/dist/server/routes/references.d.ts +9 -0
- package/dist/server/routes/references.d.ts.map +1 -0
- package/dist/server.js +91 -0
- package/dist/server.js.map +1 -0
- package/dist/utils/backup.d.ts +21 -0
- package/dist/utils/backup.d.ts.map +1 -0
- package/dist/utils/file.d.ts +9 -0
- package/dist/utils/file.d.ts.map +1 -0
- package/dist/utils/hash.d.ts +9 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search feature type definitions
|
|
3
|
+
*/
|
|
4
|
+
import type { CslItem } from "../../core/csl-json/types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Field specifier for field-specific search
|
|
7
|
+
*/
|
|
8
|
+
export type FieldSpecifier = "author" | "title" | "year" | "doi" | "pmid" | "pmcid" | "url" | "keyword";
|
|
9
|
+
/**
|
|
10
|
+
* Search token representing a parsed query element
|
|
11
|
+
*/
|
|
12
|
+
export interface SearchToken {
|
|
13
|
+
/** The original query text (may include quotes) */
|
|
14
|
+
raw: string;
|
|
15
|
+
/** The normalized value to search for */
|
|
16
|
+
value: string;
|
|
17
|
+
/** Field to search in (if specified with field: prefix) */
|
|
18
|
+
field?: FieldSpecifier;
|
|
19
|
+
/** Whether this is a phrase search (enclosed in quotes) */
|
|
20
|
+
isPhrase: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parsed search query
|
|
24
|
+
*/
|
|
25
|
+
export interface SearchQuery {
|
|
26
|
+
/** Original query string */
|
|
27
|
+
original: string;
|
|
28
|
+
/** Parsed tokens */
|
|
29
|
+
tokens: SearchToken[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Match strength indicator
|
|
33
|
+
*/
|
|
34
|
+
export type MatchStrength = "exact" | "partial" | "none";
|
|
35
|
+
/**
|
|
36
|
+
* Field match result
|
|
37
|
+
*/
|
|
38
|
+
export interface FieldMatch {
|
|
39
|
+
/** Field name that matched */
|
|
40
|
+
field: string;
|
|
41
|
+
/** Strength of the match */
|
|
42
|
+
strength: MatchStrength;
|
|
43
|
+
/** Matched value */
|
|
44
|
+
value: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Token match result
|
|
48
|
+
*/
|
|
49
|
+
export interface TokenMatch {
|
|
50
|
+
/** The token that was matched */
|
|
51
|
+
token: SearchToken;
|
|
52
|
+
/** Fields that matched this token */
|
|
53
|
+
matches: FieldMatch[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Search result for a single reference
|
|
57
|
+
*/
|
|
58
|
+
export interface SearchResult {
|
|
59
|
+
/** The reference that matched */
|
|
60
|
+
reference: CslItem;
|
|
61
|
+
/** Match information for each token */
|
|
62
|
+
tokenMatches: TokenMatch[];
|
|
63
|
+
/** Overall match strength (highest strength among all tokens) */
|
|
64
|
+
overallStrength: MatchStrength;
|
|
65
|
+
/** Match score (for sorting) */
|
|
66
|
+
score: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Search options
|
|
70
|
+
*/
|
|
71
|
+
export interface SearchOptions {
|
|
72
|
+
/** Sort order (default: by relevance) */
|
|
73
|
+
sortBy?: "relevance" | "year" | "author" | "title";
|
|
74
|
+
/** Limit number of results */
|
|
75
|
+
limit?: number;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/features/search/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAE5D;;GAEG;AACH,MAAM,MAAM,cAAc,GACtB,QAAQ,GACR,OAAO,GACP,MAAM,GACN,KAAK,GACL,MAAM,GACN,OAAO,GACP,KAAK,GACL,SAAS,CAAC;AAEd;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAC;IACZ,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,2DAA2D;IAC3D,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB;IACpB,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEzD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,QAAQ,EAAE,aAAa,CAAC;IACxB,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,iCAAiC;IACjC,KAAK,EAAE,WAAW,CAAC;IACnB,qCAAqC;IACrC,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,SAAS,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,YAAY,EAAE,UAAU,EAAE,CAAC;IAC3B,iEAAiE;IACjE,eAAe,EAAE,aAAa,CAAC;IAC/B,gCAAgC;IAChC,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,yCAAyC;IACzC,MAAM,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;IACnD,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reference-manager - A local reference management tool using CSL-JSON
|
|
3
|
+
*
|
|
4
|
+
* Main library entry point
|
|
5
|
+
*/
|
|
6
|
+
export * from "./core/index.js";
|
|
7
|
+
export * as Utils from "./utils/index.js";
|
|
8
|
+
export * as Config from "./config/index.js";
|
|
9
|
+
export * as Search from "./features/search/index.js";
|
|
10
|
+
export * as Duplicate from "./features/duplicate/index.js";
|
|
11
|
+
export * as Merge from "./features/merge/index.js";
|
|
12
|
+
export { FileWatcher, type FileWatcherOptions } from "./features/file-watcher/index.js";
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,cAAc,iBAAiB,CAAC;AAGhC,OAAO,KAAK,KAAK,MAAM,kBAAkB,CAAC;AAC1C,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAG5C,OAAO,KAAK,MAAM,MAAM,4BAA4B,CAAC;AACrD,OAAO,KAAK,SAAS,MAAM,+BAA+B,CAAC;AAC3D,OAAO,KAAK,KAAK,MAAM,2BAA2B,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,kCAAkC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import { C as CslLibrarySchema, c as computeFileHash, a as computeHash, b as backupConfigSchema, d as configSchema, e as defaultConfig, g as getDefaultBackupDirectory, f as getDefaultCurrentDirConfigFilename, h as getDefaultLibraryPath, i as getDefaultUserConfigPath, l as loadConfig, j as logLevelSchema, n as normalizePartialConfig, p as partialConfigSchema, w as watchConfigSchema, k as normalize, s as search, m as sortResults, t as tokenize, o as detectDuplicate } from "./chunks/detector-BF8Mcc72.js";
|
|
2
|
+
import { q, L, R, D, E, x, y, A, B, z, r, u, v } from "./chunks/detector-BF8Mcc72.js";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import { mkdir, readFile, copyFile, stat, unlink, readdir } from "node:fs/promises";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import writeFileAtomicLib from "write-file-atomic";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { EventEmitter } from "node:events";
|
|
12
|
+
import chokidar from "chokidar";
|
|
13
|
+
function validateCslJson(data) {
|
|
14
|
+
const parseResult = CslLibrarySchema.safeParse(data);
|
|
15
|
+
if (!parseResult.success) {
|
|
16
|
+
throw new Error(`Invalid CSL-JSON structure: ${parseResult.error.message}`);
|
|
17
|
+
}
|
|
18
|
+
return parseResult.data;
|
|
19
|
+
}
|
|
20
|
+
function createLogger(level = "info") {
|
|
21
|
+
const shouldLogInfo = level === "info" || level === "debug";
|
|
22
|
+
const shouldLogDebug = level === "debug";
|
|
23
|
+
function formatMessage(...args) {
|
|
24
|
+
return `${args.map((arg) => String(arg)).join(" ")}
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
info(...args) {
|
|
29
|
+
if (shouldLogInfo) {
|
|
30
|
+
process.stderr.write(formatMessage(...args));
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
debug(...args) {
|
|
34
|
+
if (shouldLogDebug) {
|
|
35
|
+
process.stderr.write(formatMessage(...args));
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
error(...args) {
|
|
39
|
+
process.stderr.write(formatMessage(...args));
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function writeFileAtomic(filePath, content) {
|
|
44
|
+
await ensureDirectoryExists(dirname(filePath));
|
|
45
|
+
await writeFileAtomicLib(filePath, content, { encoding: "utf-8" });
|
|
46
|
+
}
|
|
47
|
+
async function ensureDirectoryExists(dirPath) {
|
|
48
|
+
await mkdir(dirPath, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
const DEFAULT_MAX_GENERATIONS = 50;
|
|
51
|
+
const DEFAULT_MAX_AGE_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
52
|
+
async function resolvePackageName() {
|
|
53
|
+
try {
|
|
54
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
55
|
+
let currentDir = dirname(currentFile);
|
|
56
|
+
for (let i = 0; i < 10; i++) {
|
|
57
|
+
const packageJsonPath = join(currentDir, "package.json");
|
|
58
|
+
if (existsSync(packageJsonPath)) {
|
|
59
|
+
const content = await readFile(packageJsonPath, "utf-8");
|
|
60
|
+
const pkg = JSON.parse(content);
|
|
61
|
+
return pkg.name;
|
|
62
|
+
}
|
|
63
|
+
currentDir = dirname(currentDir);
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
return "reference-manager";
|
|
68
|
+
}
|
|
69
|
+
const packageName = await resolvePackageName();
|
|
70
|
+
function getBackupDirectory(libraryName) {
|
|
71
|
+
const pkgName = packageName ?? "reference-manager";
|
|
72
|
+
return join(tmpdir(), pkgName, "backups", libraryName);
|
|
73
|
+
}
|
|
74
|
+
async function createBackup(sourceFile, libraryName) {
|
|
75
|
+
const backupDir = getBackupDirectory(libraryName);
|
|
76
|
+
await ensureDirectoryExists(backupDir);
|
|
77
|
+
const timestamp = Date.now();
|
|
78
|
+
const backupFileName = `${timestamp}.backup`;
|
|
79
|
+
const backupPath = join(backupDir, backupFileName);
|
|
80
|
+
await copyFile(sourceFile, backupPath);
|
|
81
|
+
return backupPath;
|
|
82
|
+
}
|
|
83
|
+
async function listBackups(libraryName) {
|
|
84
|
+
const backupDir = getBackupDirectory(libraryName);
|
|
85
|
+
if (!existsSync(backupDir)) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const files = await readdir(backupDir);
|
|
89
|
+
const backupFiles = files.filter((f) => f.endsWith(".backup")).map((f) => join(backupDir, f));
|
|
90
|
+
const filesWithStats = await Promise.all(
|
|
91
|
+
backupFiles.map(async (file) => {
|
|
92
|
+
const stats = await stat(file);
|
|
93
|
+
return { file, mtime: stats.mtimeMs };
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
filesWithStats.sort((a, b) => b.mtime - a.mtime);
|
|
97
|
+
return filesWithStats.map((f) => f.file);
|
|
98
|
+
}
|
|
99
|
+
async function cleanupOldBackups(libraryName, options) {
|
|
100
|
+
const maxGenerations = options?.maxGenerations ?? DEFAULT_MAX_GENERATIONS;
|
|
101
|
+
const maxAgeMs = options?.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
102
|
+
const backups = await listBackups(libraryName);
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
const backupsToDelete = [];
|
|
105
|
+
for (let i = 0; i < backups.length; i++) {
|
|
106
|
+
const backupPath = backups[i];
|
|
107
|
+
if (!backupPath) continue;
|
|
108
|
+
const stats = await stat(backupPath);
|
|
109
|
+
const age = now - stats.mtimeMs;
|
|
110
|
+
if (i >= maxGenerations || age > maxAgeMs) {
|
|
111
|
+
backupsToDelete.push(backupPath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
await Promise.all(backupsToDelete.map((backup) => unlink(backup)));
|
|
115
|
+
}
|
|
116
|
+
const index$4 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
117
|
+
__proto__: null,
|
|
118
|
+
cleanupOldBackups,
|
|
119
|
+
computeFileHash,
|
|
120
|
+
computeHash,
|
|
121
|
+
createBackup,
|
|
122
|
+
createLogger,
|
|
123
|
+
ensureDirectoryExists,
|
|
124
|
+
getBackupDirectory,
|
|
125
|
+
listBackups,
|
|
126
|
+
writeFileAtomic
|
|
127
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
128
|
+
const index$3 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
129
|
+
__proto__: null,
|
|
130
|
+
backupConfigSchema,
|
|
131
|
+
configSchema,
|
|
132
|
+
defaultConfig,
|
|
133
|
+
getDefaultBackupDirectory,
|
|
134
|
+
getDefaultCurrentDirConfigFilename,
|
|
135
|
+
getDefaultLibraryPath,
|
|
136
|
+
getDefaultUserConfigPath,
|
|
137
|
+
loadConfig,
|
|
138
|
+
logLevelSchema,
|
|
139
|
+
normalizePartialConfig,
|
|
140
|
+
partialConfigSchema,
|
|
141
|
+
watchConfigSchema
|
|
142
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
143
|
+
const index$2 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
144
|
+
__proto__: null,
|
|
145
|
+
normalize,
|
|
146
|
+
search,
|
|
147
|
+
sortResults,
|
|
148
|
+
tokenize
|
|
149
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
150
|
+
const index$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
151
|
+
__proto__: null,
|
|
152
|
+
detectDuplicate
|
|
153
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
154
|
+
function getItemUuid(item) {
|
|
155
|
+
return item.custom?.uuid || item.id || "unknown";
|
|
156
|
+
}
|
|
157
|
+
function getTimestamp(item) {
|
|
158
|
+
return item.custom?.timestamp || item.custom?.created_at || "1970-01-01T00:00:00.000Z";
|
|
159
|
+
}
|
|
160
|
+
function deepEqual(a, b) {
|
|
161
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
162
|
+
}
|
|
163
|
+
function resolveFieldConflict(localValue, remoteValue, localTimestamp, remoteTimestamp, options) {
|
|
164
|
+
if (localTimestamp > remoteTimestamp) {
|
|
165
|
+
return localValue;
|
|
166
|
+
}
|
|
167
|
+
if (remoteTimestamp > localTimestamp) {
|
|
168
|
+
return remoteValue;
|
|
169
|
+
}
|
|
170
|
+
if (options?.prefer === "remote") {
|
|
171
|
+
return remoteValue;
|
|
172
|
+
}
|
|
173
|
+
return localValue;
|
|
174
|
+
}
|
|
175
|
+
function determineResolution(fieldConflicts, localTimestamp, remoteTimestamp, options) {
|
|
176
|
+
const hasRealConflicts = fieldConflicts.every((fc) => fc.resolved !== void 0);
|
|
177
|
+
const localIsNewer = fieldConflicts.some(
|
|
178
|
+
(fc) => fc.local !== fc.remote && localTimestamp > remoteTimestamp
|
|
179
|
+
);
|
|
180
|
+
const remoteIsNewer = fieldConflicts.some(
|
|
181
|
+
(fc) => fc.local !== fc.remote && remoteTimestamp > localTimestamp
|
|
182
|
+
);
|
|
183
|
+
if (hasRealConflicts && localIsNewer) return "auto-lww";
|
|
184
|
+
if (hasRealConflicts && remoteIsNewer) return "auto-lww";
|
|
185
|
+
if (options?.prefer === "local") return "prefer-local";
|
|
186
|
+
if (options?.prefer === "remote") return "prefer-remote";
|
|
187
|
+
return "unresolved";
|
|
188
|
+
}
|
|
189
|
+
function mergeField(key, baseValue, localValue, remoteValue, localTimestamp, remoteTimestamp, options) {
|
|
190
|
+
const localChanged = !deepEqual(baseValue, localValue);
|
|
191
|
+
const remoteChanged = !deepEqual(baseValue, remoteValue);
|
|
192
|
+
if (!localChanged && !remoteChanged) {
|
|
193
|
+
return { value: baseValue, conflict: null };
|
|
194
|
+
}
|
|
195
|
+
if (localChanged && !remoteChanged) {
|
|
196
|
+
return { value: localValue, conflict: null };
|
|
197
|
+
}
|
|
198
|
+
if (!localChanged && remoteChanged) {
|
|
199
|
+
return { value: remoteValue, conflict: null };
|
|
200
|
+
}
|
|
201
|
+
if (deepEqual(localValue, remoteValue)) {
|
|
202
|
+
return { value: localValue, conflict: null };
|
|
203
|
+
}
|
|
204
|
+
const resolved = resolveFieldConflict(
|
|
205
|
+
localValue,
|
|
206
|
+
remoteValue,
|
|
207
|
+
localTimestamp,
|
|
208
|
+
remoteTimestamp,
|
|
209
|
+
options
|
|
210
|
+
);
|
|
211
|
+
if (key === "custom") {
|
|
212
|
+
return { value: resolved, conflict: null };
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
value: resolved,
|
|
216
|
+
conflict: {
|
|
217
|
+
field: key,
|
|
218
|
+
base: baseValue,
|
|
219
|
+
local: localValue,
|
|
220
|
+
remote: remoteValue,
|
|
221
|
+
resolved
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function mergeItem(base, local, remote, options) {
|
|
226
|
+
const uuid = getItemUuid(base);
|
|
227
|
+
const localTimestamp = getTimestamp(local);
|
|
228
|
+
const remoteTimestamp = getTimestamp(remote);
|
|
229
|
+
const merged = { ...base };
|
|
230
|
+
const fieldConflicts = [];
|
|
231
|
+
const allKeys = /* @__PURE__ */ new Set([
|
|
232
|
+
...Object.keys(base),
|
|
233
|
+
...Object.keys(local),
|
|
234
|
+
...Object.keys(remote)
|
|
235
|
+
]);
|
|
236
|
+
for (const key of allKeys) {
|
|
237
|
+
const baseValue = base[key];
|
|
238
|
+
const localValue = local[key];
|
|
239
|
+
const remoteValue = remote[key];
|
|
240
|
+
const { value, conflict } = mergeField(
|
|
241
|
+
key,
|
|
242
|
+
baseValue,
|
|
243
|
+
localValue,
|
|
244
|
+
remoteValue,
|
|
245
|
+
localTimestamp,
|
|
246
|
+
remoteTimestamp,
|
|
247
|
+
options
|
|
248
|
+
);
|
|
249
|
+
merged[key] = value;
|
|
250
|
+
if (conflict) {
|
|
251
|
+
fieldConflicts.push(conflict);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (fieldConflicts.length > 0) {
|
|
255
|
+
const resolution = determineResolution(
|
|
256
|
+
fieldConflicts,
|
|
257
|
+
localTimestamp,
|
|
258
|
+
remoteTimestamp,
|
|
259
|
+
options
|
|
260
|
+
);
|
|
261
|
+
return {
|
|
262
|
+
merged,
|
|
263
|
+
conflict: {
|
|
264
|
+
uuid,
|
|
265
|
+
id: base.id || "unknown",
|
|
266
|
+
fields: fieldConflicts,
|
|
267
|
+
localTimestamp,
|
|
268
|
+
remoteTimestamp,
|
|
269
|
+
resolution
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return { merged, conflict: null };
|
|
274
|
+
}
|
|
275
|
+
function buildItemMaps(base, local, remote) {
|
|
276
|
+
const baseMap = /* @__PURE__ */ new Map();
|
|
277
|
+
const localMap = /* @__PURE__ */ new Map();
|
|
278
|
+
const remoteMap = /* @__PURE__ */ new Map();
|
|
279
|
+
for (const item of base) {
|
|
280
|
+
baseMap.set(getItemUuid(item), item);
|
|
281
|
+
}
|
|
282
|
+
for (const item of local) {
|
|
283
|
+
localMap.set(getItemUuid(item), item);
|
|
284
|
+
}
|
|
285
|
+
for (const item of remote) {
|
|
286
|
+
remoteMap.set(getItemUuid(item), item);
|
|
287
|
+
}
|
|
288
|
+
return { baseMap, localMap, remoteMap };
|
|
289
|
+
}
|
|
290
|
+
function mergeExistingItem(baseItem, localItem, remoteItem, options, merged, conflicts) {
|
|
291
|
+
const { merged: mergedItem, conflict } = mergeItem(baseItem, localItem, remoteItem, options);
|
|
292
|
+
merged.push(mergedItem);
|
|
293
|
+
if (conflict) {
|
|
294
|
+
conflicts.push(conflict);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function handleDualAddition(uuid, localItem, remoteItem, options, merged, conflicts) {
|
|
298
|
+
if (deepEqual(localItem, remoteItem)) {
|
|
299
|
+
merged.push(localItem);
|
|
300
|
+
} else {
|
|
301
|
+
const syntheticBase = {
|
|
302
|
+
id: uuid,
|
|
303
|
+
type: "article",
|
|
304
|
+
custom: {
|
|
305
|
+
uuid,
|
|
306
|
+
created_at: "1970-01-01T00:00:00.000Z",
|
|
307
|
+
timestamp: "1970-01-01T00:00:00.000Z"
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
const { merged: mergedItem, conflict } = mergeItem(
|
|
311
|
+
syntheticBase,
|
|
312
|
+
localItem,
|
|
313
|
+
remoteItem,
|
|
314
|
+
options
|
|
315
|
+
);
|
|
316
|
+
merged.push(mergedItem);
|
|
317
|
+
if (conflict) {
|
|
318
|
+
conflicts.push(conflict);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function processItem(uuid, baseMap, localMap, remoteMap, options, result) {
|
|
323
|
+
const baseItem = baseMap.get(uuid);
|
|
324
|
+
const localItem = localMap.get(uuid);
|
|
325
|
+
const remoteItem = remoteMap.get(uuid);
|
|
326
|
+
if (baseItem && localItem && remoteItem) {
|
|
327
|
+
mergeExistingItem(baseItem, localItem, remoteItem, options, result.merged, result.conflicts);
|
|
328
|
+
} else if (!baseItem && localItem && remoteItem) {
|
|
329
|
+
handleDualAddition(uuid, localItem, remoteItem, options, result.merged, result.conflicts);
|
|
330
|
+
} else if (!baseItem && localItem && !remoteItem) {
|
|
331
|
+
result.merged.push(localItem);
|
|
332
|
+
result.localOnly.push(localItem);
|
|
333
|
+
} else if (!baseItem && !localItem && remoteItem) {
|
|
334
|
+
result.merged.push(remoteItem);
|
|
335
|
+
result.remoteOnly.push(remoteItem);
|
|
336
|
+
} else if (baseItem && !localItem && remoteItem) {
|
|
337
|
+
result.deletedInLocal.push(baseItem);
|
|
338
|
+
} else if (baseItem && localItem && !remoteItem) {
|
|
339
|
+
result.deletedInRemote.push(baseItem);
|
|
340
|
+
} else if (baseItem && !localItem && !remoteItem) {
|
|
341
|
+
result.deletedInLocal.push(baseItem);
|
|
342
|
+
result.deletedInRemote.push(baseItem);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function threeWayMerge(base, local, remote, options) {
|
|
346
|
+
const { baseMap, localMap, remoteMap } = buildItemMaps(base, local, remote);
|
|
347
|
+
const result = {
|
|
348
|
+
merged: [],
|
|
349
|
+
conflicts: [],
|
|
350
|
+
localOnly: [],
|
|
351
|
+
remoteOnly: [],
|
|
352
|
+
deletedInLocal: [],
|
|
353
|
+
deletedInRemote: []
|
|
354
|
+
};
|
|
355
|
+
const allUuids = /* @__PURE__ */ new Set([...baseMap.keys(), ...localMap.keys(), ...remoteMap.keys()]);
|
|
356
|
+
for (const uuid of allUuids) {
|
|
357
|
+
processItem(uuid, baseMap, localMap, remoteMap, options, result);
|
|
358
|
+
}
|
|
359
|
+
let status = "success";
|
|
360
|
+
if (result.conflicts.length > 0) {
|
|
361
|
+
const hasUnresolved = result.conflicts.some((c) => c.resolution === "unresolved");
|
|
362
|
+
status = hasUnresolved ? "conflict" : "auto-resolved";
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
status,
|
|
366
|
+
...result
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
370
|
+
__proto__: null,
|
|
371
|
+
threeWayMerge
|
|
372
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
373
|
+
const DEFAULT_DEBOUNCE_MS = 500;
|
|
374
|
+
const DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
375
|
+
const DEFAULT_RETRY_DELAY_MS = 200;
|
|
376
|
+
const DEFAULT_MAX_RETRIES = 10;
|
|
377
|
+
function shouldIgnore(filePath) {
|
|
378
|
+
const basename = path.basename(filePath);
|
|
379
|
+
if (basename.endsWith(".tmp")) return true;
|
|
380
|
+
if (basename.endsWith(".bak")) return true;
|
|
381
|
+
if (basename.includes(".conflict.")) return true;
|
|
382
|
+
if (basename.endsWith(".lock")) return true;
|
|
383
|
+
if (basename.startsWith(".") && basename.endsWith(".swp")) return true;
|
|
384
|
+
if (basename.endsWith("~")) return true;
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
class FileWatcher extends EventEmitter {
|
|
388
|
+
watchPath;
|
|
389
|
+
debounceMs;
|
|
390
|
+
pollIntervalMs;
|
|
391
|
+
usePolling;
|
|
392
|
+
retryDelayMs;
|
|
393
|
+
maxRetries;
|
|
394
|
+
watcher = null;
|
|
395
|
+
watching = false;
|
|
396
|
+
debounceTimers = /* @__PURE__ */ new Map();
|
|
397
|
+
constructor(watchPath, options) {
|
|
398
|
+
super();
|
|
399
|
+
this.watchPath = watchPath;
|
|
400
|
+
this.debounceMs = options?.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
401
|
+
this.pollIntervalMs = options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
402
|
+
this.usePolling = options?.usePolling ?? false;
|
|
403
|
+
this.retryDelayMs = options?.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
404
|
+
this.maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Start watching for file changes
|
|
408
|
+
*/
|
|
409
|
+
async start() {
|
|
410
|
+
if (this.watching) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
return new Promise((resolve, reject) => {
|
|
414
|
+
this.watcher = chokidar.watch(this.watchPath, {
|
|
415
|
+
ignored: shouldIgnore,
|
|
416
|
+
persistent: true,
|
|
417
|
+
usePolling: this.usePolling,
|
|
418
|
+
interval: this.pollIntervalMs,
|
|
419
|
+
ignoreInitial: true,
|
|
420
|
+
awaitWriteFinish: false
|
|
421
|
+
});
|
|
422
|
+
this.watcher.on("ready", () => {
|
|
423
|
+
this.watching = true;
|
|
424
|
+
this.emit("ready");
|
|
425
|
+
resolve();
|
|
426
|
+
});
|
|
427
|
+
this.watcher.on("error", (error) => {
|
|
428
|
+
this.emit("error", error);
|
|
429
|
+
if (!this.watching) {
|
|
430
|
+
reject(error);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
this.watcher.on("change", (filePath) => {
|
|
434
|
+
this.handleFileChange(filePath);
|
|
435
|
+
});
|
|
436
|
+
this.watcher.on("add", (filePath) => {
|
|
437
|
+
this.handleFileChange(filePath);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Handle file change with debouncing
|
|
443
|
+
*/
|
|
444
|
+
handleFileChange(filePath) {
|
|
445
|
+
const existingTimer = this.debounceTimers.get(filePath);
|
|
446
|
+
if (existingTimer) {
|
|
447
|
+
clearTimeout(existingTimer);
|
|
448
|
+
}
|
|
449
|
+
const timer = setTimeout(() => {
|
|
450
|
+
this.debounceTimers.delete(filePath);
|
|
451
|
+
this.emit("change", filePath);
|
|
452
|
+
this.tryParseJsonFile(filePath);
|
|
453
|
+
}, this.debounceMs);
|
|
454
|
+
this.debounceTimers.set(filePath, timer);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Try to parse JSON file with retries
|
|
458
|
+
*/
|
|
459
|
+
async tryParseJsonFile(filePath) {
|
|
460
|
+
if (path.extname(filePath).toLowerCase() !== ".json") {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
let lastError = null;
|
|
464
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
465
|
+
try {
|
|
466
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
467
|
+
const parsed = JSON.parse(content);
|
|
468
|
+
this.emit("parsed", filePath, parsed);
|
|
469
|
+
return;
|
|
470
|
+
} catch (error) {
|
|
471
|
+
lastError = error;
|
|
472
|
+
if (attempt < this.maxRetries) {
|
|
473
|
+
await this.delay(this.retryDelayMs);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
this.emit("parseError", filePath, lastError);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Delay helper
|
|
481
|
+
*/
|
|
482
|
+
delay(ms) {
|
|
483
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Stop watching for file changes
|
|
487
|
+
*/
|
|
488
|
+
close() {
|
|
489
|
+
if (this.watcher) {
|
|
490
|
+
this.watcher.close();
|
|
491
|
+
this.watcher = null;
|
|
492
|
+
}
|
|
493
|
+
for (const timer of this.debounceTimers.values()) {
|
|
494
|
+
clearTimeout(timer);
|
|
495
|
+
}
|
|
496
|
+
this.debounceTimers.clear();
|
|
497
|
+
this.watching = false;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Get the watched path
|
|
501
|
+
*/
|
|
502
|
+
getPath() {
|
|
503
|
+
return this.watchPath;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Check if the watcher is currently active
|
|
507
|
+
*/
|
|
508
|
+
isWatching() {
|
|
509
|
+
return this.watching;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Get the debounce time in milliseconds
|
|
513
|
+
*/
|
|
514
|
+
getDebounceMs() {
|
|
515
|
+
return this.debounceMs;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Get the poll interval in milliseconds
|
|
519
|
+
*/
|
|
520
|
+
getPollIntervalMs() {
|
|
521
|
+
return this.pollIntervalMs;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Get the retry delay in milliseconds
|
|
525
|
+
*/
|
|
526
|
+
getRetryDelayMs() {
|
|
527
|
+
return this.retryDelayMs;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Get the maximum number of retries
|
|
531
|
+
*/
|
|
532
|
+
getMaxRetries() {
|
|
533
|
+
return this.maxRetries;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
export {
|
|
537
|
+
index$3 as Config,
|
|
538
|
+
q as CslItemSchema,
|
|
539
|
+
CslLibrarySchema,
|
|
540
|
+
index$1 as Duplicate,
|
|
541
|
+
FileWatcher,
|
|
542
|
+
L as Library,
|
|
543
|
+
index as Merge,
|
|
544
|
+
R as Reference,
|
|
545
|
+
index$2 as Search,
|
|
546
|
+
index$4 as Utils,
|
|
547
|
+
D as ensureCustomMetadata,
|
|
548
|
+
E as extractUuidFromCustom,
|
|
549
|
+
x as generateId,
|
|
550
|
+
y as generateIdWithCollisionCheck,
|
|
551
|
+
A as generateUuid,
|
|
552
|
+
B as isValidUuid,
|
|
553
|
+
z as normalizeText,
|
|
554
|
+
r as parseCslJson,
|
|
555
|
+
u as serializeCslJson,
|
|
556
|
+
validateCslJson,
|
|
557
|
+
v as writeCslJson
|
|
558
|
+
};
|
|
559
|
+
//# sourceMappingURL=index.js.map
|