@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.
- package/README.md +70 -3
- package/dist/chunks/{action-menu-CTtINmWd.js → action-menu-BN2HHFPR.js} +2 -2
- package/dist/chunks/{action-menu-CTtINmWd.js.map → action-menu-BN2HHFPR.js.map} +1 -1
- package/dist/chunks/{file-watcher-D7oyc-9z.js → file-watcher-DdhXSm1l.js} +3 -3
- package/dist/chunks/file-watcher-DdhXSm1l.js.map +1 -0
- package/dist/chunks/{index-_7NEUoS7.js → index-DmMZCOno.js} +316 -41
- package/dist/chunks/index-DmMZCOno.js.map +1 -0
- package/dist/chunks/{loader-CLCZRS4m.js → loader-C-bdImuO.js} +2 -2
- package/dist/chunks/{loader-CLCZRS4m.js.map → loader-C-bdImuO.js.map} +1 -1
- package/dist/cli/commands/add.d.ts +4 -0
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/remove.d.ts +7 -16
- package/dist/cli/commands/remove.d.ts.map +1 -1
- package/dist/cli/commands/update.d.ts +40 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli.js +473 -151
- package/dist/cli.js.map +1 -1
- package/dist/core/library.d.ts.map +1 -1
- package/dist/features/duplicate/types.d.ts +1 -1
- package/dist/features/duplicate/types.d.ts.map +1 -1
- package/dist/features/import/detector.d.ts +1 -1
- package/dist/features/import/detector.d.ts.map +1 -1
- package/dist/features/import/fetcher.d.ts +6 -0
- package/dist/features/import/fetcher.d.ts.map +1 -1
- package/dist/features/import/importer.d.ts +3 -1
- package/dist/features/import/importer.d.ts.map +1 -1
- package/dist/features/import/parser.d.ts +16 -0
- package/dist/features/import/parser.d.ts.map +1 -1
- package/dist/features/operations/add.d.ts +4 -0
- package/dist/features/operations/add.d.ts.map +1 -1
- package/dist/features/operations/index.d.ts +2 -0
- package/dist/features/operations/index.d.ts.map +1 -1
- package/dist/features/operations/json-output.d.ts +93 -0
- package/dist/features/operations/json-output.d.ts.map +1 -0
- package/dist/features/operations/remove.d.ts +11 -0
- package/dist/features/operations/remove.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/server.js +2 -2
- package/package.json +1 -1
- package/dist/chunks/file-watcher-D7oyc-9z.js.map +0 -1
- 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-
|
|
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-
|
|
11
|
-
import { o as openWithSystemApp, l as loadConfig } from "./chunks/loader-
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
21560
|
-
}
|
|
21561
|
-
|
|
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 (
|
|
21570
|
-
|
|
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
|
|
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-
|
|
21695
|
-
const { search } = await import("./chunks/file-watcher-
|
|
21696
|
-
const { tokenize } = await import("./chunks/file-watcher-
|
|
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,
|
|
21808
|
-
|
|
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
|
-
|
|
22457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22477
|
-
|
|
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
|
-
|
|
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(
|
|
22484
|
-
|
|
22485
|
-
|
|
22486
|
-
|
|
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
|
-
${
|
|
22491
|
-
}
|
|
22492
|
-
|
|
22493
|
-
|
|
22494
|
-
|
|
22495
|
-
|
|
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 (
|
|
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.
|
|
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
|
|
22827
|
+
const refToRemove = await context.library.find(identifier, { idType: useUuid ? "uuid" : "id" });
|
|
22523
22828
|
if (!refToRemove) {
|
|
22524
|
-
|
|
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
|
-
|
|
22531
|
-
|
|
22532
|
-
process.stderr.write(`Error: ${
|
|
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
|
|
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
|
-
|
|
22550
|
-
|
|
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
|
|
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
|
-
|
|
22605
|
-
|
|
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
|
-
|
|
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
|
}
|