@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
package/dist/cli.js ADDED
@@ -0,0 +1,981 @@
1
+ import { Command } from "commander";
2
+ import { z } from "zod";
3
+ import { o as detectDuplicate, R as Reference, t as tokenize, s as search$1, m as sortResults, l as loadConfig, L as Library } from "./chunks/detector-BF8Mcc72.js";
4
+ import { promises, readFileSync } from "node:fs";
5
+ import * as os from "node:os";
6
+ import * as path from "node:path";
7
+ import { stdin, stdout } from "node:process";
8
+ import { spawn } from "node:child_process";
9
+ function getPortfilePath() {
10
+ const tmpDir = os.tmpdir();
11
+ return path.join(tmpDir, "reference-manager", "server.port");
12
+ }
13
+ async function writePortfile(portfilePath, port, pid, library, started_at) {
14
+ const dir = path.dirname(portfilePath);
15
+ await promises.mkdir(dir, { recursive: true });
16
+ const data = { port, pid, library };
17
+ if (started_at !== void 0) {
18
+ data.started_at = started_at;
19
+ }
20
+ const content = JSON.stringify(data, null, 2);
21
+ await promises.writeFile(portfilePath, content, "utf-8");
22
+ }
23
+ async function readPortfile(portfilePath) {
24
+ try {
25
+ const content = await promises.readFile(portfilePath, "utf-8");
26
+ const data = JSON.parse(content);
27
+ if (typeof data.port !== "number" || typeof data.pid !== "number") {
28
+ return null;
29
+ }
30
+ const result = {
31
+ port: data.port,
32
+ pid: data.pid
33
+ };
34
+ if (typeof data.library === "string") {
35
+ result.library = data.library;
36
+ }
37
+ if (typeof data.started_at === "string") {
38
+ result.started_at = data.started_at;
39
+ }
40
+ return result;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+ async function portfileExists(portfilePath) {
46
+ try {
47
+ await promises.access(portfilePath);
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+ async function removePortfile(portfilePath) {
54
+ try {
55
+ await promises.unlink(portfilePath);
56
+ } catch {
57
+ }
58
+ }
59
+ function isProcessRunning(pid) {
60
+ if (pid <= 0) {
61
+ return false;
62
+ }
63
+ try {
64
+ process.kill(pid, 0);
65
+ return true;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+ function generateSuffix(index) {
71
+ const alphabet = "abcdefghijklmnopqrstuvwxyz";
72
+ let suffix = "";
73
+ let n = index;
74
+ do {
75
+ suffix = alphabet[n % 26] + suffix;
76
+ n = Math.floor(n / 26) - 1;
77
+ } while (n >= 0);
78
+ return suffix;
79
+ }
80
+ function resolveIdCollision(baseId, existing) {
81
+ const existingIds = new Set(existing.map((item) => item.id));
82
+ if (!existingIds.has(baseId)) {
83
+ return { id: baseId, changed: false };
84
+ }
85
+ let index = 0;
86
+ let newId;
87
+ do {
88
+ const suffix = generateSuffix(index);
89
+ newId = `${baseId}${suffix}`;
90
+ index++;
91
+ } while (existingIds.has(newId));
92
+ return { id: newId, changed: true };
93
+ }
94
+ async function add(existing, newItem, options) {
95
+ if (!options.force) {
96
+ const duplicateResult = detectDuplicate(newItem, existing);
97
+ if (duplicateResult.matches.length > 0) {
98
+ return {
99
+ added: false,
100
+ item: newItem,
101
+ idChanged: false,
102
+ duplicate: duplicateResult.matches[0]
103
+ };
104
+ }
105
+ }
106
+ const originalId = newItem.id;
107
+ const { id, changed } = resolveIdCollision(originalId, existing);
108
+ const finalItem = {
109
+ ...newItem,
110
+ id
111
+ };
112
+ return {
113
+ added: true,
114
+ item: finalItem,
115
+ idChanged: changed,
116
+ originalId: changed ? originalId : void 0
117
+ };
118
+ }
119
+ function mapEntryType(cslType) {
120
+ const typeMap = {
121
+ article: "article",
122
+ "article-journal": "article",
123
+ "article-magazine": "article",
124
+ "article-newspaper": "article",
125
+ book: "book",
126
+ chapter: "inbook",
127
+ "paper-conference": "inproceedings",
128
+ thesis: "phdthesis",
129
+ report: "techreport",
130
+ webpage: "misc"
131
+ };
132
+ return typeMap[cslType] || "misc";
133
+ }
134
+ function formatBibtexAuthor(author) {
135
+ if (author.literal) {
136
+ return author.literal;
137
+ }
138
+ const family = author.family || "";
139
+ const given = author.given || "";
140
+ return given ? `${family}, ${given}` : family;
141
+ }
142
+ function formatBibtexAuthors(authors) {
143
+ return authors.map(formatBibtexAuthor).join(" and ");
144
+ }
145
+ function formatField(name, value) {
146
+ return ` ${name} = {${value}},`;
147
+ }
148
+ function addBasicFields(lines, item) {
149
+ if (item.title) {
150
+ lines.push(formatField("title", item.title));
151
+ }
152
+ if (item.author && item.author.length > 0) {
153
+ lines.push(formatField("author", formatBibtexAuthors(item.author)));
154
+ }
155
+ const year = item.issued?.["date-parts"]?.[0]?.[0];
156
+ if (year) {
157
+ lines.push(formatField("year", String(year)));
158
+ }
159
+ }
160
+ function addPublicationDetails(lines, item, entryType) {
161
+ if (item["container-title"]) {
162
+ if (entryType === "article") {
163
+ lines.push(formatField("journal", item["container-title"]));
164
+ } else if (entryType === "inbook" || entryType === "inproceedings") {
165
+ lines.push(formatField("booktitle", item["container-title"]));
166
+ }
167
+ }
168
+ if (item.volume) {
169
+ lines.push(formatField("volume", item.volume));
170
+ }
171
+ if (item.issue) {
172
+ lines.push(formatField("number", item.issue));
173
+ }
174
+ if (item.page) {
175
+ lines.push(formatField("pages", item.page));
176
+ }
177
+ if (item.publisher) {
178
+ lines.push(formatField("publisher", item.publisher));
179
+ }
180
+ }
181
+ function addIdentifierFields(lines, item) {
182
+ if (item.DOI) {
183
+ lines.push(formatField("doi", item.DOI));
184
+ }
185
+ if (item.URL) {
186
+ lines.push(formatField("url", item.URL));
187
+ }
188
+ if (item.PMID) {
189
+ lines.push(formatField("note", `PMID: ${item.PMID}`));
190
+ } else if (item.PMCID) {
191
+ lines.push(formatField("note", `PMCID: ${item.PMCID}`));
192
+ }
193
+ }
194
+ function formatSingleBibtexEntry(ref) {
195
+ const item = ref.getItem();
196
+ const entryType = mapEntryType(item.type);
197
+ const lines = [];
198
+ lines.push(`@${entryType}{${item.id},`);
199
+ addBasicFields(lines, item);
200
+ addPublicationDetails(lines, item, entryType);
201
+ addIdentifierFields(lines, item);
202
+ lines.push("}");
203
+ return lines.join("\n");
204
+ }
205
+ function formatBibtex(references) {
206
+ if (references.length === 0) {
207
+ return "";
208
+ }
209
+ return references.map(formatSingleBibtexEntry).join("\n\n");
210
+ }
211
+ function formatJson(references) {
212
+ const items = references.map((ref) => ref.getItem());
213
+ return JSON.stringify(items);
214
+ }
215
+ function formatAuthor(author) {
216
+ const family = author.family || "";
217
+ const givenInitial = author.given ? `${author.given.charAt(0)}.` : "";
218
+ return givenInitial ? `${family}, ${givenInitial}` : family;
219
+ }
220
+ function formatAuthors(authors) {
221
+ return authors.map(formatAuthor).join("; ");
222
+ }
223
+ function formatSingleReference(ref) {
224
+ const item = ref.getItem();
225
+ const lines = [];
226
+ const header = item.title ? `[${item.id}] ${item.title}` : `[${item.id}]`;
227
+ lines.push(header);
228
+ if (item.author && item.author.length > 0) {
229
+ lines.push(` Authors: ${formatAuthors(item.author)}`);
230
+ }
231
+ const year = item.issued?.["date-parts"]?.[0]?.[0];
232
+ lines.push(` Year: ${year || "(no year)"}`);
233
+ lines.push(` Type: ${item.type}`);
234
+ if (item.DOI) {
235
+ lines.push(` DOI: ${item.DOI}`);
236
+ }
237
+ if (item.PMID) {
238
+ lines.push(` PMID: ${item.PMID}`);
239
+ }
240
+ if (item.PMCID) {
241
+ lines.push(` PMCID: ${item.PMCID}`);
242
+ }
243
+ if (item.URL) {
244
+ lines.push(` URL: ${item.URL}`);
245
+ }
246
+ lines.push(` UUID: ${ref.getUuid()}`);
247
+ return lines.join("\n");
248
+ }
249
+ function formatPretty(references) {
250
+ if (references.length === 0) {
251
+ return "";
252
+ }
253
+ return references.map(formatSingleReference).join("\n\n");
254
+ }
255
+ async function list(items, options) {
256
+ const outputOptions = [options.json, options.idsOnly, options.uuid, options.bibtex].filter(
257
+ Boolean
258
+ );
259
+ if (outputOptions.length > 1) {
260
+ throw new Error(
261
+ "Multiple output formats specified. Only one of --json, --ids-only, --uuid, --bibtex can be used."
262
+ );
263
+ }
264
+ const references = items.map((item) => new Reference(item));
265
+ if (options.json) {
266
+ process.stdout.write(formatJson(references));
267
+ } else if (options.idsOnly) {
268
+ for (const item of items) {
269
+ process.stdout.write(`${item.id}
270
+ `);
271
+ }
272
+ } else if (options.uuid) {
273
+ for (const item of items) {
274
+ if (item.custom) {
275
+ process.stdout.write(`${item.custom.uuid}
276
+ `);
277
+ }
278
+ }
279
+ } else if (options.bibtex) {
280
+ process.stdout.write(formatBibtex(references));
281
+ } else {
282
+ process.stdout.write(formatPretty(references));
283
+ }
284
+ }
285
+ async function search(items, query, options) {
286
+ const outputOptions = [options.json, options.idsOnly, options.uuid, options.bibtex].filter(
287
+ Boolean
288
+ );
289
+ if (outputOptions.length > 1) {
290
+ throw new Error(
291
+ "Multiple output formats specified. Only one of --json, --ids-only, --uuid, --bibtex can be used."
292
+ );
293
+ }
294
+ const searchQuery = tokenize(query);
295
+ const results = search$1(items, searchQuery.tokens);
296
+ const sorted = sortResults(results);
297
+ const matchedItems = sorted.map((result) => result.reference);
298
+ const references = matchedItems.map((item) => new Reference(item));
299
+ if (options.json) {
300
+ process.stdout.write(formatJson(references));
301
+ } else if (options.idsOnly) {
302
+ for (const item of matchedItems) {
303
+ process.stdout.write(`${item.id}
304
+ `);
305
+ }
306
+ } else if (options.uuid) {
307
+ for (const item of matchedItems) {
308
+ if (item.custom) {
309
+ process.stdout.write(`${item.custom.uuid}
310
+ `);
311
+ }
312
+ }
313
+ } else if (options.bibtex) {
314
+ process.stdout.write(formatBibtex(references));
315
+ } else {
316
+ process.stdout.write(formatPretty(references));
317
+ }
318
+ }
319
+ async function serverStart(options) {
320
+ const existingStatus = await serverStatus(options.portfilePath);
321
+ if (existingStatus !== null) {
322
+ throw new Error("Server is already running");
323
+ }
324
+ const port = options.port ?? 3e3;
325
+ const pid = process.pid;
326
+ const started_at = (/* @__PURE__ */ new Date()).toISOString();
327
+ await writePortfile(options.portfilePath, port, pid, options.library, started_at);
328
+ }
329
+ async function serverStop(portfilePath) {
330
+ const status = await serverStatus(portfilePath);
331
+ if (status === null) {
332
+ throw new Error("Server is not running");
333
+ }
334
+ await removePortfile(portfilePath);
335
+ process.stdout.write("Server stopped successfully\n");
336
+ }
337
+ async function serverStatus(portfilePath) {
338
+ const portfileData = await readPortfile(portfilePath);
339
+ if (portfileData === null) {
340
+ return null;
341
+ }
342
+ if (!isProcessRunning(portfileData.pid)) {
343
+ await removePortfile(portfilePath);
344
+ return null;
345
+ }
346
+ const result = {
347
+ port: portfileData.port,
348
+ pid: portfileData.pid,
349
+ library: portfileData.library ?? ""
350
+ };
351
+ if (portfileData.started_at !== void 0) {
352
+ result.started_at = portfileData.started_at;
353
+ }
354
+ return result;
355
+ }
356
+ async function update(items, identifier, updates, options) {
357
+ let foundIndex = -1;
358
+ if (options.byUuid) {
359
+ foundIndex = items.findIndex((item) => item.custom?.uuid === identifier);
360
+ } else {
361
+ foundIndex = items.findIndex((item) => item.id === identifier);
362
+ }
363
+ if (foundIndex === -1) {
364
+ return {
365
+ updated: false,
366
+ items
367
+ };
368
+ }
369
+ const existingItem = items[foundIndex];
370
+ if (!existingItem) {
371
+ return {
372
+ updated: false,
373
+ items
374
+ };
375
+ }
376
+ const updatedItem = {
377
+ ...existingItem,
378
+ ...updates,
379
+ // Ensure required fields
380
+ id: updates.id ?? existingItem.id,
381
+ type: updates.type ?? existingItem.type,
382
+ // Preserve UUID and created_at
383
+ custom: {
384
+ ...existingItem.custom || {},
385
+ ...updates.custom || {},
386
+ uuid: existingItem.custom?.uuid || "",
387
+ created_at: existingItem.custom?.created_at || (/* @__PURE__ */ new Date()).toISOString(),
388
+ // Update timestamp
389
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
390
+ }
391
+ };
392
+ const updatedItems = [...items.slice(0, foundIndex), updatedItem, ...items.slice(foundIndex + 1)];
393
+ return {
394
+ updated: true,
395
+ item: updatedItem,
396
+ items: updatedItems
397
+ };
398
+ }
399
+ async function readJsonInput(file) {
400
+ if (file) {
401
+ try {
402
+ return readFileSync(file, "utf-8");
403
+ } catch (error) {
404
+ throw new Error(
405
+ `I/O error: Cannot read file ${file}: ${error instanceof Error ? error.message : String(error)}`
406
+ );
407
+ }
408
+ }
409
+ const chunks = [];
410
+ for await (const chunk of stdin) {
411
+ chunks.push(chunk);
412
+ }
413
+ return Buffer.concat(chunks).toString("utf-8");
414
+ }
415
+ function parseJsonInput(input) {
416
+ if (!input || input.trim() === "") {
417
+ throw new Error("Parse error: Empty input");
418
+ }
419
+ try {
420
+ return JSON.parse(input);
421
+ } catch (error) {
422
+ throw new Error(
423
+ `Parse error: Invalid JSON: ${error instanceof Error ? error.message : String(error)}`
424
+ );
425
+ }
426
+ }
427
+ async function loadConfigWithOverrides(options) {
428
+ const config = await loadConfig();
429
+ const overrides = {};
430
+ if (options.library) {
431
+ overrides.library = options.library;
432
+ }
433
+ if (options.quiet) {
434
+ overrides.logLevel = "silent";
435
+ } else if (options.verbose) {
436
+ overrides.logLevel = "debug";
437
+ } else if (options.logLevel) {
438
+ overrides.logLevel = options.logLevel;
439
+ }
440
+ if (options.backup !== void 0 || options.backupDir) {
441
+ overrides.backup = {
442
+ ...config.backup,
443
+ ...options.backup !== void 0 && { enabled: options.backup },
444
+ ...options.backupDir && { directory: options.backupDir }
445
+ };
446
+ }
447
+ return { ...config, ...overrides };
448
+ }
449
+ function isTTY() {
450
+ return Boolean(stdin.isTTY && stdout.isTTY);
451
+ }
452
+ async function readConfirmation(prompt) {
453
+ if (!isTTY()) {
454
+ return true;
455
+ }
456
+ stdout.write(`${prompt} (y/N): `);
457
+ const chunks = [];
458
+ for await (const chunk of stdin) {
459
+ chunks.push(chunk);
460
+ const input2 = Buffer.concat(chunks).toString("utf-8");
461
+ if (input2.includes("\n")) {
462
+ break;
463
+ }
464
+ }
465
+ const input = Buffer.concat(chunks).toString("utf-8").trim().toLowerCase();
466
+ return input === "y" || input === "yes";
467
+ }
468
+ class ServerClient {
469
+ constructor(baseUrl) {
470
+ this.baseUrl = baseUrl;
471
+ }
472
+ /**
473
+ * Get all references from the server.
474
+ * @returns Array of CSL items
475
+ */
476
+ async getAll() {
477
+ const url = `${this.baseUrl}/api/references`;
478
+ const response = await fetch(url);
479
+ if (!response.ok) {
480
+ throw new Error(await response.text());
481
+ }
482
+ return await response.json();
483
+ }
484
+ /**
485
+ * Find reference by UUID.
486
+ * @param uuid - Reference UUID
487
+ * @returns CSL item or null if not found
488
+ */
489
+ async findByUuid(uuid) {
490
+ const url = `${this.baseUrl}/api/references/${uuid}`;
491
+ const response = await fetch(url);
492
+ if (response.status === 404) {
493
+ return null;
494
+ }
495
+ if (!response.ok) {
496
+ throw new Error(await response.text());
497
+ }
498
+ return await response.json();
499
+ }
500
+ /**
501
+ * Add new reference to the library.
502
+ * @param item - CSL item to add
503
+ * @returns Created CSL item with UUID
504
+ */
505
+ async add(item) {
506
+ const url = `${this.baseUrl}/api/references`;
507
+ const response = await fetch(url, {
508
+ method: "POST",
509
+ headers: { "Content-Type": "application/json" },
510
+ body: JSON.stringify(item)
511
+ });
512
+ if (!response.ok) {
513
+ throw new Error(await response.text());
514
+ }
515
+ return await response.json();
516
+ }
517
+ /**
518
+ * Update existing reference.
519
+ * @param uuid - Reference UUID
520
+ * @param item - Updated CSL item
521
+ * @returns Updated CSL item
522
+ */
523
+ async update(uuid, item) {
524
+ const url = `${this.baseUrl}/api/references/${uuid}`;
525
+ const response = await fetch(url, {
526
+ method: "PUT",
527
+ headers: { "Content-Type": "application/json" },
528
+ body: JSON.stringify(item)
529
+ });
530
+ if (!response.ok) {
531
+ throw new Error(await response.text());
532
+ }
533
+ return await response.json();
534
+ }
535
+ /**
536
+ * Remove reference by UUID.
537
+ * @param uuid - Reference UUID
538
+ */
539
+ async remove(uuid) {
540
+ const url = `${this.baseUrl}/api/references/${uuid}`;
541
+ const response = await fetch(url, {
542
+ method: "DELETE"
543
+ });
544
+ if (!response.ok) {
545
+ throw new Error(await response.text());
546
+ }
547
+ }
548
+ }
549
+ async function getServerConnection(libraryPath, config) {
550
+ const portfilePath = getPortfilePath();
551
+ const portfileData = await readPortfile(portfilePath);
552
+ if (!portfileData) {
553
+ if (config.server.autoStart) {
554
+ await startServerDaemon(libraryPath);
555
+ await waitForPortfile(5e3);
556
+ return await getServerConnection(libraryPath, config);
557
+ }
558
+ return null;
559
+ }
560
+ if (!isProcessRunning(portfileData.pid)) {
561
+ await removePortfile(portfilePath);
562
+ return null;
563
+ }
564
+ if (!portfileData.library || portfileData.library !== libraryPath) {
565
+ return null;
566
+ }
567
+ return {
568
+ baseUrl: `http://localhost:${portfileData.port}`,
569
+ pid: portfileData.pid
570
+ };
571
+ }
572
+ async function startServerDaemon(libraryPath, _config) {
573
+ const binaryPath = process.argv[1] || process.execPath;
574
+ const child = spawn(
575
+ process.execPath,
576
+ [binaryPath, "server", "start", "--daemon", "--library", libraryPath],
577
+ {
578
+ detached: true,
579
+ stdio: "ignore"
580
+ }
581
+ );
582
+ child.unref();
583
+ }
584
+ async function waitForPortfile(timeoutMs) {
585
+ const portfilePath = getPortfilePath();
586
+ const startTime = Date.now();
587
+ const checkInterval = 50;
588
+ while (Date.now() - startTime < timeoutMs) {
589
+ if (await portfileExists(portfilePath)) {
590
+ return;
591
+ }
592
+ await new Promise((resolve) => setTimeout(resolve, checkInterval));
593
+ }
594
+ throw new Error(`Server failed to start: portfile not created within ${timeoutMs}ms`);
595
+ }
596
+ const version = "0.1.0";
597
+ const description = "A local reference management tool using CSL-JSON as the single source of truth";
598
+ const packageJson = {
599
+ version,
600
+ description
601
+ };
602
+ function createProgram() {
603
+ const program = new Command();
604
+ program.name("reference-manager").version(packageJson.version).description(packageJson.description);
605
+ program.option("--library <path>", "Override library file path").option("--log-level <level>", "Override log level (silent|info|debug)").option("--config <path>", "Use specific config file").option("--quiet", "Suppress all non-error output").option("--verbose", "Enable verbose output").option("--no-backup", "Disable backup creation").option("--backup-dir <path>", "Override backup directory");
606
+ registerListCommand(program);
607
+ registerSearchCommand(program);
608
+ registerAddCommand(program);
609
+ registerRemoveCommand(program);
610
+ registerUpdateCommand(program);
611
+ registerServerCommand(program);
612
+ return program;
613
+ }
614
+ function registerListCommand(program) {
615
+ program.command("list").description("List all references in the library").option("--json", "Output in JSON format").option("--ids-only", "Output only citation keys").option("--uuid", "Output only UUIDs").option("--bibtex", "Output in BibTeX format").action(async (options) => {
616
+ try {
617
+ const globalOpts = program.opts();
618
+ const config = await loadConfigWithOverrides({ ...globalOpts, ...options });
619
+ const server = await getServerConnection(config.library, config);
620
+ let items;
621
+ if (server) {
622
+ const client = new ServerClient(server.baseUrl);
623
+ items = await client.getAll();
624
+ } else {
625
+ const library = await Library.load(config.library);
626
+ items = library.getAll().map((ref) => ref.getItem());
627
+ }
628
+ await list(items, {
629
+ json: options.json,
630
+ idsOnly: options.idsOnly,
631
+ uuid: options.uuid,
632
+ bibtex: options.bibtex
633
+ });
634
+ process.exit(0);
635
+ } catch (error) {
636
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
637
+ `);
638
+ process.exit(4);
639
+ }
640
+ });
641
+ }
642
+ function registerSearchCommand(program) {
643
+ program.command("search").description("Search references").argument("<query>", "Search query").option("--json", "Output in JSON format").option("--ids-only", "Output only citation keys").option("--uuid", "Output only UUIDs").option("--bibtex", "Output in BibTeX format").action(async (query, options) => {
644
+ try {
645
+ const globalOpts = program.opts();
646
+ const config = await loadConfigWithOverrides({ ...globalOpts, ...options });
647
+ const server = await getServerConnection(config.library, config);
648
+ let items;
649
+ if (server) {
650
+ const client = new ServerClient(server.baseUrl);
651
+ items = await client.getAll();
652
+ } else {
653
+ const library = await Library.load(config.library);
654
+ items = library.getAll().map((ref) => ref.getItem());
655
+ }
656
+ await search(items, query, {
657
+ json: options.json,
658
+ idsOnly: options.idsOnly,
659
+ uuid: options.uuid,
660
+ bibtex: options.bibtex
661
+ });
662
+ process.exit(0);
663
+ } catch (error) {
664
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
665
+ `);
666
+ process.exit(4);
667
+ }
668
+ });
669
+ }
670
+ async function addViaServer(items, server, force) {
671
+ const client = new ServerClient(server.baseUrl);
672
+ for (const item of items) {
673
+ try {
674
+ await client.add(item);
675
+ process.stderr.write(`Added reference: [${item.id}]
676
+ `);
677
+ } catch (error) {
678
+ if (!force && error instanceof Error && error.message.includes("Duplicate")) {
679
+ process.stderr.write(`Error: ${error.message}
680
+ `);
681
+ process.exit(1);
682
+ }
683
+ throw error;
684
+ }
685
+ }
686
+ }
687
+ async function addViaLibrary(items, libraryPath, force) {
688
+ const library = await Library.load(libraryPath);
689
+ const existingItems = library.getAll().map((ref) => ref.getItem());
690
+ for (const item of items) {
691
+ const result = await add(existingItems, item, { force });
692
+ if (result.added) {
693
+ library.add(result.item);
694
+ process.stderr.write(`Added reference: [${result.item.id}]
695
+ `);
696
+ existingItems.push(result.item);
697
+ } else if (result.duplicate) {
698
+ throw new Error(
699
+ `Duplicate detected: ${result.duplicate.type} match with existing reference [${result.duplicate.existing.id}]`
700
+ );
701
+ }
702
+ }
703
+ await library.save();
704
+ }
705
+ function handleAddError(error) {
706
+ const message = error instanceof Error ? error.message : String(error);
707
+ if (message.includes("Parse error")) {
708
+ process.stderr.write(`Error: ${message}
709
+ `);
710
+ process.exit(3);
711
+ }
712
+ if (message.includes("Duplicate")) {
713
+ process.stderr.write(`Error: ${message}
714
+ `);
715
+ process.exit(1);
716
+ }
717
+ process.stderr.write(`Error: ${message}
718
+ `);
719
+ process.exit(4);
720
+ }
721
+ async function handleAddAction(file, options, program) {
722
+ try {
723
+ const globalOpts = program.opts();
724
+ const config = await loadConfigWithOverrides({ ...globalOpts, ...options });
725
+ const inputStr = await readJsonInput(file);
726
+ const input = parseJsonInput(inputStr);
727
+ const items = Array.isArray(input) ? input : [input];
728
+ const server = await getServerConnection(config.library, config);
729
+ if (server) {
730
+ await addViaServer(items, server, options.force ?? false);
731
+ } else {
732
+ await addViaLibrary(items, config.library, options.force ?? false);
733
+ }
734
+ process.exit(0);
735
+ } catch (error) {
736
+ handleAddError(error);
737
+ }
738
+ }
739
+ function registerAddCommand(program) {
740
+ program.command("add").description("Add new reference(s) to the library").argument("[file]", "JSON file to add (or use stdin)").option("-f, --force", "Skip duplicate detection").action(async (file, options) => {
741
+ await handleAddAction(file, options, program);
742
+ });
743
+ }
744
+ async function findReferenceToRemove(identifier, byUuid, server, libraryPath) {
745
+ if (server) {
746
+ const client = new ServerClient(server.baseUrl);
747
+ const items = await client.getAll();
748
+ return byUuid ? items.find((item) => item.custom?.uuid === identifier) : items.find((item) => item.id === identifier);
749
+ }
750
+ const library = await Library.load(libraryPath);
751
+ const ref = byUuid ? library.findByUuid(identifier) : library.findById(identifier);
752
+ return ref?.getItem();
753
+ }
754
+ async function confirmRemoval(refToRemove, force) {
755
+ if (force || !isTTY()) {
756
+ return true;
757
+ }
758
+ const authors = Array.isArray(refToRemove.author) ? refToRemove.author.map((a) => `${a.family || ""}, ${a.given?.[0] || ""}.`).join("; ") : "(no authors)";
759
+ const confirmMsg = `Remove reference [${refToRemove.id}]?
760
+ Title: ${refToRemove.title || "(no title)"}
761
+ Authors: ${authors}
762
+ Continue?`;
763
+ return await readConfirmation(confirmMsg);
764
+ }
765
+ async function removeReference(identifier, refToRemove, byUuid, server, libraryPath) {
766
+ if (server) {
767
+ const client = new ServerClient(server.baseUrl);
768
+ if (!refToRemove.custom?.uuid) {
769
+ throw new Error("Reference missing UUID");
770
+ }
771
+ await client.remove(refToRemove.custom.uuid);
772
+ return;
773
+ }
774
+ const library = await Library.load(libraryPath);
775
+ const removed = byUuid ? library.removeByUuid(identifier) : library.removeById(identifier);
776
+ if (!removed) {
777
+ throw new Error("Reference not found");
778
+ }
779
+ await library.save();
780
+ }
781
+ function handleRemoveError(error) {
782
+ const message = error instanceof Error ? error.message : String(error);
783
+ if (message.includes("not found")) {
784
+ process.stderr.write(`Error: ${message}
785
+ `);
786
+ process.exit(1);
787
+ }
788
+ process.stderr.write(`Error: ${message}
789
+ `);
790
+ process.exit(4);
791
+ }
792
+ async function handleRemoveAction(identifier, options, program) {
793
+ try {
794
+ const globalOpts = program.opts();
795
+ const config = await loadConfigWithOverrides({ ...globalOpts, ...options });
796
+ const server = await getServerConnection(config.library, config);
797
+ const refToRemove = await findReferenceToRemove(
798
+ identifier,
799
+ options.uuid ?? false,
800
+ server,
801
+ config.library
802
+ );
803
+ if (!refToRemove) {
804
+ process.stderr.write(`Error: Reference not found: ${identifier}
805
+ `);
806
+ process.exit(1);
807
+ }
808
+ const confirmed = await confirmRemoval(refToRemove, options.force ?? false);
809
+ if (!confirmed) {
810
+ process.stderr.write("Cancelled.\n");
811
+ process.exit(2);
812
+ }
813
+ await removeReference(identifier, refToRemove, options.uuid ?? false, server, config.library);
814
+ process.stderr.write(`Removed reference: [${refToRemove.id}]
815
+ `);
816
+ process.exit(0);
817
+ } catch (error) {
818
+ handleRemoveError(error);
819
+ }
820
+ }
821
+ function registerRemoveCommand(program) {
822
+ program.command("remove").description("Remove a reference from the library").argument("<identifier>", "Citation key or UUID").option("--uuid", "Interpret identifier as UUID").option("-f, --force", "Skip confirmation prompt").action(async (identifier, options) => {
823
+ await handleRemoveAction(identifier, options, program);
824
+ });
825
+ }
826
+ async function updateViaServer(identifier, updates, byUuid, server) {
827
+ const client = new ServerClient(server.baseUrl);
828
+ const items = await client.getAll();
829
+ const refToUpdate = byUuid ? items.find((item) => item.custom?.uuid === identifier) : items.find((item) => item.id === identifier);
830
+ if (!refToUpdate || !refToUpdate.custom?.uuid) {
831
+ process.stderr.write(`Error: Reference not found: ${identifier}
832
+ `);
833
+ process.exit(1);
834
+ }
835
+ const updatedItem = { ...refToUpdate, ...updates };
836
+ await client.update(refToUpdate.custom.uuid, updatedItem);
837
+ }
838
+ async function updateViaLibrary(identifier, updates, byUuid, libraryPath) {
839
+ const library = await Library.load(libraryPath);
840
+ const items = library.getAll().map((ref) => ref.getItem());
841
+ const result = await update(items, identifier, updates, { byUuid });
842
+ if (!result.updated || !result.item) {
843
+ throw new Error("Reference not found");
844
+ }
845
+ if (result.item.custom?.uuid) {
846
+ library.removeByUuid(result.item.custom.uuid);
847
+ library.add(result.item);
848
+ }
849
+ await library.save();
850
+ }
851
+ function handleUpdateError(error) {
852
+ const message = error instanceof Error ? error.message : String(error);
853
+ if (message.includes("Parse error")) {
854
+ process.stderr.write(`Error: ${message}
855
+ `);
856
+ process.exit(3);
857
+ }
858
+ if (message.includes("not found") || message.includes("validation")) {
859
+ process.stderr.write(`Error: ${message}
860
+ `);
861
+ process.exit(1);
862
+ }
863
+ process.stderr.write(`Error: ${message}
864
+ `);
865
+ process.exit(4);
866
+ }
867
+ async function handleUpdateAction(identifier, file, options, program) {
868
+ try {
869
+ const globalOpts = program.opts();
870
+ const config = await loadConfigWithOverrides({ ...globalOpts, ...options });
871
+ const inputStr = await readJsonInput(file);
872
+ const updates = parseJsonInput(inputStr);
873
+ const updatesSchema = z.record(z.string(), z.unknown());
874
+ const validatedUpdates = updatesSchema.parse(updates);
875
+ const server = await getServerConnection(config.library, config);
876
+ if (server) {
877
+ await updateViaServer(identifier, validatedUpdates, options.uuid ?? false, server);
878
+ } else {
879
+ await updateViaLibrary(
880
+ identifier,
881
+ validatedUpdates,
882
+ options.uuid ?? false,
883
+ config.library
884
+ );
885
+ }
886
+ process.stderr.write(`Updated reference: [${identifier}]
887
+ `);
888
+ process.exit(0);
889
+ } catch (error) {
890
+ handleUpdateError(error);
891
+ }
892
+ }
893
+ function registerUpdateCommand(program) {
894
+ program.command("update").description("Update fields of an existing reference").argument("<identifier>", "Citation key or UUID").argument("[file]", "JSON file with updates (or use stdin)").option("--uuid", "Interpret identifier as UUID").action(async (identifier, file, options) => {
895
+ await handleUpdateAction(identifier, file, options, program);
896
+ });
897
+ }
898
+ function registerServerCommand(program) {
899
+ const serverCmd = program.command("server").description("Manage HTTP server for library access");
900
+ serverCmd.command("start").description("Start HTTP server").option("--port <port>", "Specify port number").option("-d, --daemon", "Run in background").action(async (options) => {
901
+ try {
902
+ const globalOpts = program.opts();
903
+ const config = await loadConfigWithOverrides({ ...globalOpts, ...options });
904
+ const portfilePath = getPortfilePath();
905
+ const startOptions = {
906
+ library: config.library,
907
+ portfilePath,
908
+ daemon: options.daemon,
909
+ ...options.port && { port: Number.parseInt(options.port, 10) }
910
+ };
911
+ await serverStart(startOptions);
912
+ process.exit(0);
913
+ } catch (error) {
914
+ const message = error instanceof Error ? error.message : String(error);
915
+ if (message.includes("already running") || message.includes("conflict")) {
916
+ process.stderr.write(`Error: ${message}
917
+ `);
918
+ process.exit(1);
919
+ }
920
+ process.stderr.write(`Error: ${message}
921
+ `);
922
+ process.exit(4);
923
+ }
924
+ });
925
+ serverCmd.command("stop").description("Stop running server").action(async () => {
926
+ try {
927
+ const portfilePath = getPortfilePath();
928
+ await serverStop(portfilePath);
929
+ process.stderr.write("Server stopped.\n");
930
+ process.exit(0);
931
+ } catch (error) {
932
+ const message = error instanceof Error ? error.message : String(error);
933
+ if (message.includes("not running")) {
934
+ process.stderr.write(`Error: ${message}
935
+ `);
936
+ process.exit(1);
937
+ }
938
+ process.stderr.write(`Error: ${message}
939
+ `);
940
+ process.exit(4);
941
+ }
942
+ });
943
+ serverCmd.command("status").description("Check server status").action(async () => {
944
+ try {
945
+ const portfilePath = getPortfilePath();
946
+ const status = await serverStatus(portfilePath);
947
+ if (status) {
948
+ process.stdout.write(
949
+ `Server is running
950
+ Port: ${status.port}
951
+ PID: ${status.pid}
952
+ Library: ${status.library}
953
+ `
954
+ );
955
+ process.exit(0);
956
+ } else {
957
+ process.stdout.write("Server not running\n");
958
+ process.exit(1);
959
+ }
960
+ } catch (error) {
961
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
962
+ `);
963
+ process.exit(4);
964
+ }
965
+ });
966
+ }
967
+ async function main(argv) {
968
+ const program = createProgram();
969
+ process.on("SIGINT", () => {
970
+ process.exit(130);
971
+ });
972
+ process.on("SIGTERM", () => {
973
+ process.exit(0);
974
+ });
975
+ await program.parseAsync(argv);
976
+ }
977
+ export {
978
+ createProgram,
979
+ main
980
+ };
981
+ //# sourceMappingURL=cli.js.map