@ncukondo/reference-manager 0.8.0 → 0.9.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 (42) hide show
  1. package/README.md +70 -3
  2. package/dist/chunks/{action-menu-CTtINmWd.js → action-menu-BN2HHFPR.js} +2 -2
  3. package/dist/chunks/{action-menu-CTtINmWd.js.map → action-menu-BN2HHFPR.js.map} +1 -1
  4. package/dist/chunks/{file-watcher-D7oyc-9z.js → file-watcher-DdhXSm1l.js} +3 -3
  5. package/dist/chunks/file-watcher-DdhXSm1l.js.map +1 -0
  6. package/dist/chunks/{index-_7NEUoS7.js → index-DmMZCOno.js} +316 -41
  7. package/dist/chunks/index-DmMZCOno.js.map +1 -0
  8. package/dist/chunks/{loader-CLCZRS4m.js → loader-C-bdImuO.js} +2 -2
  9. package/dist/chunks/{loader-CLCZRS4m.js.map → loader-C-bdImuO.js.map} +1 -1
  10. package/dist/cli/commands/add.d.ts +4 -0
  11. package/dist/cli/commands/add.d.ts.map +1 -1
  12. package/dist/cli/commands/remove.d.ts +7 -16
  13. package/dist/cli/commands/remove.d.ts.map +1 -1
  14. package/dist/cli/commands/update.d.ts +40 -0
  15. package/dist/cli/commands/update.d.ts.map +1 -1
  16. package/dist/cli/index.d.ts.map +1 -1
  17. package/dist/cli.js +473 -151
  18. package/dist/cli.js.map +1 -1
  19. package/dist/core/library.d.ts.map +1 -1
  20. package/dist/features/duplicate/types.d.ts +1 -1
  21. package/dist/features/duplicate/types.d.ts.map +1 -1
  22. package/dist/features/import/detector.d.ts +1 -1
  23. package/dist/features/import/detector.d.ts.map +1 -1
  24. package/dist/features/import/fetcher.d.ts +6 -0
  25. package/dist/features/import/fetcher.d.ts.map +1 -1
  26. package/dist/features/import/importer.d.ts +3 -1
  27. package/dist/features/import/importer.d.ts.map +1 -1
  28. package/dist/features/import/parser.d.ts +16 -0
  29. package/dist/features/import/parser.d.ts.map +1 -1
  30. package/dist/features/operations/add.d.ts +4 -0
  31. package/dist/features/operations/add.d.ts.map +1 -1
  32. package/dist/features/operations/index.d.ts +2 -0
  33. package/dist/features/operations/index.d.ts.map +1 -1
  34. package/dist/features/operations/json-output.d.ts +93 -0
  35. package/dist/features/operations/json-output.d.ts.map +1 -0
  36. package/dist/features/operations/remove.d.ts +11 -0
  37. package/dist/features/operations/remove.d.ts.map +1 -1
  38. package/dist/index.js +3 -3
  39. package/dist/server.js +2 -2
  40. package/package.json +1 -1
  41. package/dist/chunks/file-watcher-D7oyc-9z.js.map +0 -1
  42. package/dist/chunks/index-_7NEUoS7.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,25 +1,127 @@
1
1
  import { Command } from "commander";
2
2
  import { ZodOptional as ZodOptional$2, z } from "zod";
3
- import { p as pickDefined, q as sortOrderSchema, r as paginationOptionsSchema, L as Library, F as FileWatcher, u as sortFieldSchema, v as searchSortFieldSchema } from "./chunks/file-watcher-D7oyc-9z.js";
3
+ import { p as pickDefined, q as sortOrderSchema, r as paginationOptionsSchema, L as Library, F as FileWatcher, u as sortFieldSchema, v as searchSortFieldSchema } from "./chunks/file-watcher-DdhXSm1l.js";
4
4
  import { promises, existsSync, mkdtempSync, writeFileSync, readFileSync } from "node:fs";
5
5
  import * as os from "node:os";
6
6
  import { tmpdir } from "node:os";
7
7
  import * as path from "node:path";
8
8
  import { join, extname } from "node:path";
9
9
  import { mkdir, unlink, rename, copyFile, rm, readFile } from "node:fs/promises";
10
- import { u as updateReference, B as BUILTIN_STYLES, s as startServerWithFileWatcher } from "./chunks/index-_7NEUoS7.js";
11
- import { o as openWithSystemApp, l as loadConfig } from "./chunks/loader-CLCZRS4m.js";
10
+ import { u as updateReference, B as BUILTIN_STYLES, s as startServerWithFileWatcher, g as getFulltextAttachmentTypes } from "./chunks/index-DmMZCOno.js";
11
+ import { o as openWithSystemApp, l as loadConfig } from "./chunks/loader-C-bdImuO.js";
12
12
  import process$1, { stdin, stdout } from "node:process";
13
13
  import { spawn } from "node:child_process";
14
14
  import { serve } from "@hono/node-server";
15
15
  const name = "@ncukondo/reference-manager";
16
- const version$1 = "0.8.0";
16
+ const version$1 = "0.9.0";
17
17
  const description$1 = "A local reference management tool using CSL-JSON as the single source of truth";
18
18
  const packageJson = {
19
19
  name,
20
20
  version: version$1,
21
21
  description: description$1
22
22
  };
23
+ function formatAddJsonOutput(result, options) {
24
+ const { full = false, sources = /* @__PURE__ */ new Map(), items: items2 = /* @__PURE__ */ new Map() } = options;
25
+ const added = result.added.map((item) => {
26
+ const output = {
27
+ source: sources.get(item.id) ?? "",
28
+ id: item.id,
29
+ uuid: item.uuid,
30
+ title: item.title
31
+ };
32
+ if (item.idChanged && item.originalId) {
33
+ output.idChanged = true;
34
+ output.originalId = item.originalId;
35
+ }
36
+ if (full) {
37
+ const cslItem = items2.get(item.id);
38
+ if (cslItem) {
39
+ output.item = cslItem;
40
+ }
41
+ }
42
+ return output;
43
+ });
44
+ const skipped = result.skipped.map((item) => ({
45
+ source: item.source,
46
+ reason: "duplicate",
47
+ existingId: item.existingId,
48
+ duplicateType: item.duplicateType
49
+ }));
50
+ const failed = result.failed.map((item) => ({
51
+ source: item.source,
52
+ reason: item.reason,
53
+ error: item.error
54
+ }));
55
+ return {
56
+ summary: {
57
+ total: added.length + skipped.length + failed.length,
58
+ added: added.length,
59
+ skipped: skipped.length,
60
+ failed: failed.length
61
+ },
62
+ added,
63
+ skipped,
64
+ failed
65
+ };
66
+ }
67
+ function formatRemoveJsonOutput(result, id2, options) {
68
+ const { full = false } = options;
69
+ if (!result.removed || !result.removedItem) {
70
+ return {
71
+ success: false,
72
+ id: id2,
73
+ error: `Reference not found: ${id2}`
74
+ };
75
+ }
76
+ const uuid2 = result.removedItem.custom?.uuid;
77
+ const output = {
78
+ success: true,
79
+ id: id2,
80
+ ...uuid2 && { uuid: uuid2 },
81
+ title: typeof result.removedItem.title === "string" ? result.removedItem.title : ""
82
+ };
83
+ if (full) {
84
+ output.item = result.removedItem;
85
+ }
86
+ return output;
87
+ }
88
+ function formatUpdateJsonOutput(result, originalId, options) {
89
+ const { full = false, before } = options;
90
+ if (!result.updated || !result.item) {
91
+ if (result.idCollision) {
92
+ return {
93
+ success: false,
94
+ id: originalId,
95
+ error: "ID collision: target ID already exists"
96
+ };
97
+ }
98
+ return {
99
+ success: false,
100
+ id: originalId,
101
+ error: `Reference not found: ${originalId}`
102
+ };
103
+ }
104
+ const item = result.item;
105
+ const uuid2 = item.custom?.uuid;
106
+ const finalId = result.idChanged && result.newId ? result.newId : item.id ?? originalId;
107
+ const output = {
108
+ success: true,
109
+ id: finalId,
110
+ ...uuid2 && { uuid: uuid2 },
111
+ title: typeof item.title === "string" ? item.title : ""
112
+ };
113
+ if (result.idChanged && result.newId) {
114
+ output.idChanged = true;
115
+ output.previousId = originalId;
116
+ }
117
+ if (full) {
118
+ if (before) {
119
+ output.before = before;
120
+ }
121
+ output.after = item;
122
+ }
123
+ return output;
124
+ }
23
125
  function getPortfilePath() {
24
126
  const tmpDir = os.tmpdir();
25
127
  return path.join(tmpDir, "reference-manager", "server.port");
@@ -21022,19 +21124,19 @@ class OperationsLibrary {
21022
21124
  }
21023
21125
  // High-level operations
21024
21126
  async search(options) {
21025
- const { searchReferences } = await import("./chunks/index-_7NEUoS7.js").then((n) => n.e);
21127
+ const { searchReferences } = await import("./chunks/index-DmMZCOno.js").then((n) => n.e);
21026
21128
  return searchReferences(this.library, options);
21027
21129
  }
21028
21130
  async list(options) {
21029
- const { listReferences } = await import("./chunks/index-_7NEUoS7.js").then((n) => n.l);
21131
+ const { listReferences } = await import("./chunks/index-DmMZCOno.js").then((n) => n.l);
21030
21132
  return listReferences(this.library, options ?? {});
21031
21133
  }
21032
21134
  async cite(options) {
21033
- const { citeReferences } = await import("./chunks/index-_7NEUoS7.js").then((n) => n.d);
21135
+ const { citeReferences } = await import("./chunks/index-DmMZCOno.js").then((n) => n.d);
21034
21136
  return citeReferences(this.library, options);
21035
21137
  }
21036
21138
  async import(inputs, options) {
21037
- const { addReferences } = await import("./chunks/index-_7NEUoS7.js").then((n) => n.b);
21139
+ const { addReferences } = await import("./chunks/index-DmMZCOno.js").then((n) => n.b);
21038
21140
  return addReferences(inputs, this.library, options ?? {});
21039
21141
  }
21040
21142
  }
@@ -21547,7 +21649,16 @@ async function mcpStart(options) {
21547
21649
  return result;
21548
21650
  }
21549
21651
  async function executeRemove(options, context) {
21550
- const { identifier, idType = "id" } = options;
21652
+ const { identifier, idType = "id", fulltextDirectory, deleteFulltext = false } = options;
21653
+ if (context.mode === "local" && deleteFulltext && fulltextDirectory) {
21654
+ const { removeReference } = await import("./chunks/index-DmMZCOno.js").then((n) => n.r);
21655
+ return removeReference(context.library, {
21656
+ identifier,
21657
+ idType,
21658
+ fulltextDirectory,
21659
+ deleteFulltext
21660
+ });
21661
+ }
21551
21662
  return context.library.remove(identifier, { idType });
21552
21663
  }
21553
21664
  function formatRemoveOutput(result, identifier) {
@@ -21555,46 +21666,24 @@ function formatRemoveOutput(result, identifier) {
21555
21666
  return `Reference not found: ${identifier}`;
21556
21667
  }
21557
21668
  const item = result.removedItem;
21669
+ let output = "";
21558
21670
  if (item) {
21559
- return `Removed: [${item.id}] ${item.title || "(no title)"}`;
21560
- }
21561
- return `Removed reference: ${identifier}`;
21562
- }
21563
- function getFulltextAttachmentTypes(item) {
21564
- const types2 = [];
21565
- const fulltext = item.custom?.fulltext;
21566
- if (fulltext?.pdf) {
21567
- types2.push("pdf");
21671
+ output = `Removed: [${item.id}] ${item.title || "(no title)"}`;
21672
+ } else {
21673
+ output = `Removed reference: ${identifier}`;
21568
21674
  }
21569
- if (fulltext?.markdown) {
21570
- types2.push("markdown");
21675
+ if (result.deletedFulltextTypes && result.deletedFulltextTypes.length > 0) {
21676
+ const typeLabels = result.deletedFulltextTypes.map((t) => t === "pdf" ? "PDF" : "Markdown");
21677
+ output += `
21678
+ Deleted fulltext files: ${typeLabels.join(" and ")}`;
21571
21679
  }
21572
- return types2;
21680
+ return output;
21573
21681
  }
21574
21682
  function formatFulltextWarning(types2) {
21575
21683
  const typeLabels = types2.map((t) => t === "pdf" ? "PDF" : "Markdown");
21576
21684
  const fileTypes = typeLabels.join(" and ");
21577
21685
  return `Warning: This reference has fulltext files attached (${fileTypes}). Use --force to also delete the fulltext files.`;
21578
21686
  }
21579
- async function deleteFulltextFiles(item, fulltextDirectory) {
21580
- const fulltext = item.custom?.fulltext;
21581
- if (!fulltext) {
21582
- return;
21583
- }
21584
- const filesToDelete = [];
21585
- if (fulltext.pdf) {
21586
- filesToDelete.push(join(fulltextDirectory, fulltext.pdf));
21587
- }
21588
- if (fulltext.markdown) {
21589
- filesToDelete.push(join(fulltextDirectory, fulltext.markdown));
21590
- }
21591
- for (const filePath of filesToDelete) {
21592
- try {
21593
- await unlink(filePath);
21594
- } catch {
21595
- }
21596
- }
21597
- }
21598
21687
  const VALID_SEARCH_SORT_FIELDS = /* @__PURE__ */ new Set([
21599
21688
  "created",
21600
21689
  "updated",
@@ -21691,9 +21780,9 @@ async function executeInteractiveSearch(options, context, config2) {
21691
21780
  validateInteractiveOptions(options);
21692
21781
  const { checkTTY } = await import("./chunks/tty-CDBIQraQ.js");
21693
21782
  const { runSearchPrompt } = await import("./chunks/search-prompt-RtHDJFgL.js");
21694
- const { runActionMenu } = await import("./chunks/action-menu-CTtINmWd.js");
21695
- const { search } = await import("./chunks/file-watcher-D7oyc-9z.js").then((n) => n.y);
21696
- const { tokenize } = await import("./chunks/file-watcher-D7oyc-9z.js").then((n) => n.x);
21783
+ const { runActionMenu } = await import("./chunks/action-menu-BN2HHFPR.js");
21784
+ const { search } = await import("./chunks/file-watcher-DdhXSm1l.js").then((n) => n.y);
21785
+ const { tokenize } = await import("./chunks/file-watcher-DdhXSm1l.js").then((n) => n.x);
21697
21786
  checkTTY();
21698
21787
  const allReferences = await context.library.getAll();
21699
21788
  const searchFn = (query) => {
@@ -21803,9 +21892,181 @@ async function serverStatus(portfilePath) {
21803
21892
  }
21804
21893
  return result;
21805
21894
  }
21895
+ function parseSetOption(input) {
21896
+ const match = input.match(/^([a-zA-Z][a-zA-Z0-9_.]*)([\+\-]?=)(.*)$/);
21897
+ if (!match) {
21898
+ throw new Error(
21899
+ `Invalid --set syntax: "${input}". Use field=value, field+=value, or field-=value`
21900
+ );
21901
+ }
21902
+ return {
21903
+ field: match[1],
21904
+ operator: match[2],
21905
+ value: match[3]
21906
+ };
21907
+ }
21908
+ const PROTECTED_FIELDS = /* @__PURE__ */ new Set([
21909
+ "custom.uuid",
21910
+ "custom.created_at",
21911
+ "custom.timestamp",
21912
+ "custom.fulltext"
21913
+ ]);
21914
+ const STRING_FIELDS = /* @__PURE__ */ new Set([
21915
+ "title",
21916
+ "abstract",
21917
+ "type",
21918
+ "DOI",
21919
+ "PMID",
21920
+ "PMCID",
21921
+ "ISBN",
21922
+ "ISSN",
21923
+ "URL",
21924
+ "publisher",
21925
+ "publisher-place",
21926
+ "page",
21927
+ "volume",
21928
+ "issue",
21929
+ "container-title",
21930
+ "note",
21931
+ "id"
21932
+ ]);
21933
+ const ARRAY_FIELDS = /* @__PURE__ */ new Set(["custom.tags", "custom.additional_urls", "keyword"]);
21934
+ const NAME_FIELDS = /* @__PURE__ */ new Set(["author", "editor"]);
21935
+ const DATE_RAW_FIELDS = /* @__PURE__ */ new Set(["issued.raw", "accessed.raw"]);
21936
+ function parseAuthorValue(value) {
21937
+ return value.split(";").map((author) => {
21938
+ const trimmed = author.trim();
21939
+ const parts = trimmed.split(",").map((p) => p.trim());
21940
+ if (parts.length >= 2 && parts[0] && parts[1]) {
21941
+ return { family: parts[0], given: parts[1] };
21942
+ }
21943
+ return { family: trimmed };
21944
+ });
21945
+ }
21946
+ function applyArrayOperator(operator, value) {
21947
+ if (operator === "+=") return { $add: value };
21948
+ if (operator === "-=") return { $remove: value };
21949
+ return value.split(",").map((v) => v.trim());
21950
+ }
21951
+ function handleCustomArrayField(result, field, operator, value) {
21952
+ const child = field.split(".")[1];
21953
+ if (!child) return;
21954
+ if (!result.custom) result.custom = {};
21955
+ result.custom[child] = applyArrayOperator(operator, value);
21956
+ }
21957
+ function applySingleOperation(result, op) {
21958
+ const { field, operator, value } = op;
21959
+ if (PROTECTED_FIELDS.has(field)) {
21960
+ throw new Error(`Cannot set protected field: ${field}`);
21961
+ }
21962
+ if (STRING_FIELDS.has(field)) {
21963
+ result[field] = value === "" ? void 0 : value;
21964
+ return;
21965
+ }
21966
+ if (ARRAY_FIELDS.has(field)) {
21967
+ if (field.startsWith("custom.")) {
21968
+ handleCustomArrayField(result, field, operator, value);
21969
+ } else {
21970
+ result[field] = applyArrayOperator(operator, value);
21971
+ }
21972
+ return;
21973
+ }
21974
+ if (NAME_FIELDS.has(field)) {
21975
+ result[field] = parseAuthorValue(value);
21976
+ return;
21977
+ }
21978
+ if (DATE_RAW_FIELDS.has(field)) {
21979
+ const dateField = field.split(".")[0];
21980
+ if (dateField) result[dateField] = { raw: value };
21981
+ return;
21982
+ }
21983
+ throw new Error(`Unsupported field: ${field}`);
21984
+ }
21985
+ function applySetOperations(operations) {
21986
+ const result = {};
21987
+ for (const op of operations) {
21988
+ applySingleOperation(result, op);
21989
+ }
21990
+ return result;
21991
+ }
21992
+ function hasArrayOperations(updates) {
21993
+ for (const value of Object.values(updates)) {
21994
+ if (typeof value !== "object" || value === null) continue;
21995
+ const obj = value;
21996
+ if ("$add" in obj || "$remove" in obj) return true;
21997
+ if (hasArrayOperations(obj)) return true;
21998
+ }
21999
+ return false;
22000
+ }
22001
+ function resolveArrayOperations(updates, existingItem) {
22002
+ const result = {};
22003
+ for (const [key, value] of Object.entries(updates)) {
22004
+ if (key === "custom" && typeof value === "object" && value !== null) {
22005
+ result.custom = resolveCustomArrayOperations(
22006
+ value,
22007
+ existingItem.custom
22008
+ );
22009
+ } else if (isArrayOperation(value)) {
22010
+ result[key] = resolveTopLevelArrayOperation(
22011
+ value,
22012
+ existingItem[key]
22013
+ );
22014
+ } else {
22015
+ result[key] = value;
22016
+ }
22017
+ }
22018
+ return result;
22019
+ }
22020
+ function isArrayOperation(value) {
22021
+ if (typeof value !== "object" || value === null) return false;
22022
+ const obj = value;
22023
+ return "$add" in obj || "$remove" in obj;
22024
+ }
22025
+ function resolveCustomArrayOperations(customUpdates, existingCustom) {
22026
+ const result = {};
22027
+ for (const [customKey, customValue] of Object.entries(customUpdates)) {
22028
+ if (isArrayOperation(customValue)) {
22029
+ const op = customValue;
22030
+ const existingArray = existingCustom?.[customKey] ?? [];
22031
+ result[customKey] = applyArrayOperation(existingArray, op);
22032
+ } else {
22033
+ result[customKey] = customValue;
22034
+ }
22035
+ }
22036
+ return result;
22037
+ }
22038
+ function resolveTopLevelArrayOperation(op, existingArray) {
22039
+ return applyArrayOperation(existingArray ?? [], op);
22040
+ }
22041
+ function applyArrayOperation(existingArray, op) {
22042
+ const newArray = [...existingArray];
22043
+ if ("$add" in op && op.$add && !newArray.includes(op.$add)) {
22044
+ newArray.push(op.$add);
22045
+ }
22046
+ if ("$remove" in op && op.$remove) {
22047
+ const idx = newArray.indexOf(op.$remove);
22048
+ if (idx >= 0) newArray.splice(idx, 1);
22049
+ }
22050
+ return newArray;
22051
+ }
21806
22052
  async function executeUpdate(options, context) {
21807
- const { identifier, updates, idType = "id" } = options;
21808
- return context.library.update(identifier, updates, { idType });
22053
+ const { identifier, idType = "id" } = options;
22054
+ let { updates } = options;
22055
+ if (hasArrayOperations(updates)) {
22056
+ const existingItem = await context.library.find(identifier, { idType });
22057
+ if (!existingItem) {
22058
+ return { updated: false };
22059
+ }
22060
+ updates = resolveArrayOperations(
22061
+ updates,
22062
+ existingItem
22063
+ );
22064
+ }
22065
+ const result = await context.library.update(identifier, updates, { idType });
22066
+ if (result.updated) {
22067
+ await context.library.save();
22068
+ }
22069
+ return result;
21809
22070
  }
21810
22071
  function formatUpdateOutput(result, identifier) {
21811
22072
  if (!result.updated) {
@@ -22420,7 +22681,53 @@ function registerSearchCommand(program) {
22420
22681
  await handleSearchAction(query ?? "", options, program);
22421
22682
  });
22422
22683
  }
22684
+ function buildAddOptions(inputs, options, config2, stdinContent) {
22685
+ const addOptions = {
22686
+ inputs,
22687
+ force: options.force ?? false
22688
+ };
22689
+ if (options.format !== void 0) {
22690
+ addOptions.format = options.format;
22691
+ }
22692
+ if (options.verbose !== void 0) {
22693
+ addOptions.verbose = options.verbose;
22694
+ }
22695
+ if (stdinContent?.trim()) {
22696
+ addOptions.stdinContent = stdinContent;
22697
+ }
22698
+ const pubmedConfig = {};
22699
+ if (config2.pubmed.email !== void 0) {
22700
+ pubmedConfig.email = config2.pubmed.email;
22701
+ }
22702
+ if (config2.pubmed.apiKey !== void 0) {
22703
+ pubmedConfig.apiKey = config2.pubmed.apiKey;
22704
+ }
22705
+ if (Object.keys(pubmedConfig).length > 0) {
22706
+ addOptions.pubmedConfig = pubmedConfig;
22707
+ }
22708
+ return addOptions;
22709
+ }
22710
+ async function outputAddResultJson(result, context, full) {
22711
+ const sources = /* @__PURE__ */ new Map();
22712
+ for (const item of result.added) {
22713
+ sources.set(item.id, "");
22714
+ }
22715
+ const items2 = /* @__PURE__ */ new Map();
22716
+ if (full) {
22717
+ for (const added of result.added) {
22718
+ const item = await context.library.find(added.id, { idType: "id" });
22719
+ if (item) {
22720
+ items2.set(added.id, item);
22721
+ }
22722
+ }
22723
+ }
22724
+ const options = { full, sources, items: items2 };
22725
+ const jsonOutput = formatAddJsonOutput(result, options);
22726
+ process.stdout.write(`${JSON.stringify(jsonOutput)}
22727
+ `);
22728
+ }
22423
22729
  async function handleAddAction(inputs, options, program) {
22730
+ const outputFormat = options.output ?? "text";
22424
22731
  try {
22425
22732
  const globalOpts = program.opts();
22426
22733
  const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
@@ -22429,38 +22736,25 @@ async function handleAddAction(inputs, options, program) {
22429
22736
  stdinContent = await readStdinContent();
22430
22737
  }
22431
22738
  const context = await createExecutionContext(config2, Library.load);
22432
- const addOptions = {
22433
- inputs,
22434
- force: options.force ?? false
22435
- };
22436
- if (options.format !== void 0) {
22437
- addOptions.format = options.format;
22438
- }
22439
- if (options.verbose !== void 0) {
22440
- addOptions.verbose = options.verbose;
22441
- }
22442
- if (stdinContent?.trim()) {
22443
- addOptions.stdinContent = stdinContent;
22444
- }
22445
- const pubmedConfig = {};
22446
- if (config2.pubmed.email !== void 0) {
22447
- pubmedConfig.email = config2.pubmed.email;
22448
- }
22449
- if (config2.pubmed.apiKey !== void 0) {
22450
- pubmedConfig.apiKey = config2.pubmed.apiKey;
22451
- }
22452
- if (Object.keys(pubmedConfig).length > 0) {
22453
- addOptions.pubmedConfig = pubmedConfig;
22454
- }
22739
+ const addOptions = buildAddOptions(inputs, options, config2, stdinContent);
22455
22740
  const result = await executeAdd(addOptions, context);
22456
- const output = formatAddOutput(result, options.verbose ?? false);
22457
- process.stderr.write(`${output}
22741
+ if (outputFormat === "json") {
22742
+ await outputAddResultJson(result, context, options.full ?? false);
22743
+ } else {
22744
+ const output = formatAddOutput(result, options.verbose ?? false);
22745
+ process.stderr.write(`${output}
22458
22746
  `);
22747
+ }
22459
22748
  process.exit(getExitCode(result));
22460
22749
  } catch (error) {
22461
22750
  const message = error instanceof Error ? error.message : String(error);
22462
- process.stderr.write(`Error: ${message}
22751
+ if (outputFormat === "json") {
22752
+ process.stdout.write(`${JSON.stringify({ success: false, error: message })}
22463
22753
  `);
22754
+ } else {
22755
+ process.stderr.write(`Error: ${message}
22756
+ `);
22757
+ }
22464
22758
  process.exit(1);
22465
22759
  }
22466
22760
  }
@@ -22469,102 +22763,99 @@ function registerAddCommand(program) {
22469
22763
  "--format <format>",
22470
22764
  "Explicit input format: json|bibtex|ris|pmid|doi|isbn|auto",
22471
22765
  "auto"
22472
- ).option("--verbose", "Show detailed error information").action(async (inputs, options) => {
22766
+ ).option("--verbose", "Show detailed error information").option("-o, --output <format>", "Output format: json|text", "text").option("--full", "Include full CSL-JSON data in JSON output").action(async (inputs, options) => {
22473
22767
  await handleAddAction(inputs, options, program);
22474
22768
  });
22475
22769
  }
22476
- async function findReferenceToRemove(identifier, useUuid, context) {
22477
- return context.library.find(identifier, { idType: useUuid ? "uuid" : "id" });
22770
+ function outputRemoveNotFoundAndExit(identifier, outputFormat) {
22771
+ if (outputFormat === "json") {
22772
+ const jsonOutput = formatRemoveJsonOutput({ removed: false }, identifier, {});
22773
+ process.stdout.write(`${JSON.stringify(jsonOutput)}
22774
+ `);
22775
+ } else {
22776
+ process.stderr.write(`Error: Reference not found: ${identifier}
22777
+ `);
22778
+ }
22779
+ process.exit(1);
22478
22780
  }
22479
- async function confirmRemoval(refToRemove, force, fulltextWarning) {
22781
+ function outputRemoveResult(result, identifier, outputFormat, full) {
22782
+ if (outputFormat === "json") {
22783
+ const jsonOutput = formatRemoveJsonOutput(result, identifier, { full });
22784
+ process.stdout.write(`${JSON.stringify(jsonOutput)}
22785
+ `);
22786
+ } else {
22787
+ const output = formatRemoveOutput(result, identifier);
22788
+ process.stderr.write(`${output}
22789
+ `);
22790
+ }
22791
+ }
22792
+ async function confirmRemoveIfNeeded(item, hasFulltext, force) {
22480
22793
  if (force || !isTTY()) {
22481
22794
  return true;
22482
22795
  }
22483
- const authors = Array.isArray(refToRemove.author) ? refToRemove.author.map((a) => `${a.family || ""}, ${a.given?.[0] || ""}.`).join("; ") : "(no authors)";
22484
- let confirmMsg = `Remove reference [${refToRemove.id}]?
22485
- Title: ${refToRemove.title || "(no title)"}
22486
- Authors: ${authors}`;
22487
- if (fulltextWarning) {
22488
- confirmMsg += `
22796
+ const authors = Array.isArray(item.author) ? item.author.map((a) => `${a.family || ""}, ${a.given?.[0] || ""}.`).join("; ") : "(no authors)";
22797
+ const fulltextTypes = hasFulltext ? getFulltextAttachmentTypes(item) : [];
22798
+ const warning = hasFulltext ? formatFulltextWarning(fulltextTypes) : "";
22799
+ const warningPart = warning ? `
22489
22800
 
22490
- ${fulltextWarning}`;
22491
- }
22492
- confirmMsg += "\nContinue?";
22493
- return await readConfirmation(confirmMsg);
22494
- }
22495
- function handleRemoveError(error) {
22801
+ ${warning}` : "";
22802
+ const confirmMsg = `Remove reference [${item.id}]?
22803
+ Title: ${item.title || "(no title)"}
22804
+ Authors: ${authors}${warningPart}
22805
+ Continue?`;
22806
+ return readConfirmation(confirmMsg);
22807
+ }
22808
+ function handleRemoveError(error, identifier, outputFormat) {
22496
22809
  const message = error instanceof Error ? error.message : String(error);
22497
- if (message.includes("not found")) {
22810
+ if (outputFormat === "json") {
22811
+ process.stdout.write(`${JSON.stringify({ success: false, id: identifier, error: message })}
22812
+ `);
22813
+ } else {
22498
22814
  process.stderr.write(`Error: ${message}
22499
22815
  `);
22500
- process.exit(1);
22501
22816
  }
22502
- process.stderr.write(`Error: ${message}
22503
- `);
22504
- process.exit(4);
22505
- }
22506
- function formatTypeLabels(types2) {
22507
- return types2.map((t) => t === "pdf" ? "PDF" : "Markdown").join(" and ");
22508
- }
22509
- async function handleFulltextDeletion(item, fulltextDirectory, types2, output) {
22510
- await deleteFulltextFiles(item, fulltextDirectory);
22511
- const typeLabels = formatTypeLabels(types2);
22512
- process.stderr.write(`${output}
22513
- `);
22514
- process.stderr.write(`Deleted fulltext files: ${typeLabels}
22515
- `);
22817
+ process.exit(message.includes("not found") ? 1 : 4);
22516
22818
  }
22517
22819
  async function handleRemoveAction(identifier, options, program) {
22820
+ const outputFormat = options.output ?? "text";
22821
+ const useUuid = options.uuid ?? false;
22822
+ const force = options.force ?? false;
22518
22823
  try {
22519
22824
  const globalOpts = program.opts();
22520
22825
  const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
22521
22826
  const context = await createExecutionContext(config2, Library.load);
22522
- const refToRemove = await findReferenceToRemove(identifier, options.uuid ?? false, context);
22827
+ const refToRemove = await context.library.find(identifier, { idType: useUuid ? "uuid" : "id" });
22523
22828
  if (!refToRemove) {
22524
- process.stderr.write(`Error: Reference not found: ${identifier}
22525
- `);
22526
- process.exit(1);
22829
+ outputRemoveNotFoundAndExit(identifier, outputFormat);
22527
22830
  }
22528
22831
  const fulltextTypes = getFulltextAttachmentTypes(refToRemove);
22529
22832
  const hasFulltext = fulltextTypes.length > 0;
22530
- if (hasFulltext && !isTTY() && !options.force) {
22531
- const warning = formatFulltextWarning(fulltextTypes);
22532
- process.stderr.write(`Error: ${warning}
22833
+ const requiresForce = hasFulltext && !isTTY() && !force;
22834
+ if (requiresForce) {
22835
+ process.stderr.write(`Error: ${formatFulltextWarning(fulltextTypes)}
22533
22836
  `);
22534
22837
  process.exit(1);
22535
22838
  }
22536
- const fulltextWarning = hasFulltext && !options.force ? formatFulltextWarning(fulltextTypes) : void 0;
22537
- const confirmed = await confirmRemoval(refToRemove, options.force ?? false, fulltextWarning);
22839
+ const confirmed = await confirmRemoveIfNeeded(refToRemove, hasFulltext, force);
22538
22840
  if (!confirmed) {
22539
22841
  process.stderr.write("Cancelled.\n");
22540
22842
  process.exit(2);
22541
22843
  }
22542
22844
  const removeOptions = {
22543
- identifier
22845
+ identifier,
22846
+ idType: useUuid ? "uuid" : "id",
22847
+ fulltextDirectory: config2.fulltext.directory,
22848
+ deleteFulltext: force && hasFulltext
22544
22849
  };
22545
- if (options.uuid) {
22546
- removeOptions.idType = "uuid";
22547
- }
22548
22850
  const result = await executeRemove(removeOptions, context);
22549
- const output = formatRemoveOutput(result, identifier);
22550
- if (!result.removed) {
22551
- process.stderr.write(`${output}
22552
- `);
22553
- process.exit(1);
22554
- }
22555
- if (hasFulltext && options.force) {
22556
- await handleFulltextDeletion(refToRemove, config2.fulltext.directory, fulltextTypes, output);
22557
- } else {
22558
- process.stderr.write(`${output}
22559
- `);
22560
- }
22561
- process.exit(0);
22851
+ outputRemoveResult(result, identifier, outputFormat, options.full ?? false);
22852
+ process.exit(result.removed ? 0 : 1);
22562
22853
  } catch (error) {
22563
- handleRemoveError(error);
22854
+ handleRemoveError(error, identifier, outputFormat);
22564
22855
  }
22565
22856
  }
22566
22857
  function registerRemoveCommand(program) {
22567
- 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) => {
22858
+ 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").option("-o, --output <format>", "Output format: json|text", "text").option("--full", "Include full CSL-JSON data in JSON output").action(async (identifier, options) => {
22568
22859
  await handleRemoveAction(identifier, options, program);
22569
22860
  });
22570
22861
  }
@@ -22584,39 +22875,70 @@ function handleUpdateError(error) {
22584
22875
  `);
22585
22876
  process.exit(4);
22586
22877
  }
22878
+ function parseUpdateInput(setOptions, file) {
22879
+ if (setOptions && setOptions.length > 0 && file) {
22880
+ throw new Error("Cannot use --set with a file argument. Use one or the other.");
22881
+ }
22882
+ if (setOptions && setOptions.length > 0) {
22883
+ const operations = setOptions.map((s) => parseSetOption(s));
22884
+ return applySetOperations(operations);
22885
+ }
22886
+ return readJsonInput(file).then((inputStr) => {
22887
+ const updates = parseJsonInput(inputStr);
22888
+ const updatesSchema = z.record(z.string(), z.unknown());
22889
+ return updatesSchema.parse(updates);
22890
+ });
22891
+ }
22892
+ function outputUpdateResult(result, identifier, outputFormat, full, beforeItem) {
22893
+ if (outputFormat === "json") {
22894
+ const jsonOptions = {
22895
+ full,
22896
+ ...beforeItem && { before: beforeItem }
22897
+ };
22898
+ const jsonOutput = formatUpdateJsonOutput(result, identifier, jsonOptions);
22899
+ process.stdout.write(`${JSON.stringify(jsonOutput)}
22900
+ `);
22901
+ } else {
22902
+ const output = formatUpdateOutput(result, identifier);
22903
+ process.stderr.write(`${output}
22904
+ `);
22905
+ }
22906
+ }
22907
+ function handleUpdateErrorWithFormat(error, identifier, outputFormat) {
22908
+ const message = error instanceof Error ? error.message : String(error);
22909
+ if (outputFormat === "json") {
22910
+ process.stdout.write(`${JSON.stringify({ success: false, id: identifier, error: message })}
22911
+ `);
22912
+ process.exit(message.includes("not found") || message.includes("validation") ? 1 : 4);
22913
+ }
22914
+ handleUpdateError(error);
22915
+ }
22587
22916
  async function handleUpdateAction(identifier, file, options, program) {
22917
+ const outputFormat = options.output ?? "text";
22588
22918
  try {
22589
22919
  const globalOpts = program.opts();
22590
22920
  const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
22591
- const inputStr = await readJsonInput(file);
22592
- const updates = parseJsonInput(inputStr);
22593
- const updatesSchema = z.record(z.string(), z.unknown());
22594
- const validatedUpdates = updatesSchema.parse(updates);
22921
+ const validatedUpdates = await parseUpdateInput(options.set, file);
22595
22922
  const context = await createExecutionContext(config2, Library.load);
22923
+ const idType = options.uuid ? "uuid" : "id";
22924
+ const beforeItem = options.full ? await context.library.find(identifier, { idType }) : void 0;
22596
22925
  const updateOptions = {
22597
22926
  identifier,
22598
- updates: validatedUpdates
22927
+ updates: validatedUpdates,
22928
+ ...options.uuid && { idType: "uuid" }
22599
22929
  };
22600
- if (options.uuid) {
22601
- updateOptions.idType = "uuid";
22602
- }
22603
22930
  const result = await executeUpdate(updateOptions, context);
22604
- const output = formatUpdateOutput(result, identifier);
22605
- if (result.updated) {
22606
- process.stderr.write(`${output}
22607
- `);
22608
- process.exit(0);
22609
- } else {
22610
- process.stderr.write(`${output}
22611
- `);
22612
- process.exit(1);
22613
- }
22931
+ outputUpdateResult(result, identifier, outputFormat, options.full ?? false, beforeItem);
22932
+ process.exit(result.updated ? 0 : 1);
22614
22933
  } catch (error) {
22615
- handleUpdateError(error);
22934
+ handleUpdateErrorWithFormat(error, identifier, outputFormat);
22616
22935
  }
22617
22936
  }
22937
+ function collectSetOption(value, previous) {
22938
+ return previous.concat([value]);
22939
+ }
22618
22940
  function registerUpdateCommand(program) {
22619
- 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) => {
22941
+ 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").option("--set <field=value>", "Set field value (repeatable)", collectSetOption, []).option("-o, --output <format>", "Output format: json|text", "text").option("--full", "Include full CSL-JSON data in JSON output").action(async (identifier, file, options) => {
22620
22942
  await handleUpdateAction(identifier, file, options, program);
22621
22943
  });
22622
22944
  }