@ncukondo/reference-manager 0.7.1 → 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 +71 -4
- 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-BItrdVWG.js → loader-C-bdImuO.js} +44 -7
- package/dist/chunks/loader-C-bdImuO.js.map +1 -0
- package/dist/cli/commands/add.d.ts +4 -0
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/fulltext.d.ts +20 -3
- package/dist/cli/commands/fulltext.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 +582 -152
- 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/fulltext/index.d.ts +1 -0
- package/dist/features/operations/fulltext/index.d.ts.map +1 -1
- package/dist/features/operations/fulltext/open.d.ts +36 -0
- package/dist/features/operations/fulltext/open.d.ts.map +1 -0
- 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 +5 -3
- package/dist/index.js.map +1 -1
- package/dist/server.js +2 -2
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/opener.d.ts +13 -0
- package/dist/utils/opener.d.ts.map +1 -0
- 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/chunks/loader-BItrdVWG.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-
|
|
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";
|
|
11
12
|
import process$1, { stdin, stdout } from "node:process";
|
|
12
|
-
import { l as loadConfig } from "./chunks/loader-BItrdVWG.js";
|
|
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");
|
|
@@ -607,6 +709,54 @@ async function fulltextDetach(library, options) {
|
|
|
607
709
|
return handleDetachError(error);
|
|
608
710
|
}
|
|
609
711
|
}
|
|
712
|
+
function getFulltextPath(item, type2, fulltextDirectory) {
|
|
713
|
+
const fulltext = item.custom?.fulltext;
|
|
714
|
+
if (!fulltext) return void 0;
|
|
715
|
+
const filename = type2 === "pdf" ? fulltext.pdf : fulltext.markdown;
|
|
716
|
+
if (!filename) return void 0;
|
|
717
|
+
return join(fulltextDirectory, filename);
|
|
718
|
+
}
|
|
719
|
+
function determineTypeToOpen(item) {
|
|
720
|
+
const fulltext = item.custom?.fulltext;
|
|
721
|
+
if (!fulltext) return void 0;
|
|
722
|
+
if (fulltext.pdf) return "pdf";
|
|
723
|
+
if (fulltext.markdown) return "markdown";
|
|
724
|
+
return void 0;
|
|
725
|
+
}
|
|
726
|
+
async function fulltextOpen(library, options) {
|
|
727
|
+
const { identifier, type: type2, idType = "id", fulltextDirectory } = options;
|
|
728
|
+
const item = await library.find(identifier, { idType });
|
|
729
|
+
if (!item) {
|
|
730
|
+
return { success: false, error: `Reference not found: ${identifier}` };
|
|
731
|
+
}
|
|
732
|
+
const typeToOpen = type2 ?? determineTypeToOpen(item);
|
|
733
|
+
if (!typeToOpen) {
|
|
734
|
+
return { success: false, error: `No fulltext attached to reference: ${identifier}` };
|
|
735
|
+
}
|
|
736
|
+
const filePath = getFulltextPath(item, typeToOpen, fulltextDirectory);
|
|
737
|
+
if (!filePath) {
|
|
738
|
+
return { success: false, error: `No ${typeToOpen} attached to reference: ${identifier}` };
|
|
739
|
+
}
|
|
740
|
+
if (!existsSync(filePath)) {
|
|
741
|
+
return {
|
|
742
|
+
success: false,
|
|
743
|
+
error: `Fulltext file not found: ${filePath} (metadata exists but file is missing)`
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
await openWithSystemApp(filePath);
|
|
748
|
+
return {
|
|
749
|
+
success: true,
|
|
750
|
+
openedType: typeToOpen,
|
|
751
|
+
openedPath: filePath
|
|
752
|
+
};
|
|
753
|
+
} catch (_error) {
|
|
754
|
+
return {
|
|
755
|
+
success: false,
|
|
756
|
+
error: `Failed to open file: ${filePath}`
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
}
|
|
610
760
|
async function executeFulltextAttach(options, context) {
|
|
611
761
|
const operationOptions = {
|
|
612
762
|
identifier: options.identifier,
|
|
@@ -640,6 +790,15 @@ async function executeFulltextDetach(options, context) {
|
|
|
640
790
|
};
|
|
641
791
|
return fulltextDetach(context.library, operationOptions);
|
|
642
792
|
}
|
|
793
|
+
async function executeFulltextOpen(options, context) {
|
|
794
|
+
const operationOptions = {
|
|
795
|
+
identifier: options.identifier,
|
|
796
|
+
type: options.type,
|
|
797
|
+
idType: options.idType,
|
|
798
|
+
fulltextDirectory: options.fulltextDirectory
|
|
799
|
+
};
|
|
800
|
+
return fulltextOpen(context.library, operationOptions);
|
|
801
|
+
}
|
|
643
802
|
function formatFulltextAttachOutput(result) {
|
|
644
803
|
if (result.requiresConfirmation) {
|
|
645
804
|
return `File already attached: ${result.existingFile}
|
|
@@ -686,6 +845,12 @@ function formatFulltextDetachOutput(result) {
|
|
|
686
845
|
}
|
|
687
846
|
return lines.join("\n");
|
|
688
847
|
}
|
|
848
|
+
function formatFulltextOpenOutput(result) {
|
|
849
|
+
if (!result.success) {
|
|
850
|
+
return `Error: ${result.error}`;
|
|
851
|
+
}
|
|
852
|
+
return `Opened ${result.openedType}: ${result.openedPath}`;
|
|
853
|
+
}
|
|
689
854
|
function getFulltextExitCode(result) {
|
|
690
855
|
return result.success ? 0 : 1;
|
|
691
856
|
}
|
|
@@ -20959,19 +21124,19 @@ class OperationsLibrary {
|
|
|
20959
21124
|
}
|
|
20960
21125
|
// High-level operations
|
|
20961
21126
|
async search(options) {
|
|
20962
|
-
const { searchReferences } = await import("./chunks/index-
|
|
21127
|
+
const { searchReferences } = await import("./chunks/index-DmMZCOno.js").then((n) => n.e);
|
|
20963
21128
|
return searchReferences(this.library, options);
|
|
20964
21129
|
}
|
|
20965
21130
|
async list(options) {
|
|
20966
|
-
const { listReferences } = await import("./chunks/index-
|
|
21131
|
+
const { listReferences } = await import("./chunks/index-DmMZCOno.js").then((n) => n.l);
|
|
20967
21132
|
return listReferences(this.library, options ?? {});
|
|
20968
21133
|
}
|
|
20969
21134
|
async cite(options) {
|
|
20970
|
-
const { citeReferences } = await import("./chunks/index-
|
|
21135
|
+
const { citeReferences } = await import("./chunks/index-DmMZCOno.js").then((n) => n.d);
|
|
20971
21136
|
return citeReferences(this.library, options);
|
|
20972
21137
|
}
|
|
20973
21138
|
async import(inputs, options) {
|
|
20974
|
-
const { addReferences } = await import("./chunks/index-
|
|
21139
|
+
const { addReferences } = await import("./chunks/index-DmMZCOno.js").then((n) => n.b);
|
|
20975
21140
|
return addReferences(inputs, this.library, options ?? {});
|
|
20976
21141
|
}
|
|
20977
21142
|
}
|
|
@@ -21484,7 +21649,16 @@ async function mcpStart(options) {
|
|
|
21484
21649
|
return result;
|
|
21485
21650
|
}
|
|
21486
21651
|
async function executeRemove(options, context) {
|
|
21487
|
-
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
|
+
}
|
|
21488
21662
|
return context.library.remove(identifier, { idType });
|
|
21489
21663
|
}
|
|
21490
21664
|
function formatRemoveOutput(result, identifier) {
|
|
@@ -21492,46 +21666,24 @@ function formatRemoveOutput(result, identifier) {
|
|
|
21492
21666
|
return `Reference not found: ${identifier}`;
|
|
21493
21667
|
}
|
|
21494
21668
|
const item = result.removedItem;
|
|
21669
|
+
let output = "";
|
|
21495
21670
|
if (item) {
|
|
21496
|
-
|
|
21497
|
-
}
|
|
21498
|
-
|
|
21499
|
-
}
|
|
21500
|
-
function getFulltextAttachmentTypes(item) {
|
|
21501
|
-
const types2 = [];
|
|
21502
|
-
const fulltext = item.custom?.fulltext;
|
|
21503
|
-
if (fulltext?.pdf) {
|
|
21504
|
-
types2.push("pdf");
|
|
21671
|
+
output = `Removed: [${item.id}] ${item.title || "(no title)"}`;
|
|
21672
|
+
} else {
|
|
21673
|
+
output = `Removed reference: ${identifier}`;
|
|
21505
21674
|
}
|
|
21506
|
-
if (
|
|
21507
|
-
|
|
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 ")}`;
|
|
21508
21679
|
}
|
|
21509
|
-
return
|
|
21680
|
+
return output;
|
|
21510
21681
|
}
|
|
21511
21682
|
function formatFulltextWarning(types2) {
|
|
21512
21683
|
const typeLabels = types2.map((t) => t === "pdf" ? "PDF" : "Markdown");
|
|
21513
21684
|
const fileTypes = typeLabels.join(" and ");
|
|
21514
21685
|
return `Warning: This reference has fulltext files attached (${fileTypes}). Use --force to also delete the fulltext files.`;
|
|
21515
21686
|
}
|
|
21516
|
-
async function deleteFulltextFiles(item, fulltextDirectory) {
|
|
21517
|
-
const fulltext = item.custom?.fulltext;
|
|
21518
|
-
if (!fulltext) {
|
|
21519
|
-
return;
|
|
21520
|
-
}
|
|
21521
|
-
const filesToDelete = [];
|
|
21522
|
-
if (fulltext.pdf) {
|
|
21523
|
-
filesToDelete.push(join(fulltextDirectory, fulltext.pdf));
|
|
21524
|
-
}
|
|
21525
|
-
if (fulltext.markdown) {
|
|
21526
|
-
filesToDelete.push(join(fulltextDirectory, fulltext.markdown));
|
|
21527
|
-
}
|
|
21528
|
-
for (const filePath of filesToDelete) {
|
|
21529
|
-
try {
|
|
21530
|
-
await unlink(filePath);
|
|
21531
|
-
} catch {
|
|
21532
|
-
}
|
|
21533
|
-
}
|
|
21534
|
-
}
|
|
21535
21687
|
const VALID_SEARCH_SORT_FIELDS = /* @__PURE__ */ new Set([
|
|
21536
21688
|
"created",
|
|
21537
21689
|
"updated",
|
|
@@ -21628,9 +21780,9 @@ async function executeInteractiveSearch(options, context, config2) {
|
|
|
21628
21780
|
validateInteractiveOptions(options);
|
|
21629
21781
|
const { checkTTY } = await import("./chunks/tty-CDBIQraQ.js");
|
|
21630
21782
|
const { runSearchPrompt } = await import("./chunks/search-prompt-RtHDJFgL.js");
|
|
21631
|
-
const { runActionMenu } = await import("./chunks/action-menu-
|
|
21632
|
-
const { search } = await import("./chunks/file-watcher-
|
|
21633
|
-
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);
|
|
21634
21786
|
checkTTY();
|
|
21635
21787
|
const allReferences = await context.library.getAll();
|
|
21636
21788
|
const searchFn = (query) => {
|
|
@@ -21740,9 +21892,181 @@ async function serverStatus(portfilePath) {
|
|
|
21740
21892
|
}
|
|
21741
21893
|
return result;
|
|
21742
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
|
+
}
|
|
21743
22052
|
async function executeUpdate(options, context) {
|
|
21744
|
-
const { identifier,
|
|
21745
|
-
|
|
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;
|
|
21746
22070
|
}
|
|
21747
22071
|
function formatUpdateOutput(result, identifier) {
|
|
21748
22072
|
if (!result.updated) {
|
|
@@ -22005,7 +22329,7 @@ const OPTION_VALUES = {
|
|
|
22005
22329
|
"--log-level": LOG_LEVELS
|
|
22006
22330
|
};
|
|
22007
22331
|
const ID_COMPLETION_COMMANDS = /* @__PURE__ */ new Set(["cite", "remove", "update"]);
|
|
22008
|
-
const ID_COMPLETION_FULLTEXT_SUBCOMMANDS = /* @__PURE__ */ new Set(["attach", "get", "detach"]);
|
|
22332
|
+
const ID_COMPLETION_FULLTEXT_SUBCOMMANDS = /* @__PURE__ */ new Set(["attach", "get", "detach", "open"]);
|
|
22009
22333
|
function toCompletionItems(values) {
|
|
22010
22334
|
return values.map((name2) => ({ name: name2 }));
|
|
22011
22335
|
}
|
|
@@ -22357,7 +22681,53 @@ function registerSearchCommand(program) {
|
|
|
22357
22681
|
await handleSearchAction(query ?? "", options, program);
|
|
22358
22682
|
});
|
|
22359
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
|
+
}
|
|
22360
22729
|
async function handleAddAction(inputs, options, program) {
|
|
22730
|
+
const outputFormat = options.output ?? "text";
|
|
22361
22731
|
try {
|
|
22362
22732
|
const globalOpts = program.opts();
|
|
22363
22733
|
const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
@@ -22366,38 +22736,25 @@ async function handleAddAction(inputs, options, program) {
|
|
|
22366
22736
|
stdinContent = await readStdinContent();
|
|
22367
22737
|
}
|
|
22368
22738
|
const context = await createExecutionContext(config2, Library.load);
|
|
22369
|
-
const addOptions =
|
|
22370
|
-
inputs,
|
|
22371
|
-
force: options.force ?? false
|
|
22372
|
-
};
|
|
22373
|
-
if (options.format !== void 0) {
|
|
22374
|
-
addOptions.format = options.format;
|
|
22375
|
-
}
|
|
22376
|
-
if (options.verbose !== void 0) {
|
|
22377
|
-
addOptions.verbose = options.verbose;
|
|
22378
|
-
}
|
|
22379
|
-
if (stdinContent?.trim()) {
|
|
22380
|
-
addOptions.stdinContent = stdinContent;
|
|
22381
|
-
}
|
|
22382
|
-
const pubmedConfig = {};
|
|
22383
|
-
if (config2.pubmed.email !== void 0) {
|
|
22384
|
-
pubmedConfig.email = config2.pubmed.email;
|
|
22385
|
-
}
|
|
22386
|
-
if (config2.pubmed.apiKey !== void 0) {
|
|
22387
|
-
pubmedConfig.apiKey = config2.pubmed.apiKey;
|
|
22388
|
-
}
|
|
22389
|
-
if (Object.keys(pubmedConfig).length > 0) {
|
|
22390
|
-
addOptions.pubmedConfig = pubmedConfig;
|
|
22391
|
-
}
|
|
22739
|
+
const addOptions = buildAddOptions(inputs, options, config2, stdinContent);
|
|
22392
22740
|
const result = await executeAdd(addOptions, context);
|
|
22393
|
-
|
|
22394
|
-
|
|
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}
|
|
22395
22746
|
`);
|
|
22747
|
+
}
|
|
22396
22748
|
process.exit(getExitCode(result));
|
|
22397
22749
|
} catch (error) {
|
|
22398
22750
|
const message = error instanceof Error ? error.message : String(error);
|
|
22399
|
-
|
|
22751
|
+
if (outputFormat === "json") {
|
|
22752
|
+
process.stdout.write(`${JSON.stringify({ success: false, error: message })}
|
|
22400
22753
|
`);
|
|
22754
|
+
} else {
|
|
22755
|
+
process.stderr.write(`Error: ${message}
|
|
22756
|
+
`);
|
|
22757
|
+
}
|
|
22401
22758
|
process.exit(1);
|
|
22402
22759
|
}
|
|
22403
22760
|
}
|
|
@@ -22406,102 +22763,99 @@ function registerAddCommand(program) {
|
|
|
22406
22763
|
"--format <format>",
|
|
22407
22764
|
"Explicit input format: json|bibtex|ris|pmid|doi|isbn|auto",
|
|
22408
22765
|
"auto"
|
|
22409
|
-
).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) => {
|
|
22410
22767
|
await handleAddAction(inputs, options, program);
|
|
22411
22768
|
});
|
|
22412
22769
|
}
|
|
22413
|
-
|
|
22414
|
-
|
|
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);
|
|
22415
22780
|
}
|
|
22416
|
-
|
|
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) {
|
|
22417
22793
|
if (force || !isTTY()) {
|
|
22418
22794
|
return true;
|
|
22419
22795
|
}
|
|
22420
|
-
const authors = Array.isArray(
|
|
22421
|
-
|
|
22422
|
-
|
|
22423
|
-
|
|
22424
|
-
if (fulltextWarning) {
|
|
22425
|
-
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 ? `
|
|
22426
22800
|
|
|
22427
|
-
${
|
|
22428
|
-
}
|
|
22429
|
-
|
|
22430
|
-
|
|
22431
|
-
|
|
22432
|
-
|
|
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) {
|
|
22433
22809
|
const message = error instanceof Error ? error.message : String(error);
|
|
22434
|
-
if (
|
|
22810
|
+
if (outputFormat === "json") {
|
|
22811
|
+
process.stdout.write(`${JSON.stringify({ success: false, id: identifier, error: message })}
|
|
22812
|
+
`);
|
|
22813
|
+
} else {
|
|
22435
22814
|
process.stderr.write(`Error: ${message}
|
|
22436
22815
|
`);
|
|
22437
|
-
process.exit(1);
|
|
22438
22816
|
}
|
|
22439
|
-
process.
|
|
22440
|
-
`);
|
|
22441
|
-
process.exit(4);
|
|
22442
|
-
}
|
|
22443
|
-
function formatTypeLabels(types2) {
|
|
22444
|
-
return types2.map((t) => t === "pdf" ? "PDF" : "Markdown").join(" and ");
|
|
22445
|
-
}
|
|
22446
|
-
async function handleFulltextDeletion(item, fulltextDirectory, types2, output) {
|
|
22447
|
-
await deleteFulltextFiles(item, fulltextDirectory);
|
|
22448
|
-
const typeLabels = formatTypeLabels(types2);
|
|
22449
|
-
process.stderr.write(`${output}
|
|
22450
|
-
`);
|
|
22451
|
-
process.stderr.write(`Deleted fulltext files: ${typeLabels}
|
|
22452
|
-
`);
|
|
22817
|
+
process.exit(message.includes("not found") ? 1 : 4);
|
|
22453
22818
|
}
|
|
22454
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;
|
|
22455
22823
|
try {
|
|
22456
22824
|
const globalOpts = program.opts();
|
|
22457
22825
|
const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
22458
22826
|
const context = await createExecutionContext(config2, Library.load);
|
|
22459
|
-
const refToRemove = await
|
|
22827
|
+
const refToRemove = await context.library.find(identifier, { idType: useUuid ? "uuid" : "id" });
|
|
22460
22828
|
if (!refToRemove) {
|
|
22461
|
-
|
|
22462
|
-
`);
|
|
22463
|
-
process.exit(1);
|
|
22829
|
+
outputRemoveNotFoundAndExit(identifier, outputFormat);
|
|
22464
22830
|
}
|
|
22465
22831
|
const fulltextTypes = getFulltextAttachmentTypes(refToRemove);
|
|
22466
22832
|
const hasFulltext = fulltextTypes.length > 0;
|
|
22467
|
-
|
|
22468
|
-
|
|
22469
|
-
process.stderr.write(`Error: ${
|
|
22833
|
+
const requiresForce = hasFulltext && !isTTY() && !force;
|
|
22834
|
+
if (requiresForce) {
|
|
22835
|
+
process.stderr.write(`Error: ${formatFulltextWarning(fulltextTypes)}
|
|
22470
22836
|
`);
|
|
22471
22837
|
process.exit(1);
|
|
22472
22838
|
}
|
|
22473
|
-
const
|
|
22474
|
-
const confirmed = await confirmRemoval(refToRemove, options.force ?? false, fulltextWarning);
|
|
22839
|
+
const confirmed = await confirmRemoveIfNeeded(refToRemove, hasFulltext, force);
|
|
22475
22840
|
if (!confirmed) {
|
|
22476
22841
|
process.stderr.write("Cancelled.\n");
|
|
22477
22842
|
process.exit(2);
|
|
22478
22843
|
}
|
|
22479
22844
|
const removeOptions = {
|
|
22480
|
-
identifier
|
|
22845
|
+
identifier,
|
|
22846
|
+
idType: useUuid ? "uuid" : "id",
|
|
22847
|
+
fulltextDirectory: config2.fulltext.directory,
|
|
22848
|
+
deleteFulltext: force && hasFulltext
|
|
22481
22849
|
};
|
|
22482
|
-
if (options.uuid) {
|
|
22483
|
-
removeOptions.idType = "uuid";
|
|
22484
|
-
}
|
|
22485
22850
|
const result = await executeRemove(removeOptions, context);
|
|
22486
|
-
|
|
22487
|
-
|
|
22488
|
-
process.stderr.write(`${output}
|
|
22489
|
-
`);
|
|
22490
|
-
process.exit(1);
|
|
22491
|
-
}
|
|
22492
|
-
if (hasFulltext && options.force) {
|
|
22493
|
-
await handleFulltextDeletion(refToRemove, config2.fulltext.directory, fulltextTypes, output);
|
|
22494
|
-
} else {
|
|
22495
|
-
process.stderr.write(`${output}
|
|
22496
|
-
`);
|
|
22497
|
-
}
|
|
22498
|
-
process.exit(0);
|
|
22851
|
+
outputRemoveResult(result, identifier, outputFormat, options.full ?? false);
|
|
22852
|
+
process.exit(result.removed ? 0 : 1);
|
|
22499
22853
|
} catch (error) {
|
|
22500
|
-
handleRemoveError(error);
|
|
22854
|
+
handleRemoveError(error, identifier, outputFormat);
|
|
22501
22855
|
}
|
|
22502
22856
|
}
|
|
22503
22857
|
function registerRemoveCommand(program) {
|
|
22504
|
-
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) => {
|
|
22505
22859
|
await handleRemoveAction(identifier, options, program);
|
|
22506
22860
|
});
|
|
22507
22861
|
}
|
|
@@ -22521,39 +22875,70 @@ function handleUpdateError(error) {
|
|
|
22521
22875
|
`);
|
|
22522
22876
|
process.exit(4);
|
|
22523
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
|
+
}
|
|
22524
22916
|
async function handleUpdateAction(identifier, file, options, program) {
|
|
22917
|
+
const outputFormat = options.output ?? "text";
|
|
22525
22918
|
try {
|
|
22526
22919
|
const globalOpts = program.opts();
|
|
22527
22920
|
const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
22528
|
-
const
|
|
22529
|
-
const updates = parseJsonInput(inputStr);
|
|
22530
|
-
const updatesSchema = z.record(z.string(), z.unknown());
|
|
22531
|
-
const validatedUpdates = updatesSchema.parse(updates);
|
|
22921
|
+
const validatedUpdates = await parseUpdateInput(options.set, file);
|
|
22532
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;
|
|
22533
22925
|
const updateOptions = {
|
|
22534
22926
|
identifier,
|
|
22535
|
-
updates: validatedUpdates
|
|
22927
|
+
updates: validatedUpdates,
|
|
22928
|
+
...options.uuid && { idType: "uuid" }
|
|
22536
22929
|
};
|
|
22537
|
-
if (options.uuid) {
|
|
22538
|
-
updateOptions.idType = "uuid";
|
|
22539
|
-
}
|
|
22540
22930
|
const result = await executeUpdate(updateOptions, context);
|
|
22541
|
-
|
|
22542
|
-
|
|
22543
|
-
process.stderr.write(`${output}
|
|
22544
|
-
`);
|
|
22545
|
-
process.exit(0);
|
|
22546
|
-
} else {
|
|
22547
|
-
process.stderr.write(`${output}
|
|
22548
|
-
`);
|
|
22549
|
-
process.exit(1);
|
|
22550
|
-
}
|
|
22931
|
+
outputUpdateResult(result, identifier, outputFormat, options.full ?? false, beforeItem);
|
|
22932
|
+
process.exit(result.updated ? 0 : 1);
|
|
22551
22933
|
} catch (error) {
|
|
22552
|
-
|
|
22934
|
+
handleUpdateErrorWithFormat(error, identifier, outputFormat);
|
|
22553
22935
|
}
|
|
22554
22936
|
}
|
|
22937
|
+
function collectSetOption(value, previous) {
|
|
22938
|
+
return previous.concat([value]);
|
|
22939
|
+
}
|
|
22555
22940
|
function registerUpdateCommand(program) {
|
|
22556
|
-
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) => {
|
|
22557
22942
|
await handleUpdateAction(identifier, file, options, program);
|
|
22558
22943
|
});
|
|
22559
22944
|
}
|
|
@@ -22787,6 +23172,48 @@ async function handleFulltextDetachAction(identifier, options, program) {
|
|
|
22787
23172
|
process.exit(4);
|
|
22788
23173
|
}
|
|
22789
23174
|
}
|
|
23175
|
+
async function handleFulltextOpenAction(identifierArg, options, program) {
|
|
23176
|
+
try {
|
|
23177
|
+
const config2 = await loadConfigWithOverrides(program.opts());
|
|
23178
|
+
let identifier;
|
|
23179
|
+
if (identifierArg) {
|
|
23180
|
+
identifier = identifierArg;
|
|
23181
|
+
} else {
|
|
23182
|
+
if (isTTY()) {
|
|
23183
|
+
process.stderr.write("Error: Identifier is required when running interactively.\n");
|
|
23184
|
+
process.exit(1);
|
|
23185
|
+
}
|
|
23186
|
+
const stdinId = (await readStdinContent()).split("\n")[0]?.trim() ?? "";
|
|
23187
|
+
if (!stdinId) {
|
|
23188
|
+
process.stderr.write("Error: No identifier provided from stdin.\n");
|
|
23189
|
+
process.exit(1);
|
|
23190
|
+
}
|
|
23191
|
+
identifier = stdinId;
|
|
23192
|
+
}
|
|
23193
|
+
const context = await createExecutionContext(config2, Library.load);
|
|
23194
|
+
const openOptions = {
|
|
23195
|
+
identifier,
|
|
23196
|
+
fulltextDirectory: config2.fulltext.directory,
|
|
23197
|
+
...options.pdf && { type: "pdf" },
|
|
23198
|
+
...options.markdown && { type: "markdown" },
|
|
23199
|
+
...options.uuid && { idType: "uuid" }
|
|
23200
|
+
};
|
|
23201
|
+
const result = await executeFulltextOpen(openOptions, context);
|
|
23202
|
+
const output = formatFulltextOpenOutput(result);
|
|
23203
|
+
if (result.success) {
|
|
23204
|
+
process.stderr.write(`${output}
|
|
23205
|
+
`);
|
|
23206
|
+
} else {
|
|
23207
|
+
process.stderr.write(`${output}
|
|
23208
|
+
`);
|
|
23209
|
+
}
|
|
23210
|
+
process.exit(getFulltextExitCode(result));
|
|
23211
|
+
} catch (error) {
|
|
23212
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
23213
|
+
`);
|
|
23214
|
+
process.exit(4);
|
|
23215
|
+
}
|
|
23216
|
+
}
|
|
22790
23217
|
function registerFulltextCommand(program) {
|
|
22791
23218
|
const fulltextCmd = program.command("fulltext").description("Manage full-text files attached to references");
|
|
22792
23219
|
fulltextCmd.command("attach").description("Attach a full-text file to a reference").argument("<identifier>", "Citation key or UUID").argument("[file-path]", "Path to the file to attach").option("--pdf [path]", "Attach as PDF (path optional if provided as argument)").option("--markdown [path]", "Attach as Markdown (path optional if provided as argument)").option("--move", "Move file instead of copy").option("-f, --force", "Overwrite existing attachment").option("--uuid", "Interpret identifier as UUID").action(async (identifier, filePath, options) => {
|
|
@@ -22798,6 +23225,9 @@ function registerFulltextCommand(program) {
|
|
|
22798
23225
|
fulltextCmd.command("detach").description("Detach full-text file from a reference").argument("<identifier>", "Citation key or UUID").option("--pdf", "Detach PDF only").option("--markdown", "Detach Markdown only").option("--delete", "Also delete the file from disk").option("-f, --force", "Skip confirmation for delete").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
|
|
22799
23226
|
await handleFulltextDetachAction(identifier, options, program);
|
|
22800
23227
|
});
|
|
23228
|
+
fulltextCmd.command("open").description("Open full-text file with system default application").argument("[identifier]", "Citation key or UUID (reads from stdin if not provided)").option("--pdf", "Open PDF file").option("--markdown", "Open Markdown file").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
|
|
23229
|
+
await handleFulltextOpenAction(identifier, options, program);
|
|
23230
|
+
});
|
|
22801
23231
|
}
|
|
22802
23232
|
async function main(argv) {
|
|
22803
23233
|
const program = createProgram();
|