@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.
Files changed (117) hide show
  1. package/README.md +167 -0
  2. package/bin/reference-manager.js +5 -0
  3. package/dist/chunks/detector-BF8Mcc72.js +1415 -0
  4. package/dist/chunks/detector-BF8Mcc72.js.map +1 -0
  5. package/dist/cli/commands/add.d.ts +22 -0
  6. package/dist/cli/commands/add.d.ts.map +1 -0
  7. package/dist/cli/commands/index.d.ts +16 -0
  8. package/dist/cli/commands/index.d.ts.map +1 -0
  9. package/dist/cli/commands/list.d.ts +15 -0
  10. package/dist/cli/commands/list.d.ts.map +1 -0
  11. package/dist/cli/commands/remove.d.ts +19 -0
  12. package/dist/cli/commands/remove.d.ts.map +1 -0
  13. package/dist/cli/commands/search.d.ts +16 -0
  14. package/dist/cli/commands/search.d.ts.map +1 -0
  15. package/dist/cli/commands/server.d.ts +32 -0
  16. package/dist/cli/commands/server.d.ts.map +1 -0
  17. package/dist/cli/commands/update.d.ts +20 -0
  18. package/dist/cli/commands/update.d.ts.map +1 -0
  19. package/dist/cli/helpers.d.ts +61 -0
  20. package/dist/cli/helpers.d.ts.map +1 -0
  21. package/dist/cli/index.d.ts +13 -0
  22. package/dist/cli/index.d.ts.map +1 -0
  23. package/dist/cli/output/bibtex.d.ts +6 -0
  24. package/dist/cli/output/bibtex.d.ts.map +1 -0
  25. package/dist/cli/output/index.d.ts +7 -0
  26. package/dist/cli/output/index.d.ts.map +1 -0
  27. package/dist/cli/output/json.d.ts +6 -0
  28. package/dist/cli/output/json.d.ts.map +1 -0
  29. package/dist/cli/output/pretty.d.ts +6 -0
  30. package/dist/cli/output/pretty.d.ts.map +1 -0
  31. package/dist/cli/server-client.d.ts +38 -0
  32. package/dist/cli/server-client.d.ts.map +1 -0
  33. package/dist/cli/server-detection.d.ts +27 -0
  34. package/dist/cli/server-detection.d.ts.map +1 -0
  35. package/dist/cli.js +981 -0
  36. package/dist/cli.js.map +1 -0
  37. package/dist/config/defaults.d.ts +29 -0
  38. package/dist/config/defaults.d.ts.map +1 -0
  39. package/dist/config/index.d.ts +10 -0
  40. package/dist/config/index.d.ts.map +1 -0
  41. package/dist/config/loader.d.ts +27 -0
  42. package/dist/config/loader.d.ts.map +1 -0
  43. package/dist/config/schema.d.ts +129 -0
  44. package/dist/config/schema.d.ts.map +1 -0
  45. package/dist/core/csl-json/parser.d.ts +9 -0
  46. package/dist/core/csl-json/parser.d.ts.map +1 -0
  47. package/dist/core/csl-json/serializer.d.ts +15 -0
  48. package/dist/core/csl-json/serializer.d.ts.map +1 -0
  49. package/dist/core/csl-json/types.d.ts +124 -0
  50. package/dist/core/csl-json/types.d.ts.map +1 -0
  51. package/dist/core/csl-json/validator.d.ts +19 -0
  52. package/dist/core/csl-json/validator.d.ts.map +1 -0
  53. package/dist/core/identifier/generator.d.ts +17 -0
  54. package/dist/core/identifier/generator.d.ts.map +1 -0
  55. package/dist/core/identifier/normalize.d.ts +20 -0
  56. package/dist/core/identifier/normalize.d.ts.map +1 -0
  57. package/dist/core/identifier/uuid.d.ts +24 -0
  58. package/dist/core/identifier/uuid.d.ts.map +1 -0
  59. package/dist/core/index.d.ts +15 -0
  60. package/dist/core/index.d.ts.map +1 -0
  61. package/dist/core/library.d.ts +73 -0
  62. package/dist/core/library.d.ts.map +1 -0
  63. package/dist/core/reference.d.ts +86 -0
  64. package/dist/core/reference.d.ts.map +1 -0
  65. package/dist/features/duplicate/detector.d.ts +19 -0
  66. package/dist/features/duplicate/detector.d.ts.map +1 -0
  67. package/dist/features/duplicate/index.d.ts +6 -0
  68. package/dist/features/duplicate/index.d.ts.map +1 -0
  69. package/dist/features/duplicate/types.d.ts +45 -0
  70. package/dist/features/duplicate/types.d.ts.map +1 -0
  71. package/dist/features/file-watcher/file-watcher.d.ts +83 -0
  72. package/dist/features/file-watcher/file-watcher.d.ts.map +1 -0
  73. package/dist/features/file-watcher/index.d.ts +2 -0
  74. package/dist/features/file-watcher/index.d.ts.map +1 -0
  75. package/dist/features/merge/index.d.ts +8 -0
  76. package/dist/features/merge/index.d.ts.map +1 -0
  77. package/dist/features/merge/three-way.d.ts +16 -0
  78. package/dist/features/merge/three-way.d.ts.map +1 -0
  79. package/dist/features/merge/types.d.ts +74 -0
  80. package/dist/features/merge/types.d.ts.map +1 -0
  81. package/dist/features/search/index.d.ts +9 -0
  82. package/dist/features/search/index.d.ts.map +1 -0
  83. package/dist/features/search/matcher.d.ts +18 -0
  84. package/dist/features/search/matcher.d.ts.map +1 -0
  85. package/dist/features/search/normalizer.d.ts +12 -0
  86. package/dist/features/search/normalizer.d.ts.map +1 -0
  87. package/dist/features/search/sorter.d.ts +11 -0
  88. package/dist/features/search/sorter.d.ts.map +1 -0
  89. package/dist/features/search/tokenizer.d.ts +6 -0
  90. package/dist/features/search/tokenizer.d.ts.map +1 -0
  91. package/dist/features/search/types.d.ts +77 -0
  92. package/dist/features/search/types.d.ts.map +1 -0
  93. package/dist/index.d.ts +13 -0
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +559 -0
  96. package/dist/index.js.map +1 -0
  97. package/dist/server/index.d.ts +9 -0
  98. package/dist/server/index.d.ts.map +1 -0
  99. package/dist/server/portfile.d.ts +43 -0
  100. package/dist/server/portfile.d.ts.map +1 -0
  101. package/dist/server/routes/health.d.ts +7 -0
  102. package/dist/server/routes/health.d.ts.map +1 -0
  103. package/dist/server/routes/references.d.ts +9 -0
  104. package/dist/server/routes/references.d.ts.map +1 -0
  105. package/dist/server.js +91 -0
  106. package/dist/server.js.map +1 -0
  107. package/dist/utils/backup.d.ts +21 -0
  108. package/dist/utils/backup.d.ts.map +1 -0
  109. package/dist/utils/file.d.ts +9 -0
  110. package/dist/utils/file.d.ts.map +1 -0
  111. package/dist/utils/hash.d.ts +9 -0
  112. package/dist/utils/hash.d.ts.map +1 -0
  113. package/dist/utils/index.d.ts +5 -0
  114. package/dist/utils/index.d.ts.map +1 -0
  115. package/dist/utils/logger.d.ts +8 -0
  116. package/dist/utils/logger.d.ts.map +1 -0
  117. 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"}
@@ -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