@ncukondo/reference-manager 0.1.0 → 0.3.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 +40 -0
- package/dist/chunks/detector-DHztTaFY.js +619 -0
- package/dist/chunks/detector-DHztTaFY.js.map +1 -0
- package/dist/chunks/{detector-BF8Mcc72.js → loader-mQ25o6cV.js} +303 -664
- package/dist/chunks/loader-mQ25o6cV.js.map +1 -0
- package/dist/chunks/search-Be9vzUIH.js +29541 -0
- package/dist/chunks/search-Be9vzUIH.js.map +1 -0
- package/dist/cli/commands/add.d.ts +44 -16
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/cite.d.ts +49 -0
- package/dist/cli/commands/cite.d.ts.map +1 -0
- package/dist/cli/commands/fulltext.d.ts +101 -0
- package/dist/cli/commands/fulltext.d.ts.map +1 -0
- package/dist/cli/commands/index.d.ts +14 -10
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/list.d.ts +23 -6
- package/dist/cli/commands/list.d.ts.map +1 -1
- package/dist/cli/commands/remove.d.ts +47 -12
- package/dist/cli/commands/remove.d.ts.map +1 -1
- package/dist/cli/commands/search.d.ts +24 -7
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/commands/update.d.ts +26 -13
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/execution-context.d.ts +60 -0
- package/dist/cli/execution-context.d.ts.map +1 -0
- package/dist/cli/helpers.d.ts +18 -0
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/server-client.d.ts +73 -10
- package/dist/cli/server-client.d.ts.map +1 -1
- package/dist/cli.js +1200 -528
- package/dist/cli.js.map +1 -1
- package/dist/config/csl-styles.d.ts +83 -0
- package/dist/config/csl-styles.d.ts.map +1 -0
- package/dist/config/defaults.d.ts +10 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/schema.d.ts +84 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/core/csl-json/types.d.ts +15 -3
- package/dist/core/csl-json/types.d.ts.map +1 -1
- package/dist/core/library.d.ts +60 -0
- package/dist/core/library.d.ts.map +1 -1
- package/dist/features/format/bibtex.d.ts +6 -0
- package/dist/features/format/bibtex.d.ts.map +1 -0
- package/dist/features/format/citation-csl.d.ts +41 -0
- package/dist/features/format/citation-csl.d.ts.map +1 -0
- package/dist/features/format/citation-fallback.d.ts +24 -0
- package/dist/features/format/citation-fallback.d.ts.map +1 -0
- package/dist/features/format/index.d.ts +10 -0
- package/dist/features/format/index.d.ts.map +1 -0
- package/dist/features/format/json.d.ts +6 -0
- package/dist/features/format/json.d.ts.map +1 -0
- package/dist/features/format/pretty.d.ts +6 -0
- package/dist/features/format/pretty.d.ts.map +1 -0
- package/dist/features/fulltext/filename.d.ts +17 -0
- package/dist/features/fulltext/filename.d.ts.map +1 -0
- package/dist/features/fulltext/index.d.ts +7 -0
- package/dist/features/fulltext/index.d.ts.map +1 -0
- package/dist/features/fulltext/manager.d.ts +109 -0
- package/dist/features/fulltext/manager.d.ts.map +1 -0
- package/dist/features/fulltext/types.d.ts +12 -0
- package/dist/features/fulltext/types.d.ts.map +1 -0
- package/dist/features/import/cache.d.ts +37 -0
- package/dist/features/import/cache.d.ts.map +1 -0
- package/dist/features/import/detector.d.ts +42 -0
- package/dist/features/import/detector.d.ts.map +1 -0
- package/dist/features/import/fetcher.d.ts +49 -0
- package/dist/features/import/fetcher.d.ts.map +1 -0
- package/dist/features/import/importer.d.ts +61 -0
- package/dist/features/import/importer.d.ts.map +1 -0
- package/dist/features/import/index.d.ts +8 -0
- package/dist/features/import/index.d.ts.map +1 -0
- package/dist/features/import/normalizer.d.ts +15 -0
- package/dist/features/import/normalizer.d.ts.map +1 -0
- package/dist/features/import/parser.d.ts +33 -0
- package/dist/features/import/parser.d.ts.map +1 -0
- package/dist/features/import/rate-limiter.d.ts +45 -0
- package/dist/features/import/rate-limiter.d.ts.map +1 -0
- package/dist/features/operations/add.d.ts +65 -0
- package/dist/features/operations/add.d.ts.map +1 -0
- package/dist/features/operations/cite.d.ts +48 -0
- package/dist/features/operations/cite.d.ts.map +1 -0
- package/dist/features/operations/list.d.ts +28 -0
- package/dist/features/operations/list.d.ts.map +1 -0
- package/dist/features/operations/remove.d.ts +29 -0
- package/dist/features/operations/remove.d.ts.map +1 -0
- package/dist/features/operations/search.d.ts +30 -0
- package/dist/features/operations/search.d.ts.map +1 -0
- package/dist/features/operations/update.d.ts +39 -0
- package/dist/features/operations/update.d.ts.map +1 -0
- package/dist/index.js +18 -16
- package/dist/index.js.map +1 -1
- package/dist/server/index.d.ts +3 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/routes/add.d.ts +11 -0
- package/dist/server/routes/add.d.ts.map +1 -0
- package/dist/server/routes/cite.d.ts +9 -0
- package/dist/server/routes/cite.d.ts.map +1 -0
- package/dist/server/routes/list.d.ts +25 -0
- package/dist/server/routes/list.d.ts.map +1 -0
- package/dist/server/routes/references.d.ts.map +1 -1
- package/dist/server/routes/search.d.ts +26 -0
- package/dist/server/routes/search.d.ts.map +1 -0
- package/dist/server.js +215 -32
- package/dist/server.js.map +1 -1
- package/package.json +15 -4
- package/dist/chunks/detector-BF8Mcc72.js.map +0 -1
- package/dist/cli/output/bibtex.d.ts +0 -6
- package/dist/cli/output/bibtex.d.ts.map +0 -1
- package/dist/cli/output/index.d.ts +0 -7
- package/dist/cli/output/index.d.ts.map +0 -1
- package/dist/cli/output/json.d.ts +0 -6
- package/dist/cli/output/json.d.ts.map +0 -1
- package/dist/cli/output/pretty.d.ts +0 -6
- package/dist/cli/output/pretty.d.ts.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
4
|
-
import { promises, readFileSync } from "node:fs";
|
|
3
|
+
import { l as loadConfig, L as Library } from "./chunks/loader-mQ25o6cV.js";
|
|
4
|
+
import { promises, existsSync, mkdtempSync, writeFileSync, readFileSync } from "node:fs";
|
|
5
5
|
import * as os from "node:os";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
6
7
|
import * as path from "node:path";
|
|
7
|
-
import {
|
|
8
|
+
import { join, extname } from "node:path";
|
|
9
|
+
import { a as addReferences, c as citeReferences, u as updateReference, l as listReferences, r as removeReference, s as searchReferences } from "./chunks/search-Be9vzUIH.js";
|
|
10
|
+
import { mkdir, unlink, rename, copyFile, rm, readFile } from "node:fs/promises";
|
|
8
11
|
import { spawn } from "node:child_process";
|
|
12
|
+
import { stdin, stdout } from "node:process";
|
|
13
|
+
const version = "0.3.0";
|
|
14
|
+
const description = "A local reference management tool using CSL-JSON as the single source of truth";
|
|
15
|
+
const packageJson = {
|
|
16
|
+
version,
|
|
17
|
+
description
|
|
18
|
+
};
|
|
9
19
|
function getPortfilePath() {
|
|
10
20
|
const tmpDir = os.tmpdir();
|
|
11
21
|
return path.join(tmpDir, "reference-manager", "server.port");
|
|
@@ -67,192 +77,620 @@ function isProcessRunning(pid) {
|
|
|
67
77
|
return false;
|
|
68
78
|
}
|
|
69
79
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
function resolveIdCollision(baseId, existing) {
|
|
81
|
-
const existingIds = new Set(existing.map((item) => item.id));
|
|
82
|
-
if (!existingIds.has(baseId)) {
|
|
83
|
-
return { id: baseId, changed: false };
|
|
84
|
-
}
|
|
85
|
-
let index = 0;
|
|
86
|
-
let newId;
|
|
87
|
-
do {
|
|
88
|
-
const suffix = generateSuffix(index);
|
|
89
|
-
newId = `${baseId}${suffix}`;
|
|
90
|
-
index++;
|
|
91
|
-
} while (existingIds.has(newId));
|
|
92
|
-
return { id: newId, changed: true };
|
|
93
|
-
}
|
|
94
|
-
async function add(existing, newItem, options) {
|
|
95
|
-
if (!options.force) {
|
|
96
|
-
const duplicateResult = detectDuplicate(newItem, existing);
|
|
97
|
-
if (duplicateResult.matches.length > 0) {
|
|
98
|
-
return {
|
|
99
|
-
added: false,
|
|
100
|
-
item: newItem,
|
|
101
|
-
idChanged: false,
|
|
102
|
-
duplicate: duplicateResult.matches[0]
|
|
103
|
-
};
|
|
80
|
+
const MAX_ERROR_LENGTH = 80;
|
|
81
|
+
async function executeAdd(options, context) {
|
|
82
|
+
const { inputs, force, format, pubmedConfig, stdinContent } = options;
|
|
83
|
+
if (context.type === "server") {
|
|
84
|
+
const serverOptions = { force };
|
|
85
|
+
if (format !== void 0) {
|
|
86
|
+
serverOptions.format = format;
|
|
87
|
+
}
|
|
88
|
+
if (stdinContent !== void 0) {
|
|
89
|
+
serverOptions.stdinContent = stdinContent;
|
|
104
90
|
}
|
|
91
|
+
return context.client.addFromInputs(inputs, serverOptions);
|
|
105
92
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
93
|
+
const addOptions = { force };
|
|
94
|
+
if (format !== void 0) {
|
|
95
|
+
addOptions.format = format;
|
|
96
|
+
}
|
|
97
|
+
if (pubmedConfig !== void 0) {
|
|
98
|
+
addOptions.pubmedConfig = pubmedConfig;
|
|
99
|
+
}
|
|
100
|
+
if (stdinContent !== void 0) {
|
|
101
|
+
addOptions.stdinContent = stdinContent;
|
|
102
|
+
}
|
|
103
|
+
return addReferences(inputs, context.library, addOptions);
|
|
104
|
+
}
|
|
105
|
+
function formatAddedItem(item) {
|
|
106
|
+
const idPart = item.idChanged ? `${item.id} (was: ${item.originalId})` : item.id;
|
|
107
|
+
const title = item.title ?? "(no title)";
|
|
108
|
+
return ` - ${idPart}: "${title}"`;
|
|
109
|
+
}
|
|
110
|
+
function formatFailedItem(item, verbose) {
|
|
111
|
+
let error = item.error;
|
|
112
|
+
if (!verbose && error.length > MAX_ERROR_LENGTH) {
|
|
113
|
+
error = `${error.substring(0, MAX_ERROR_LENGTH - 3)}...`;
|
|
114
|
+
}
|
|
115
|
+
if (!verbose && error.includes("\n")) {
|
|
116
|
+
error = error.split("\n")[0] ?? error;
|
|
117
|
+
}
|
|
118
|
+
return ` - ${item.source}: ${error}`;
|
|
119
|
+
}
|
|
120
|
+
function formatSkippedItem(item) {
|
|
121
|
+
return ` - ${item.source}: matches existing '${item.existingId}'`;
|
|
122
|
+
}
|
|
123
|
+
function formatAddOutput(result, verbose) {
|
|
124
|
+
const lines = [];
|
|
125
|
+
if (result.added.length > 0) {
|
|
126
|
+
lines.push(`Added ${result.added.length} reference(s):`);
|
|
127
|
+
for (const item of result.added) {
|
|
128
|
+
lines.push(formatAddedItem(item));
|
|
129
|
+
}
|
|
130
|
+
} else if (result.failed.length === 0 && result.skipped.length === 0) {
|
|
131
|
+
lines.push("Added 0 reference(s).");
|
|
132
|
+
}
|
|
133
|
+
if (result.failed.length > 0) {
|
|
134
|
+
if (lines.length > 0) lines.push("");
|
|
135
|
+
lines.push(`Failed to add ${result.failed.length} item(s):`);
|
|
136
|
+
for (const item of result.failed) {
|
|
137
|
+
lines.push(formatFailedItem(item, verbose));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (result.skipped.length > 0) {
|
|
141
|
+
if (lines.length > 0) lines.push("");
|
|
142
|
+
lines.push(`Skipped ${result.skipped.length} duplicate(s):`);
|
|
143
|
+
for (const item of result.skipped) {
|
|
144
|
+
lines.push(formatSkippedItem(item));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return lines.join("\n");
|
|
148
|
+
}
|
|
149
|
+
function getExitCode(result) {
|
|
150
|
+
if (result.added.length > 0) {
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
if (result.failed.length > 0) {
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
async function validateOptions$2(options) {
|
|
159
|
+
if (options.format && !["text", "html", "rtf"].includes(options.format)) {
|
|
160
|
+
throw new Error(`Invalid format '${options.format}'. Must be one of: text, html, rtf`);
|
|
161
|
+
}
|
|
162
|
+
if (options.cslFile) {
|
|
163
|
+
const fs = await import("node:fs");
|
|
164
|
+
if (!fs.existsSync(options.cslFile)) {
|
|
165
|
+
throw new Error(`CSL file '${options.cslFile}' not found`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function buildServerCiteOptions(options) {
|
|
112
170
|
return {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
171
|
+
identifiers: options.identifiers,
|
|
172
|
+
...options.uuid !== void 0 && { byUuid: options.uuid },
|
|
173
|
+
...options.inText !== void 0 && { inText: options.inText },
|
|
174
|
+
...options.style !== void 0 && { style: options.style },
|
|
175
|
+
...options.cslFile !== void 0 && { cslFile: options.cslFile },
|
|
176
|
+
...options.locale !== void 0 && { locale: options.locale },
|
|
177
|
+
...options.format !== void 0 && {
|
|
178
|
+
format: options.format === "rtf" ? "text" : options.format
|
|
179
|
+
}
|
|
117
180
|
};
|
|
118
181
|
}
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
thesis: "phdthesis",
|
|
129
|
-
report: "techreport",
|
|
130
|
-
webpage: "misc"
|
|
182
|
+
function buildOperationCiteOptions(options) {
|
|
183
|
+
return {
|
|
184
|
+
identifiers: options.identifiers,
|
|
185
|
+
...options.uuid !== void 0 && { byUuid: options.uuid },
|
|
186
|
+
...options.style !== void 0 && { style: options.style },
|
|
187
|
+
...options.cslFile !== void 0 && { cslFile: options.cslFile },
|
|
188
|
+
...options.locale !== void 0 && { locale: options.locale },
|
|
189
|
+
...options.format !== void 0 && { format: options.format },
|
|
190
|
+
...options.inText !== void 0 && { inText: options.inText }
|
|
131
191
|
};
|
|
132
|
-
return typeMap[cslType] || "misc";
|
|
133
192
|
}
|
|
134
|
-
function
|
|
135
|
-
|
|
136
|
-
|
|
193
|
+
async function executeCite(options, context) {
|
|
194
|
+
await validateOptions$2(options);
|
|
195
|
+
if (context.type === "server") {
|
|
196
|
+
return context.client.cite(buildServerCiteOptions(options));
|
|
137
197
|
}
|
|
138
|
-
|
|
139
|
-
const given = author.given || "";
|
|
140
|
-
return given ? `${family}, ${given}` : family;
|
|
198
|
+
return citeReferences(context.library, buildOperationCiteOptions(options));
|
|
141
199
|
}
|
|
142
|
-
function
|
|
143
|
-
|
|
200
|
+
function formatCiteOutput(result) {
|
|
201
|
+
const lines = [];
|
|
202
|
+
for (const r of result.results) {
|
|
203
|
+
if (r.success) {
|
|
204
|
+
lines.push(r.citation);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return lines.join("\n");
|
|
208
|
+
}
|
|
209
|
+
function formatCiteErrors(result) {
|
|
210
|
+
const lines = [];
|
|
211
|
+
for (const r of result.results) {
|
|
212
|
+
if (!r.success) {
|
|
213
|
+
lines.push(`Error for '${r.identifier}': ${r.error}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return lines.join("\n");
|
|
144
217
|
}
|
|
145
|
-
function
|
|
146
|
-
|
|
218
|
+
function getCiteExitCode(result) {
|
|
219
|
+
const hasSuccess = result.results.some((r) => r.success);
|
|
220
|
+
const hasError = result.results.some((r) => !r.success);
|
|
221
|
+
if (hasSuccess) {
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
if (hasError) {
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
return 0;
|
|
147
228
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
229
|
+
const FULLTEXT_EXTENSIONS = {
|
|
230
|
+
pdf: ".pdf",
|
|
231
|
+
markdown: ".md"
|
|
232
|
+
};
|
|
233
|
+
function generateFulltextFilename(item, type) {
|
|
234
|
+
const uuid = item.custom?.uuid;
|
|
235
|
+
if (!uuid) {
|
|
236
|
+
throw new Error("Missing uuid in custom field");
|
|
151
237
|
}
|
|
152
|
-
|
|
153
|
-
|
|
238
|
+
const parts = [item.id];
|
|
239
|
+
if (item.PMID && item.PMID.length > 0) {
|
|
240
|
+
parts.push(`PMID${item.PMID}`);
|
|
154
241
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
242
|
+
parts.push(uuid);
|
|
243
|
+
return parts.join("-") + FULLTEXT_EXTENSIONS[type];
|
|
244
|
+
}
|
|
245
|
+
class FulltextIOError extends Error {
|
|
246
|
+
constructor(message, cause) {
|
|
247
|
+
super(message);
|
|
248
|
+
this.cause = cause;
|
|
249
|
+
this.name = "FulltextIOError";
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
class FulltextNotAttachedError extends Error {
|
|
253
|
+
constructor(itemId, type) {
|
|
254
|
+
super(`No ${type} attached to reference ${itemId}`);
|
|
255
|
+
this.itemId = itemId;
|
|
256
|
+
this.type = type;
|
|
257
|
+
this.name = "FulltextNotAttachedError";
|
|
158
258
|
}
|
|
159
259
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
260
|
+
class FulltextManager {
|
|
261
|
+
constructor(fulltextDirectory) {
|
|
262
|
+
this.fulltextDirectory = fulltextDirectory;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Ensure the fulltext directory exists
|
|
266
|
+
*/
|
|
267
|
+
async ensureDirectory() {
|
|
268
|
+
await mkdir(this.fulltextDirectory, { recursive: true });
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Attach a file to a reference
|
|
272
|
+
*/
|
|
273
|
+
async attachFile(item, sourcePath, type, options) {
|
|
274
|
+
const { move = false, force = false } = options ?? {};
|
|
275
|
+
const newFilename = generateFulltextFilename(item, type);
|
|
276
|
+
this.validateSourceFile(sourcePath);
|
|
277
|
+
const existingFilename = this.getExistingFilename(item, type);
|
|
278
|
+
if (existingFilename && !force) {
|
|
279
|
+
return {
|
|
280
|
+
filename: newFilename,
|
|
281
|
+
existingFile: existingFilename,
|
|
282
|
+
overwritten: false
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
await this.ensureDirectory();
|
|
286
|
+
const deletedOldFile = await this.deleteOldFileIfNeeded(existingFilename, newFilename, force);
|
|
287
|
+
const destPath = join(this.fulltextDirectory, newFilename);
|
|
288
|
+
await this.copyOrMoveFile(sourcePath, destPath, move);
|
|
289
|
+
const result = {
|
|
290
|
+
filename: newFilename,
|
|
291
|
+
overwritten: existingFilename !== void 0
|
|
292
|
+
};
|
|
293
|
+
if (deletedOldFile) {
|
|
294
|
+
result.deletedOldFile = deletedOldFile;
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Validate that source file exists
|
|
300
|
+
*/
|
|
301
|
+
validateSourceFile(sourcePath) {
|
|
302
|
+
if (!existsSync(sourcePath)) {
|
|
303
|
+
throw new FulltextIOError(`Source file not found: ${sourcePath}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Delete old file if force mode and filename changed
|
|
308
|
+
* @returns Deleted filename or undefined
|
|
309
|
+
*/
|
|
310
|
+
async deleteOldFileIfNeeded(existingFilename, newFilename, force) {
|
|
311
|
+
if (!force || !existingFilename || existingFilename === newFilename) {
|
|
312
|
+
return void 0;
|
|
313
|
+
}
|
|
314
|
+
const oldPath = join(this.fulltextDirectory, existingFilename);
|
|
315
|
+
try {
|
|
316
|
+
await unlink(oldPath);
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
return existingFilename;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Copy or move file to destination
|
|
323
|
+
*/
|
|
324
|
+
async copyOrMoveFile(sourcePath, destPath, move) {
|
|
325
|
+
try {
|
|
326
|
+
if (move) {
|
|
327
|
+
await rename(sourcePath, destPath);
|
|
328
|
+
} else {
|
|
329
|
+
await copyFile(sourcePath, destPath);
|
|
330
|
+
}
|
|
331
|
+
} catch (error) {
|
|
332
|
+
const operation = move ? "move" : "copy";
|
|
333
|
+
throw new FulltextIOError(
|
|
334
|
+
`Failed to ${operation} file to ${destPath}`,
|
|
335
|
+
error instanceof Error ? error : void 0
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Get the full path for an attached file
|
|
341
|
+
* @returns Full path or null if not attached
|
|
342
|
+
*/
|
|
343
|
+
getFilePath(item, type) {
|
|
344
|
+
const filename = this.getExistingFilename(item, type);
|
|
345
|
+
if (!filename) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
return join(this.fulltextDirectory, filename);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Detach a file from a reference
|
|
352
|
+
*/
|
|
353
|
+
async detachFile(item, type, options) {
|
|
354
|
+
const { delete: deleteFile = false } = options ?? {};
|
|
355
|
+
const filename = this.getExistingFilename(item, type);
|
|
356
|
+
if (!filename) {
|
|
357
|
+
throw new FulltextNotAttachedError(item.id, type);
|
|
358
|
+
}
|
|
359
|
+
if (deleteFile) {
|
|
360
|
+
const filePath = join(this.fulltextDirectory, filename);
|
|
361
|
+
try {
|
|
362
|
+
await unlink(filePath);
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
filename,
|
|
368
|
+
deleted: deleteFile
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Get list of attached fulltext types
|
|
373
|
+
*/
|
|
374
|
+
getAttachedTypes(item) {
|
|
375
|
+
const types = [];
|
|
376
|
+
const fulltext = item.custom?.fulltext;
|
|
377
|
+
if (fulltext?.pdf) {
|
|
378
|
+
types.push("pdf");
|
|
379
|
+
}
|
|
380
|
+
if (fulltext?.markdown) {
|
|
381
|
+
types.push("markdown");
|
|
382
|
+
}
|
|
383
|
+
return types;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Check if item has attachment
|
|
387
|
+
* @param type Optional type to check; if omitted, checks for any attachment
|
|
388
|
+
*/
|
|
389
|
+
hasAttachment(item, type) {
|
|
390
|
+
if (type) {
|
|
391
|
+
return this.getExistingFilename(item, type) !== void 0;
|
|
166
392
|
}
|
|
393
|
+
return this.getAttachedTypes(item).length > 0;
|
|
167
394
|
}
|
|
168
|
-
|
|
169
|
-
|
|
395
|
+
/**
|
|
396
|
+
* Get existing filename from item metadata
|
|
397
|
+
*/
|
|
398
|
+
getExistingFilename(item, type) {
|
|
399
|
+
const fulltext = item.custom?.fulltext;
|
|
400
|
+
if (!fulltext) {
|
|
401
|
+
return void 0;
|
|
402
|
+
}
|
|
403
|
+
return fulltext[type];
|
|
170
404
|
}
|
|
171
|
-
|
|
172
|
-
|
|
405
|
+
}
|
|
406
|
+
function detectType(filePath) {
|
|
407
|
+
const ext = extname(filePath).toLowerCase();
|
|
408
|
+
if (ext === ".pdf") return "pdf";
|
|
409
|
+
if (ext === ".md" || ext === ".markdown") return "markdown";
|
|
410
|
+
return void 0;
|
|
411
|
+
}
|
|
412
|
+
async function findReference(identifier, context, byUuid) {
|
|
413
|
+
if (context.type === "server") {
|
|
414
|
+
return context.client.find(identifier, { byUuid });
|
|
415
|
+
}
|
|
416
|
+
const ref = byUuid ? context.library.findByUuid(identifier) : context.library.findById(identifier);
|
|
417
|
+
return ref?.getItem() ?? null;
|
|
418
|
+
}
|
|
419
|
+
async function updateFulltextMetadata(identifier, fulltext, context, byUuid) {
|
|
420
|
+
const updates = {
|
|
421
|
+
custom: { fulltext }
|
|
422
|
+
};
|
|
423
|
+
if (context.type === "server") {
|
|
424
|
+
await context.client.update(identifier, updates, { byUuid });
|
|
425
|
+
} else {
|
|
426
|
+
await updateReference(context.library, {
|
|
427
|
+
identifier,
|
|
428
|
+
updates,
|
|
429
|
+
byUuid
|
|
430
|
+
});
|
|
173
431
|
}
|
|
174
|
-
|
|
175
|
-
|
|
432
|
+
}
|
|
433
|
+
async function cleanupTempDir(tempDir) {
|
|
434
|
+
if (tempDir) {
|
|
435
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
436
|
+
});
|
|
176
437
|
}
|
|
177
|
-
|
|
178
|
-
|
|
438
|
+
}
|
|
439
|
+
function prepareStdinSource(stdinContent, fileType) {
|
|
440
|
+
try {
|
|
441
|
+
const tempDir = mkdtempSync(join(tmpdir(), "refmgr-"));
|
|
442
|
+
const ext = fileType === "pdf" ? ".pdf" : ".md";
|
|
443
|
+
const sourcePath = join(tempDir, `stdin${ext}`);
|
|
444
|
+
writeFileSync(sourcePath, stdinContent);
|
|
445
|
+
return { sourcePath, tempDir };
|
|
446
|
+
} catch (error) {
|
|
447
|
+
return {
|
|
448
|
+
error: `Failed to write stdin content: ${error instanceof Error ? error.message : String(error)}`
|
|
449
|
+
};
|
|
179
450
|
}
|
|
180
451
|
}
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
452
|
+
function buildNewFulltext(currentFulltext, fileType, filename) {
|
|
453
|
+
const newFulltext = {};
|
|
454
|
+
if (currentFulltext.pdf) newFulltext.pdf = currentFulltext.pdf;
|
|
455
|
+
if (currentFulltext.markdown) newFulltext.markdown = currentFulltext.markdown;
|
|
456
|
+
newFulltext[fileType] = filename;
|
|
457
|
+
return newFulltext;
|
|
458
|
+
}
|
|
459
|
+
function resolveFileType(explicitType, filePath, stdinContent) {
|
|
460
|
+
let fileType = explicitType;
|
|
461
|
+
if (!fileType && filePath) {
|
|
462
|
+
fileType = detectType(filePath);
|
|
184
463
|
}
|
|
185
|
-
if (
|
|
186
|
-
|
|
464
|
+
if (stdinContent && !fileType) {
|
|
465
|
+
return {
|
|
466
|
+
error: "File type must be specified with --pdf or --markdown when reading from stdin."
|
|
467
|
+
};
|
|
187
468
|
}
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
} else if (item.PMCID) {
|
|
191
|
-
lines.push(formatField("note", `PMCID: ${item.PMCID}`));
|
|
469
|
+
if (!fileType) {
|
|
470
|
+
return { error: "Cannot detect file type. Use --pdf or --markdown to specify the type." };
|
|
192
471
|
}
|
|
472
|
+
return fileType;
|
|
193
473
|
}
|
|
194
|
-
function
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
474
|
+
async function executeFulltextAttach(options, context) {
|
|
475
|
+
const {
|
|
476
|
+
identifier,
|
|
477
|
+
filePath,
|
|
478
|
+
type: explicitType,
|
|
479
|
+
move,
|
|
480
|
+
force,
|
|
481
|
+
byUuid = false,
|
|
482
|
+
fulltextDirectory,
|
|
483
|
+
stdinContent
|
|
484
|
+
} = options;
|
|
485
|
+
const item = await findReference(identifier, context, byUuid);
|
|
486
|
+
if (!item) {
|
|
487
|
+
return { success: false, error: `Reference '${identifier}' not found` };
|
|
488
|
+
}
|
|
489
|
+
const fileTypeResult = resolveFileType(explicitType, filePath, stdinContent);
|
|
490
|
+
if (typeof fileTypeResult === "object" && "error" in fileTypeResult) {
|
|
491
|
+
return { success: false, error: fileTypeResult.error };
|
|
492
|
+
}
|
|
493
|
+
const fileType = fileTypeResult;
|
|
494
|
+
let sourcePath = filePath;
|
|
495
|
+
let tempDir;
|
|
496
|
+
if (stdinContent) {
|
|
497
|
+
const stdinResult = prepareStdinSource(stdinContent, fileType);
|
|
498
|
+
if ("error" in stdinResult) {
|
|
499
|
+
return { success: false, error: stdinResult.error };
|
|
500
|
+
}
|
|
501
|
+
sourcePath = stdinResult.sourcePath;
|
|
502
|
+
tempDir = stdinResult.tempDir;
|
|
503
|
+
}
|
|
504
|
+
if (!sourcePath) {
|
|
505
|
+
return { success: false, error: "No file path or stdin content provided." };
|
|
506
|
+
}
|
|
507
|
+
const manager = new FulltextManager(fulltextDirectory);
|
|
508
|
+
try {
|
|
509
|
+
const attachOptions = {
|
|
510
|
+
...move !== void 0 && { move },
|
|
511
|
+
...force !== void 0 && { force }
|
|
512
|
+
};
|
|
513
|
+
const result = await manager.attachFile(item, sourcePath, fileType, attachOptions);
|
|
514
|
+
if (result.existingFile && !result.overwritten) {
|
|
515
|
+
await cleanupTempDir(tempDir);
|
|
516
|
+
return { success: false, existingFile: result.existingFile, requiresConfirmation: true };
|
|
517
|
+
}
|
|
518
|
+
const newFulltext = buildNewFulltext(item.custom?.fulltext ?? {}, fileType, result.filename);
|
|
519
|
+
await updateFulltextMetadata(identifier, newFulltext, context, byUuid);
|
|
520
|
+
await cleanupTempDir(tempDir);
|
|
521
|
+
return {
|
|
522
|
+
success: true,
|
|
523
|
+
filename: result.filename,
|
|
524
|
+
type: fileType,
|
|
525
|
+
overwritten: result.overwritten
|
|
526
|
+
};
|
|
527
|
+
} catch (error) {
|
|
528
|
+
await cleanupTempDir(tempDir);
|
|
529
|
+
if (error instanceof FulltextIOError) {
|
|
530
|
+
return { success: false, error: error.message };
|
|
531
|
+
}
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
204
534
|
}
|
|
205
|
-
function
|
|
206
|
-
|
|
207
|
-
|
|
535
|
+
async function getFileContent(manager, item, type, identifier) {
|
|
536
|
+
const filePath = manager.getFilePath(item, type);
|
|
537
|
+
if (!filePath) {
|
|
538
|
+
return { success: false, error: `No ${type} fulltext attached to '${identifier}'` };
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
const content = await readFile(filePath);
|
|
542
|
+
return { success: true, content };
|
|
543
|
+
} catch (error) {
|
|
544
|
+
return {
|
|
545
|
+
success: false,
|
|
546
|
+
error: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`
|
|
547
|
+
};
|
|
208
548
|
}
|
|
209
|
-
return references.map(formatSingleBibtexEntry).join("\n\n");
|
|
210
549
|
}
|
|
211
|
-
function
|
|
212
|
-
const
|
|
213
|
-
|
|
550
|
+
function getFilePaths(manager, item, types, identifier) {
|
|
551
|
+
const paths = {};
|
|
552
|
+
for (const t of types) {
|
|
553
|
+
const filePath = manager.getFilePath(item, t);
|
|
554
|
+
if (filePath) {
|
|
555
|
+
paths[t] = filePath;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (Object.keys(paths).length === 0) {
|
|
559
|
+
return { success: false, error: `No fulltext attached to '${identifier}'` };
|
|
560
|
+
}
|
|
561
|
+
return { success: true, paths };
|
|
214
562
|
}
|
|
215
|
-
function
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
|
|
563
|
+
async function executeFulltextGet(options, context) {
|
|
564
|
+
const { identifier, type, stdout: stdout2, byUuid = false, fulltextDirectory } = options;
|
|
565
|
+
const item = await findReference(identifier, context, byUuid);
|
|
566
|
+
if (!item) {
|
|
567
|
+
return { success: false, error: `Reference '${identifier}' not found` };
|
|
568
|
+
}
|
|
569
|
+
const manager = new FulltextManager(fulltextDirectory);
|
|
570
|
+
if (stdout2 && type) {
|
|
571
|
+
return getFileContent(manager, item, type, identifier);
|
|
572
|
+
}
|
|
573
|
+
const attachedTypes = type ? [type] : manager.getAttachedTypes(item);
|
|
574
|
+
if (attachedTypes.length === 0) {
|
|
575
|
+
return { success: false, error: `No fulltext attached to '${identifier}'` };
|
|
576
|
+
}
|
|
577
|
+
return getFilePaths(manager, item, attachedTypes, identifier);
|
|
219
578
|
}
|
|
220
|
-
function
|
|
221
|
-
|
|
579
|
+
async function performDetachOperations(manager, item, typesToDetach, deleteFile) {
|
|
580
|
+
const detached = [];
|
|
581
|
+
const deleted = [];
|
|
582
|
+
for (const t of typesToDetach) {
|
|
583
|
+
const detachOptions = deleteFile ? { delete: deleteFile } : {};
|
|
584
|
+
const result = await manager.detachFile(item, t, detachOptions);
|
|
585
|
+
detached.push(t);
|
|
586
|
+
if (result.deleted) {
|
|
587
|
+
deleted.push(t);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return { detached, deleted };
|
|
222
591
|
}
|
|
223
|
-
function
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
lines.push(header);
|
|
228
|
-
if (item.author && item.author.length > 0) {
|
|
229
|
-
lines.push(` Authors: ${formatAuthors(item.author)}`);
|
|
592
|
+
function buildRemainingFulltext(currentFulltext, detached) {
|
|
593
|
+
const newFulltext = {};
|
|
594
|
+
if (currentFulltext.pdf && !detached.includes("pdf")) {
|
|
595
|
+
newFulltext.pdf = currentFulltext.pdf;
|
|
230
596
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
lines.push(` Type: ${item.type}`);
|
|
234
|
-
if (item.DOI) {
|
|
235
|
-
lines.push(` DOI: ${item.DOI}`);
|
|
597
|
+
if (currentFulltext.markdown && !detached.includes("markdown")) {
|
|
598
|
+
newFulltext.markdown = currentFulltext.markdown;
|
|
236
599
|
}
|
|
237
|
-
|
|
238
|
-
|
|
600
|
+
return Object.keys(newFulltext).length > 0 ? newFulltext : void 0;
|
|
601
|
+
}
|
|
602
|
+
function handleDetachError(error) {
|
|
603
|
+
if (error instanceof FulltextNotAttachedError || error instanceof FulltextIOError) {
|
|
604
|
+
return { success: false, error: error.message };
|
|
605
|
+
}
|
|
606
|
+
throw error;
|
|
607
|
+
}
|
|
608
|
+
async function executeFulltextDetach(options, context) {
|
|
609
|
+
const { identifier, type, delete: deleteFile, byUuid = false, fulltextDirectory } = options;
|
|
610
|
+
const item = await findReference(identifier, context, byUuid);
|
|
611
|
+
if (!item) {
|
|
612
|
+
return { success: false, error: `Reference '${identifier}' not found` };
|
|
613
|
+
}
|
|
614
|
+
const manager = new FulltextManager(fulltextDirectory);
|
|
615
|
+
const typesToDetach = type ? [type] : manager.getAttachedTypes(item);
|
|
616
|
+
if (typesToDetach.length === 0) {
|
|
617
|
+
return { success: false, error: `No fulltext attached to '${identifier}'` };
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
const { detached, deleted } = await performDetachOperations(
|
|
621
|
+
manager,
|
|
622
|
+
item,
|
|
623
|
+
typesToDetach,
|
|
624
|
+
deleteFile
|
|
625
|
+
);
|
|
626
|
+
const updatedFulltext = buildRemainingFulltext(item.custom?.fulltext ?? {}, detached);
|
|
627
|
+
await updateFulltextMetadata(identifier, updatedFulltext, context, byUuid);
|
|
628
|
+
const resultData = { success: true, detached };
|
|
629
|
+
if (deleted.length > 0) {
|
|
630
|
+
resultData.deleted = deleted;
|
|
631
|
+
}
|
|
632
|
+
return resultData;
|
|
633
|
+
} catch (error) {
|
|
634
|
+
return handleDetachError(error);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function formatFulltextAttachOutput(result) {
|
|
638
|
+
if (result.requiresConfirmation) {
|
|
639
|
+
return `File already attached: ${result.existingFile}
|
|
640
|
+
Use --force to overwrite.`;
|
|
641
|
+
}
|
|
642
|
+
if (!result.success) {
|
|
643
|
+
return `Error: ${result.error}`;
|
|
644
|
+
}
|
|
645
|
+
const parts = [];
|
|
646
|
+
if (result.overwritten) {
|
|
647
|
+
parts.push(`Attached ${result.type} (overwritten): ${result.filename}`);
|
|
648
|
+
} else {
|
|
649
|
+
parts.push(`Attached ${result.type}: ${result.filename}`);
|
|
650
|
+
}
|
|
651
|
+
return parts.join("\n");
|
|
652
|
+
}
|
|
653
|
+
function formatFulltextGetOutput(result) {
|
|
654
|
+
if (!result.success) {
|
|
655
|
+
return `Error: ${result.error}`;
|
|
239
656
|
}
|
|
240
|
-
if (
|
|
241
|
-
|
|
657
|
+
if (result.content) {
|
|
658
|
+
return result.content.toString();
|
|
242
659
|
}
|
|
243
|
-
|
|
244
|
-
|
|
660
|
+
const lines = [];
|
|
661
|
+
if (result.paths?.pdf) {
|
|
662
|
+
lines.push(`pdf: ${result.paths.pdf}`);
|
|
663
|
+
}
|
|
664
|
+
if (result.paths?.markdown) {
|
|
665
|
+
lines.push(`markdown: ${result.paths.markdown}`);
|
|
245
666
|
}
|
|
246
|
-
lines.push(` UUID: ${ref.getUuid()}`);
|
|
247
667
|
return lines.join("\n");
|
|
248
668
|
}
|
|
249
|
-
function
|
|
250
|
-
if (
|
|
251
|
-
return
|
|
669
|
+
function formatFulltextDetachOutput(result) {
|
|
670
|
+
if (!result.success) {
|
|
671
|
+
return `Error: ${result.error}`;
|
|
252
672
|
}
|
|
253
|
-
|
|
673
|
+
const lines = [];
|
|
674
|
+
for (const type of result.detached ?? []) {
|
|
675
|
+
if (result.deleted?.includes(type)) {
|
|
676
|
+
lines.push(`Detached and deleted ${type}`);
|
|
677
|
+
} else {
|
|
678
|
+
lines.push(`Detached ${type}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return lines.join("\n");
|
|
682
|
+
}
|
|
683
|
+
function getFulltextExitCode(result) {
|
|
684
|
+
return result.success ? 0 : 1;
|
|
685
|
+
}
|
|
686
|
+
function getListFormat(options) {
|
|
687
|
+
if (options.json) return "json";
|
|
688
|
+
if (options.idsOnly) return "ids-only";
|
|
689
|
+
if (options.uuid) return "uuid";
|
|
690
|
+
if (options.bibtex) return "bibtex";
|
|
691
|
+
return "pretty";
|
|
254
692
|
}
|
|
255
|
-
|
|
693
|
+
function validateOptions$1(options) {
|
|
256
694
|
const outputOptions = [options.json, options.idsOnly, options.uuid, options.bibtex].filter(
|
|
257
695
|
Boolean
|
|
258
696
|
);
|
|
@@ -261,28 +699,81 @@ async function list(items, options) {
|
|
|
261
699
|
"Multiple output formats specified. Only one of --json, --ids-only, --uuid, --bibtex can be used."
|
|
262
700
|
);
|
|
263
701
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
702
|
+
}
|
|
703
|
+
async function executeList(options, context) {
|
|
704
|
+
validateOptions$1(options);
|
|
705
|
+
const format = getListFormat(options);
|
|
706
|
+
if (context.type === "server") {
|
|
707
|
+
return context.client.list({ format });
|
|
708
|
+
}
|
|
709
|
+
return listReferences(context.library, { format });
|
|
710
|
+
}
|
|
711
|
+
function formatListOutput(result) {
|
|
712
|
+
if (result.items.length === 0) {
|
|
713
|
+
return "";
|
|
714
|
+
}
|
|
715
|
+
return result.items.join("\n");
|
|
716
|
+
}
|
|
717
|
+
async function executeRemove(options, context) {
|
|
718
|
+
const { identifier, byUuid = false } = options;
|
|
719
|
+
if (context.type === "server") {
|
|
720
|
+
return context.client.remove(identifier, { byUuid });
|
|
721
|
+
}
|
|
722
|
+
return removeReference(context.library, { identifier, byUuid });
|
|
723
|
+
}
|
|
724
|
+
function formatRemoveOutput(result, identifier) {
|
|
725
|
+
if (!result.removed) {
|
|
726
|
+
return `Reference not found: ${identifier}`;
|
|
727
|
+
}
|
|
728
|
+
const item = result.item;
|
|
729
|
+
if (item) {
|
|
730
|
+
return `Removed: [${item.id}] ${item.title || "(no title)"}`;
|
|
731
|
+
}
|
|
732
|
+
return `Removed reference: ${identifier}`;
|
|
733
|
+
}
|
|
734
|
+
function getFulltextAttachmentTypes(item) {
|
|
735
|
+
const types = [];
|
|
736
|
+
const fulltext = item.custom?.fulltext;
|
|
737
|
+
if (fulltext?.pdf) {
|
|
738
|
+
types.push("pdf");
|
|
739
|
+
}
|
|
740
|
+
if (fulltext?.markdown) {
|
|
741
|
+
types.push("markdown");
|
|
742
|
+
}
|
|
743
|
+
return types;
|
|
744
|
+
}
|
|
745
|
+
function formatFulltextWarning(types) {
|
|
746
|
+
const typeLabels = types.map((t) => t === "pdf" ? "PDF" : "Markdown");
|
|
747
|
+
const fileTypes = typeLabels.join(" and ");
|
|
748
|
+
return `Warning: This reference has fulltext files attached (${fileTypes}). Use --force to also delete the fulltext files.`;
|
|
749
|
+
}
|
|
750
|
+
async function deleteFulltextFiles(item, fulltextDirectory) {
|
|
751
|
+
const fulltext = item.custom?.fulltext;
|
|
752
|
+
if (!fulltext) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const filesToDelete = [];
|
|
756
|
+
if (fulltext.pdf) {
|
|
757
|
+
filesToDelete.push(join(fulltextDirectory, fulltext.pdf));
|
|
758
|
+
}
|
|
759
|
+
if (fulltext.markdown) {
|
|
760
|
+
filesToDelete.push(join(fulltextDirectory, fulltext.markdown));
|
|
761
|
+
}
|
|
762
|
+
for (const filePath of filesToDelete) {
|
|
763
|
+
try {
|
|
764
|
+
await unlink(filePath);
|
|
765
|
+
} catch {
|
|
278
766
|
}
|
|
279
|
-
} else if (options.bibtex) {
|
|
280
|
-
process.stdout.write(formatBibtex(references));
|
|
281
|
-
} else {
|
|
282
|
-
process.stdout.write(formatPretty(references));
|
|
283
767
|
}
|
|
284
768
|
}
|
|
285
|
-
|
|
769
|
+
function getSearchFormat(options) {
|
|
770
|
+
if (options.json) return "json";
|
|
771
|
+
if (options.idsOnly) return "ids-only";
|
|
772
|
+
if (options.uuid) return "uuid";
|
|
773
|
+
if (options.bibtex) return "bibtex";
|
|
774
|
+
return "pretty";
|
|
775
|
+
}
|
|
776
|
+
function validateOptions(options) {
|
|
286
777
|
const outputOptions = [options.json, options.idsOnly, options.uuid, options.bibtex].filter(
|
|
287
778
|
Boolean
|
|
288
779
|
);
|
|
@@ -291,30 +782,20 @@ async function search(items, query, options) {
|
|
|
291
782
|
"Multiple output formats specified. Only one of --json, --ids-only, --uuid, --bibtex can be used."
|
|
292
783
|
);
|
|
293
784
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
} else if (options.uuid) {
|
|
307
|
-
for (const item of matchedItems) {
|
|
308
|
-
if (item.custom) {
|
|
309
|
-
process.stdout.write(`${item.custom.uuid}
|
|
310
|
-
`);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
} else if (options.bibtex) {
|
|
314
|
-
process.stdout.write(formatBibtex(references));
|
|
315
|
-
} else {
|
|
316
|
-
process.stdout.write(formatPretty(references));
|
|
785
|
+
}
|
|
786
|
+
async function executeSearch(options, context) {
|
|
787
|
+
validateOptions(options);
|
|
788
|
+
const format = getSearchFormat(options);
|
|
789
|
+
if (context.type === "server") {
|
|
790
|
+
return context.client.search({ query: options.query, format });
|
|
791
|
+
}
|
|
792
|
+
return searchReferences(context.library, { query: options.query, format });
|
|
793
|
+
}
|
|
794
|
+
function formatSearchOutput(result) {
|
|
795
|
+
if (result.items.length === 0) {
|
|
796
|
+
return "";
|
|
317
797
|
}
|
|
798
|
+
return result.items.join("\n");
|
|
318
799
|
}
|
|
319
800
|
async function serverStart(options) {
|
|
320
801
|
const existingStatus = await serverStatus(options.portfilePath);
|
|
@@ -353,117 +834,31 @@ async function serverStatus(portfilePath) {
|
|
|
353
834
|
}
|
|
354
835
|
return result;
|
|
355
836
|
}
|
|
356
|
-
async function
|
|
357
|
-
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
} else {
|
|
361
|
-
foundIndex = items.findIndex((item) => item.id === identifier);
|
|
362
|
-
}
|
|
363
|
-
if (foundIndex === -1) {
|
|
364
|
-
return {
|
|
365
|
-
updated: false,
|
|
366
|
-
items
|
|
367
|
-
};
|
|
837
|
+
async function executeUpdate(options, context) {
|
|
838
|
+
const { identifier, updates, byUuid = false } = options;
|
|
839
|
+
if (context.type === "server") {
|
|
840
|
+
return context.client.update(identifier, updates, { byUuid });
|
|
368
841
|
}
|
|
369
|
-
|
|
370
|
-
if (!existingItem) {
|
|
371
|
-
return {
|
|
372
|
-
updated: false,
|
|
373
|
-
items
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
const updatedItem = {
|
|
377
|
-
...existingItem,
|
|
378
|
-
...updates,
|
|
379
|
-
// Ensure required fields
|
|
380
|
-
id: updates.id ?? existingItem.id,
|
|
381
|
-
type: updates.type ?? existingItem.type,
|
|
382
|
-
// Preserve UUID and created_at
|
|
383
|
-
custom: {
|
|
384
|
-
...existingItem.custom || {},
|
|
385
|
-
...updates.custom || {},
|
|
386
|
-
uuid: existingItem.custom?.uuid || "",
|
|
387
|
-
created_at: existingItem.custom?.created_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
388
|
-
// Update timestamp
|
|
389
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
390
|
-
}
|
|
391
|
-
};
|
|
392
|
-
const updatedItems = [...items.slice(0, foundIndex), updatedItem, ...items.slice(foundIndex + 1)];
|
|
393
|
-
return {
|
|
394
|
-
updated: true,
|
|
395
|
-
item: updatedItem,
|
|
396
|
-
items: updatedItems
|
|
397
|
-
};
|
|
842
|
+
return updateReference(context.library, { identifier, updates, byUuid });
|
|
398
843
|
}
|
|
399
|
-
|
|
400
|
-
if (
|
|
401
|
-
|
|
402
|
-
return
|
|
403
|
-
} catch (error) {
|
|
404
|
-
throw new Error(
|
|
405
|
-
`I/O error: Cannot read file ${file}: ${error instanceof Error ? error.message : String(error)}`
|
|
406
|
-
);
|
|
844
|
+
function formatUpdateOutput(result, identifier) {
|
|
845
|
+
if (!result.updated) {
|
|
846
|
+
if (result.idCollision) {
|
|
847
|
+
return `Update failed: ID collision for ${identifier}`;
|
|
407
848
|
}
|
|
849
|
+
return `Reference not found: ${identifier}`;
|
|
408
850
|
}
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
function parseJsonInput(input) {
|
|
416
|
-
if (!input || input.trim() === "") {
|
|
417
|
-
throw new Error("Parse error: Empty input");
|
|
418
|
-
}
|
|
419
|
-
try {
|
|
420
|
-
return JSON.parse(input);
|
|
421
|
-
} catch (error) {
|
|
422
|
-
throw new Error(
|
|
423
|
-
`Parse error: Invalid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
async function loadConfigWithOverrides(options) {
|
|
428
|
-
const config = await loadConfig();
|
|
429
|
-
const overrides = {};
|
|
430
|
-
if (options.library) {
|
|
431
|
-
overrides.library = options.library;
|
|
432
|
-
}
|
|
433
|
-
if (options.quiet) {
|
|
434
|
-
overrides.logLevel = "silent";
|
|
435
|
-
} else if (options.verbose) {
|
|
436
|
-
overrides.logLevel = "debug";
|
|
437
|
-
} else if (options.logLevel) {
|
|
438
|
-
overrides.logLevel = options.logLevel;
|
|
851
|
+
const item = result.item;
|
|
852
|
+
const parts = [];
|
|
853
|
+
if (item) {
|
|
854
|
+
parts.push(`Updated: [${item.id}] ${item.title || "(no title)"}`);
|
|
855
|
+
} else {
|
|
856
|
+
parts.push(`Updated reference: ${identifier}`);
|
|
439
857
|
}
|
|
440
|
-
if (
|
|
441
|
-
|
|
442
|
-
...config.backup,
|
|
443
|
-
...options.backup !== void 0 && { enabled: options.backup },
|
|
444
|
-
...options.backupDir && { directory: options.backupDir }
|
|
445
|
-
};
|
|
858
|
+
if (result.idChanged && result.newId) {
|
|
859
|
+
parts.push(`ID changed to: ${result.newId}`);
|
|
446
860
|
}
|
|
447
|
-
return
|
|
448
|
-
}
|
|
449
|
-
function isTTY() {
|
|
450
|
-
return Boolean(stdin.isTTY && stdout.isTTY);
|
|
451
|
-
}
|
|
452
|
-
async function readConfirmation(prompt) {
|
|
453
|
-
if (!isTTY()) {
|
|
454
|
-
return true;
|
|
455
|
-
}
|
|
456
|
-
stdout.write(`${prompt} (y/N): `);
|
|
457
|
-
const chunks = [];
|
|
458
|
-
for await (const chunk of stdin) {
|
|
459
|
-
chunks.push(chunk);
|
|
460
|
-
const input2 = Buffer.concat(chunks).toString("utf-8");
|
|
461
|
-
if (input2.includes("\n")) {
|
|
462
|
-
break;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
const input = Buffer.concat(chunks).toString("utf-8").trim().toLowerCase();
|
|
466
|
-
return input === "y" || input === "yes";
|
|
861
|
+
return parts.join("\n");
|
|
467
862
|
}
|
|
468
863
|
class ServerClient {
|
|
469
864
|
constructor(baseUrl) {
|
|
@@ -482,12 +877,15 @@ class ServerClient {
|
|
|
482
877
|
return await response.json();
|
|
483
878
|
}
|
|
484
879
|
/**
|
|
485
|
-
* Find reference by
|
|
486
|
-
* @param
|
|
880
|
+
* Find reference by identifier.
|
|
881
|
+
* @param identifier - Citation ID or UUID
|
|
882
|
+
* @param options - Options object
|
|
883
|
+
* @param options.byUuid - If true, treat identifier as UUID; otherwise as citation ID
|
|
487
884
|
* @returns CSL item or null if not found
|
|
488
885
|
*/
|
|
489
|
-
async
|
|
490
|
-
const
|
|
886
|
+
async find(identifier, options) {
|
|
887
|
+
const path2 = options?.byUuid ? `/api/references/uuid/${identifier}` : `/api/references/id/${identifier}`;
|
|
888
|
+
const url = `${this.baseUrl}${path2}`;
|
|
491
889
|
const response = await fetch(url);
|
|
492
890
|
if (response.status === 404) {
|
|
493
891
|
return null;
|
|
@@ -516,34 +914,111 @@ class ServerClient {
|
|
|
516
914
|
}
|
|
517
915
|
/**
|
|
518
916
|
* Update existing reference.
|
|
519
|
-
* @param
|
|
520
|
-
* @param
|
|
521
|
-
* @
|
|
917
|
+
* @param identifier - Citation ID or UUID
|
|
918
|
+
* @param updates - Partial CSL item with fields to update
|
|
919
|
+
* @param options - Options object
|
|
920
|
+
* @param options.byUuid - If true, treat identifier as UUID; otherwise as citation ID
|
|
921
|
+
* @returns Update operation result
|
|
522
922
|
*/
|
|
523
|
-
async update(
|
|
524
|
-
const
|
|
923
|
+
async update(identifier, updates, options) {
|
|
924
|
+
const path2 = options?.byUuid ? `/api/references/uuid/${identifier}` : `/api/references/id/${identifier}`;
|
|
925
|
+
const url = `${this.baseUrl}${path2}`;
|
|
525
926
|
const response = await fetch(url, {
|
|
526
927
|
method: "PUT",
|
|
527
928
|
headers: { "Content-Type": "application/json" },
|
|
528
|
-
body: JSON.stringify(
|
|
929
|
+
body: JSON.stringify(updates)
|
|
529
930
|
});
|
|
530
|
-
if (!response.ok) {
|
|
931
|
+
if (!response.ok && response.status !== 404 && response.status !== 409) {
|
|
531
932
|
throw new Error(await response.text());
|
|
532
933
|
}
|
|
533
934
|
return await response.json();
|
|
534
935
|
}
|
|
535
936
|
/**
|
|
536
|
-
* Remove reference by
|
|
537
|
-
* @param
|
|
937
|
+
* Remove reference by identifier.
|
|
938
|
+
* @param identifier - Citation ID or UUID
|
|
939
|
+
* @param options - Options object
|
|
940
|
+
* @param options.byUuid - If true, treat identifier as UUID; otherwise as citation ID
|
|
941
|
+
* @returns Remove operation result
|
|
538
942
|
*/
|
|
539
|
-
async remove(
|
|
540
|
-
const
|
|
943
|
+
async remove(identifier, options) {
|
|
944
|
+
const path2 = options?.byUuid ? `/api/references/uuid/${identifier}` : `/api/references/id/${identifier}`;
|
|
945
|
+
const url = `${this.baseUrl}${path2}`;
|
|
541
946
|
const response = await fetch(url, {
|
|
542
947
|
method: "DELETE"
|
|
543
948
|
});
|
|
949
|
+
if (!response.ok && response.status !== 404) {
|
|
950
|
+
throw new Error(await response.text());
|
|
951
|
+
}
|
|
952
|
+
return await response.json();
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Add references from various input formats.
|
|
956
|
+
* @param inputs - Array of inputs (file paths, PMIDs, DOIs)
|
|
957
|
+
* @param options - Options for add operation
|
|
958
|
+
* @returns Result containing added, failed, and skipped items
|
|
959
|
+
*/
|
|
960
|
+
async addFromInputs(inputs, options) {
|
|
961
|
+
const url = `${this.baseUrl}/api/add`;
|
|
962
|
+
const response = await fetch(url, {
|
|
963
|
+
method: "POST",
|
|
964
|
+
headers: { "Content-Type": "application/json" },
|
|
965
|
+
body: JSON.stringify({ inputs, options })
|
|
966
|
+
});
|
|
967
|
+
if (!response.ok) {
|
|
968
|
+
throw new Error(await response.text());
|
|
969
|
+
}
|
|
970
|
+
return await response.json();
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Generate citations for references.
|
|
974
|
+
* @param options - Cite options including identifiers and formatting
|
|
975
|
+
* @returns Cite result with per-identifier results
|
|
976
|
+
*/
|
|
977
|
+
async cite(options) {
|
|
978
|
+
const url = `${this.baseUrl}/api/cite`;
|
|
979
|
+
const response = await fetch(url, {
|
|
980
|
+
method: "POST",
|
|
981
|
+
headers: { "Content-Type": "application/json" },
|
|
982
|
+
body: JSON.stringify(options)
|
|
983
|
+
});
|
|
984
|
+
if (!response.ok) {
|
|
985
|
+
throw new Error(await response.text());
|
|
986
|
+
}
|
|
987
|
+
return await response.json();
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* List all references with optional formatting.
|
|
991
|
+
* @param options - List options including format
|
|
992
|
+
* @returns List result with formatted items
|
|
993
|
+
*/
|
|
994
|
+
async list(options) {
|
|
995
|
+
const url = `${this.baseUrl}/api/list`;
|
|
996
|
+
const response = await fetch(url, {
|
|
997
|
+
method: "POST",
|
|
998
|
+
headers: { "Content-Type": "application/json" },
|
|
999
|
+
body: JSON.stringify(options ?? {})
|
|
1000
|
+
});
|
|
544
1001
|
if (!response.ok) {
|
|
545
1002
|
throw new Error(await response.text());
|
|
546
1003
|
}
|
|
1004
|
+
return await response.json();
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Search references with query.
|
|
1008
|
+
* @param options - Search options including query and format
|
|
1009
|
+
* @returns Search result with formatted items
|
|
1010
|
+
*/
|
|
1011
|
+
async search(options) {
|
|
1012
|
+
const url = `${this.baseUrl}/api/search`;
|
|
1013
|
+
const response = await fetch(url, {
|
|
1014
|
+
method: "POST",
|
|
1015
|
+
headers: { "Content-Type": "application/json" },
|
|
1016
|
+
body: JSON.stringify(options)
|
|
1017
|
+
});
|
|
1018
|
+
if (!response.ok) {
|
|
1019
|
+
throw new Error(await response.text());
|
|
1020
|
+
}
|
|
1021
|
+
return await response.json();
|
|
547
1022
|
}
|
|
548
1023
|
}
|
|
549
1024
|
async function getServerConnection(libraryPath, config) {
|
|
@@ -557,48 +1032,139 @@ async function getServerConnection(libraryPath, config) {
|
|
|
557
1032
|
}
|
|
558
1033
|
return null;
|
|
559
1034
|
}
|
|
560
|
-
if (!isProcessRunning(portfileData.pid)) {
|
|
561
|
-
await removePortfile(portfilePath);
|
|
562
|
-
return null;
|
|
1035
|
+
if (!isProcessRunning(portfileData.pid)) {
|
|
1036
|
+
await removePortfile(portfilePath);
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
if (!portfileData.library || portfileData.library !== libraryPath) {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
baseUrl: `http://localhost:${portfileData.port}`,
|
|
1044
|
+
pid: portfileData.pid
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
async function startServerDaemon(libraryPath, _config) {
|
|
1048
|
+
const binaryPath = process.argv[1] || process.execPath;
|
|
1049
|
+
const child = spawn(
|
|
1050
|
+
process.execPath,
|
|
1051
|
+
[binaryPath, "server", "start", "--daemon", "--library", libraryPath],
|
|
1052
|
+
{
|
|
1053
|
+
detached: true,
|
|
1054
|
+
stdio: "ignore"
|
|
1055
|
+
}
|
|
1056
|
+
);
|
|
1057
|
+
child.unref();
|
|
1058
|
+
}
|
|
1059
|
+
async function waitForPortfile(timeoutMs) {
|
|
1060
|
+
const portfilePath = getPortfilePath();
|
|
1061
|
+
const startTime = Date.now();
|
|
1062
|
+
const checkInterval = 50;
|
|
1063
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
1064
|
+
if (await portfileExists(portfilePath)) {
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
|
1068
|
+
}
|
|
1069
|
+
throw new Error(`Server failed to start: portfile not created within ${timeoutMs}ms`);
|
|
1070
|
+
}
|
|
1071
|
+
async function createExecutionContext(config, loadLibrary) {
|
|
1072
|
+
const server = await getServerConnection(config.library, config);
|
|
1073
|
+
if (server) {
|
|
1074
|
+
return {
|
|
1075
|
+
type: "server",
|
|
1076
|
+
client: new ServerClient(server.baseUrl)
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
const library = await loadLibrary(config.library);
|
|
1080
|
+
return {
|
|
1081
|
+
type: "local",
|
|
1082
|
+
library
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
async function readJsonInput(file) {
|
|
1086
|
+
if (file) {
|
|
1087
|
+
try {
|
|
1088
|
+
return readFileSync(file, "utf-8");
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
throw new Error(
|
|
1091
|
+
`I/O error: Cannot read file ${file}: ${error instanceof Error ? error.message : String(error)}`
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
const chunks = [];
|
|
1096
|
+
for await (const chunk of stdin) {
|
|
1097
|
+
chunks.push(chunk);
|
|
1098
|
+
}
|
|
1099
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
1100
|
+
}
|
|
1101
|
+
function parseJsonInput(input) {
|
|
1102
|
+
if (!input || input.trim() === "") {
|
|
1103
|
+
throw new Error("Parse error: Empty input");
|
|
1104
|
+
}
|
|
1105
|
+
try {
|
|
1106
|
+
return JSON.parse(input);
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
throw new Error(
|
|
1109
|
+
`Parse error: Invalid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
async function loadConfigWithOverrides(options) {
|
|
1114
|
+
const config = await loadConfig();
|
|
1115
|
+
const overrides = {};
|
|
1116
|
+
if (options.library) {
|
|
1117
|
+
overrides.library = options.library;
|
|
1118
|
+
}
|
|
1119
|
+
if (options.quiet) {
|
|
1120
|
+
overrides.logLevel = "silent";
|
|
1121
|
+
} else if (options.verbose) {
|
|
1122
|
+
overrides.logLevel = "debug";
|
|
1123
|
+
} else if (options.logLevel) {
|
|
1124
|
+
overrides.logLevel = options.logLevel;
|
|
563
1125
|
}
|
|
564
|
-
if (
|
|
565
|
-
|
|
1126
|
+
if (options.backup !== void 0 || options.backupDir) {
|
|
1127
|
+
overrides.backup = {
|
|
1128
|
+
...config.backup,
|
|
1129
|
+
...options.backup !== void 0 && { enabled: options.backup },
|
|
1130
|
+
...options.backupDir && { directory: options.backupDir }
|
|
1131
|
+
};
|
|
566
1132
|
}
|
|
567
|
-
return {
|
|
568
|
-
baseUrl: `http://localhost:${portfileData.port}`,
|
|
569
|
-
pid: portfileData.pid
|
|
570
|
-
};
|
|
1133
|
+
return { ...config, ...overrides };
|
|
571
1134
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const child = spawn(
|
|
575
|
-
process.execPath,
|
|
576
|
-
[binaryPath, "server", "start", "--daemon", "--library", libraryPath],
|
|
577
|
-
{
|
|
578
|
-
detached: true,
|
|
579
|
-
stdio: "ignore"
|
|
580
|
-
}
|
|
581
|
-
);
|
|
582
|
-
child.unref();
|
|
1135
|
+
function isTTY() {
|
|
1136
|
+
return Boolean(stdin.isTTY && stdout.isTTY);
|
|
583
1137
|
}
|
|
584
|
-
async function
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
1138
|
+
async function readConfirmation(prompt) {
|
|
1139
|
+
if (!isTTY()) {
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
stdout.write(`${prompt} (y/N): `);
|
|
1143
|
+
const chunks = [];
|
|
1144
|
+
for await (const chunk of stdin) {
|
|
1145
|
+
chunks.push(chunk);
|
|
1146
|
+
const input2 = Buffer.concat(chunks).toString("utf-8");
|
|
1147
|
+
if (input2.includes("\n")) {
|
|
1148
|
+
break;
|
|
591
1149
|
}
|
|
592
|
-
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
|
593
1150
|
}
|
|
594
|
-
|
|
1151
|
+
const input = Buffer.concat(chunks).toString("utf-8").trim().toLowerCase();
|
|
1152
|
+
return input === "y" || input === "yes";
|
|
1153
|
+
}
|
|
1154
|
+
async function readStdinContent() {
|
|
1155
|
+
const chunks = [];
|
|
1156
|
+
for await (const chunk of stdin) {
|
|
1157
|
+
chunks.push(chunk);
|
|
1158
|
+
}
|
|
1159
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
1160
|
+
}
|
|
1161
|
+
async function readStdinBuffer() {
|
|
1162
|
+
const chunks = [];
|
|
1163
|
+
for await (const chunk of stdin) {
|
|
1164
|
+
chunks.push(chunk);
|
|
1165
|
+
}
|
|
1166
|
+
return Buffer.concat(chunks);
|
|
595
1167
|
}
|
|
596
|
-
const version = "0.1.0";
|
|
597
|
-
const description = "A local reference management tool using CSL-JSON as the single source of truth";
|
|
598
|
-
const packageJson = {
|
|
599
|
-
version,
|
|
600
|
-
description
|
|
601
|
-
};
|
|
602
1168
|
function createProgram() {
|
|
603
1169
|
const program = new Command();
|
|
604
1170
|
program.name("reference-manager").version(packageJson.version).description(packageJson.description);
|
|
@@ -608,175 +1174,129 @@ function createProgram() {
|
|
|
608
1174
|
registerAddCommand(program);
|
|
609
1175
|
registerRemoveCommand(program);
|
|
610
1176
|
registerUpdateCommand(program);
|
|
1177
|
+
registerCiteCommand(program);
|
|
611
1178
|
registerServerCommand(program);
|
|
1179
|
+
registerFulltextCommand(program);
|
|
612
1180
|
return program;
|
|
613
1181
|
}
|
|
614
|
-
function
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
items = await client.getAll();
|
|
624
|
-
} else {
|
|
625
|
-
const library = await Library.load(config.library);
|
|
626
|
-
items = library.getAll().map((ref) => ref.getItem());
|
|
627
|
-
}
|
|
628
|
-
await list(items, {
|
|
629
|
-
json: options.json,
|
|
630
|
-
idsOnly: options.idsOnly,
|
|
631
|
-
uuid: options.uuid,
|
|
632
|
-
bibtex: options.bibtex
|
|
633
|
-
});
|
|
634
|
-
process.exit(0);
|
|
635
|
-
} catch (error) {
|
|
636
|
-
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
1182
|
+
async function handleListAction(options, program) {
|
|
1183
|
+
try {
|
|
1184
|
+
const globalOpts = program.opts();
|
|
1185
|
+
const config = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
1186
|
+
const context = await createExecutionContext(config, Library.load);
|
|
1187
|
+
const result = await executeList(options, context);
|
|
1188
|
+
const output = formatListOutput(result);
|
|
1189
|
+
if (output) {
|
|
1190
|
+
process.stdout.write(`${output}
|
|
637
1191
|
`);
|
|
638
|
-
process.exit(4);
|
|
639
1192
|
}
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
program.command("search").description("Search references").argument("<query>", "Search query").option("--json", "Output in JSON format").option("--ids-only", "Output only citation keys").option("--uuid", "Output only UUIDs").option("--bibtex", "Output in BibTeX format").action(async (query, options) => {
|
|
644
|
-
try {
|
|
645
|
-
const globalOpts = program.opts();
|
|
646
|
-
const config = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
647
|
-
const server = await getServerConnection(config.library, config);
|
|
648
|
-
let items;
|
|
649
|
-
if (server) {
|
|
650
|
-
const client = new ServerClient(server.baseUrl);
|
|
651
|
-
items = await client.getAll();
|
|
652
|
-
} else {
|
|
653
|
-
const library = await Library.load(config.library);
|
|
654
|
-
items = library.getAll().map((ref) => ref.getItem());
|
|
655
|
-
}
|
|
656
|
-
await search(items, query, {
|
|
657
|
-
json: options.json,
|
|
658
|
-
idsOnly: options.idsOnly,
|
|
659
|
-
uuid: options.uuid,
|
|
660
|
-
bibtex: options.bibtex
|
|
661
|
-
});
|
|
662
|
-
process.exit(0);
|
|
663
|
-
} catch (error) {
|
|
664
|
-
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
1193
|
+
process.exit(0);
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
665
1196
|
`);
|
|
666
|
-
|
|
667
|
-
|
|
1197
|
+
process.exit(4);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
function registerListCommand(program) {
|
|
1201
|
+
program.command("list").description("List all references in the library").option("--json", "Output in JSON format").option("--ids-only", "Output only citation keys").option("--uuid", "Output only UUIDs").option("--bibtex", "Output in BibTeX format").action(async (options) => {
|
|
1202
|
+
await handleListAction(options, program);
|
|
668
1203
|
});
|
|
669
1204
|
}
|
|
670
|
-
async function
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
process.stderr.write(`Error: ${error.message}
|
|
1205
|
+
async function handleSearchAction(query, options, program) {
|
|
1206
|
+
try {
|
|
1207
|
+
const globalOpts = program.opts();
|
|
1208
|
+
const config = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
1209
|
+
const context = await createExecutionContext(config, Library.load);
|
|
1210
|
+
const result = await executeSearch({ ...options, query }, context);
|
|
1211
|
+
const output = formatSearchOutput(result);
|
|
1212
|
+
if (output) {
|
|
1213
|
+
process.stdout.write(`${output}
|
|
680
1214
|
`);
|
|
681
|
-
process.exit(1);
|
|
682
|
-
}
|
|
683
|
-
throw error;
|
|
684
1215
|
}
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
const library = await Library.load(libraryPath);
|
|
689
|
-
const existingItems = library.getAll().map((ref) => ref.getItem());
|
|
690
|
-
for (const item of items) {
|
|
691
|
-
const result = await add(existingItems, item, { force });
|
|
692
|
-
if (result.added) {
|
|
693
|
-
library.add(result.item);
|
|
694
|
-
process.stderr.write(`Added reference: [${result.item.id}]
|
|
1216
|
+
process.exit(0);
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
695
1219
|
`);
|
|
696
|
-
|
|
697
|
-
} else if (result.duplicate) {
|
|
698
|
-
throw new Error(
|
|
699
|
-
`Duplicate detected: ${result.duplicate.type} match with existing reference [${result.duplicate.existing.id}]`
|
|
700
|
-
);
|
|
701
|
-
}
|
|
1220
|
+
process.exit(4);
|
|
702
1221
|
}
|
|
703
|
-
await library.save();
|
|
704
1222
|
}
|
|
705
|
-
function
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
`);
|
|
710
|
-
process.exit(3);
|
|
711
|
-
}
|
|
712
|
-
if (message.includes("Duplicate")) {
|
|
713
|
-
process.stderr.write(`Error: ${message}
|
|
714
|
-
`);
|
|
715
|
-
process.exit(1);
|
|
716
|
-
}
|
|
717
|
-
process.stderr.write(`Error: ${message}
|
|
718
|
-
`);
|
|
719
|
-
process.exit(4);
|
|
1223
|
+
function registerSearchCommand(program) {
|
|
1224
|
+
program.command("search").description("Search references").argument("<query>", "Search query").option("--json", "Output in JSON format").option("--ids-only", "Output only citation keys").option("--uuid", "Output only UUIDs").option("--bibtex", "Output in BibTeX format").action(async (query, options) => {
|
|
1225
|
+
await handleSearchAction(query, options, program);
|
|
1226
|
+
});
|
|
720
1227
|
}
|
|
721
|
-
async function handleAddAction(
|
|
1228
|
+
async function handleAddAction(inputs, options, program) {
|
|
722
1229
|
try {
|
|
723
1230
|
const globalOpts = program.opts();
|
|
724
1231
|
const config = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const server = await getServerConnection(config.library, config);
|
|
729
|
-
if (server) {
|
|
730
|
-
await addViaServer(items, server, options.force ?? false);
|
|
731
|
-
} else {
|
|
732
|
-
await addViaLibrary(items, config.library, options.force ?? false);
|
|
1232
|
+
let stdinContent;
|
|
1233
|
+
if (inputs.length === 0) {
|
|
1234
|
+
stdinContent = await readStdinContent();
|
|
733
1235
|
}
|
|
734
|
-
|
|
1236
|
+
const context = await createExecutionContext(config, Library.load);
|
|
1237
|
+
const addOptions = {
|
|
1238
|
+
inputs,
|
|
1239
|
+
force: options.force ?? false
|
|
1240
|
+
};
|
|
1241
|
+
if (options.format !== void 0) {
|
|
1242
|
+
addOptions.format = options.format;
|
|
1243
|
+
}
|
|
1244
|
+
if (options.verbose !== void 0) {
|
|
1245
|
+
addOptions.verbose = options.verbose;
|
|
1246
|
+
}
|
|
1247
|
+
if (stdinContent?.trim()) {
|
|
1248
|
+
addOptions.stdinContent = stdinContent;
|
|
1249
|
+
}
|
|
1250
|
+
const pubmedConfig = {};
|
|
1251
|
+
if (config.pubmed.email !== void 0) {
|
|
1252
|
+
pubmedConfig.email = config.pubmed.email;
|
|
1253
|
+
}
|
|
1254
|
+
if (config.pubmed.apiKey !== void 0) {
|
|
1255
|
+
pubmedConfig.apiKey = config.pubmed.apiKey;
|
|
1256
|
+
}
|
|
1257
|
+
if (Object.keys(pubmedConfig).length > 0) {
|
|
1258
|
+
addOptions.pubmedConfig = pubmedConfig;
|
|
1259
|
+
}
|
|
1260
|
+
const result = await executeAdd(addOptions, context);
|
|
1261
|
+
const output = formatAddOutput(result, options.verbose ?? false);
|
|
1262
|
+
process.stderr.write(`${output}
|
|
1263
|
+
`);
|
|
1264
|
+
process.exit(getExitCode(result));
|
|
735
1265
|
} catch (error) {
|
|
736
|
-
|
|
1266
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1267
|
+
process.stderr.write(`Error: ${message}
|
|
1268
|
+
`);
|
|
1269
|
+
process.exit(1);
|
|
737
1270
|
}
|
|
738
1271
|
}
|
|
739
1272
|
function registerAddCommand(program) {
|
|
740
|
-
program.command("add").description("Add new reference(s) to the library").argument("[
|
|
741
|
-
await handleAddAction(
|
|
1273
|
+
program.command("add").description("Add new reference(s) to the library").argument("[input...]", "File paths or identifiers (PMID/DOI), or use stdin").option("-f, --force", "Skip duplicate detection").option("--format <format>", "Explicit input format: json|bibtex|ris|pmid|doi|auto", "auto").option("--verbose", "Show detailed error information").action(async (inputs, options) => {
|
|
1274
|
+
await handleAddAction(inputs, options, program);
|
|
742
1275
|
});
|
|
743
1276
|
}
|
|
744
|
-
async function findReferenceToRemove(identifier, byUuid,
|
|
745
|
-
if (server) {
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
return byUuid ? items.find((item) => item.custom?.uuid === identifier) : items.find((item) => item.id === identifier);
|
|
1277
|
+
async function findReferenceToRemove(identifier, byUuid, context) {
|
|
1278
|
+
if (context.type === "server") {
|
|
1279
|
+
const item = await context.client.find(identifier, { byUuid });
|
|
1280
|
+
return item ?? void 0;
|
|
749
1281
|
}
|
|
750
|
-
const
|
|
751
|
-
const ref = byUuid ? library.findByUuid(identifier) : library.findById(identifier);
|
|
1282
|
+
const ref = byUuid ? context.library.findByUuid(identifier) : context.library.findById(identifier);
|
|
752
1283
|
return ref?.getItem();
|
|
753
1284
|
}
|
|
754
|
-
async function confirmRemoval(refToRemove, force) {
|
|
1285
|
+
async function confirmRemoval(refToRemove, force, fulltextWarning) {
|
|
755
1286
|
if (force || !isTTY()) {
|
|
756
1287
|
return true;
|
|
757
1288
|
}
|
|
758
1289
|
const authors = Array.isArray(refToRemove.author) ? refToRemove.author.map((a) => `${a.family || ""}, ${a.given?.[0] || ""}.`).join("; ") : "(no authors)";
|
|
759
|
-
|
|
1290
|
+
let confirmMsg = `Remove reference [${refToRemove.id}]?
|
|
760
1291
|
Title: ${refToRemove.title || "(no title)"}
|
|
761
|
-
Authors: ${authors}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
if (server) {
|
|
767
|
-
const client = new ServerClient(server.baseUrl);
|
|
768
|
-
if (!refToRemove.custom?.uuid) {
|
|
769
|
-
throw new Error("Reference missing UUID");
|
|
770
|
-
}
|
|
771
|
-
await client.remove(refToRemove.custom.uuid);
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
const library = await Library.load(libraryPath);
|
|
775
|
-
const removed = byUuid ? library.removeByUuid(identifier) : library.removeById(identifier);
|
|
776
|
-
if (!removed) {
|
|
777
|
-
throw new Error("Reference not found");
|
|
1292
|
+
Authors: ${authors}`;
|
|
1293
|
+
if (fulltextWarning) {
|
|
1294
|
+
confirmMsg += `
|
|
1295
|
+
|
|
1296
|
+
${fulltextWarning}`;
|
|
778
1297
|
}
|
|
779
|
-
|
|
1298
|
+
confirmMsg += "\nContinue?";
|
|
1299
|
+
return await readConfirmation(confirmMsg);
|
|
780
1300
|
}
|
|
781
1301
|
function handleRemoveError(error) {
|
|
782
1302
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -789,30 +1309,61 @@ function handleRemoveError(error) {
|
|
|
789
1309
|
`);
|
|
790
1310
|
process.exit(4);
|
|
791
1311
|
}
|
|
1312
|
+
function formatTypeLabels(types) {
|
|
1313
|
+
return types.map((t) => t === "pdf" ? "PDF" : "Markdown").join(" and ");
|
|
1314
|
+
}
|
|
1315
|
+
async function handleFulltextDeletion(item, fulltextDirectory, types, output) {
|
|
1316
|
+
await deleteFulltextFiles(item, fulltextDirectory);
|
|
1317
|
+
const typeLabels = formatTypeLabels(types);
|
|
1318
|
+
process.stderr.write(`${output}
|
|
1319
|
+
`);
|
|
1320
|
+
process.stderr.write(`Deleted fulltext files: ${typeLabels}
|
|
1321
|
+
`);
|
|
1322
|
+
}
|
|
792
1323
|
async function handleRemoveAction(identifier, options, program) {
|
|
793
1324
|
try {
|
|
794
1325
|
const globalOpts = program.opts();
|
|
795
1326
|
const config = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
796
|
-
const
|
|
797
|
-
const refToRemove = await findReferenceToRemove(
|
|
798
|
-
identifier,
|
|
799
|
-
options.uuid ?? false,
|
|
800
|
-
server,
|
|
801
|
-
config.library
|
|
802
|
-
);
|
|
1327
|
+
const context = await createExecutionContext(config, Library.load);
|
|
1328
|
+
const refToRemove = await findReferenceToRemove(identifier, options.uuid ?? false, context);
|
|
803
1329
|
if (!refToRemove) {
|
|
804
1330
|
process.stderr.write(`Error: Reference not found: ${identifier}
|
|
805
1331
|
`);
|
|
806
1332
|
process.exit(1);
|
|
807
1333
|
}
|
|
808
|
-
const
|
|
1334
|
+
const fulltextTypes = getFulltextAttachmentTypes(refToRemove);
|
|
1335
|
+
const hasFulltext = fulltextTypes.length > 0;
|
|
1336
|
+
if (hasFulltext && !isTTY() && !options.force) {
|
|
1337
|
+
const warning = formatFulltextWarning(fulltextTypes);
|
|
1338
|
+
process.stderr.write(`Error: ${warning}
|
|
1339
|
+
`);
|
|
1340
|
+
process.exit(1);
|
|
1341
|
+
}
|
|
1342
|
+
const fulltextWarning = hasFulltext && !options.force ? formatFulltextWarning(fulltextTypes) : void 0;
|
|
1343
|
+
const confirmed = await confirmRemoval(refToRemove, options.force ?? false, fulltextWarning);
|
|
809
1344
|
if (!confirmed) {
|
|
810
1345
|
process.stderr.write("Cancelled.\n");
|
|
811
1346
|
process.exit(2);
|
|
812
1347
|
}
|
|
813
|
-
|
|
814
|
-
|
|
1348
|
+
const removeOptions = {
|
|
1349
|
+
identifier
|
|
1350
|
+
};
|
|
1351
|
+
if (options.uuid !== void 0) {
|
|
1352
|
+
removeOptions.byUuid = options.uuid;
|
|
1353
|
+
}
|
|
1354
|
+
const result = await executeRemove(removeOptions, context);
|
|
1355
|
+
const output = formatRemoveOutput(result, identifier);
|
|
1356
|
+
if (!result.removed) {
|
|
1357
|
+
process.stderr.write(`${output}
|
|
815
1358
|
`);
|
|
1359
|
+
process.exit(1);
|
|
1360
|
+
}
|
|
1361
|
+
if (hasFulltext && options.force) {
|
|
1362
|
+
await handleFulltextDeletion(refToRemove, config.fulltext.directory, fulltextTypes, output);
|
|
1363
|
+
} else {
|
|
1364
|
+
process.stderr.write(`${output}
|
|
1365
|
+
`);
|
|
1366
|
+
}
|
|
816
1367
|
process.exit(0);
|
|
817
1368
|
} catch (error) {
|
|
818
1369
|
handleRemoveError(error);
|
|
@@ -823,31 +1374,6 @@ function registerRemoveCommand(program) {
|
|
|
823
1374
|
await handleRemoveAction(identifier, options, program);
|
|
824
1375
|
});
|
|
825
1376
|
}
|
|
826
|
-
async function updateViaServer(identifier, updates, byUuid, server) {
|
|
827
|
-
const client = new ServerClient(server.baseUrl);
|
|
828
|
-
const items = await client.getAll();
|
|
829
|
-
const refToUpdate = byUuid ? items.find((item) => item.custom?.uuid === identifier) : items.find((item) => item.id === identifier);
|
|
830
|
-
if (!refToUpdate || !refToUpdate.custom?.uuid) {
|
|
831
|
-
process.stderr.write(`Error: Reference not found: ${identifier}
|
|
832
|
-
`);
|
|
833
|
-
process.exit(1);
|
|
834
|
-
}
|
|
835
|
-
const updatedItem = { ...refToUpdate, ...updates };
|
|
836
|
-
await client.update(refToUpdate.custom.uuid, updatedItem);
|
|
837
|
-
}
|
|
838
|
-
async function updateViaLibrary(identifier, updates, byUuid, libraryPath) {
|
|
839
|
-
const library = await Library.load(libraryPath);
|
|
840
|
-
const items = library.getAll().map((ref) => ref.getItem());
|
|
841
|
-
const result = await update(items, identifier, updates, { byUuid });
|
|
842
|
-
if (!result.updated || !result.item) {
|
|
843
|
-
throw new Error("Reference not found");
|
|
844
|
-
}
|
|
845
|
-
if (result.item.custom?.uuid) {
|
|
846
|
-
library.removeByUuid(result.item.custom.uuid);
|
|
847
|
-
library.add(result.item);
|
|
848
|
-
}
|
|
849
|
-
await library.save();
|
|
850
|
-
}
|
|
851
1377
|
function handleUpdateError(error) {
|
|
852
1378
|
const message = error instanceof Error ? error.message : String(error);
|
|
853
1379
|
if (message.includes("Parse error")) {
|
|
@@ -872,20 +1398,25 @@ async function handleUpdateAction(identifier, file, options, program) {
|
|
|
872
1398
|
const updates = parseJsonInput(inputStr);
|
|
873
1399
|
const updatesSchema = z.record(z.string(), z.unknown());
|
|
874
1400
|
const validatedUpdates = updatesSchema.parse(updates);
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
options.uuid ?? false,
|
|
883
|
-
config.library
|
|
884
|
-
);
|
|
1401
|
+
const context = await createExecutionContext(config, Library.load);
|
|
1402
|
+
const updateOptions = {
|
|
1403
|
+
identifier,
|
|
1404
|
+
updates: validatedUpdates
|
|
1405
|
+
};
|
|
1406
|
+
if (options.uuid !== void 0) {
|
|
1407
|
+
updateOptions.byUuid = options.uuid;
|
|
885
1408
|
}
|
|
886
|
-
|
|
1409
|
+
const result = await executeUpdate(updateOptions, context);
|
|
1410
|
+
const output = formatUpdateOutput(result, identifier);
|
|
1411
|
+
if (result.updated) {
|
|
1412
|
+
process.stderr.write(`${output}
|
|
887
1413
|
`);
|
|
888
|
-
|
|
1414
|
+
process.exit(0);
|
|
1415
|
+
} else {
|
|
1416
|
+
process.stderr.write(`${output}
|
|
1417
|
+
`);
|
|
1418
|
+
process.exit(1);
|
|
1419
|
+
}
|
|
889
1420
|
} catch (error) {
|
|
890
1421
|
handleUpdateError(error);
|
|
891
1422
|
}
|
|
@@ -895,6 +1426,34 @@ function registerUpdateCommand(program) {
|
|
|
895
1426
|
await handleUpdateAction(identifier, file, options, program);
|
|
896
1427
|
});
|
|
897
1428
|
}
|
|
1429
|
+
async function handleCiteAction(identifiers, options, program) {
|
|
1430
|
+
try {
|
|
1431
|
+
const globalOpts = program.opts();
|
|
1432
|
+
const config = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
1433
|
+
const context = await createExecutionContext(config, Library.load);
|
|
1434
|
+
const result = await executeCite({ ...options, identifiers }, context);
|
|
1435
|
+
const output = formatCiteOutput(result);
|
|
1436
|
+
if (output) {
|
|
1437
|
+
process.stdout.write(`${output}
|
|
1438
|
+
`);
|
|
1439
|
+
}
|
|
1440
|
+
const errors = formatCiteErrors(result);
|
|
1441
|
+
if (errors) {
|
|
1442
|
+
process.stderr.write(`${errors}
|
|
1443
|
+
`);
|
|
1444
|
+
}
|
|
1445
|
+
process.exit(getCiteExitCode(result));
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
1448
|
+
`);
|
|
1449
|
+
process.exit(4);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
function registerCiteCommand(program) {
|
|
1453
|
+
program.command("cite").description("Generate formatted citations for references").argument("<id-or-uuid...>", "Citation keys or UUIDs to cite").option("--uuid", "Treat arguments as UUIDs instead of IDs").option("--style <style>", "CSL style name").option("--csl-file <path>", "Path to custom CSL file").option("--locale <locale>", "Locale code (e.g., en-US, ja-JP)").option("--format <format>", "Output format: text|html|rtf").option("--in-text", "Generate in-text citations instead of bibliography entries").action(async (identifiers, options) => {
|
|
1454
|
+
await handleCiteAction(identifiers, options, program);
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
898
1457
|
function registerServerCommand(program) {
|
|
899
1458
|
const serverCmd = program.command("server").description("Manage HTTP server for library access");
|
|
900
1459
|
serverCmd.command("start").description("Start HTTP server").option("--port <port>", "Specify port number").option("-d, --daemon", "Run in background").action(async (options) => {
|
|
@@ -964,6 +1523,119 @@ Library: ${status.library}
|
|
|
964
1523
|
}
|
|
965
1524
|
});
|
|
966
1525
|
}
|
|
1526
|
+
function isValidFilePath(value) {
|
|
1527
|
+
return typeof value === "string" && value !== "" && value !== "true";
|
|
1528
|
+
}
|
|
1529
|
+
function parseFulltextAttachTypeAndPath(filePathArg, options) {
|
|
1530
|
+
if (options.pdf) {
|
|
1531
|
+
return { type: "pdf", filePath: isValidFilePath(options.pdf) ? options.pdf : filePathArg };
|
|
1532
|
+
}
|
|
1533
|
+
if (options.markdown) {
|
|
1534
|
+
return {
|
|
1535
|
+
type: "markdown",
|
|
1536
|
+
filePath: isValidFilePath(options.markdown) ? options.markdown : filePathArg
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
return { type: void 0, filePath: filePathArg };
|
|
1540
|
+
}
|
|
1541
|
+
async function handleFulltextAttachAction(identifier, filePathArg, options, program) {
|
|
1542
|
+
try {
|
|
1543
|
+
const globalOpts = program.opts();
|
|
1544
|
+
const config = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
1545
|
+
const context = await createExecutionContext(config, Library.load);
|
|
1546
|
+
const { type, filePath } = parseFulltextAttachTypeAndPath(filePathArg, options);
|
|
1547
|
+
const stdinContent = !filePath && type ? await readStdinBuffer() : void 0;
|
|
1548
|
+
const attachOptions = {
|
|
1549
|
+
identifier,
|
|
1550
|
+
fulltextDirectory: config.fulltext.directory,
|
|
1551
|
+
...filePath && { filePath },
|
|
1552
|
+
...type && { type },
|
|
1553
|
+
...options.move && { move: options.move },
|
|
1554
|
+
...options.force && { force: options.force },
|
|
1555
|
+
...options.uuid && { byUuid: options.uuid },
|
|
1556
|
+
...stdinContent && { stdinContent }
|
|
1557
|
+
};
|
|
1558
|
+
const result = await executeFulltextAttach(attachOptions, context);
|
|
1559
|
+
const output = formatFulltextAttachOutput(result);
|
|
1560
|
+
process.stderr.write(`${output}
|
|
1561
|
+
`);
|
|
1562
|
+
process.exit(getFulltextExitCode(result));
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
1565
|
+
`);
|
|
1566
|
+
process.exit(4);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
async function handleFulltextGetAction(identifier, options, program) {
|
|
1570
|
+
try {
|
|
1571
|
+
const globalOpts = program.opts();
|
|
1572
|
+
const config = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
1573
|
+
const context = await createExecutionContext(config, Library.load);
|
|
1574
|
+
const getOptions = {
|
|
1575
|
+
identifier,
|
|
1576
|
+
fulltextDirectory: config.fulltext.directory,
|
|
1577
|
+
...options.pdf && { type: "pdf" },
|
|
1578
|
+
...options.markdown && { type: "markdown" },
|
|
1579
|
+
...options.stdout && { stdout: options.stdout },
|
|
1580
|
+
...options.uuid && { byUuid: options.uuid }
|
|
1581
|
+
};
|
|
1582
|
+
const result = await executeFulltextGet(getOptions, context);
|
|
1583
|
+
if (result.success && result.content && options.stdout) {
|
|
1584
|
+
process.stdout.write(result.content);
|
|
1585
|
+
} else {
|
|
1586
|
+
const output = formatFulltextGetOutput(result);
|
|
1587
|
+
if (result.success) {
|
|
1588
|
+
process.stdout.write(`${output}
|
|
1589
|
+
`);
|
|
1590
|
+
} else {
|
|
1591
|
+
process.stderr.write(`${output}
|
|
1592
|
+
`);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
process.exit(getFulltextExitCode(result));
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
1598
|
+
`);
|
|
1599
|
+
process.exit(4);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
async function handleFulltextDetachAction(identifier, options, program) {
|
|
1603
|
+
try {
|
|
1604
|
+
const globalOpts = program.opts();
|
|
1605
|
+
const config = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
1606
|
+
const context = await createExecutionContext(config, Library.load);
|
|
1607
|
+
const detachOptions = {
|
|
1608
|
+
identifier,
|
|
1609
|
+
fulltextDirectory: config.fulltext.directory,
|
|
1610
|
+
...options.pdf && { type: "pdf" },
|
|
1611
|
+
...options.markdown && { type: "markdown" },
|
|
1612
|
+
...options.delete && { delete: options.delete },
|
|
1613
|
+
...options.force && { force: options.force },
|
|
1614
|
+
...options.uuid && { byUuid: options.uuid }
|
|
1615
|
+
};
|
|
1616
|
+
const result = await executeFulltextDetach(detachOptions, context);
|
|
1617
|
+
const output = formatFulltextDetachOutput(result);
|
|
1618
|
+
process.stderr.write(`${output}
|
|
1619
|
+
`);
|
|
1620
|
+
process.exit(getFulltextExitCode(result));
|
|
1621
|
+
} catch (error) {
|
|
1622
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
1623
|
+
`);
|
|
1624
|
+
process.exit(4);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
function registerFulltextCommand(program) {
|
|
1628
|
+
const fulltextCmd = program.command("fulltext").description("Manage full-text files attached to references");
|
|
1629
|
+
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) => {
|
|
1630
|
+
await handleFulltextAttachAction(identifier, filePath, options, program);
|
|
1631
|
+
});
|
|
1632
|
+
fulltextCmd.command("get").description("Get full-text file path or content").argument("<identifier>", "Citation key or UUID").option("--pdf", "Get PDF file only").option("--markdown", "Get Markdown file only").option("--stdout", "Output file content to stdout").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
|
|
1633
|
+
await handleFulltextGetAction(identifier, options, program);
|
|
1634
|
+
});
|
|
1635
|
+
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) => {
|
|
1636
|
+
await handleFulltextDetachAction(identifier, options, program);
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
967
1639
|
async function main(argv) {
|
|
968
1640
|
const program = createProgram();
|
|
969
1641
|
process.on("SIGINT", () => {
|