@ncukondo/reference-manager 0.14.0 → 0.15.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 +157 -13
- package/bin/reference-manager.js +0 -0
- package/dist/chunks/{action-menu-DNlpGiwS.js → action-menu-DvwR6nMj.js} +3 -3
- package/dist/chunks/{action-menu-DNlpGiwS.js.map → action-menu-DvwR6nMj.js.map} +1 -1
- package/dist/chunks/{file-watcher-D2Y-SlcE.js → file-watcher-B_WpVHSV.js} +18 -18
- package/dist/chunks/{file-watcher-D2Y-SlcE.js.map → file-watcher-B_WpVHSV.js.map} +1 -1
- package/dist/chunks/index-B_WCu-ZQ.js +10 -0
- package/dist/chunks/index-B_WCu-ZQ.js.map +1 -0
- package/dist/chunks/{index-UpzsmbyY.js → index-Bv5IgsL-.js} +2453 -497
- package/dist/chunks/index-Bv5IgsL-.js.map +1 -0
- package/dist/chunks/{index-4KSTJ3rp.js → index-DHgeuWGP.js} +122 -41
- package/dist/chunks/index-DHgeuWGP.js.map +1 -0
- package/dist/chunks/{loader-C1EpnyPm.js → loader-4FFB4igw.js} +66 -27
- package/dist/chunks/loader-4FFB4igw.js.map +1 -0
- package/dist/chunks/{reference-select-DSVwE9iu.js → reference-select-B9w9CLa1.js} +3 -3
- package/dist/chunks/{reference-select-DSVwE9iu.js.map → reference-select-B9w9CLa1.js.map} +1 -1
- package/dist/chunks/{style-select-CHjDTyq2.js → style-select-BNQHC79W.js} +2 -2
- package/dist/chunks/{style-select-CHjDTyq2.js.map → style-select-BNQHC79W.js.map} +1 -1
- package/dist/chunks/{tty-CDBIQraQ.js → tty-BMyaEOhX.js} +2 -2
- package/dist/chunks/tty-BMyaEOhX.js.map +1 -0
- package/dist/cli/commands/attach.d.ts +204 -0
- package/dist/cli/commands/attach.d.ts.map +1 -0
- package/dist/cli/commands/cite.d.ts +1 -1
- package/dist/cli/commands/config.d.ts +9 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/export.d.ts +1 -1
- package/dist/cli/commands/fulltext.d.ts +2 -2
- package/dist/cli/commands/fulltext.d.ts.map +1 -1
- package/dist/cli/commands/list.d.ts +2 -1
- package/dist/cli/commands/list.d.ts.map +1 -1
- package/dist/cli/commands/search.d.ts +3 -2
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/completion.d.ts +8 -0
- package/dist/cli/completion.d.ts.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/server-client.d.ts +37 -1
- package/dist/cli/server-client.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/config/defaults.d.ts +7 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/env-override.d.ts +36 -0
- package/dist/config/env-override.d.ts.map +1 -0
- package/dist/config/key-parser.d.ts +46 -0
- package/dist/config/key-parser.d.ts.map +1 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/schema.d.ts +22 -8
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/toml-writer.d.ts +25 -0
- package/dist/config/toml-writer.d.ts.map +1 -0
- package/dist/config/value-validator.d.ts +21 -0
- package/dist/config/value-validator.d.ts.map +1 -0
- package/dist/features/attachments/directory-manager.d.ts +40 -0
- package/dist/features/attachments/directory-manager.d.ts.map +1 -0
- package/dist/features/attachments/directory.d.ts +36 -0
- package/dist/features/attachments/directory.d.ts.map +1 -0
- package/dist/features/attachments/filename.d.ts +30 -0
- package/dist/features/attachments/filename.d.ts.map +1 -0
- package/dist/features/attachments/types.d.ts +38 -0
- package/dist/features/attachments/types.d.ts.map +1 -0
- package/dist/features/config/edit.d.ts +38 -0
- package/dist/features/config/edit.d.ts.map +1 -0
- package/dist/features/config/get.d.ts +35 -0
- package/dist/features/config/get.d.ts.map +1 -0
- package/dist/features/config/list-keys.d.ts +16 -0
- package/dist/features/config/list-keys.d.ts.map +1 -0
- package/dist/features/config/path.d.ts +29 -0
- package/dist/features/config/path.d.ts.map +1 -0
- package/dist/features/config/set.d.ts +27 -0
- package/dist/features/config/set.d.ts.map +1 -0
- package/dist/features/config/show.d.ts +20 -0
- package/dist/features/config/show.d.ts.map +1 -0
- package/dist/features/config/unset.d.ts +17 -0
- package/dist/features/config/unset.d.ts.map +1 -0
- package/dist/features/config/write-target.d.ts +30 -0
- package/dist/features/config/write-target.d.ts.map +1 -0
- package/dist/features/fulltext/manager.d.ts +1 -1
- package/dist/features/fulltext/manager.d.ts.map +1 -1
- package/dist/features/import/importer.d.ts.map +1 -1
- package/dist/features/interactive/tty.d.ts +2 -2
- package/dist/features/operations/attachments/add.d.ts +42 -0
- package/dist/features/operations/attachments/add.d.ts.map +1 -0
- package/dist/features/operations/attachments/detach.d.ts +38 -0
- package/dist/features/operations/attachments/detach.d.ts.map +1 -0
- package/dist/features/operations/attachments/get.d.ts +35 -0
- package/dist/features/operations/attachments/get.d.ts.map +1 -0
- package/dist/features/operations/attachments/index.d.ts +16 -0
- package/dist/features/operations/attachments/index.d.ts.map +1 -0
- package/dist/features/operations/attachments/list.d.ts +32 -0
- package/dist/features/operations/attachments/list.d.ts.map +1 -0
- package/dist/features/operations/attachments/open.d.ts +39 -0
- package/dist/features/operations/attachments/open.d.ts.map +1 -0
- package/dist/features/operations/attachments/sync.d.ts +50 -0
- package/dist/features/operations/attachments/sync.d.ts.map +1 -0
- package/dist/features/operations/fulltext/attach.d.ts +8 -2
- package/dist/features/operations/fulltext/attach.d.ts.map +1 -1
- package/dist/features/operations/fulltext/detach.d.ts +9 -3
- package/dist/features/operations/fulltext/detach.d.ts.map +1 -1
- package/dist/features/operations/fulltext/get.d.ts +8 -2
- package/dist/features/operations/fulltext/get.d.ts.map +1 -1
- package/dist/features/operations/fulltext/open.d.ts +8 -2
- package/dist/features/operations/fulltext/open.d.ts.map +1 -1
- package/dist/features/operations/fulltext-adapter/fulltext-adapter.d.ts +39 -0
- package/dist/features/operations/fulltext-adapter/fulltext-adapter.d.ts.map +1 -0
- package/dist/features/operations/fulltext-adapter/index.d.ts +7 -0
- package/dist/features/operations/fulltext-adapter/index.d.ts.map +1 -0
- package/dist/features/operations/index.d.ts +1 -0
- package/dist/features/operations/index.d.ts.map +1 -1
- package/dist/features/operations/library-operations.d.ts +43 -0
- package/dist/features/operations/library-operations.d.ts.map +1 -1
- package/dist/features/operations/operations-library.d.ts +7 -0
- package/dist/features/operations/operations-library.d.ts.map +1 -1
- package/dist/features/operations/remove.d.ts +1 -0
- package/dist/features/operations/remove.d.ts.map +1 -1
- package/dist/index.js +15 -15
- package/dist/index.js.map +1 -1
- package/dist/server.js +3 -3
- package/dist/utils/opener.d.ts +6 -1
- package/dist/utils/opener.d.ts.map +1 -1
- package/dist/utils/path.d.ts +28 -0
- package/dist/utils/path.d.ts.map +1 -0
- package/package.json +2 -1
- package/dist/chunks/index-4KSTJ3rp.js.map +0 -1
- package/dist/chunks/index-UpzsmbyY.js.map +0 -1
- package/dist/chunks/loader-C1EpnyPm.js.map +0 -1
- package/dist/chunks/tty-CDBIQraQ.js.map +0 -1
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { L as Library, p as pickDefined,
|
|
2
|
+
import { L as Library, p as pickDefined, a as sortOrderSchema, v as paginationOptionsSchema, F as FileWatcher, b as sortFieldSchema, u as searchSortFieldSchema } from "./file-watcher-B_WpVHSV.js";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
|
-
import { promises, readFileSync, existsSync,
|
|
4
|
+
import { promises, readFileSync, existsSync, writeFileSync, mkdirSync, mkdtempSync } 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
|
-
import { join,
|
|
8
|
+
import path__default, { extname, join, basename, dirname } from "node:path";
|
|
9
|
+
import fs__default, { stat, rename, copyFile, readFile, unlink, readdir, mkdir, rm } from "node:fs/promises";
|
|
10
|
+
import { g as getExtension, i as isValidFulltextFiles, a as isReservedRole, F as FULLTEXT_ROLE, b as formatToExtension, c as findFulltextFiles, d as findFulltextFile, e as extensionToFormat, B as BUILTIN_STYLES, h as getFulltextAttachmentTypes, s as startServerWithFileWatcher } from "./index-DHgeuWGP.js";
|
|
11
|
+
import { o as openWithSystemApp, l as loadConfig, e as getDefaultCurrentDirConfigFilename, h as getDefaultUserConfigPath } from "./loader-4FFB4igw.js";
|
|
9
12
|
import { spawn, spawnSync } from "node:child_process";
|
|
10
13
|
import process$1, { stdin, stdout } from "node:process";
|
|
11
|
-
import {
|
|
12
|
-
import { mkdir, unlink, rename, copyFile, rm, readFile } from "node:fs/promises";
|
|
13
|
-
import { u as updateReference, B as BUILTIN_STYLES, g as getFulltextAttachmentTypes, s as startServerWithFileWatcher } from "./index-4KSTJ3rp.js";
|
|
14
|
+
import { parse as parse$2, stringify as stringify$2 } from "@iarna/toml";
|
|
14
15
|
import "@citation-js/core";
|
|
15
16
|
import "@citation-js/plugin-csl";
|
|
16
17
|
import { ZodOptional as ZodOptional$2, z } from "zod";
|
|
17
18
|
import { serve } from "@hono/node-server";
|
|
18
19
|
const name = "@ncukondo/reference-manager";
|
|
19
|
-
const version$1 = "0.
|
|
20
|
+
const version$1 = "0.15.0";
|
|
20
21
|
const description$1 = "A local reference management tool using CSL-JSON as the single source of truth";
|
|
21
22
|
const packageJson = {
|
|
22
23
|
name,
|
|
@@ -260,6 +261,598 @@ function getExitCode(result) {
|
|
|
260
261
|
}
|
|
261
262
|
return 0;
|
|
262
263
|
}
|
|
264
|
+
function normalizePathForOutput(p) {
|
|
265
|
+
return p.replace(/\\/g, "/");
|
|
266
|
+
}
|
|
267
|
+
function extractUuidPrefix(uuid2) {
|
|
268
|
+
const normalized = uuid2.replace(/-/g, "");
|
|
269
|
+
return normalized.slice(0, 8);
|
|
270
|
+
}
|
|
271
|
+
function generateDirectoryName(ref2) {
|
|
272
|
+
const uuid2 = ref2.custom?.uuid;
|
|
273
|
+
if (!uuid2) {
|
|
274
|
+
throw new Error("Reference must have custom.uuid");
|
|
275
|
+
}
|
|
276
|
+
const uuidPrefix = extractUuidPrefix(uuid2);
|
|
277
|
+
const pmid = ref2.PMID?.trim();
|
|
278
|
+
if (pmid) {
|
|
279
|
+
return `${ref2.id}-PMID${pmid}-${uuidPrefix}`;
|
|
280
|
+
}
|
|
281
|
+
return `${ref2.id}-${uuidPrefix}`;
|
|
282
|
+
}
|
|
283
|
+
function getDirectoryPath(ref2, baseDir) {
|
|
284
|
+
const existingDir = ref2.custom?.attachments?.directory;
|
|
285
|
+
if (existingDir) {
|
|
286
|
+
return normalizePathForOutput(path__default.join(baseDir, existingDir));
|
|
287
|
+
}
|
|
288
|
+
const dirName = generateDirectoryName(ref2);
|
|
289
|
+
return normalizePathForOutput(path__default.join(baseDir, dirName));
|
|
290
|
+
}
|
|
291
|
+
async function ensureDirectory(ref2, baseDir) {
|
|
292
|
+
const dirPath = getDirectoryPath(ref2, baseDir);
|
|
293
|
+
try {
|
|
294
|
+
await fs__default.mkdir(dirPath, { recursive: true });
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (error.code !== "EEXIST") {
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return dirPath;
|
|
301
|
+
}
|
|
302
|
+
async function deleteDirectoryIfEmpty(dirPath) {
|
|
303
|
+
try {
|
|
304
|
+
const files = await fs__default.readdir(dirPath);
|
|
305
|
+
if (files.length === 0) {
|
|
306
|
+
await fs__default.rmdir(dirPath);
|
|
307
|
+
}
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (error.code !== "ENOENT") {
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function slugifyLabel(label) {
|
|
315
|
+
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
316
|
+
}
|
|
317
|
+
function generateFilename(role, ext, label) {
|
|
318
|
+
if (label) {
|
|
319
|
+
const slug = slugifyLabel(label);
|
|
320
|
+
return `${role}-${slug}.${ext}`;
|
|
321
|
+
}
|
|
322
|
+
return `${role}.${ext}`;
|
|
323
|
+
}
|
|
324
|
+
function parseFilename(filename) {
|
|
325
|
+
if (!filename) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
const ext = path__default.extname(filename);
|
|
329
|
+
const extWithoutDot = ext.startsWith(".") ? ext.slice(1) : ext;
|
|
330
|
+
const basename2 = ext ? filename.slice(0, -ext.length) : filename;
|
|
331
|
+
const firstHyphenIndex = basename2.indexOf("-");
|
|
332
|
+
if (firstHyphenIndex === -1) {
|
|
333
|
+
return {
|
|
334
|
+
role: basename2,
|
|
335
|
+
ext: extWithoutDot
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const role = basename2.slice(0, firstHyphenIndex);
|
|
339
|
+
const label = basename2.slice(firstHyphenIndex + 1);
|
|
340
|
+
if (label) {
|
|
341
|
+
return {
|
|
342
|
+
role,
|
|
343
|
+
ext: extWithoutDot,
|
|
344
|
+
label
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
role,
|
|
349
|
+
ext: extWithoutDot
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
async function checkSourceFile(filePath) {
|
|
353
|
+
try {
|
|
354
|
+
await stat(filePath);
|
|
355
|
+
return null;
|
|
356
|
+
} catch {
|
|
357
|
+
return `Source file not found: ${filePath}`;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function validateFulltextConstraint(existingFiles, newFile) {
|
|
361
|
+
if (newFile.role !== "fulltext") {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
const newExt = getExtension(newFile.filename);
|
|
365
|
+
const existingFulltexts = existingFiles.filter((f) => f.role === "fulltext");
|
|
366
|
+
for (const existing of existingFulltexts) {
|
|
367
|
+
const existingExt = getExtension(existing.filename);
|
|
368
|
+
if (existingExt === newExt) {
|
|
369
|
+
return `A fulltext ${newExt.toUpperCase()} already exists. Use --force to overwrite.`;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const testFiles = [...existingFiles, newFile];
|
|
373
|
+
if (!isValidFulltextFiles(testFiles)) {
|
|
374
|
+
return "fulltext role allows max 2 files (1 PDF + 1 Markdown)";
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
function findExistingFile(files, filename) {
|
|
379
|
+
return files.find((f) => f.filename === filename);
|
|
380
|
+
}
|
|
381
|
+
async function copyOrMoveFile(sourcePath, destPath, move) {
|
|
382
|
+
try {
|
|
383
|
+
if (move) {
|
|
384
|
+
await rename(sourcePath, destPath);
|
|
385
|
+
} else {
|
|
386
|
+
await copyFile(sourcePath, destPath);
|
|
387
|
+
}
|
|
388
|
+
return null;
|
|
389
|
+
} catch (error) {
|
|
390
|
+
return `Failed to ${move ? "move" : "copy"} file: ${error instanceof Error ? error.message : String(error)}`;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async function updateAttachmentMetadata$1(library, item, updatedAttachments) {
|
|
394
|
+
await library.update(item.id, {
|
|
395
|
+
custom: {
|
|
396
|
+
...item.custom,
|
|
397
|
+
attachments: updatedAttachments
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
function buildUpdatedFiles$1(existingFiles, newFile, existingFile) {
|
|
402
|
+
if (existingFile) {
|
|
403
|
+
return existingFiles.map((f) => f.filename === newFile.filename ? newFile : f);
|
|
404
|
+
}
|
|
405
|
+
return [...existingFiles, newFile];
|
|
406
|
+
}
|
|
407
|
+
async function addAttachment(library, options) {
|
|
408
|
+
const {
|
|
409
|
+
identifier,
|
|
410
|
+
filePath,
|
|
411
|
+
role,
|
|
412
|
+
label,
|
|
413
|
+
move = false,
|
|
414
|
+
force = false,
|
|
415
|
+
idType = "id",
|
|
416
|
+
attachmentsDirectory
|
|
417
|
+
} = options;
|
|
418
|
+
const item = await library.find(identifier, { idType });
|
|
419
|
+
if (!item) {
|
|
420
|
+
return { success: false, error: `Reference '${identifier}' not found` };
|
|
421
|
+
}
|
|
422
|
+
const uuid2 = item.custom?.uuid;
|
|
423
|
+
if (!uuid2) {
|
|
424
|
+
return { success: false, error: "Reference has no UUID. Cannot create attachment directory." };
|
|
425
|
+
}
|
|
426
|
+
const sourceError = await checkSourceFile(filePath);
|
|
427
|
+
if (sourceError) {
|
|
428
|
+
return { success: false, error: sourceError };
|
|
429
|
+
}
|
|
430
|
+
const ext = extname(filePath).slice(1).toLowerCase();
|
|
431
|
+
const filename = generateFilename(role, ext, label);
|
|
432
|
+
const existingAttachments = item.custom?.attachments;
|
|
433
|
+
const existingFiles = existingAttachments?.files ?? [];
|
|
434
|
+
const newFile = {
|
|
435
|
+
filename,
|
|
436
|
+
role,
|
|
437
|
+
...label && { label }
|
|
438
|
+
};
|
|
439
|
+
const existingFile = findExistingFile(existingFiles, filename);
|
|
440
|
+
if (!existingFile || !force) {
|
|
441
|
+
const constraintError = validateFulltextConstraint(existingFiles, newFile);
|
|
442
|
+
if (constraintError) {
|
|
443
|
+
return { success: false, error: constraintError };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (existingFile && !force) {
|
|
447
|
+
return {
|
|
448
|
+
success: false,
|
|
449
|
+
existingFile: filename,
|
|
450
|
+
requiresConfirmation: true
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const ref2 = item;
|
|
454
|
+
const dirPath = await ensureDirectory(ref2, attachmentsDirectory);
|
|
455
|
+
const dirName = existingAttachments?.directory ?? generateDirectoryName(ref2);
|
|
456
|
+
const destPath = join(dirPath, filename);
|
|
457
|
+
const copyError = await copyOrMoveFile(filePath, destPath, move);
|
|
458
|
+
if (copyError) {
|
|
459
|
+
return { success: false, error: copyError };
|
|
460
|
+
}
|
|
461
|
+
const updatedFiles = buildUpdatedFiles$1(existingFiles, newFile, existingFile);
|
|
462
|
+
const updatedAttachments = {
|
|
463
|
+
directory: dirName,
|
|
464
|
+
files: updatedFiles
|
|
465
|
+
};
|
|
466
|
+
await updateAttachmentMetadata$1(library, item, updatedAttachments);
|
|
467
|
+
await library.save();
|
|
468
|
+
return {
|
|
469
|
+
success: true,
|
|
470
|
+
filename,
|
|
471
|
+
directory: dirName,
|
|
472
|
+
overwritten: !!existingFile
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
async function listAttachments(library, options) {
|
|
476
|
+
const { identifier, idType = "id", role } = options;
|
|
477
|
+
const item = await library.find(identifier, { idType });
|
|
478
|
+
if (!item) {
|
|
479
|
+
return { success: false, files: [], error: `Reference '${identifier}' not found` };
|
|
480
|
+
}
|
|
481
|
+
const attachments = item.custom?.attachments;
|
|
482
|
+
if (!attachments || attachments.files.length === 0) {
|
|
483
|
+
return { success: false, files: [], error: `No attachments for reference '${identifier}'` };
|
|
484
|
+
}
|
|
485
|
+
let files = attachments.files;
|
|
486
|
+
if (role) {
|
|
487
|
+
files = files.filter((f) => f.role === role);
|
|
488
|
+
if (files.length === 0) {
|
|
489
|
+
return {
|
|
490
|
+
success: false,
|
|
491
|
+
files: [],
|
|
492
|
+
error: `No ${role} attachments for reference '${identifier}'`
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
success: true,
|
|
498
|
+
directory: attachments.directory,
|
|
499
|
+
files
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function findAttachment(files, filename, role) {
|
|
503
|
+
if (filename) {
|
|
504
|
+
return files.find((f) => f.filename === filename);
|
|
505
|
+
}
|
|
506
|
+
if (role) {
|
|
507
|
+
return files.find((f) => f.role === role);
|
|
508
|
+
}
|
|
509
|
+
return void 0;
|
|
510
|
+
}
|
|
511
|
+
async function getAttachment(library, options) {
|
|
512
|
+
const {
|
|
513
|
+
identifier,
|
|
514
|
+
filename,
|
|
515
|
+
role,
|
|
516
|
+
idType = "id",
|
|
517
|
+
stdout: stdout2 = false,
|
|
518
|
+
attachmentsDirectory
|
|
519
|
+
} = options;
|
|
520
|
+
const item = await library.find(identifier, { idType });
|
|
521
|
+
if (!item) {
|
|
522
|
+
return { success: false, error: `Reference '${identifier}' not found` };
|
|
523
|
+
}
|
|
524
|
+
const attachments = item.custom?.attachments;
|
|
525
|
+
if (!attachments || attachments.files.length === 0) {
|
|
526
|
+
return { success: false, error: `No attachments for reference '${identifier}'` };
|
|
527
|
+
}
|
|
528
|
+
const attachment = findAttachment(attachments.files, filename, role);
|
|
529
|
+
if (!attachment) {
|
|
530
|
+
if (filename) {
|
|
531
|
+
return { success: false, error: `Attachment '${filename}' not found` };
|
|
532
|
+
}
|
|
533
|
+
if (role) {
|
|
534
|
+
return { success: false, error: `No ${role} attachment found` };
|
|
535
|
+
}
|
|
536
|
+
return { success: false, error: "No filename or role specified" };
|
|
537
|
+
}
|
|
538
|
+
const filePath = join(attachmentsDirectory, attachments.directory, attachment.filename);
|
|
539
|
+
const normalizedPath = normalizePathForOutput(filePath);
|
|
540
|
+
if (stdout2) {
|
|
541
|
+
try {
|
|
542
|
+
const content = await readFile(filePath);
|
|
543
|
+
return { success: true, path: normalizedPath, content };
|
|
544
|
+
} catch {
|
|
545
|
+
return {
|
|
546
|
+
success: false,
|
|
547
|
+
error: `File not found on disk: ${normalizedPath}`
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return { success: true, path: normalizedPath };
|
|
552
|
+
}
|
|
553
|
+
function findFilesToDetach(files, filename, role, all) {
|
|
554
|
+
if (filename) {
|
|
555
|
+
const file = files.find((f) => f.filename === filename);
|
|
556
|
+
return file ? [file] : [];
|
|
557
|
+
}
|
|
558
|
+
if (role && all) {
|
|
559
|
+
return files.filter((f) => f.role === role);
|
|
560
|
+
}
|
|
561
|
+
return [];
|
|
562
|
+
}
|
|
563
|
+
async function deleteFiles(dirPath, filenames) {
|
|
564
|
+
const deleted = [];
|
|
565
|
+
for (const filename of filenames) {
|
|
566
|
+
try {
|
|
567
|
+
await unlink(join(dirPath, filename));
|
|
568
|
+
deleted.push(filename);
|
|
569
|
+
} catch {
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return deleted;
|
|
573
|
+
}
|
|
574
|
+
async function updateMetadata(library, item, attachments, remainingFiles) {
|
|
575
|
+
const updatedAttachments = remainingFiles.length > 0 ? { directory: attachments.directory, files: remainingFiles } : void 0;
|
|
576
|
+
await library.update(item.id, {
|
|
577
|
+
custom: {
|
|
578
|
+
...item.custom,
|
|
579
|
+
attachments: updatedAttachments
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
async function tryDeleteEmptyDirectory(dirPath) {
|
|
584
|
+
try {
|
|
585
|
+
await deleteDirectoryIfEmpty(dirPath);
|
|
586
|
+
try {
|
|
587
|
+
await stat(dirPath);
|
|
588
|
+
return false;
|
|
589
|
+
} catch {
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
} catch {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function errorResult$1(error) {
|
|
597
|
+
return { success: false, detached: [], deleted: [], error };
|
|
598
|
+
}
|
|
599
|
+
async function detachAttachment(library, options) {
|
|
600
|
+
const {
|
|
601
|
+
identifier,
|
|
602
|
+
filename,
|
|
603
|
+
role,
|
|
604
|
+
all = false,
|
|
605
|
+
removeFiles = false,
|
|
606
|
+
idType = "id",
|
|
607
|
+
attachmentsDirectory
|
|
608
|
+
} = options;
|
|
609
|
+
if (!filename && !role) {
|
|
610
|
+
return errorResult$1("Either filename or role must be specified");
|
|
611
|
+
}
|
|
612
|
+
const item = await library.find(identifier, { idType });
|
|
613
|
+
if (!item) {
|
|
614
|
+
return errorResult$1(`Reference '${identifier}' not found`);
|
|
615
|
+
}
|
|
616
|
+
const attachments = item.custom?.attachments;
|
|
617
|
+
if (!attachments || attachments.files.length === 0) {
|
|
618
|
+
return errorResult$1(`No attachments for reference '${identifier}'`);
|
|
619
|
+
}
|
|
620
|
+
const filesToDetach = findFilesToDetach(attachments.files, filename, role, all);
|
|
621
|
+
if (filesToDetach.length === 0) {
|
|
622
|
+
if (filename) {
|
|
623
|
+
return errorResult$1(`Attachment '${filename}' not found`);
|
|
624
|
+
}
|
|
625
|
+
return errorResult$1(`No ${role} attachments found`);
|
|
626
|
+
}
|
|
627
|
+
const detachedFilenames = filesToDetach.map((f) => f.filename);
|
|
628
|
+
const dirPath = join(attachmentsDirectory, attachments.directory);
|
|
629
|
+
const deletedFiles = removeFiles ? await deleteFiles(dirPath, detachedFilenames) : [];
|
|
630
|
+
const remainingFiles = attachments.files.filter((f) => !detachedFilenames.includes(f.filename));
|
|
631
|
+
await updateMetadata(library, item, attachments, remainingFiles);
|
|
632
|
+
await library.save();
|
|
633
|
+
const directoryDeleted = removeFiles && remainingFiles.length === 0 ? await tryDeleteEmptyDirectory(dirPath) : false;
|
|
634
|
+
return {
|
|
635
|
+
success: true,
|
|
636
|
+
detached: detachedFilenames,
|
|
637
|
+
deleted: deletedFiles,
|
|
638
|
+
directoryDeleted
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function errorResult(error) {
|
|
642
|
+
return { success: false, newFiles: [], missingFiles: [], applied: false, error };
|
|
643
|
+
}
|
|
644
|
+
function inferFromFilename(filename) {
|
|
645
|
+
const parsed = parseFilename(filename);
|
|
646
|
+
if (!parsed) {
|
|
647
|
+
return { filename, role: "other", label: filename };
|
|
648
|
+
}
|
|
649
|
+
const { role, label } = parsed;
|
|
650
|
+
if (isReservedRole(role)) {
|
|
651
|
+
return label ? { filename, role, label } : { filename, role };
|
|
652
|
+
}
|
|
653
|
+
const basename2 = label ? `${role}-${label}` : role;
|
|
654
|
+
return { filename, role: "other", label: basename2 };
|
|
655
|
+
}
|
|
656
|
+
async function getFilesOnDisk(dirPath) {
|
|
657
|
+
try {
|
|
658
|
+
const entries = await readdir(dirPath);
|
|
659
|
+
const files = [];
|
|
660
|
+
for (const entry of entries) {
|
|
661
|
+
const entryPath = join(dirPath, entry);
|
|
662
|
+
const stats = await stat(entryPath);
|
|
663
|
+
if (stats.isFile()) {
|
|
664
|
+
files.push(entry);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return files;
|
|
668
|
+
} catch {
|
|
669
|
+
return [];
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
async function directoryExists(dirPath) {
|
|
673
|
+
try {
|
|
674
|
+
const stats = await stat(dirPath);
|
|
675
|
+
return stats.isDirectory();
|
|
676
|
+
} catch {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function findNewFiles(diskFiles, metadataFilenames) {
|
|
681
|
+
return diskFiles.filter((f) => !metadataFilenames.has(f)).map(inferFromFilename);
|
|
682
|
+
}
|
|
683
|
+
function findMissingFiles(metadataFiles, diskFilenames) {
|
|
684
|
+
return metadataFiles.filter((f) => !diskFilenames.has(f.filename)).map((f) => f.filename);
|
|
685
|
+
}
|
|
686
|
+
function buildUpdatedFiles(metadataFiles, newFiles, missingFiles, shouldApplyNew, shouldApplyFix) {
|
|
687
|
+
let updatedFiles = [...metadataFiles];
|
|
688
|
+
if (shouldApplyNew) {
|
|
689
|
+
for (const newFile of newFiles) {
|
|
690
|
+
const attachmentFile = {
|
|
691
|
+
filename: newFile.filename,
|
|
692
|
+
role: newFile.role,
|
|
693
|
+
...newFile.label && { label: newFile.label }
|
|
694
|
+
};
|
|
695
|
+
updatedFiles.push(attachmentFile);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (shouldApplyFix) {
|
|
699
|
+
const missingSet = new Set(missingFiles);
|
|
700
|
+
updatedFiles = updatedFiles.filter((f) => !missingSet.has(f.filename));
|
|
701
|
+
}
|
|
702
|
+
return updatedFiles;
|
|
703
|
+
}
|
|
704
|
+
async function updateAttachmentMetadata(library, item, attachments, updatedFiles) {
|
|
705
|
+
await library.update(item.id, {
|
|
706
|
+
custom: {
|
|
707
|
+
...item.custom,
|
|
708
|
+
attachments: {
|
|
709
|
+
...attachments,
|
|
710
|
+
files: updatedFiles
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
async function syncAttachments(library, options) {
|
|
716
|
+
const { identifier, yes = false, fix = false, idType = "id", attachmentsDirectory } = options;
|
|
717
|
+
const item = await library.find(identifier, { idType });
|
|
718
|
+
if (!item) {
|
|
719
|
+
return errorResult(`Reference '${identifier}' not found`);
|
|
720
|
+
}
|
|
721
|
+
const attachments = item.custom?.attachments;
|
|
722
|
+
if (!attachments?.directory) {
|
|
723
|
+
return errorResult(`No attachments for reference: ${identifier}`);
|
|
724
|
+
}
|
|
725
|
+
const dirPath = join(attachmentsDirectory, attachments.directory);
|
|
726
|
+
if (!await directoryExists(dirPath)) {
|
|
727
|
+
return errorResult(`Attachments directory does not exist: ${attachments.directory}`);
|
|
728
|
+
}
|
|
729
|
+
const metadataFiles = attachments.files || [];
|
|
730
|
+
const metadataFilenames = new Set(metadataFiles.map((f) => f.filename));
|
|
731
|
+
const diskFiles = await getFilesOnDisk(dirPath);
|
|
732
|
+
const diskFilenames = new Set(diskFiles);
|
|
733
|
+
const newFiles = findNewFiles(diskFiles, metadataFilenames);
|
|
734
|
+
const missingFiles = findMissingFiles(metadataFiles, diskFilenames);
|
|
735
|
+
const shouldApplyNew = yes && newFiles.length > 0;
|
|
736
|
+
const shouldApplyFix = fix && missingFiles.length > 0;
|
|
737
|
+
const shouldApply = shouldApplyNew || shouldApplyFix;
|
|
738
|
+
if (shouldApply) {
|
|
739
|
+
const updatedFiles = buildUpdatedFiles(
|
|
740
|
+
metadataFiles,
|
|
741
|
+
newFiles,
|
|
742
|
+
missingFiles,
|
|
743
|
+
shouldApplyNew,
|
|
744
|
+
shouldApplyFix
|
|
745
|
+
);
|
|
746
|
+
await updateAttachmentMetadata(library, item, attachments, updatedFiles);
|
|
747
|
+
await library.save();
|
|
748
|
+
}
|
|
749
|
+
return { success: true, newFiles, missingFiles, applied: shouldApply };
|
|
750
|
+
}
|
|
751
|
+
async function pathExists(path2) {
|
|
752
|
+
try {
|
|
753
|
+
await stat(path2);
|
|
754
|
+
return true;
|
|
755
|
+
} catch {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function findFileByRole(attachments, role) {
|
|
760
|
+
if (!attachments?.files) {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
const file = attachments.files.find((f) => f.role === role);
|
|
764
|
+
return file?.filename ?? null;
|
|
765
|
+
}
|
|
766
|
+
async function resolveDirectory(ref2, attachmentsDirectory) {
|
|
767
|
+
const attachments = ref2.custom?.attachments;
|
|
768
|
+
let dirPath;
|
|
769
|
+
let directoryCreated = false;
|
|
770
|
+
if (attachments?.directory) {
|
|
771
|
+
dirPath = join(attachmentsDirectory, attachments.directory);
|
|
772
|
+
} else {
|
|
773
|
+
dirPath = await ensureDirectory(ref2, attachmentsDirectory);
|
|
774
|
+
directoryCreated = true;
|
|
775
|
+
}
|
|
776
|
+
if (!await pathExists(dirPath)) {
|
|
777
|
+
dirPath = await ensureDirectory(ref2, attachmentsDirectory);
|
|
778
|
+
directoryCreated = true;
|
|
779
|
+
}
|
|
780
|
+
return { dirPath, directoryCreated };
|
|
781
|
+
}
|
|
782
|
+
async function updateDirectoryMetadata(library, ref2, dirPath) {
|
|
783
|
+
const dirName = basename(dirPath);
|
|
784
|
+
const item = ref2;
|
|
785
|
+
await library.update(ref2.id, {
|
|
786
|
+
custom: {
|
|
787
|
+
...item.custom,
|
|
788
|
+
attachments: {
|
|
789
|
+
directory: dirName,
|
|
790
|
+
files: []
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
await library.save();
|
|
795
|
+
}
|
|
796
|
+
async function resolveTargetPath(dirPath, filename, role, attachments) {
|
|
797
|
+
if (filename) {
|
|
798
|
+
const targetPath = join(dirPath, filename);
|
|
799
|
+
if (!await pathExists(targetPath)) {
|
|
800
|
+
return { error: `Attachment file not found: ${filename}` };
|
|
801
|
+
}
|
|
802
|
+
return { path: targetPath };
|
|
803
|
+
}
|
|
804
|
+
if (role) {
|
|
805
|
+
const foundFilename = findFileByRole(attachments, role);
|
|
806
|
+
if (!foundFilename) {
|
|
807
|
+
return { error: `No ${role} attachment found` };
|
|
808
|
+
}
|
|
809
|
+
const targetPath = join(dirPath, foundFilename);
|
|
810
|
+
if (!await pathExists(targetPath)) {
|
|
811
|
+
return { error: `Attachment file not found: ${foundFilename}` };
|
|
812
|
+
}
|
|
813
|
+
return { path: targetPath };
|
|
814
|
+
}
|
|
815
|
+
return { path: dirPath };
|
|
816
|
+
}
|
|
817
|
+
async function openAttachment(library, options) {
|
|
818
|
+
const {
|
|
819
|
+
identifier,
|
|
820
|
+
filename,
|
|
821
|
+
role,
|
|
822
|
+
print = false,
|
|
823
|
+
idType = "id",
|
|
824
|
+
attachmentsDirectory
|
|
825
|
+
} = options;
|
|
826
|
+
const item = await library.find(identifier, { idType });
|
|
827
|
+
if (!item) {
|
|
828
|
+
return { success: false, error: `Reference '${identifier}' not found` };
|
|
829
|
+
}
|
|
830
|
+
const ref2 = item;
|
|
831
|
+
if (!ref2.custom?.uuid) {
|
|
832
|
+
return {
|
|
833
|
+
success: false,
|
|
834
|
+
error: "Reference has no UUID. Cannot determine attachment directory."
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
const attachments = ref2.custom?.attachments;
|
|
838
|
+
const { dirPath, directoryCreated } = await resolveDirectory(ref2, attachmentsDirectory);
|
|
839
|
+
if (directoryCreated && !attachments?.directory) {
|
|
840
|
+
await updateDirectoryMetadata(library, ref2, dirPath);
|
|
841
|
+
}
|
|
842
|
+
const targetResult = await resolveTargetPath(dirPath, filename, role, attachments);
|
|
843
|
+
if ("error" in targetResult) {
|
|
844
|
+
return { success: false, error: targetResult.error };
|
|
845
|
+
}
|
|
846
|
+
if (!print) {
|
|
847
|
+
await openWithSystemApp(targetResult.path);
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
success: true,
|
|
851
|
+
// Normalize for output (forward slashes for cross-platform consistency)
|
|
852
|
+
path: normalizePathForOutput(targetResult.path),
|
|
853
|
+
directoryCreated
|
|
854
|
+
};
|
|
855
|
+
}
|
|
263
856
|
class OperationsLibrary {
|
|
264
857
|
constructor(library, citationConfig) {
|
|
265
858
|
this.library = library;
|
|
@@ -286,15 +879,15 @@ class OperationsLibrary {
|
|
|
286
879
|
}
|
|
287
880
|
// High-level operations
|
|
288
881
|
async search(options) {
|
|
289
|
-
const { searchReferences } = await import("./index-
|
|
882
|
+
const { searchReferences } = await import("./index-DHgeuWGP.js").then((n) => n.n);
|
|
290
883
|
return searchReferences(this.library, options);
|
|
291
884
|
}
|
|
292
885
|
async list(options) {
|
|
293
|
-
const { listReferences } = await import("./index-
|
|
886
|
+
const { listReferences } = await import("./index-DHgeuWGP.js").then((n) => n.m);
|
|
294
887
|
return listReferences(this.library, options ?? {});
|
|
295
888
|
}
|
|
296
889
|
async cite(options) {
|
|
297
|
-
const { citeReferences } = await import("./index-
|
|
890
|
+
const { citeReferences } = await import("./index-DHgeuWGP.js").then((n) => n.l);
|
|
298
891
|
const defaultStyle = options.defaultStyle ?? this.citationConfig?.defaultStyle;
|
|
299
892
|
const cslDirectory = options.cslDirectory ?? this.citationConfig?.cslDirectory;
|
|
300
893
|
const mergedOptions = {
|
|
@@ -305,9 +898,34 @@ class OperationsLibrary {
|
|
|
305
898
|
return citeReferences(this.library, mergedOptions);
|
|
306
899
|
}
|
|
307
900
|
async import(inputs, options) {
|
|
308
|
-
const { addReferences } = await import("./index-
|
|
901
|
+
const { addReferences } = await import("./index-DHgeuWGP.js").then((n) => n.k);
|
|
309
902
|
return addReferences(inputs, this.library, options ?? {});
|
|
310
903
|
}
|
|
904
|
+
// Attachment operations
|
|
905
|
+
async attachAdd(options) {
|
|
906
|
+
const { addAttachment: addAttachment2 } = await import("./index-B_WCu-ZQ.js");
|
|
907
|
+
return addAttachment2(this.library, options);
|
|
908
|
+
}
|
|
909
|
+
async attachList(options) {
|
|
910
|
+
const { listAttachments: listAttachments2 } = await import("./index-B_WCu-ZQ.js");
|
|
911
|
+
return listAttachments2(this.library, options);
|
|
912
|
+
}
|
|
913
|
+
async attachGet(options) {
|
|
914
|
+
const { getAttachment: getAttachment2 } = await import("./index-B_WCu-ZQ.js");
|
|
915
|
+
return getAttachment2(this.library, options);
|
|
916
|
+
}
|
|
917
|
+
async attachDetach(options) {
|
|
918
|
+
const { detachAttachment: detachAttachment2 } = await import("./index-B_WCu-ZQ.js");
|
|
919
|
+
return detachAttachment2(this.library, options);
|
|
920
|
+
}
|
|
921
|
+
async attachSync(options) {
|
|
922
|
+
const { syncAttachments: syncAttachments2 } = await import("./index-B_WCu-ZQ.js");
|
|
923
|
+
return syncAttachments2(this.library, options);
|
|
924
|
+
}
|
|
925
|
+
async attachOpen(options) {
|
|
926
|
+
const { openAttachment: openAttachment2 } = await import("./index-B_WCu-ZQ.js");
|
|
927
|
+
return openAttachment2(this.library, options);
|
|
928
|
+
}
|
|
311
929
|
}
|
|
312
930
|
class ServerClient {
|
|
313
931
|
constructor(baseUrl) {
|
|
@@ -494,20 +1112,125 @@ class ServerClient {
|
|
|
494
1112
|
}
|
|
495
1113
|
return await response.json();
|
|
496
1114
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
1115
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1116
|
+
// Attachment operations
|
|
1117
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1118
|
+
/**
|
|
1119
|
+
* Add attachment to a reference.
|
|
1120
|
+
* @param options - Add attachment options
|
|
1121
|
+
* @returns Result of the add operation
|
|
1122
|
+
*/
|
|
1123
|
+
async attachAdd(options) {
|
|
1124
|
+
const url = `${this.baseUrl}/api/attachments/add`;
|
|
1125
|
+
const response = await fetch(url, {
|
|
1126
|
+
method: "POST",
|
|
1127
|
+
headers: { "Content-Type": "application/json" },
|
|
1128
|
+
body: JSON.stringify(options)
|
|
1129
|
+
});
|
|
1130
|
+
if (!response.ok) {
|
|
1131
|
+
throw new Error(await response.text());
|
|
506
1132
|
}
|
|
507
|
-
return
|
|
1133
|
+
return await response.json();
|
|
508
1134
|
}
|
|
509
|
-
|
|
510
|
-
|
|
1135
|
+
/**
|
|
1136
|
+
* List attachments for a reference.
|
|
1137
|
+
* @param options - List attachments options
|
|
1138
|
+
* @returns List of attachments
|
|
1139
|
+
*/
|
|
1140
|
+
async attachList(options) {
|
|
1141
|
+
const url = `${this.baseUrl}/api/attachments/list`;
|
|
1142
|
+
const response = await fetch(url, {
|
|
1143
|
+
method: "POST",
|
|
1144
|
+
headers: { "Content-Type": "application/json" },
|
|
1145
|
+
body: JSON.stringify(options)
|
|
1146
|
+
});
|
|
1147
|
+
if (!response.ok) {
|
|
1148
|
+
throw new Error(await response.text());
|
|
1149
|
+
}
|
|
1150
|
+
return await response.json();
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Get attachment file path or content.
|
|
1154
|
+
* @param options - Get attachment options
|
|
1155
|
+
* @returns Attachment file path or content
|
|
1156
|
+
*/
|
|
1157
|
+
async attachGet(options) {
|
|
1158
|
+
const url = `${this.baseUrl}/api/attachments/get`;
|
|
1159
|
+
const response = await fetch(url, {
|
|
1160
|
+
method: "POST",
|
|
1161
|
+
headers: { "Content-Type": "application/json" },
|
|
1162
|
+
body: JSON.stringify(options)
|
|
1163
|
+
});
|
|
1164
|
+
if (!response.ok) {
|
|
1165
|
+
throw new Error(await response.text());
|
|
1166
|
+
}
|
|
1167
|
+
return await response.json();
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Detach attachment from a reference.
|
|
1171
|
+
* @param options - Detach attachment options
|
|
1172
|
+
* @returns Result of the detach operation
|
|
1173
|
+
*/
|
|
1174
|
+
async attachDetach(options) {
|
|
1175
|
+
const url = `${this.baseUrl}/api/attachments/detach`;
|
|
1176
|
+
const response = await fetch(url, {
|
|
1177
|
+
method: "POST",
|
|
1178
|
+
headers: { "Content-Type": "application/json" },
|
|
1179
|
+
body: JSON.stringify(options)
|
|
1180
|
+
});
|
|
1181
|
+
if (!response.ok) {
|
|
1182
|
+
throw new Error(await response.text());
|
|
1183
|
+
}
|
|
1184
|
+
return await response.json();
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Sync attachments with files on disk.
|
|
1188
|
+
* @param options - Sync attachment options
|
|
1189
|
+
* @returns Sync result
|
|
1190
|
+
*/
|
|
1191
|
+
async attachSync(options) {
|
|
1192
|
+
const url = `${this.baseUrl}/api/attachments/sync`;
|
|
1193
|
+
const response = await fetch(url, {
|
|
1194
|
+
method: "POST",
|
|
1195
|
+
headers: { "Content-Type": "application/json" },
|
|
1196
|
+
body: JSON.stringify(options)
|
|
1197
|
+
});
|
|
1198
|
+
if (!response.ok) {
|
|
1199
|
+
throw new Error(await response.text());
|
|
1200
|
+
}
|
|
1201
|
+
return await response.json();
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Open attachment directory or file.
|
|
1205
|
+
* @param options - Open attachment options
|
|
1206
|
+
* @returns Result of the open operation
|
|
1207
|
+
*/
|
|
1208
|
+
async attachOpen(options) {
|
|
1209
|
+
const url = `${this.baseUrl}/api/attachments/open`;
|
|
1210
|
+
const response = await fetch(url, {
|
|
1211
|
+
method: "POST",
|
|
1212
|
+
headers: { "Content-Type": "application/json" },
|
|
1213
|
+
body: JSON.stringify(options)
|
|
1214
|
+
});
|
|
1215
|
+
if (!response.ok) {
|
|
1216
|
+
throw new Error(await response.text());
|
|
1217
|
+
}
|
|
1218
|
+
return await response.json();
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
async function getServerConnection(libraryPath, config2) {
|
|
1222
|
+
const portfilePath = getPortfilePath();
|
|
1223
|
+
const portfileData = await readPortfile(portfilePath);
|
|
1224
|
+
if (!portfileData) {
|
|
1225
|
+
if (config2.server.autoStart) {
|
|
1226
|
+
await startServerDaemon$1(libraryPath);
|
|
1227
|
+
await waitForPortfile(5e3);
|
|
1228
|
+
return await getServerConnection(libraryPath, config2);
|
|
1229
|
+
}
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
if (!isProcessRunning(portfileData.pid)) {
|
|
1233
|
+
await removePortfile(portfilePath);
|
|
511
1234
|
return null;
|
|
512
1235
|
}
|
|
513
1236
|
if (!portfileData.library || portfileData.library !== libraryPath) {
|
|
@@ -652,145 +1375,1411 @@ async function readStdinBuffer() {
|
|
|
652
1375
|
}
|
|
653
1376
|
return Buffer.concat(chunks);
|
|
654
1377
|
}
|
|
655
|
-
async function
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
1378
|
+
async function executeAttachOpen(options, context) {
|
|
1379
|
+
const operationOptions = {
|
|
1380
|
+
identifier: options.identifier,
|
|
1381
|
+
attachmentsDirectory: options.attachmentsDirectory,
|
|
1382
|
+
...options.filename !== void 0 && { filename: options.filename },
|
|
1383
|
+
...options.role !== void 0 && { role: options.role },
|
|
1384
|
+
...options.print !== void 0 && { print: options.print },
|
|
1385
|
+
...options.idType !== void 0 && { idType: options.idType }
|
|
1386
|
+
};
|
|
1387
|
+
return openAttachment(context.library, operationOptions);
|
|
665
1388
|
}
|
|
666
|
-
function
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
...options.
|
|
673
|
-
...options.
|
|
674
|
-
...options.
|
|
1389
|
+
async function executeAttachAdd(options, context) {
|
|
1390
|
+
const operationOptions = {
|
|
1391
|
+
identifier: options.identifier,
|
|
1392
|
+
filePath: options.filePath,
|
|
1393
|
+
role: options.role,
|
|
1394
|
+
attachmentsDirectory: options.attachmentsDirectory,
|
|
1395
|
+
...options.label !== void 0 && { label: options.label },
|
|
1396
|
+
...options.move !== void 0 && { move: options.move },
|
|
1397
|
+
...options.force !== void 0 && { force: options.force },
|
|
1398
|
+
...options.idType !== void 0 && { idType: options.idType }
|
|
675
1399
|
};
|
|
1400
|
+
return addAttachment(context.library, operationOptions);
|
|
676
1401
|
}
|
|
677
|
-
async function
|
|
678
|
-
|
|
679
|
-
|
|
1402
|
+
async function executeAttachList(options, context) {
|
|
1403
|
+
const operationOptions = {
|
|
1404
|
+
identifier: options.identifier,
|
|
1405
|
+
...options.role !== void 0 && { role: options.role },
|
|
1406
|
+
...options.idType !== void 0 && { idType: options.idType }
|
|
1407
|
+
};
|
|
1408
|
+
return listAttachments(context.library, operationOptions);
|
|
680
1409
|
}
|
|
681
|
-
function
|
|
1410
|
+
async function executeAttachGet(options, context) {
|
|
1411
|
+
const operationOptions = {
|
|
1412
|
+
identifier: options.identifier,
|
|
1413
|
+
attachmentsDirectory: options.attachmentsDirectory,
|
|
1414
|
+
...options.filename !== void 0 && { filename: options.filename },
|
|
1415
|
+
...options.role !== void 0 && { role: options.role },
|
|
1416
|
+
...options.stdout !== void 0 && { stdout: options.stdout },
|
|
1417
|
+
...options.idType !== void 0 && { idType: options.idType }
|
|
1418
|
+
};
|
|
1419
|
+
return getAttachment(context.library, operationOptions);
|
|
1420
|
+
}
|
|
1421
|
+
async function executeAttachDetach(options, context) {
|
|
1422
|
+
const operationOptions = {
|
|
1423
|
+
identifier: options.identifier,
|
|
1424
|
+
attachmentsDirectory: options.attachmentsDirectory,
|
|
1425
|
+
...options.filename !== void 0 && { filename: options.filename },
|
|
1426
|
+
...options.role !== void 0 && { role: options.role },
|
|
1427
|
+
...options.all !== void 0 && { all: options.all },
|
|
1428
|
+
...options.removeFiles !== void 0 && { removeFiles: options.removeFiles },
|
|
1429
|
+
...options.idType !== void 0 && { idType: options.idType }
|
|
1430
|
+
};
|
|
1431
|
+
return detachAttachment(context.library, operationOptions);
|
|
1432
|
+
}
|
|
1433
|
+
async function executeAttachSync(options, context) {
|
|
1434
|
+
const operationOptions = {
|
|
1435
|
+
identifier: options.identifier,
|
|
1436
|
+
attachmentsDirectory: options.attachmentsDirectory,
|
|
1437
|
+
...options.yes !== void 0 && { yes: options.yes },
|
|
1438
|
+
...options.fix !== void 0 && { fix: options.fix },
|
|
1439
|
+
...options.idType !== void 0 && { idType: options.idType }
|
|
1440
|
+
};
|
|
1441
|
+
return syncAttachments(context.library, operationOptions);
|
|
1442
|
+
}
|
|
1443
|
+
function formatAttachOpenOutput(result) {
|
|
1444
|
+
if (!result.success) {
|
|
1445
|
+
return `Error: ${result.error}`;
|
|
1446
|
+
}
|
|
1447
|
+
if (result.directoryCreated) {
|
|
1448
|
+
return `Created and opened: ${result.path}`;
|
|
1449
|
+
}
|
|
1450
|
+
return `Opened: ${result.path}`;
|
|
1451
|
+
}
|
|
1452
|
+
function formatAttachAddOutput(result) {
|
|
1453
|
+
if (result.requiresConfirmation) {
|
|
1454
|
+
return `File already exists: ${result.existingFile}
|
|
1455
|
+
Use --force to overwrite.`;
|
|
1456
|
+
}
|
|
1457
|
+
if (!result.success) {
|
|
1458
|
+
return `Error: ${result.error}`;
|
|
1459
|
+
}
|
|
1460
|
+
if (result.overwritten) {
|
|
1461
|
+
return `Added (overwritten): ${result.filename}`;
|
|
1462
|
+
}
|
|
1463
|
+
return `Added: ${result.filename}`;
|
|
1464
|
+
}
|
|
1465
|
+
function formatAttachListOutput(result, identifier) {
|
|
1466
|
+
if (!result.success) {
|
|
1467
|
+
return `Error: ${result.error}`;
|
|
1468
|
+
}
|
|
1469
|
+
if (result.files.length === 0) {
|
|
1470
|
+
return `No attachments for reference: ${identifier}`;
|
|
1471
|
+
}
|
|
1472
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1473
|
+
for (const file of result.files) {
|
|
1474
|
+
const existing = grouped.get(file.role) ?? [];
|
|
1475
|
+
existing.push(file);
|
|
1476
|
+
grouped.set(file.role, existing);
|
|
1477
|
+
}
|
|
682
1478
|
const lines = [];
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1479
|
+
lines.push(`Attachments for ${identifier} (${result.directory}/):`);
|
|
1480
|
+
lines.push("");
|
|
1481
|
+
for (const [role, files] of grouped) {
|
|
1482
|
+
lines.push(`${role}:`);
|
|
1483
|
+
for (const file of files) {
|
|
1484
|
+
if (file.label) {
|
|
1485
|
+
lines.push(` ${file.filename} - "${file.label}"`);
|
|
1486
|
+
} else {
|
|
1487
|
+
lines.push(` ${file.filename}`);
|
|
1488
|
+
}
|
|
686
1489
|
}
|
|
1490
|
+
lines.push("");
|
|
687
1491
|
}
|
|
688
|
-
return lines.join("\n");
|
|
1492
|
+
return lines.join("\n").trimEnd();
|
|
689
1493
|
}
|
|
690
|
-
function
|
|
1494
|
+
function formatAttachDetachOutput(result) {
|
|
1495
|
+
if (!result.success) {
|
|
1496
|
+
return `Error: ${result.error}`;
|
|
1497
|
+
}
|
|
691
1498
|
const lines = [];
|
|
692
|
-
for (const
|
|
693
|
-
if (
|
|
694
|
-
lines.push(`
|
|
1499
|
+
for (const filename of result.detached) {
|
|
1500
|
+
if (result.deleted.includes(filename)) {
|
|
1501
|
+
lines.push(`Detached and deleted: ${filename}`);
|
|
1502
|
+
} else {
|
|
1503
|
+
lines.push(`Detached: ${filename}`);
|
|
695
1504
|
}
|
|
696
1505
|
}
|
|
1506
|
+
if (result.directoryDeleted) {
|
|
1507
|
+
lines.push("Directory removed.");
|
|
1508
|
+
}
|
|
697
1509
|
return lines.join("\n");
|
|
698
1510
|
}
|
|
699
|
-
function
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
1511
|
+
function pluralize(count, singular) {
|
|
1512
|
+
return count > 1 ? `${singular}s` : singular;
|
|
1513
|
+
}
|
|
1514
|
+
function formatNewFilesSection(result, lines) {
|
|
1515
|
+
const count = result.newFiles.length;
|
|
1516
|
+
if (count === 0) return;
|
|
1517
|
+
const verb = result.applied ? "Added" : "Found";
|
|
1518
|
+
const suffix = result.applied ? "" : " new";
|
|
1519
|
+
lines.push(`${verb} ${count}${suffix} ${pluralize(count, "file")}:`);
|
|
1520
|
+
for (const file of result.newFiles) {
|
|
1521
|
+
const labelPart = file.label ? `, label: "${file.label}"` : "";
|
|
1522
|
+
lines.push(` ${file.filename} → role: ${file.role}${labelPart}`);
|
|
1523
|
+
}
|
|
1524
|
+
lines.push("");
|
|
1525
|
+
}
|
|
1526
|
+
function formatMissingFilesSection(result, lines) {
|
|
1527
|
+
const count = result.missingFiles.length;
|
|
1528
|
+
if (count === 0) return;
|
|
1529
|
+
const header = result.applied ? `Removed ${count} missing ${pluralize(count, "file")} from metadata:` : `Missing ${count} ${pluralize(count, "file")} (in metadata but not on disk):`;
|
|
1530
|
+
lines.push(header);
|
|
1531
|
+
for (const filename of result.missingFiles) {
|
|
1532
|
+
lines.push(` ${filename}`);
|
|
704
1533
|
}
|
|
705
|
-
|
|
706
|
-
|
|
1534
|
+
lines.push("");
|
|
1535
|
+
}
|
|
1536
|
+
function formatAttachSyncOutput(result) {
|
|
1537
|
+
if (!result.success) {
|
|
1538
|
+
return `Error: ${result.error}`;
|
|
707
1539
|
}
|
|
708
|
-
|
|
1540
|
+
const hasNewFiles = result.newFiles.length > 0;
|
|
1541
|
+
const hasMissingFiles = result.missingFiles.length > 0;
|
|
1542
|
+
if (!hasNewFiles && !hasMissingFiles) {
|
|
1543
|
+
return "Already in sync.";
|
|
1544
|
+
}
|
|
1545
|
+
const lines = [];
|
|
1546
|
+
formatNewFilesSection(result, lines);
|
|
1547
|
+
formatMissingFilesSection(result, lines);
|
|
1548
|
+
if (!result.applied) {
|
|
1549
|
+
if (hasNewFiles) {
|
|
1550
|
+
lines.push("Run with --yes to add new files");
|
|
1551
|
+
}
|
|
1552
|
+
if (hasMissingFiles) {
|
|
1553
|
+
lines.push("Run with --fix to remove missing files");
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return lines.join("\n").trimEnd();
|
|
709
1557
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1558
|
+
function getAttachExitCode(result) {
|
|
1559
|
+
return result.success ? 0 : 1;
|
|
1560
|
+
}
|
|
1561
|
+
async function executeInteractiveSelect$1(context, config2) {
|
|
1562
|
+
const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
|
|
713
1563
|
const allReferences = await context.library.getAll();
|
|
714
1564
|
const identifiers = await selectReferencesOrExit(
|
|
715
1565
|
allReferences,
|
|
716
|
-
{ multiSelect:
|
|
717
|
-
config2.cli.
|
|
1566
|
+
{ multiSelect: false },
|
|
1567
|
+
config2.cli.tui
|
|
718
1568
|
);
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1569
|
+
return identifiers[0];
|
|
1570
|
+
}
|
|
1571
|
+
async function resolveIdentifier(identifierArg, context, config2) {
|
|
1572
|
+
if (identifierArg) {
|
|
1573
|
+
return identifierArg;
|
|
1574
|
+
}
|
|
1575
|
+
if (isTTY()) {
|
|
1576
|
+
return executeInteractiveSelect$1(context, config2);
|
|
1577
|
+
}
|
|
1578
|
+
const stdinId = await readIdentifierFromStdin();
|
|
1579
|
+
if (!stdinId) {
|
|
1580
|
+
process.stderr.write(
|
|
1581
|
+
"Error: No identifier provided. Provide an ID, pipe one via stdin, or run interactively in a TTY.\n"
|
|
1582
|
+
);
|
|
1583
|
+
process.exit(1);
|
|
1584
|
+
}
|
|
1585
|
+
return stdinId;
|
|
1586
|
+
}
|
|
1587
|
+
function displayNamingConvention(identifier, dirPath) {
|
|
1588
|
+
process.stderr.write(`
|
|
1589
|
+
Opening attachments directory for ${identifier}...
|
|
1590
|
+
|
|
1591
|
+
`);
|
|
1592
|
+
process.stderr.write("File naming convention:\n");
|
|
1593
|
+
process.stderr.write(" fulltext.pdf / fulltext.md - Paper body\n");
|
|
1594
|
+
process.stderr.write(" supplement-{label}.ext - Supplementary materials\n");
|
|
1595
|
+
process.stderr.write(" notes-{label}.ext - Your notes\n");
|
|
1596
|
+
process.stderr.write(" draft-{label}.ext - Draft versions\n");
|
|
1597
|
+
process.stderr.write(" {custom}-{label}.ext - Custom role\n\n");
|
|
1598
|
+
process.stderr.write(`Directory: ${dirPath}/
|
|
1599
|
+
|
|
1600
|
+
`);
|
|
1601
|
+
}
|
|
1602
|
+
async function waitForEnter() {
|
|
1603
|
+
return new Promise((resolve2) => {
|
|
1604
|
+
process.stderr.write("Press Enter when done editing...");
|
|
1605
|
+
process.stdin.setRawMode(true);
|
|
1606
|
+
process.stdin.resume();
|
|
1607
|
+
process.stdin.once("data", () => {
|
|
1608
|
+
process.stdin.setRawMode(false);
|
|
1609
|
+
process.stdin.pause();
|
|
1610
|
+
process.stderr.write("\n\n");
|
|
1611
|
+
resolve2();
|
|
724
1612
|
});
|
|
725
|
-
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
function displayInteractiveSyncResult(result, identifier) {
|
|
1616
|
+
if (result.newFiles.length === 0) {
|
|
1617
|
+
process.stderr.write("No new files detected.\n");
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
process.stderr.write("Scanning directory...\n\n");
|
|
1621
|
+
process.stderr.write(
|
|
1622
|
+
`Found ${result.newFiles.length} new file${result.newFiles.length > 1 ? "s" : ""}:
|
|
1623
|
+
`
|
|
1624
|
+
);
|
|
1625
|
+
for (const file of result.newFiles) {
|
|
1626
|
+
const labelPart = file.label ? `, label: "${file.label}"` : "";
|
|
1627
|
+
process.stderr.write(` ✓ ${file.filename} → role: ${file.role}${labelPart}
|
|
1628
|
+
`);
|
|
1629
|
+
}
|
|
1630
|
+
process.stderr.write(`
|
|
1631
|
+
Updated metadata for ${identifier}.
|
|
1632
|
+
`);
|
|
1633
|
+
}
|
|
1634
|
+
async function runInteractiveMode(identifier, dirPath, attachmentsDirectory, idType, context) {
|
|
1635
|
+
displayNamingConvention(identifier, dirPath);
|
|
1636
|
+
await waitForEnter();
|
|
1637
|
+
const syncResult = await executeAttachSync(
|
|
1638
|
+
{
|
|
1639
|
+
identifier,
|
|
1640
|
+
attachmentsDirectory,
|
|
1641
|
+
yes: true,
|
|
1642
|
+
...idType && { idType }
|
|
1643
|
+
},
|
|
1644
|
+
context
|
|
1645
|
+
);
|
|
1646
|
+
if (syncResult.success) {
|
|
1647
|
+
displayInteractiveSyncResult(syncResult, identifier);
|
|
1648
|
+
} else {
|
|
1649
|
+
process.stderr.write(`Sync error: ${syncResult.error}
|
|
1650
|
+
`);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
function buildOpenOptions(identifier, filenameArg, options, attachmentsDirectory) {
|
|
1654
|
+
return {
|
|
1655
|
+
identifier,
|
|
1656
|
+
attachmentsDirectory,
|
|
1657
|
+
...filenameArg && { filename: filenameArg },
|
|
1658
|
+
...options.print && { print: options.print },
|
|
1659
|
+
...options.role && { role: options.role },
|
|
1660
|
+
...options.uuid && { idType: "uuid" }
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
async function handleAttachOpenAction(identifierArg, filenameArg, options, globalOpts) {
|
|
1664
|
+
try {
|
|
1665
|
+
const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
1666
|
+
const context = await createExecutionContext(config2, Library.load);
|
|
1667
|
+
const identifier = await resolveIdentifier(identifierArg, context, config2);
|
|
1668
|
+
const isDirectoryMode = !filenameArg && !options.role;
|
|
1669
|
+
const shouldUseInteractive = isTTY() && isDirectoryMode && !options.print && !options.noSync;
|
|
1670
|
+
const openOptions = buildOpenOptions(
|
|
1671
|
+
identifier,
|
|
1672
|
+
filenameArg,
|
|
1673
|
+
options,
|
|
1674
|
+
config2.attachments.directory
|
|
1675
|
+
);
|
|
1676
|
+
const result = await executeAttachOpen(openOptions, context);
|
|
1677
|
+
if (!result.success) {
|
|
1678
|
+
process.stderr.write(`Error: ${result.error}
|
|
1679
|
+
`);
|
|
1680
|
+
process.exit(1);
|
|
1681
|
+
}
|
|
1682
|
+
if (options.print) {
|
|
1683
|
+
process.stdout.write(`${result.path}
|
|
1684
|
+
`);
|
|
726
1685
|
process.exit(0);
|
|
727
1686
|
}
|
|
728
|
-
|
|
1687
|
+
if (shouldUseInteractive) {
|
|
1688
|
+
await runInteractiveMode(
|
|
1689
|
+
identifier,
|
|
1690
|
+
result.path ?? "",
|
|
1691
|
+
config2.attachments.directory,
|
|
1692
|
+
options.uuid ? "uuid" : void 0,
|
|
1693
|
+
context
|
|
1694
|
+
);
|
|
1695
|
+
} else {
|
|
1696
|
+
process.stderr.write(`${formatAttachOpenOutput(result)}
|
|
1697
|
+
`);
|
|
1698
|
+
}
|
|
1699
|
+
process.exit(0);
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
1702
|
+
`);
|
|
1703
|
+
process.exit(4);
|
|
729
1704
|
}
|
|
730
|
-
return executeCite({ ...options, ...style && { style }, identifiers }, context);
|
|
731
1705
|
}
|
|
732
|
-
async function
|
|
1706
|
+
async function handleAttachAddAction(identifierArg, filePathArg, options, globalOpts) {
|
|
733
1707
|
try {
|
|
734
1708
|
const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
735
1709
|
const context = await createExecutionContext(config2, Library.load);
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1710
|
+
const identifier = await resolveIdentifier(identifierArg, context, config2);
|
|
1711
|
+
const addOptions = {
|
|
1712
|
+
identifier,
|
|
1713
|
+
filePath: filePathArg,
|
|
1714
|
+
role: options.role,
|
|
1715
|
+
attachmentsDirectory: config2.attachments.directory,
|
|
1716
|
+
...options.label && { label: options.label },
|
|
1717
|
+
...options.move && { move: options.move },
|
|
1718
|
+
...options.force && { force: options.force },
|
|
1719
|
+
...options.uuid && { idType: "uuid" }
|
|
1720
|
+
};
|
|
1721
|
+
const result = await executeAttachAdd(addOptions, context);
|
|
1722
|
+
const output = formatAttachAddOutput(result);
|
|
1723
|
+
process.stderr.write(`${output}
|
|
1724
|
+
`);
|
|
1725
|
+
process.exit(getAttachExitCode(result));
|
|
1726
|
+
} catch (error) {
|
|
1727
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
1728
|
+
`);
|
|
1729
|
+
process.exit(4);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
async function handleAttachListAction(identifierArg, options, globalOpts) {
|
|
1733
|
+
try {
|
|
1734
|
+
const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
1735
|
+
const context = await createExecutionContext(config2, Library.load);
|
|
1736
|
+
const identifier = await resolveIdentifier(identifierArg, context, config2);
|
|
1737
|
+
const listOptions = {
|
|
1738
|
+
identifier,
|
|
1739
|
+
attachmentsDirectory: config2.attachments.directory,
|
|
1740
|
+
...options.role && { role: options.role },
|
|
1741
|
+
...options.uuid && { idType: "uuid" }
|
|
1742
|
+
};
|
|
1743
|
+
const result = await executeAttachList(listOptions, context);
|
|
1744
|
+
const output = formatAttachListOutput(result, identifier);
|
|
1745
|
+
process.stdout.write(`${output}
|
|
1746
|
+
`);
|
|
1747
|
+
process.exit(getAttachExitCode(result));
|
|
1748
|
+
} catch (error) {
|
|
1749
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
1750
|
+
`);
|
|
1751
|
+
process.exit(4);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
async function handleAttachGetAction(identifierArg, filenameArg, options, globalOpts) {
|
|
1755
|
+
try {
|
|
1756
|
+
const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
1757
|
+
const context = await createExecutionContext(config2, Library.load);
|
|
1758
|
+
const identifier = await resolveIdentifier(identifierArg, context, config2);
|
|
1759
|
+
const getOptions = {
|
|
1760
|
+
identifier,
|
|
1761
|
+
attachmentsDirectory: config2.attachments.directory,
|
|
1762
|
+
...filenameArg && { filename: filenameArg },
|
|
1763
|
+
...options.role && { role: options.role },
|
|
1764
|
+
...options.stdout && { stdout: options.stdout },
|
|
1765
|
+
...options.uuid && { idType: "uuid" }
|
|
1766
|
+
};
|
|
1767
|
+
const result = await executeAttachGet(getOptions, context);
|
|
1768
|
+
if (result.success && result.content && options.stdout) {
|
|
1769
|
+
process.stdout.write(result.content);
|
|
1770
|
+
} else if (result.success) {
|
|
1771
|
+
process.stdout.write(`${result.path}
|
|
1772
|
+
`);
|
|
1773
|
+
} else {
|
|
1774
|
+
process.stderr.write(`Error: ${result.error}
|
|
1775
|
+
`);
|
|
1776
|
+
}
|
|
1777
|
+
process.exit(getAttachExitCode(result));
|
|
1778
|
+
} catch (error) {
|
|
1779
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
1780
|
+
`);
|
|
1781
|
+
process.exit(4);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
async function handleAttachDetachAction(identifierArg, filenameArg, options, globalOpts) {
|
|
1785
|
+
try {
|
|
1786
|
+
const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
1787
|
+
const context = await createExecutionContext(config2, Library.load);
|
|
1788
|
+
const identifier = await resolveIdentifier(identifierArg, context, config2);
|
|
1789
|
+
const detachOptions = {
|
|
1790
|
+
identifier,
|
|
1791
|
+
attachmentsDirectory: config2.attachments.directory,
|
|
1792
|
+
...filenameArg && { filename: filenameArg },
|
|
1793
|
+
...options.role && { role: options.role },
|
|
1794
|
+
...options.all && { all: options.all },
|
|
1795
|
+
...options.removeFiles && { removeFiles: options.removeFiles },
|
|
1796
|
+
...options.uuid && { idType: "uuid" }
|
|
1797
|
+
};
|
|
1798
|
+
const result = await executeAttachDetach(detachOptions, context);
|
|
1799
|
+
const output = formatAttachDetachOutput(result);
|
|
1800
|
+
process.stderr.write(`${output}
|
|
1801
|
+
`);
|
|
1802
|
+
process.exit(getAttachExitCode(result));
|
|
1803
|
+
} catch (error) {
|
|
1804
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
1805
|
+
`);
|
|
1806
|
+
process.exit(4);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
async function handleAttachSyncAction(identifierArg, options, globalOpts) {
|
|
1810
|
+
try {
|
|
1811
|
+
const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
1812
|
+
const context = await createExecutionContext(config2, Library.load);
|
|
1813
|
+
const identifier = await resolveIdentifier(identifierArg, context, config2);
|
|
1814
|
+
const syncOptions = {
|
|
1815
|
+
identifier,
|
|
1816
|
+
attachmentsDirectory: config2.attachments.directory,
|
|
1817
|
+
...options.yes && { yes: options.yes },
|
|
1818
|
+
...options.fix && { fix: options.fix },
|
|
1819
|
+
...options.uuid && { idType: "uuid" }
|
|
1820
|
+
};
|
|
1821
|
+
const result = await executeAttachSync(syncOptions, context);
|
|
1822
|
+
const output = formatAttachSyncOutput(result);
|
|
1823
|
+
process.stderr.write(`${output}
|
|
1824
|
+
`);
|
|
1825
|
+
process.exit(getAttachExitCode(result));
|
|
1826
|
+
} catch (error) {
|
|
1827
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
1828
|
+
`);
|
|
1829
|
+
process.exit(4);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
async function validateOptions$2(options) {
|
|
1833
|
+
if (options.output && !["text", "html", "rtf"].includes(options.output)) {
|
|
1834
|
+
throw new Error(`Invalid output format '${options.output}'. Must be one of: text, html, rtf`);
|
|
1835
|
+
}
|
|
1836
|
+
if (options.cslFile) {
|
|
1837
|
+
const fs2 = await import("node:fs");
|
|
1838
|
+
if (!fs2.existsSync(options.cslFile)) {
|
|
1839
|
+
throw new Error(`CSL file '${options.cslFile}' not found`);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
function buildCiteOptions(options) {
|
|
1844
|
+
return {
|
|
1845
|
+
identifiers: options.identifiers,
|
|
1846
|
+
...options.uuid && { idType: "uuid" },
|
|
1847
|
+
...options.style !== void 0 && { style: options.style },
|
|
1848
|
+
...options.cslFile !== void 0 && { cslFile: options.cslFile },
|
|
1849
|
+
...options.locale !== void 0 && { locale: options.locale },
|
|
1850
|
+
...options.output !== void 0 && { format: options.output },
|
|
1851
|
+
...options.inText !== void 0 && { inText: options.inText }
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
async function executeCite(options, context) {
|
|
1855
|
+
await validateOptions$2(options);
|
|
1856
|
+
return context.library.cite(buildCiteOptions(options));
|
|
1857
|
+
}
|
|
1858
|
+
function formatCiteOutput(result) {
|
|
1859
|
+
const lines = [];
|
|
1860
|
+
for (const r of result.results) {
|
|
1861
|
+
if (r.success) {
|
|
1862
|
+
lines.push(r.citation);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
return lines.join("\n");
|
|
1866
|
+
}
|
|
1867
|
+
function formatCiteErrors(result) {
|
|
1868
|
+
const lines = [];
|
|
1869
|
+
for (const r of result.results) {
|
|
1870
|
+
if (!r.success) {
|
|
1871
|
+
lines.push(`Error for '${r.identifier}': ${r.error}`);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
return lines.join("\n");
|
|
1875
|
+
}
|
|
1876
|
+
function getCiteExitCode(result) {
|
|
1877
|
+
const hasSuccess = result.results.some((r) => r.success);
|
|
1878
|
+
const hasError = result.results.some((r) => !r.success);
|
|
1879
|
+
if (hasSuccess) {
|
|
1880
|
+
return 0;
|
|
1881
|
+
}
|
|
1882
|
+
if (hasError) {
|
|
1883
|
+
return 1;
|
|
1884
|
+
}
|
|
1885
|
+
return 0;
|
|
1886
|
+
}
|
|
1887
|
+
async function executeInteractiveCite(options, context, config2) {
|
|
1888
|
+
const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
|
|
1889
|
+
const { runStyleSelect } = await import("./style-select-BNQHC79W.js");
|
|
1890
|
+
const allReferences = await context.library.getAll();
|
|
1891
|
+
const identifiers = await selectReferencesOrExit(
|
|
1892
|
+
allReferences,
|
|
1893
|
+
{ multiSelect: true },
|
|
1894
|
+
config2.cli.tui
|
|
1895
|
+
);
|
|
1896
|
+
let style = options.style;
|
|
1897
|
+
if (!style && !options.cslFile) {
|
|
1898
|
+
const styleResult = await runStyleSelect({
|
|
1899
|
+
cslDirectory: config2.citation.cslDirectory,
|
|
1900
|
+
defaultStyle: config2.citation.defaultStyle
|
|
1901
|
+
});
|
|
1902
|
+
if (styleResult.cancelled) {
|
|
1903
|
+
process.exit(0);
|
|
1904
|
+
}
|
|
1905
|
+
style = styleResult.style;
|
|
1906
|
+
}
|
|
1907
|
+
return executeCite({ ...options, ...style && { style }, identifiers }, context);
|
|
1908
|
+
}
|
|
1909
|
+
async function handleCiteAction(identifiers, options, globalOpts) {
|
|
1910
|
+
try {
|
|
1911
|
+
const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
1912
|
+
const context = await createExecutionContext(config2, Library.load);
|
|
1913
|
+
let result;
|
|
1914
|
+
if (identifiers.length === 0) {
|
|
1915
|
+
if (isTTY()) {
|
|
1916
|
+
result = await executeInteractiveCite(options, context, config2);
|
|
1917
|
+
} else {
|
|
1918
|
+
const stdinIds = await readIdentifiersFromStdin();
|
|
1919
|
+
if (stdinIds.length === 0) {
|
|
1920
|
+
process.stderr.write(
|
|
1921
|
+
"Error: No identifiers provided. Provide IDs, pipe them via stdin, or run interactively in a TTY.\n"
|
|
1922
|
+
);
|
|
1923
|
+
process.exit(1);
|
|
1924
|
+
}
|
|
1925
|
+
result = await executeCite({ ...options, identifiers: stdinIds }, context);
|
|
1926
|
+
}
|
|
1927
|
+
} else {
|
|
1928
|
+
result = await executeCite({ ...options, identifiers }, context);
|
|
1929
|
+
}
|
|
1930
|
+
const output = formatCiteOutput(result);
|
|
1931
|
+
if (output) {
|
|
1932
|
+
process.stdout.write(`${output}
|
|
1933
|
+
`);
|
|
1934
|
+
}
|
|
1935
|
+
const errors2 = formatCiteErrors(result);
|
|
1936
|
+
if (errors2) {
|
|
1937
|
+
process.stderr.write(`${errors2}
|
|
1938
|
+
`);
|
|
1939
|
+
}
|
|
1940
|
+
process.exit(getCiteExitCode(result));
|
|
1941
|
+
} catch (error) {
|
|
1942
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
1943
|
+
`);
|
|
1944
|
+
process.exit(4);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
const ENV_OVERRIDE_MAP = {
|
|
1948
|
+
REFERENCE_MANAGER_LIBRARY: "library",
|
|
1949
|
+
REFERENCE_MANAGER_ATTACHMENTS_DIR: "attachments.directory",
|
|
1950
|
+
REFERENCE_MANAGER_CLI_DEFAULT_LIMIT: "cli.default_limit",
|
|
1951
|
+
REFERENCE_MANAGER_MCP_DEFAULT_LIMIT: "mcp.default_limit",
|
|
1952
|
+
PUBMED_EMAIL: "pubmed.email",
|
|
1953
|
+
PUBMED_API_KEY: "pubmed.api_key"
|
|
1954
|
+
};
|
|
1955
|
+
const KEY_TO_ENV_MAP = Object.fromEntries(
|
|
1956
|
+
Object.entries(ENV_OVERRIDE_MAP).map(([envVar, configKey]) => [configKey, envVar])
|
|
1957
|
+
);
|
|
1958
|
+
function getEnvOverrideInfo(configKey) {
|
|
1959
|
+
const envVar = KEY_TO_ENV_MAP[configKey];
|
|
1960
|
+
if (!envVar) {
|
|
1961
|
+
return null;
|
|
1962
|
+
}
|
|
1963
|
+
const value = process.env[envVar];
|
|
1964
|
+
return {
|
|
1965
|
+
envVar,
|
|
1966
|
+
value: value ?? null
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
const CONFIG_KEY_REGISTRY = [
|
|
1970
|
+
// Top-level keys
|
|
1971
|
+
{ key: "library", type: "string", description: "Path to library file" },
|
|
1972
|
+
{
|
|
1973
|
+
key: "log_level",
|
|
1974
|
+
type: "enum",
|
|
1975
|
+
description: "Log level",
|
|
1976
|
+
enumValues: ["silent", "info", "debug"]
|
|
1977
|
+
},
|
|
1978
|
+
// backup section
|
|
1979
|
+
{ key: "backup.max_generations", type: "integer", description: "Maximum backup generations" },
|
|
1980
|
+
{ key: "backup.max_age_days", type: "integer", description: "Maximum backup age in days" },
|
|
1981
|
+
{ key: "backup.directory", type: "string", description: "Backup directory path" },
|
|
1982
|
+
// watch section
|
|
1983
|
+
{ key: "watch.debounce_ms", type: "integer", description: "File watch debounce delay (ms)" },
|
|
1984
|
+
{ key: "watch.poll_interval_ms", type: "integer", description: "File watch poll interval (ms)" },
|
|
1985
|
+
{
|
|
1986
|
+
key: "watch.retry_interval_ms",
|
|
1987
|
+
type: "integer",
|
|
1988
|
+
description: "File watch retry interval (ms)"
|
|
1989
|
+
},
|
|
1990
|
+
{ key: "watch.max_retries", type: "integer", description: "Maximum file watch retries" },
|
|
1991
|
+
// server section
|
|
1992
|
+
{ key: "server.auto_start", type: "boolean", description: "Auto-start server on CLI commands" },
|
|
1993
|
+
{
|
|
1994
|
+
key: "server.auto_stop_minutes",
|
|
1995
|
+
type: "integer",
|
|
1996
|
+
description: "Auto-stop server after idle minutes (0 = never)"
|
|
1997
|
+
},
|
|
1998
|
+
// citation section
|
|
1999
|
+
{ key: "citation.default_style", type: "string", description: "Default citation style" },
|
|
2000
|
+
{ key: "citation.csl_directory", type: "string[]", description: "CSL style file directories" },
|
|
2001
|
+
{ key: "citation.default_locale", type: "string", description: "Default locale for citations" },
|
|
2002
|
+
{
|
|
2003
|
+
key: "citation.default_format",
|
|
2004
|
+
type: "enum",
|
|
2005
|
+
description: "Default format",
|
|
2006
|
+
enumValues: ["text", "html", "rtf"]
|
|
2007
|
+
},
|
|
2008
|
+
// pubmed section
|
|
2009
|
+
{ key: "pubmed.email", type: "string", description: "Email for PubMed API", optional: true },
|
|
2010
|
+
{ key: "pubmed.api_key", type: "string", description: "API key for PubMed", optional: true },
|
|
2011
|
+
// fulltext section
|
|
2012
|
+
{ key: "fulltext.directory", type: "string", description: "Fulltext storage directory" },
|
|
2013
|
+
// cli section
|
|
2014
|
+
{
|
|
2015
|
+
key: "cli.default_limit",
|
|
2016
|
+
type: "integer",
|
|
2017
|
+
description: "Default result limit (0 = unlimited)"
|
|
2018
|
+
},
|
|
2019
|
+
{
|
|
2020
|
+
key: "cli.default_sort",
|
|
2021
|
+
type: "enum",
|
|
2022
|
+
description: "Default sort field",
|
|
2023
|
+
enumValues: ["created", "updated", "published", "author", "title"]
|
|
2024
|
+
},
|
|
2025
|
+
{
|
|
2026
|
+
key: "cli.default_order",
|
|
2027
|
+
type: "enum",
|
|
2028
|
+
description: "Default sort order",
|
|
2029
|
+
enumValues: ["asc", "desc"]
|
|
2030
|
+
},
|
|
2031
|
+
// cli.tui section
|
|
2032
|
+
{
|
|
2033
|
+
key: "cli.tui.limit",
|
|
2034
|
+
type: "integer",
|
|
2035
|
+
description: "Result limit in TUI mode"
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
key: "cli.tui.debounce_ms",
|
|
2039
|
+
type: "integer",
|
|
2040
|
+
description: "Search debounce delay (ms)"
|
|
2041
|
+
},
|
|
2042
|
+
// cli.edit section
|
|
2043
|
+
{
|
|
2044
|
+
key: "cli.edit.default_format",
|
|
2045
|
+
type: "enum",
|
|
2046
|
+
description: "Default edit format",
|
|
2047
|
+
enumValues: ["yaml", "json"]
|
|
2048
|
+
},
|
|
2049
|
+
// mcp section
|
|
2050
|
+
{ key: "mcp.default_limit", type: "integer", description: "Default result limit for MCP" }
|
|
2051
|
+
];
|
|
2052
|
+
const KEY_MAP = new Map(CONFIG_KEY_REGISTRY.map((info) => [info.key, info]));
|
|
2053
|
+
let allKeysCache = null;
|
|
2054
|
+
function parseConfigKey(key) {
|
|
2055
|
+
return key.split(".");
|
|
2056
|
+
}
|
|
2057
|
+
function isValidConfigKey(key) {
|
|
2058
|
+
if (!key) {
|
|
2059
|
+
return false;
|
|
2060
|
+
}
|
|
2061
|
+
return KEY_MAP.has(key);
|
|
2062
|
+
}
|
|
2063
|
+
function getConfigKeyInfo(key) {
|
|
2064
|
+
return KEY_MAP.get(key) ?? null;
|
|
2065
|
+
}
|
|
2066
|
+
function getAllConfigKeys(section) {
|
|
2067
|
+
if (!allKeysCache) {
|
|
2068
|
+
allKeysCache = CONFIG_KEY_REGISTRY.map((info) => info.key).sort();
|
|
2069
|
+
}
|
|
2070
|
+
if (!section) {
|
|
2071
|
+
return allKeysCache;
|
|
2072
|
+
}
|
|
2073
|
+
const prefix = `${section}.`;
|
|
2074
|
+
return allKeysCache.filter((key) => key.startsWith(prefix));
|
|
2075
|
+
}
|
|
2076
|
+
function toInternalPath(key) {
|
|
2077
|
+
const segments = parseConfigKey(key);
|
|
2078
|
+
return segments.map((segment, index) => {
|
|
2079
|
+
if (index === 0) {
|
|
2080
|
+
if (segment === "log_level") {
|
|
2081
|
+
return "logLevel";
|
|
2082
|
+
}
|
|
2083
|
+
return segment;
|
|
2084
|
+
}
|
|
2085
|
+
return snakeToCamel(segment);
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
function snakeToCamel(str2) {
|
|
2089
|
+
return str2.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
2090
|
+
}
|
|
2091
|
+
function validateConfigValue(key, value) {
|
|
2092
|
+
const keyInfo = getConfigKeyInfo(key);
|
|
2093
|
+
if (!keyInfo) {
|
|
2094
|
+
return { valid: false, error: `Unknown configuration key: '${key}'` };
|
|
2095
|
+
}
|
|
2096
|
+
return validateByType(keyInfo, value);
|
|
2097
|
+
}
|
|
2098
|
+
function validateByType(keyInfo, value) {
|
|
2099
|
+
switch (keyInfo.type) {
|
|
2100
|
+
case "string":
|
|
2101
|
+
return validateString(keyInfo, value);
|
|
2102
|
+
case "integer":
|
|
2103
|
+
return validateInteger(keyInfo, value);
|
|
2104
|
+
case "boolean":
|
|
2105
|
+
return validateBoolean(value);
|
|
2106
|
+
case "enum":
|
|
2107
|
+
return validateEnum(keyInfo, value);
|
|
2108
|
+
case "string[]":
|
|
2109
|
+
return validateStringArray(value);
|
|
2110
|
+
default:
|
|
2111
|
+
return { valid: false, error: `Unknown type: ${keyInfo.type}` };
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
function validateString(keyInfo, value) {
|
|
2115
|
+
if (typeof value !== "string") {
|
|
2116
|
+
return { valid: false, error: `Expected string, received ${typeof value}` };
|
|
2117
|
+
}
|
|
2118
|
+
if (value === "" && !keyInfo.optional) {
|
|
2119
|
+
return { valid: false, error: "Value cannot be empty" };
|
|
2120
|
+
}
|
|
2121
|
+
return { valid: true, value };
|
|
2122
|
+
}
|
|
2123
|
+
function validateInteger(keyInfo, value) {
|
|
2124
|
+
if (typeof value !== "number") {
|
|
2125
|
+
return { valid: false, error: `Expected number, received ${typeof value}` };
|
|
2126
|
+
}
|
|
2127
|
+
if (!Number.isInteger(value)) {
|
|
2128
|
+
return { valid: false, error: "Value must be an integer" };
|
|
2129
|
+
}
|
|
2130
|
+
const requiresPositive = keyInfo.key.startsWith("backup.") && keyInfo.key !== "backup.directory";
|
|
2131
|
+
if (requiresPositive && value <= 0) {
|
|
2132
|
+
return { valid: false, error: "Value must be a positive integer" };
|
|
2133
|
+
}
|
|
2134
|
+
if (!requiresPositive && value < 0) {
|
|
2135
|
+
return { valid: false, error: "Value must be a non-negative integer" };
|
|
2136
|
+
}
|
|
2137
|
+
return { valid: true, value };
|
|
2138
|
+
}
|
|
2139
|
+
function validateBoolean(value) {
|
|
2140
|
+
if (typeof value !== "boolean") {
|
|
2141
|
+
return { valid: false, error: `Expected boolean, received ${typeof value}` };
|
|
2142
|
+
}
|
|
2143
|
+
return { valid: true, value };
|
|
2144
|
+
}
|
|
2145
|
+
function validateEnum(keyInfo, value) {
|
|
2146
|
+
if (typeof value !== "string") {
|
|
2147
|
+
return { valid: false, error: `Expected string, received ${typeof value}` };
|
|
2148
|
+
}
|
|
2149
|
+
const allowedValues = keyInfo.enumValues ?? [];
|
|
2150
|
+
if (!allowedValues.includes(value)) {
|
|
2151
|
+
return {
|
|
2152
|
+
valid: false,
|
|
2153
|
+
error: `Invalid value '${value}'. Expected one of: ${allowedValues.join(", ")}`
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
return { valid: true, value };
|
|
2157
|
+
}
|
|
2158
|
+
function validateStringArray(value) {
|
|
2159
|
+
if (!Array.isArray(value)) {
|
|
2160
|
+
return { valid: false, error: "Expected array" };
|
|
2161
|
+
}
|
|
2162
|
+
for (const item of value) {
|
|
2163
|
+
if (typeof item !== "string") {
|
|
2164
|
+
return { valid: false, error: "All array elements must be strings" };
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
return { valid: true, value };
|
|
2168
|
+
}
|
|
2169
|
+
function parseValueForKey(key, stringValue) {
|
|
2170
|
+
const keyInfo = getConfigKeyInfo(key);
|
|
2171
|
+
if (!keyInfo) {
|
|
2172
|
+
return null;
|
|
2173
|
+
}
|
|
2174
|
+
switch (keyInfo.type) {
|
|
2175
|
+
case "string":
|
|
2176
|
+
case "enum":
|
|
2177
|
+
return stringValue;
|
|
2178
|
+
case "integer": {
|
|
2179
|
+
const num = Number(stringValue);
|
|
2180
|
+
if (Number.isNaN(num)) {
|
|
2181
|
+
return stringValue;
|
|
2182
|
+
}
|
|
2183
|
+
return num;
|
|
2184
|
+
}
|
|
2185
|
+
case "boolean":
|
|
2186
|
+
if (stringValue === "true") return true;
|
|
2187
|
+
if (stringValue === "false") return false;
|
|
2188
|
+
return stringValue;
|
|
2189
|
+
// Let validation catch the error
|
|
2190
|
+
case "string[]":
|
|
2191
|
+
return stringValue.split(",").map((s) => s.trim());
|
|
2192
|
+
default:
|
|
2193
|
+
return stringValue;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
function getConfigEditTarget(options, context) {
|
|
2197
|
+
const cwd = process.cwd();
|
|
2198
|
+
let path2;
|
|
2199
|
+
if (options.local) {
|
|
2200
|
+
path2 = join(cwd, getDefaultCurrentDirConfigFilename());
|
|
2201
|
+
} else {
|
|
2202
|
+
path2 = getDefaultUserConfigPath();
|
|
2203
|
+
}
|
|
2204
|
+
return {
|
|
2205
|
+
path: path2,
|
|
2206
|
+
exists: existsSync(path2)
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
function createConfigTemplate() {
|
|
2210
|
+
return `# Reference Manager Configuration
|
|
2211
|
+
# Documentation: https://github.com/ncukondo/reference-manager#configuration
|
|
2212
|
+
|
|
2213
|
+
# library = "~/.local/share/reference-manager/library.json"
|
|
2214
|
+
# log_level = "info" # silent, info, debug
|
|
2215
|
+
|
|
2216
|
+
[backup]
|
|
2217
|
+
# max_generations = 50
|
|
2218
|
+
# max_age_days = 365
|
|
2219
|
+
# directory = "~/.cache/reference-manager/backups"
|
|
2220
|
+
|
|
2221
|
+
[server]
|
|
2222
|
+
# auto_start = false
|
|
2223
|
+
# auto_stop_minutes = 0
|
|
2224
|
+
|
|
2225
|
+
[citation]
|
|
2226
|
+
# default_style = "apa"
|
|
2227
|
+
# default_locale = "en-US"
|
|
2228
|
+
# default_format = "text" # text, html, rtf
|
|
2229
|
+
# csl_directory = ["~/.local/share/reference-manager/csl"]
|
|
2230
|
+
|
|
2231
|
+
[pubmed]
|
|
2232
|
+
# email = ""
|
|
2233
|
+
# api_key = ""
|
|
2234
|
+
|
|
2235
|
+
[fulltext]
|
|
2236
|
+
# directory = "~/.local/share/reference-manager/fulltext"
|
|
2237
|
+
|
|
2238
|
+
[cli]
|
|
2239
|
+
# default_limit = 0 # 0 = unlimited
|
|
2240
|
+
# default_sort = "updated" # created, updated, published, author, title
|
|
2241
|
+
# default_order = "desc" # asc, desc
|
|
2242
|
+
|
|
2243
|
+
[cli.tui]
|
|
2244
|
+
# limit = 20
|
|
2245
|
+
# debounce_ms = 200
|
|
2246
|
+
|
|
2247
|
+
[cli.edit]
|
|
2248
|
+
# default_format = "yaml" # yaml, json
|
|
2249
|
+
|
|
2250
|
+
[mcp]
|
|
2251
|
+
# default_limit = 20
|
|
2252
|
+
`;
|
|
2253
|
+
}
|
|
2254
|
+
function getConfigValue(config2, key, options) {
|
|
2255
|
+
if (!isValidConfigKey(key)) {
|
|
2256
|
+
return { found: false, error: `Unknown configuration key: '${key}'` };
|
|
2257
|
+
}
|
|
2258
|
+
if (options.envOverride !== void 0 && !options.configOnly) {
|
|
2259
|
+
return {
|
|
2260
|
+
found: true,
|
|
2261
|
+
value: options.envOverride,
|
|
2262
|
+
fromEnv: true
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
const internalPath = toInternalPath(key);
|
|
2266
|
+
const value = getNestedValue(config2, internalPath);
|
|
2267
|
+
if (value === void 0 || value === null) {
|
|
2268
|
+
return { found: false, error: `Value for '${key}' is not set` };
|
|
2269
|
+
}
|
|
2270
|
+
return { found: true, value };
|
|
2271
|
+
}
|
|
2272
|
+
function getNestedValue(obj, path2) {
|
|
2273
|
+
let current = obj;
|
|
2274
|
+
for (const segment of path2) {
|
|
2275
|
+
if (current === null || current === void 0) {
|
|
2276
|
+
return void 0;
|
|
2277
|
+
}
|
|
2278
|
+
if (typeof current !== "object") {
|
|
2279
|
+
return void 0;
|
|
2280
|
+
}
|
|
2281
|
+
current = current[segment];
|
|
2282
|
+
}
|
|
2283
|
+
return current;
|
|
2284
|
+
}
|
|
2285
|
+
function formatValue(value) {
|
|
2286
|
+
if (Array.isArray(value)) {
|
|
2287
|
+
return value.join(",");
|
|
2288
|
+
}
|
|
2289
|
+
if (typeof value === "object" && value !== null) {
|
|
2290
|
+
return JSON.stringify(value);
|
|
2291
|
+
}
|
|
2292
|
+
return String(value);
|
|
2293
|
+
}
|
|
2294
|
+
const get = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
2295
|
+
__proto__: null,
|
|
2296
|
+
formatValue,
|
|
2297
|
+
getConfigValue
|
|
2298
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
2299
|
+
function listConfigKeys(options) {
|
|
2300
|
+
const keys = getAllConfigKeys(options.section);
|
|
2301
|
+
if (keys.length === 0) {
|
|
2302
|
+
return "";
|
|
2303
|
+
}
|
|
2304
|
+
const entries = keys.map((key) => {
|
|
2305
|
+
const info = getConfigKeyInfo(key);
|
|
2306
|
+
if (!info) {
|
|
2307
|
+
return null;
|
|
2308
|
+
}
|
|
2309
|
+
return {
|
|
2310
|
+
key: info.key,
|
|
2311
|
+
type: info.type,
|
|
2312
|
+
description: info.description
|
|
2313
|
+
};
|
|
2314
|
+
}).filter((e) => e !== null);
|
|
2315
|
+
const maxKeyWidth = Math.max(...entries.map((e) => e.key.length));
|
|
2316
|
+
const maxTypeWidth = Math.max(...entries.map((e) => e.type.length));
|
|
2317
|
+
const lines = entries.map((entry) => {
|
|
2318
|
+
const keyPadded = entry.key.padEnd(maxKeyWidth);
|
|
2319
|
+
const typePadded = entry.type.padEnd(maxTypeWidth);
|
|
2320
|
+
return `${keyPadded} ${typePadded} ${entry.description}`;
|
|
2321
|
+
});
|
|
2322
|
+
return lines.join("\n");
|
|
2323
|
+
}
|
|
2324
|
+
function getExistenceStatus(path2) {
|
|
2325
|
+
return existsSync(path2) ? "exists" : "not found";
|
|
2326
|
+
}
|
|
2327
|
+
function showConfigPaths(options, context) {
|
|
2328
|
+
const cwd = process.cwd();
|
|
2329
|
+
const userConfigPath = getDefaultUserConfigPath();
|
|
2330
|
+
const localConfigPath = join(cwd, getDefaultCurrentDirConfigFilename());
|
|
2331
|
+
const envConfigPath = process.env.REFERENCE_MANAGER_CONFIG;
|
|
2332
|
+
if (options.user) {
|
|
2333
|
+
return userConfigPath;
|
|
2334
|
+
}
|
|
2335
|
+
if (options.local) {
|
|
2336
|
+
return localConfigPath;
|
|
2337
|
+
}
|
|
2338
|
+
const lines = [];
|
|
2339
|
+
lines.push(`User: ${userConfigPath} (${getExistenceStatus(userConfigPath)})`);
|
|
2340
|
+
lines.push(`Local: ${localConfigPath} (${getExistenceStatus(localConfigPath)})`);
|
|
2341
|
+
if (envConfigPath) {
|
|
2342
|
+
lines.push(
|
|
2343
|
+
`Env: ${envConfigPath} (${getExistenceStatus(envConfigPath)}) (REFERENCE_MANAGER_CONFIG)`
|
|
2344
|
+
);
|
|
2345
|
+
}
|
|
2346
|
+
return lines.join("\n");
|
|
2347
|
+
}
|
|
2348
|
+
function serializeToTOML$1(obj) {
|
|
2349
|
+
return stringify$2(obj);
|
|
2350
|
+
}
|
|
2351
|
+
function parseKeyPath(key) {
|
|
2352
|
+
return key.split(".");
|
|
2353
|
+
}
|
|
2354
|
+
function setNestedValue(obj, keyPath, value) {
|
|
2355
|
+
if (keyPath.length === 0) {
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
let current = obj;
|
|
2359
|
+
for (let i = 0; i < keyPath.length - 1; i++) {
|
|
2360
|
+
const segment = keyPath[i];
|
|
2361
|
+
if (!(segment in current) || typeof current[segment] !== "object") {
|
|
2362
|
+
current[segment] = {};
|
|
2363
|
+
}
|
|
2364
|
+
current = current[segment];
|
|
2365
|
+
}
|
|
2366
|
+
const finalKey = keyPath[keyPath.length - 1];
|
|
2367
|
+
current[finalKey] = value;
|
|
2368
|
+
}
|
|
2369
|
+
function removeNestedValue(obj, keyPath) {
|
|
2370
|
+
if (keyPath.length === 0) {
|
|
2371
|
+
return false;
|
|
2372
|
+
}
|
|
2373
|
+
const firstKey = keyPath[0];
|
|
2374
|
+
if (keyPath.length === 1) {
|
|
2375
|
+
if (firstKey in obj) {
|
|
2376
|
+
delete obj[firstKey];
|
|
2377
|
+
return true;
|
|
2378
|
+
}
|
|
2379
|
+
return false;
|
|
2380
|
+
}
|
|
2381
|
+
if (!(firstKey in obj) || typeof obj[firstKey] !== "object") {
|
|
2382
|
+
return false;
|
|
2383
|
+
}
|
|
2384
|
+
const nested = obj[firstKey];
|
|
2385
|
+
const removed = removeNestedValue(nested, keyPath.slice(1));
|
|
2386
|
+
if (removed && Object.keys(nested).length === 0) {
|
|
2387
|
+
delete obj[firstKey];
|
|
2388
|
+
}
|
|
2389
|
+
return removed;
|
|
2390
|
+
}
|
|
2391
|
+
function loadExistingTOML(filePath) {
|
|
2392
|
+
if (!existsSync(filePath)) {
|
|
2393
|
+
return {};
|
|
2394
|
+
}
|
|
2395
|
+
try {
|
|
2396
|
+
const content = readFileSync(filePath, "utf-8");
|
|
2397
|
+
return parse$2(content);
|
|
2398
|
+
} catch {
|
|
2399
|
+
return {};
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
async function writeTOMLValue(filePath, key, value) {
|
|
2403
|
+
const dir = dirname(filePath);
|
|
2404
|
+
await mkdir(dir, { recursive: true });
|
|
2405
|
+
const obj = loadExistingTOML(filePath);
|
|
2406
|
+
const keyPath = parseKeyPath(key);
|
|
2407
|
+
setNestedValue(obj, keyPath, value);
|
|
2408
|
+
const content = serializeToTOML$1(obj);
|
|
2409
|
+
writeFileSync(filePath, content, "utf-8");
|
|
2410
|
+
}
|
|
2411
|
+
async function removeTOMLKey(filePath, key) {
|
|
2412
|
+
if (!existsSync(filePath)) {
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
const obj = loadExistingTOML(filePath);
|
|
2416
|
+
const keyPath = parseKeyPath(key);
|
|
2417
|
+
const removed = removeNestedValue(obj, keyPath);
|
|
2418
|
+
if (removed) {
|
|
2419
|
+
const content = serializeToTOML$1(obj);
|
|
2420
|
+
writeFileSync(filePath, content, "utf-8");
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
async function setConfigValue(configPath, key, value, options = {}) {
|
|
2424
|
+
if (!isValidConfigKey(key)) {
|
|
2425
|
+
return { success: false, error: `Unknown configuration key: '${key}'` };
|
|
2426
|
+
}
|
|
2427
|
+
const validation2 = validateConfigValue(key, value);
|
|
2428
|
+
if (!validation2.valid) {
|
|
2429
|
+
return { success: false, error: validation2.error ?? "Validation failed" };
|
|
2430
|
+
}
|
|
2431
|
+
try {
|
|
2432
|
+
await writeTOMLValue(configPath, key, value);
|
|
2433
|
+
} catch (error) {
|
|
2434
|
+
return {
|
|
2435
|
+
success: false,
|
|
2436
|
+
error: `Failed to write config: ${error instanceof Error ? error.message : String(error)}`
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
if (options.envOverrideInfo?.value !== void 0 && options.envOverrideInfo?.value !== null) {
|
|
2440
|
+
const warning = `Warning: '${key}' is overridden by environment variable ${options.envOverrideInfo.envVar}
|
|
2441
|
+
Environment value: ${options.envOverrideInfo.value}
|
|
2442
|
+
Config file value: ${value} (saved but inactive)
|
|
2443
|
+
|
|
2444
|
+
The environment variable takes precedence. To use the config file value,
|
|
2445
|
+
unset the environment variable: unset ${options.envOverrideInfo.envVar}`;
|
|
2446
|
+
return { success: true, warning };
|
|
2447
|
+
}
|
|
2448
|
+
return { success: true };
|
|
2449
|
+
}
|
|
2450
|
+
function toSnakeCaseConfig(config2) {
|
|
2451
|
+
const result = {};
|
|
2452
|
+
result.library = config2.library;
|
|
2453
|
+
result.log_level = config2.logLevel;
|
|
2454
|
+
result.backup = {
|
|
2455
|
+
max_generations: config2.backup.maxGenerations,
|
|
2456
|
+
max_age_days: config2.backup.maxAgeDays,
|
|
2457
|
+
directory: config2.backup.directory
|
|
2458
|
+
};
|
|
2459
|
+
result.watch = {
|
|
2460
|
+
debounce_ms: config2.watch.debounceMs,
|
|
2461
|
+
poll_interval_ms: config2.watch.pollIntervalMs,
|
|
2462
|
+
retry_interval_ms: config2.watch.retryIntervalMs,
|
|
2463
|
+
max_retries: config2.watch.maxRetries
|
|
2464
|
+
};
|
|
2465
|
+
result.server = {
|
|
2466
|
+
auto_start: config2.server.autoStart,
|
|
2467
|
+
auto_stop_minutes: config2.server.autoStopMinutes
|
|
2468
|
+
};
|
|
2469
|
+
result.citation = {
|
|
2470
|
+
default_style: config2.citation.defaultStyle,
|
|
2471
|
+
csl_directory: config2.citation.cslDirectory,
|
|
2472
|
+
default_locale: config2.citation.defaultLocale,
|
|
2473
|
+
default_format: config2.citation.defaultFormat
|
|
2474
|
+
};
|
|
2475
|
+
result.pubmed = {
|
|
2476
|
+
email: config2.pubmed.email,
|
|
2477
|
+
api_key: config2.pubmed.apiKey
|
|
2478
|
+
};
|
|
2479
|
+
result.fulltext = {
|
|
2480
|
+
directory: config2.fulltext.directory
|
|
2481
|
+
};
|
|
2482
|
+
result.cli = {
|
|
2483
|
+
default_limit: config2.cli.defaultLimit,
|
|
2484
|
+
default_sort: config2.cli.defaultSort,
|
|
2485
|
+
default_order: config2.cli.defaultOrder,
|
|
2486
|
+
tui: {
|
|
2487
|
+
limit: config2.cli.tui.limit,
|
|
2488
|
+
debounce_ms: config2.cli.tui.debounceMs
|
|
2489
|
+
},
|
|
2490
|
+
edit: {
|
|
2491
|
+
default_format: config2.cli.edit.defaultFormat
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2494
|
+
result.mcp = {
|
|
2495
|
+
default_limit: config2.mcp.defaultLimit
|
|
2496
|
+
};
|
|
2497
|
+
return result;
|
|
2498
|
+
}
|
|
2499
|
+
function filterSection(config2, section) {
|
|
2500
|
+
const sectionValue = config2[section];
|
|
2501
|
+
if (sectionValue === void 0) {
|
|
2502
|
+
return {};
|
|
2503
|
+
}
|
|
2504
|
+
return { [section]: sectionValue };
|
|
2505
|
+
}
|
|
2506
|
+
function serializeToTOML(config2, withSources) {
|
|
2507
|
+
const lines = [];
|
|
2508
|
+
if (withSources) {
|
|
2509
|
+
lines.push("# Effective configuration");
|
|
2510
|
+
lines.push("# Source priority: CLI > current dir > env > user > default");
|
|
2511
|
+
lines.push("");
|
|
2512
|
+
}
|
|
2513
|
+
for (const [key, value] of Object.entries(config2)) {
|
|
2514
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2515
|
+
lines.push(formatTOMLValue(key, value));
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
const hasTopLevel = Object.values(config2).some(
|
|
2519
|
+
(v) => typeof v !== "object" || v === null || Array.isArray(v)
|
|
2520
|
+
);
|
|
2521
|
+
if (hasTopLevel) {
|
|
2522
|
+
lines.push("");
|
|
2523
|
+
}
|
|
2524
|
+
for (const [key, value] of Object.entries(config2)) {
|
|
2525
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
2526
|
+
serializeSection(lines, key, value);
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
return lines.join("\n");
|
|
2530
|
+
}
|
|
2531
|
+
function serializeSection(lines, prefix, obj) {
|
|
2532
|
+
const scalarEntries = [];
|
|
2533
|
+
const objectEntries = [];
|
|
2534
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2535
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
2536
|
+
objectEntries.push([key, value]);
|
|
2537
|
+
} else {
|
|
2538
|
+
scalarEntries.push([key, value]);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
if (scalarEntries.length > 0) {
|
|
2542
|
+
lines.push(`[${prefix}]`);
|
|
2543
|
+
for (const [key, value] of scalarEntries) {
|
|
2544
|
+
lines.push(formatTOMLValue(key, value));
|
|
2545
|
+
}
|
|
2546
|
+
lines.push("");
|
|
2547
|
+
}
|
|
2548
|
+
for (const [key, value] of objectEntries) {
|
|
2549
|
+
serializeSection(lines, `${prefix}.${key}`, value);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
function formatTOMLValue(key, value) {
|
|
2553
|
+
if (typeof value === "string") {
|
|
2554
|
+
return `${key} = "${value}"`;
|
|
2555
|
+
}
|
|
2556
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
2557
|
+
return `${key} = ${value}`;
|
|
2558
|
+
}
|
|
2559
|
+
if (Array.isArray(value)) {
|
|
2560
|
+
const items2 = value.map((v) => typeof v === "string" ? `"${v}"` : String(v));
|
|
2561
|
+
return `${key} = [ ${items2.join(", ")} ]`;
|
|
2562
|
+
}
|
|
2563
|
+
if (value === void 0 || value === null) {
|
|
2564
|
+
return `# ${key} = (not set)`;
|
|
2565
|
+
}
|
|
2566
|
+
return `${key} = ${JSON.stringify(value)}`;
|
|
2567
|
+
}
|
|
2568
|
+
function showConfig(config2, options) {
|
|
2569
|
+
let snakeCaseConfig = toSnakeCaseConfig(config2);
|
|
2570
|
+
if (options.section) {
|
|
2571
|
+
snakeCaseConfig = filterSection(snakeCaseConfig, options.section);
|
|
2572
|
+
}
|
|
2573
|
+
if (options.json) {
|
|
2574
|
+
return JSON.stringify(snakeCaseConfig, null, 2);
|
|
2575
|
+
}
|
|
2576
|
+
return serializeToTOML(snakeCaseConfig, options.sources ?? false);
|
|
2577
|
+
}
|
|
2578
|
+
async function unsetConfigValue(configPath, key) {
|
|
2579
|
+
if (!isValidConfigKey(key)) {
|
|
2580
|
+
return { success: false, error: `Unknown configuration key: '${key}'` };
|
|
2581
|
+
}
|
|
2582
|
+
try {
|
|
2583
|
+
await removeTOMLKey(configPath, key);
|
|
2584
|
+
} catch (error) {
|
|
2585
|
+
return {
|
|
2586
|
+
success: false,
|
|
2587
|
+
error: `Failed to update config: ${error instanceof Error ? error.message : String(error)}`
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
return { success: true };
|
|
2591
|
+
}
|
|
2592
|
+
function resolveWriteTarget(options) {
|
|
2593
|
+
const { local, user, cwd, userConfigPath } = options;
|
|
2594
|
+
const localConfigPath = join(cwd, getDefaultCurrentDirConfigFilename());
|
|
2595
|
+
if (local) {
|
|
2596
|
+
return localConfigPath;
|
|
2597
|
+
}
|
|
2598
|
+
if (user) {
|
|
2599
|
+
return userConfigPath;
|
|
2600
|
+
}
|
|
2601
|
+
if (existsSync(localConfigPath)) {
|
|
2602
|
+
return localConfigPath;
|
|
2603
|
+
}
|
|
2604
|
+
return userConfigPath;
|
|
2605
|
+
}
|
|
2606
|
+
function createTempFile(content, format2) {
|
|
2607
|
+
const timestamp2 = Date.now();
|
|
2608
|
+
const extension = format2 === "yaml" ? ".yaml" : ".json";
|
|
2609
|
+
const fileName = `ref-edit-${timestamp2}${extension}`;
|
|
2610
|
+
const filePath = path.join(os.tmpdir(), fileName);
|
|
2611
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
2612
|
+
return filePath;
|
|
2613
|
+
}
|
|
2614
|
+
function openEditor(editor, filePath) {
|
|
2615
|
+
const command = `${editor} "${filePath}"`;
|
|
2616
|
+
const result = spawnSync(command, {
|
|
2617
|
+
stdio: "inherit",
|
|
2618
|
+
shell: true
|
|
2619
|
+
});
|
|
2620
|
+
return result.status ?? 1;
|
|
2621
|
+
}
|
|
2622
|
+
function readTempFile(filePath) {
|
|
2623
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
2624
|
+
}
|
|
2625
|
+
function deleteTempFile(filePath) {
|
|
2626
|
+
try {
|
|
2627
|
+
fs.unlinkSync(filePath);
|
|
2628
|
+
} catch {
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
function resolveEditor(platform) {
|
|
2632
|
+
const visual = process.env.VISUAL;
|
|
2633
|
+
if (visual && visual.trim() !== "") {
|
|
2634
|
+
return visual;
|
|
2635
|
+
}
|
|
2636
|
+
const editor = process.env.EDITOR;
|
|
2637
|
+
if (editor && editor.trim() !== "") {
|
|
2638
|
+
return editor;
|
|
2639
|
+
}
|
|
2640
|
+
const currentPlatform = process.platform;
|
|
2641
|
+
return currentPlatform === "win32" ? "notepad" : "vi";
|
|
2642
|
+
}
|
|
2643
|
+
function registerConfigCommand(program) {
|
|
2644
|
+
const configCmd = program.command("config").description("Manage configuration settings");
|
|
2645
|
+
configCmd.command("show").description("Display effective configuration").option("-o, --output <format>", "Output format: text|json").option("--section <name>", "Show only a specific section").option("--sources", "Include source information for each value").action(async (options) => {
|
|
2646
|
+
try {
|
|
2647
|
+
const config2 = loadConfig();
|
|
2648
|
+
const output = showConfig(config2, {
|
|
2649
|
+
json: options.output === "json",
|
|
2650
|
+
section: options.section,
|
|
2651
|
+
sources: options.sources
|
|
2652
|
+
});
|
|
2653
|
+
process.stdout.write(`${output}
|
|
2654
|
+
`);
|
|
2655
|
+
process.exit(0);
|
|
2656
|
+
} catch (error) {
|
|
2657
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
2658
|
+
`);
|
|
2659
|
+
process.exit(1);
|
|
2660
|
+
}
|
|
2661
|
+
});
|
|
2662
|
+
configCmd.command("get <key>").description("Get a specific configuration value").option("--config-only", "Return only the config file value (ignore env vars)").action(async (key, options) => {
|
|
2663
|
+
try {
|
|
2664
|
+
const config2 = loadConfig();
|
|
2665
|
+
const envOverrideValue = options.configOnly ? null : getEnvOverrideInfo(key)?.value ?? null;
|
|
2666
|
+
const getOptions = {
|
|
2667
|
+
configOnly: options.configOnly
|
|
2668
|
+
};
|
|
2669
|
+
if (envOverrideValue !== null) {
|
|
2670
|
+
getOptions.envOverride = envOverrideValue;
|
|
2671
|
+
}
|
|
2672
|
+
const result = getConfigValue(config2, key, getOptions);
|
|
2673
|
+
if (result.found) {
|
|
2674
|
+
const { formatValue: formatValue2 } = await Promise.resolve().then(() => get);
|
|
2675
|
+
process.stdout.write(`${formatValue2(result.value)}
|
|
2676
|
+
`);
|
|
2677
|
+
process.exit(0);
|
|
2678
|
+
} else {
|
|
2679
|
+
process.exit(1);
|
|
2680
|
+
}
|
|
2681
|
+
} catch (error) {
|
|
2682
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
2683
|
+
`);
|
|
2684
|
+
process.exit(1);
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
configCmd.command("set <key> <value>").description("Set a configuration value").option("--local", "Write to current directory config (create if not exists)").option("--user", "Write to user config (ignore local config even if exists)").action(async (key, value, options) => {
|
|
2688
|
+
try {
|
|
2689
|
+
const configPath = resolveWriteTarget({
|
|
2690
|
+
local: options.local,
|
|
2691
|
+
user: options.user,
|
|
2692
|
+
cwd: process.cwd(),
|
|
2693
|
+
userConfigPath: getDefaultUserConfigPath()
|
|
2694
|
+
});
|
|
2695
|
+
const parsedValue = parseValueForKey(key, value);
|
|
2696
|
+
const envOverrideInfo = getEnvOverrideInfo(key);
|
|
2697
|
+
const setOptions = envOverrideInfo ? { envOverrideInfo } : {};
|
|
2698
|
+
const result = await setConfigValue(configPath, key, parsedValue, setOptions);
|
|
2699
|
+
if (!result.success) {
|
|
2700
|
+
process.stderr.write(`Error: ${result.error}
|
|
2701
|
+
`);
|
|
2702
|
+
process.exit(1);
|
|
2703
|
+
}
|
|
2704
|
+
if (result.warning) {
|
|
2705
|
+
process.stderr.write(`${result.warning}
|
|
2706
|
+
`);
|
|
2707
|
+
}
|
|
2708
|
+
process.exit(0);
|
|
2709
|
+
} catch (error) {
|
|
2710
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
2711
|
+
`);
|
|
2712
|
+
process.exit(1);
|
|
2713
|
+
}
|
|
2714
|
+
});
|
|
2715
|
+
configCmd.command("unset <key>").description("Remove a configuration value (revert to default)").option("--local", "Remove from current directory config").option("--user", "Remove from user config (ignore local config even if exists)").action(async (key, options) => {
|
|
2716
|
+
try {
|
|
2717
|
+
const configPath = resolveWriteTarget({
|
|
2718
|
+
local: options.local,
|
|
2719
|
+
user: options.user,
|
|
2720
|
+
cwd: process.cwd(),
|
|
2721
|
+
userConfigPath: getDefaultUserConfigPath()
|
|
2722
|
+
});
|
|
2723
|
+
const result = await unsetConfigValue(configPath, key);
|
|
2724
|
+
if (!result.success) {
|
|
2725
|
+
process.stderr.write(`Error: ${result.error}
|
|
2726
|
+
`);
|
|
2727
|
+
process.exit(1);
|
|
2728
|
+
}
|
|
2729
|
+
process.exit(0);
|
|
2730
|
+
} catch (error) {
|
|
2731
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
2732
|
+
`);
|
|
2733
|
+
process.exit(1);
|
|
2734
|
+
}
|
|
2735
|
+
});
|
|
2736
|
+
configCmd.command("keys").description("List all available configuration keys").option("--section <name>", "List keys only in a specific section").action(async (options) => {
|
|
2737
|
+
try {
|
|
2738
|
+
const output = listConfigKeys({ section: options.section });
|
|
2739
|
+
if (output) {
|
|
2740
|
+
process.stdout.write(`${output}
|
|
2741
|
+
`);
|
|
2742
|
+
}
|
|
2743
|
+
process.exit(0);
|
|
2744
|
+
} catch (error) {
|
|
2745
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
2746
|
+
`);
|
|
2747
|
+
process.exit(1);
|
|
752
2748
|
}
|
|
753
|
-
|
|
754
|
-
|
|
2749
|
+
});
|
|
2750
|
+
configCmd.command("path").description("Show configuration file paths").option("--user", "Show only user config path").option("--local", "Show only local config path").action(async (options) => {
|
|
2751
|
+
try {
|
|
2752
|
+
const output = showConfigPaths({ user: options.user, local: options.local });
|
|
755
2753
|
process.stdout.write(`${output}
|
|
756
2754
|
`);
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
process.stderr.write(`${errors2}
|
|
2755
|
+
process.exit(0);
|
|
2756
|
+
} catch (error) {
|
|
2757
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
761
2758
|
`);
|
|
2759
|
+
process.exit(1);
|
|
762
2760
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
2761
|
+
});
|
|
2762
|
+
configCmd.command("edit").description("Open configuration file in editor").option("--local", "Edit current directory config").action(async (options) => {
|
|
2763
|
+
try {
|
|
2764
|
+
if (!process.stdin.isTTY) {
|
|
2765
|
+
process.stderr.write("Error: config edit requires a terminal (TTY)\n");
|
|
2766
|
+
process.exit(1);
|
|
2767
|
+
}
|
|
2768
|
+
const target = getConfigEditTarget({ local: options.local });
|
|
2769
|
+
if (!target.exists) {
|
|
2770
|
+
const template = createConfigTemplate();
|
|
2771
|
+
mkdirSync(dirname(target.path), { recursive: true });
|
|
2772
|
+
writeFileSync(target.path, template, "utf-8");
|
|
2773
|
+
}
|
|
2774
|
+
const editor = resolveEditor();
|
|
2775
|
+
const exitCode = openEditor(editor, target.path);
|
|
2776
|
+
process.exit(exitCode);
|
|
2777
|
+
} catch (error) {
|
|
2778
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
766
2779
|
`);
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}
|
|
770
|
-
function createTempFile(content, format2) {
|
|
771
|
-
const timestamp2 = Date.now();
|
|
772
|
-
const extension = format2 === "yaml" ? ".yaml" : ".json";
|
|
773
|
-
const fileName = `ref-edit-${timestamp2}${extension}`;
|
|
774
|
-
const filePath = path.join(os.tmpdir(), fileName);
|
|
775
|
-
fs.writeFileSync(filePath, content, "utf-8");
|
|
776
|
-
return filePath;
|
|
777
|
-
}
|
|
778
|
-
function openEditor(editor, filePath) {
|
|
779
|
-
const command = `${editor} "${filePath}"`;
|
|
780
|
-
const result = spawnSync(command, {
|
|
781
|
-
stdio: "inherit",
|
|
782
|
-
shell: true
|
|
2780
|
+
process.exit(1);
|
|
2781
|
+
}
|
|
783
2782
|
});
|
|
784
|
-
return result.status ?? 1;
|
|
785
|
-
}
|
|
786
|
-
function readTempFile(filePath) {
|
|
787
|
-
return fs.readFileSync(filePath, "utf-8");
|
|
788
|
-
}
|
|
789
|
-
function deleteTempFile(filePath) {
|
|
790
|
-
try {
|
|
791
|
-
fs.unlinkSync(filePath);
|
|
792
|
-
} catch {
|
|
793
|
-
}
|
|
794
2783
|
}
|
|
795
2784
|
function datePartsToIso(dateParts) {
|
|
796
2785
|
if (!dateParts || dateParts.length === 0 || !dateParts[0]) {
|
|
@@ -3576,18 +5565,6 @@ ${yamlContent}`);
|
|
|
3576
5565
|
}
|
|
3577
5566
|
return sections.join("\n---\n\n");
|
|
3578
5567
|
}
|
|
3579
|
-
function resolveEditor(platform) {
|
|
3580
|
-
const visual = process.env.VISUAL;
|
|
3581
|
-
if (visual && visual.trim() !== "") {
|
|
3582
|
-
return visual;
|
|
3583
|
-
}
|
|
3584
|
-
const editor = process.env.EDITOR;
|
|
3585
|
-
if (editor && editor.trim() !== "") {
|
|
3586
|
-
return editor;
|
|
3587
|
-
}
|
|
3588
|
-
const currentPlatform = process.platform;
|
|
3589
|
-
return currentPlatform === "win32" ? "notepad" : "vi";
|
|
3590
|
-
}
|
|
3591
5568
|
function serialize(items2, format2) {
|
|
3592
5569
|
return format2 === "yaml" ? serializeToYaml(items2) : serializeToJson(items2);
|
|
3593
5570
|
}
|
|
@@ -3741,12 +5718,12 @@ function formatEditOutput(result) {
|
|
|
3741
5718
|
return lines.join("\n");
|
|
3742
5719
|
}
|
|
3743
5720
|
async function executeInteractiveEdit(options, context, config2) {
|
|
3744
|
-
const { selectReferencesOrExit } = await import("./reference-select-
|
|
5721
|
+
const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
|
|
3745
5722
|
const allReferences = await context.library.getAll();
|
|
3746
5723
|
const identifiers = await selectReferencesOrExit(
|
|
3747
5724
|
allReferences,
|
|
3748
5725
|
{ multiSelect: true },
|
|
3749
|
-
config2.cli.
|
|
5726
|
+
config2.cli.tui
|
|
3750
5727
|
);
|
|
3751
5728
|
const format2 = options.format ?? config2.cli.edit.defaultFormat;
|
|
3752
5729
|
return executeEditCommand(
|
|
@@ -6977,7 +8954,7 @@ async function executeExport(options, context) {
|
|
|
6977
8954
|
return { items: items2, notFound };
|
|
6978
8955
|
}
|
|
6979
8956
|
function formatExportOutput(result, options) {
|
|
6980
|
-
const format2 = options.
|
|
8957
|
+
const format2 = options.output ?? "json";
|
|
6981
8958
|
const singleIdRequest = (options.ids?.length ?? 0) === 1 && !options.all && !options.search;
|
|
6982
8959
|
const data = result.items.length === 1 && singleIdRequest ? result.items[0] : result.items;
|
|
6983
8960
|
if (format2 === "json") {
|
|
@@ -6994,183 +8971,6 @@ function formatExportOutput(result, options) {
|
|
|
6994
8971
|
function getExportExitCode(result) {
|
|
6995
8972
|
return result.notFound.length > 0 ? 1 : 0;
|
|
6996
8973
|
}
|
|
6997
|
-
const FULLTEXT_EXTENSIONS = {
|
|
6998
|
-
pdf: ".pdf",
|
|
6999
|
-
markdown: ".md"
|
|
7000
|
-
};
|
|
7001
|
-
function generateFulltextFilename(item, type2) {
|
|
7002
|
-
const uuid2 = item.custom?.uuid;
|
|
7003
|
-
if (!uuid2) {
|
|
7004
|
-
throw new Error("Missing uuid in custom field");
|
|
7005
|
-
}
|
|
7006
|
-
const parts = [item.id];
|
|
7007
|
-
if (item.PMID && item.PMID.length > 0) {
|
|
7008
|
-
parts.push(`PMID${item.PMID}`);
|
|
7009
|
-
}
|
|
7010
|
-
parts.push(uuid2);
|
|
7011
|
-
return parts.join("-") + FULLTEXT_EXTENSIONS[type2];
|
|
7012
|
-
}
|
|
7013
|
-
class FulltextIOError extends Error {
|
|
7014
|
-
constructor(message, cause) {
|
|
7015
|
-
super(message);
|
|
7016
|
-
this.cause = cause;
|
|
7017
|
-
this.name = "FulltextIOError";
|
|
7018
|
-
}
|
|
7019
|
-
}
|
|
7020
|
-
class FulltextNotAttachedError extends Error {
|
|
7021
|
-
constructor(itemId, type2) {
|
|
7022
|
-
super(`No ${type2} attached to reference ${itemId}`);
|
|
7023
|
-
this.itemId = itemId;
|
|
7024
|
-
this.type = type2;
|
|
7025
|
-
this.name = "FulltextNotAttachedError";
|
|
7026
|
-
}
|
|
7027
|
-
}
|
|
7028
|
-
class FulltextManager {
|
|
7029
|
-
constructor(fulltextDirectory) {
|
|
7030
|
-
this.fulltextDirectory = fulltextDirectory;
|
|
7031
|
-
}
|
|
7032
|
-
/**
|
|
7033
|
-
* Ensure the fulltext directory exists
|
|
7034
|
-
*/
|
|
7035
|
-
async ensureDirectory() {
|
|
7036
|
-
await mkdir(this.fulltextDirectory, { recursive: true });
|
|
7037
|
-
}
|
|
7038
|
-
/**
|
|
7039
|
-
* Attach a file to a reference
|
|
7040
|
-
*/
|
|
7041
|
-
async attachFile(item, sourcePath, type2, options) {
|
|
7042
|
-
const { move = false, force = false } = options ?? {};
|
|
7043
|
-
const newFilename = generateFulltextFilename(item, type2);
|
|
7044
|
-
this.validateSourceFile(sourcePath);
|
|
7045
|
-
const existingFilename = this.getExistingFilename(item, type2);
|
|
7046
|
-
if (existingFilename && !force) {
|
|
7047
|
-
return {
|
|
7048
|
-
filename: newFilename,
|
|
7049
|
-
existingFile: existingFilename,
|
|
7050
|
-
overwritten: false
|
|
7051
|
-
};
|
|
7052
|
-
}
|
|
7053
|
-
await this.ensureDirectory();
|
|
7054
|
-
const deletedOldFile = await this.deleteOldFileIfNeeded(existingFilename, newFilename, force);
|
|
7055
|
-
const destPath = join(this.fulltextDirectory, newFilename);
|
|
7056
|
-
await this.copyOrMoveFile(sourcePath, destPath, move);
|
|
7057
|
-
const result = {
|
|
7058
|
-
filename: newFilename,
|
|
7059
|
-
overwritten: existingFilename !== void 0
|
|
7060
|
-
};
|
|
7061
|
-
if (deletedOldFile) {
|
|
7062
|
-
result.deletedOldFile = deletedOldFile;
|
|
7063
|
-
}
|
|
7064
|
-
return result;
|
|
7065
|
-
}
|
|
7066
|
-
/**
|
|
7067
|
-
* Validate that source file exists
|
|
7068
|
-
*/
|
|
7069
|
-
validateSourceFile(sourcePath) {
|
|
7070
|
-
if (!existsSync(sourcePath)) {
|
|
7071
|
-
throw new FulltextIOError(`Source file not found: ${sourcePath}`);
|
|
7072
|
-
}
|
|
7073
|
-
}
|
|
7074
|
-
/**
|
|
7075
|
-
* Delete old file if force mode and filename changed
|
|
7076
|
-
* @returns Deleted filename or undefined
|
|
7077
|
-
*/
|
|
7078
|
-
async deleteOldFileIfNeeded(existingFilename, newFilename, force) {
|
|
7079
|
-
if (!force || !existingFilename || existingFilename === newFilename) {
|
|
7080
|
-
return void 0;
|
|
7081
|
-
}
|
|
7082
|
-
const oldPath = join(this.fulltextDirectory, existingFilename);
|
|
7083
|
-
try {
|
|
7084
|
-
await unlink(oldPath);
|
|
7085
|
-
} catch {
|
|
7086
|
-
}
|
|
7087
|
-
return existingFilename;
|
|
7088
|
-
}
|
|
7089
|
-
/**
|
|
7090
|
-
* Copy or move file to destination
|
|
7091
|
-
*/
|
|
7092
|
-
async copyOrMoveFile(sourcePath, destPath, move) {
|
|
7093
|
-
try {
|
|
7094
|
-
if (move) {
|
|
7095
|
-
await rename(sourcePath, destPath);
|
|
7096
|
-
} else {
|
|
7097
|
-
await copyFile(sourcePath, destPath);
|
|
7098
|
-
}
|
|
7099
|
-
} catch (error) {
|
|
7100
|
-
const operation = move ? "move" : "copy";
|
|
7101
|
-
throw new FulltextIOError(
|
|
7102
|
-
`Failed to ${operation} file to ${destPath}`,
|
|
7103
|
-
error instanceof Error ? error : void 0
|
|
7104
|
-
);
|
|
7105
|
-
}
|
|
7106
|
-
}
|
|
7107
|
-
/**
|
|
7108
|
-
* Get the full path for an attached file
|
|
7109
|
-
* @returns Full path or null if not attached
|
|
7110
|
-
*/
|
|
7111
|
-
getFilePath(item, type2) {
|
|
7112
|
-
const filename = this.getExistingFilename(item, type2);
|
|
7113
|
-
if (!filename) {
|
|
7114
|
-
return null;
|
|
7115
|
-
}
|
|
7116
|
-
return join(this.fulltextDirectory, filename);
|
|
7117
|
-
}
|
|
7118
|
-
/**
|
|
7119
|
-
* Detach a file from a reference
|
|
7120
|
-
*/
|
|
7121
|
-
async detachFile(item, type2, options) {
|
|
7122
|
-
const { delete: deleteFile = false } = options ?? {};
|
|
7123
|
-
const filename = this.getExistingFilename(item, type2);
|
|
7124
|
-
if (!filename) {
|
|
7125
|
-
throw new FulltextNotAttachedError(item.id, type2);
|
|
7126
|
-
}
|
|
7127
|
-
if (deleteFile) {
|
|
7128
|
-
const filePath = join(this.fulltextDirectory, filename);
|
|
7129
|
-
try {
|
|
7130
|
-
await unlink(filePath);
|
|
7131
|
-
} catch {
|
|
7132
|
-
}
|
|
7133
|
-
}
|
|
7134
|
-
return {
|
|
7135
|
-
filename,
|
|
7136
|
-
deleted: deleteFile
|
|
7137
|
-
};
|
|
7138
|
-
}
|
|
7139
|
-
/**
|
|
7140
|
-
* Get list of attached fulltext types
|
|
7141
|
-
*/
|
|
7142
|
-
getAttachedTypes(item) {
|
|
7143
|
-
const types2 = [];
|
|
7144
|
-
const fulltext = item.custom?.fulltext;
|
|
7145
|
-
if (fulltext?.pdf) {
|
|
7146
|
-
types2.push("pdf");
|
|
7147
|
-
}
|
|
7148
|
-
if (fulltext?.markdown) {
|
|
7149
|
-
types2.push("markdown");
|
|
7150
|
-
}
|
|
7151
|
-
return types2;
|
|
7152
|
-
}
|
|
7153
|
-
/**
|
|
7154
|
-
* Check if item has attachment
|
|
7155
|
-
* @param type Optional type to check; if omitted, checks for any attachment
|
|
7156
|
-
*/
|
|
7157
|
-
hasAttachment(item, type2) {
|
|
7158
|
-
if (type2) {
|
|
7159
|
-
return this.getExistingFilename(item, type2) !== void 0;
|
|
7160
|
-
}
|
|
7161
|
-
return this.getAttachedTypes(item).length > 0;
|
|
7162
|
-
}
|
|
7163
|
-
/**
|
|
7164
|
-
* Get existing filename from item metadata
|
|
7165
|
-
*/
|
|
7166
|
-
getExistingFilename(item, type2) {
|
|
7167
|
-
const fulltext = item.custom?.fulltext;
|
|
7168
|
-
if (!fulltext) {
|
|
7169
|
-
return void 0;
|
|
7170
|
-
}
|
|
7171
|
-
return fulltext[type2];
|
|
7172
|
-
}
|
|
7173
|
-
}
|
|
7174
8974
|
function detectType(filePath) {
|
|
7175
8975
|
const ext = extname(filePath).toLowerCase();
|
|
7176
8976
|
if (ext === ".pdf") return "pdf";
|
|
@@ -7195,8 +8995,8 @@ function resolveFileType(explicitType, filePath, stdinContent) {
|
|
|
7195
8995
|
function prepareStdinSource(stdinContent, fileType) {
|
|
7196
8996
|
try {
|
|
7197
8997
|
const tempDir = mkdtempSync(join(tmpdir(), "refmgr-"));
|
|
7198
|
-
const ext = fileType
|
|
7199
|
-
const sourcePath = join(tempDir, `stdin
|
|
8998
|
+
const ext = formatToExtension(fileType);
|
|
8999
|
+
const sourcePath = join(tempDir, `stdin.${ext}`);
|
|
7200
9000
|
writeFileSync(sourcePath, stdinContent);
|
|
7201
9001
|
return { sourcePath, tempDir };
|
|
7202
9002
|
} catch (error) {
|
|
@@ -7211,13 +9011,6 @@ async function cleanupTempDir(tempDir) {
|
|
|
7211
9011
|
});
|
|
7212
9012
|
}
|
|
7213
9013
|
}
|
|
7214
|
-
function buildNewFulltext(currentFulltext, fileType, filename) {
|
|
7215
|
-
const newFulltext = {};
|
|
7216
|
-
if (currentFulltext.pdf) newFulltext.pdf = currentFulltext.pdf;
|
|
7217
|
-
if (currentFulltext.markdown) newFulltext.markdown = currentFulltext.markdown;
|
|
7218
|
-
newFulltext[fileType] = filename;
|
|
7219
|
-
return newFulltext;
|
|
7220
|
-
}
|
|
7221
9014
|
function prepareSourcePath(filePath, stdinContent, fileType) {
|
|
7222
9015
|
if (stdinContent) {
|
|
7223
9016
|
return prepareStdinSource(stdinContent, fileType);
|
|
@@ -7227,12 +9020,26 @@ function prepareSourcePath(filePath, stdinContent, fileType) {
|
|
|
7227
9020
|
}
|
|
7228
9021
|
return { sourcePath: filePath };
|
|
7229
9022
|
}
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
9023
|
+
function convertResult(result, fileType) {
|
|
9024
|
+
if (result.success) {
|
|
9025
|
+
return {
|
|
9026
|
+
success: true,
|
|
9027
|
+
filename: result.filename,
|
|
9028
|
+
type: fileType,
|
|
9029
|
+
overwritten: result.overwritten
|
|
9030
|
+
};
|
|
9031
|
+
}
|
|
9032
|
+
if (result.requiresConfirmation) {
|
|
9033
|
+
return {
|
|
9034
|
+
success: false,
|
|
9035
|
+
existingFile: result.existingFile,
|
|
9036
|
+
requiresConfirmation: true
|
|
9037
|
+
};
|
|
9038
|
+
}
|
|
9039
|
+
return {
|
|
9040
|
+
success: false,
|
|
9041
|
+
error: result.error
|
|
7234
9042
|
};
|
|
7235
|
-
return manager.attachFile(item, sourcePath, fileType, attachOptions);
|
|
7236
9043
|
}
|
|
7237
9044
|
async function fulltextAttach(library, options) {
|
|
7238
9045
|
const {
|
|
@@ -7245,71 +9052,78 @@ async function fulltextAttach(library, options) {
|
|
|
7245
9052
|
fulltextDirectory,
|
|
7246
9053
|
stdinContent
|
|
7247
9054
|
} = options;
|
|
7248
|
-
const item = await library.find(identifier, { idType });
|
|
7249
|
-
if (!item) {
|
|
7250
|
-
return { success: false, error: `Reference '${identifier}' not found` };
|
|
7251
|
-
}
|
|
7252
9055
|
const fileTypeResult = resolveFileType(explicitType, filePath, stdinContent);
|
|
7253
9056
|
if (typeof fileTypeResult === "object" && "error" in fileTypeResult) {
|
|
9057
|
+
const item = await library.find(identifier, { idType });
|
|
9058
|
+
if (!item) {
|
|
9059
|
+
return { success: false, error: `Reference '${identifier}' not found` };
|
|
9060
|
+
}
|
|
7254
9061
|
return { success: false, error: fileTypeResult.error };
|
|
7255
9062
|
}
|
|
7256
9063
|
const fileType = fileTypeResult;
|
|
7257
9064
|
const sourceResult = prepareSourcePath(filePath, stdinContent, fileType);
|
|
7258
9065
|
if ("error" in sourceResult) {
|
|
9066
|
+
const item = await library.find(identifier, { idType });
|
|
9067
|
+
if (!item) {
|
|
9068
|
+
return { success: false, error: `Reference '${identifier}' not found` };
|
|
9069
|
+
}
|
|
7259
9070
|
return { success: false, error: sourceResult.error };
|
|
7260
9071
|
}
|
|
7261
9072
|
const { sourcePath, tempDir } = sourceResult;
|
|
7262
|
-
const manager = new FulltextManager(fulltextDirectory);
|
|
7263
9073
|
try {
|
|
7264
|
-
const result = await
|
|
7265
|
-
if (result.existingFile && !result.overwritten) {
|
|
7266
|
-
await cleanupTempDir(tempDir);
|
|
7267
|
-
return { success: false, existingFile: result.existingFile, requiresConfirmation: true };
|
|
7268
|
-
}
|
|
7269
|
-
const newFulltext = buildNewFulltext(item.custom?.fulltext ?? {}, fileType, result.filename);
|
|
7270
|
-
await updateReference(library, {
|
|
9074
|
+
const result = await addAttachment(library, {
|
|
7271
9075
|
identifier,
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
9076
|
+
filePath: sourcePath,
|
|
9077
|
+
role: FULLTEXT_ROLE,
|
|
9078
|
+
move: move ?? false,
|
|
9079
|
+
force: force ?? false,
|
|
9080
|
+
idType,
|
|
9081
|
+
attachmentsDirectory: fulltextDirectory
|
|
7276
9082
|
});
|
|
7277
9083
|
await cleanupTempDir(tempDir);
|
|
7278
|
-
return
|
|
7279
|
-
success: true,
|
|
7280
|
-
filename: result.filename,
|
|
7281
|
-
type: fileType,
|
|
7282
|
-
overwritten: result.overwritten
|
|
7283
|
-
};
|
|
9084
|
+
return convertResult(result, fileType);
|
|
7284
9085
|
} catch (error) {
|
|
7285
9086
|
await cleanupTempDir(tempDir);
|
|
7286
|
-
if (error instanceof FulltextIOError) {
|
|
7287
|
-
return { success: false, error: error.message };
|
|
7288
|
-
}
|
|
7289
9087
|
throw error;
|
|
7290
9088
|
}
|
|
7291
9089
|
}
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
9090
|
+
function buildFilePath$1(attachmentsDirectory, directory, filename) {
|
|
9091
|
+
return normalizePathForOutput(join(attachmentsDirectory, directory, filename));
|
|
9092
|
+
}
|
|
9093
|
+
async function getFileContent(filePath) {
|
|
9094
|
+
const content = await readFile(filePath);
|
|
9095
|
+
return { success: true, content };
|
|
9096
|
+
}
|
|
9097
|
+
async function handleStdoutMode(attachments, type2, identifier, fulltextDirectory) {
|
|
9098
|
+
const file = findFulltextFile(attachments, type2);
|
|
9099
|
+
if (!file || !attachments?.directory) {
|
|
7295
9100
|
return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
|
|
7296
9101
|
}
|
|
9102
|
+
const filePath = buildFilePath$1(fulltextDirectory, attachments.directory, file.filename);
|
|
7297
9103
|
try {
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
9104
|
+
return await getFileContent(filePath);
|
|
9105
|
+
} catch {
|
|
9106
|
+
return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
|
|
9107
|
+
}
|
|
9108
|
+
}
|
|
9109
|
+
function getSingleTypePath(attachments, type2, identifier, fulltextDirectory) {
|
|
9110
|
+
const file = findFulltextFile(attachments, type2);
|
|
9111
|
+
if (!file || !attachments?.directory) {
|
|
9112
|
+
return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
|
|
7305
9113
|
}
|
|
9114
|
+
const filePath = buildFilePath$1(fulltextDirectory, attachments.directory, file.filename);
|
|
9115
|
+
const paths = {};
|
|
9116
|
+
paths[type2] = filePath;
|
|
9117
|
+
return { success: true, paths };
|
|
7306
9118
|
}
|
|
7307
|
-
function
|
|
9119
|
+
function getAllFulltextPaths(attachments, fulltextFiles, fulltextDirectory, identifier) {
|
|
7308
9120
|
const paths = {};
|
|
7309
|
-
for (const
|
|
7310
|
-
const
|
|
7311
|
-
|
|
7312
|
-
|
|
9121
|
+
for (const file of fulltextFiles) {
|
|
9122
|
+
const ext = file.filename.split(".").pop() || "";
|
|
9123
|
+
const format2 = extensionToFormat(ext);
|
|
9124
|
+
if (format2) {
|
|
9125
|
+
const filePath = buildFilePath$1(fulltextDirectory, attachments.directory, file.filename);
|
|
9126
|
+
paths[format2] = filePath;
|
|
7313
9127
|
}
|
|
7314
9128
|
}
|
|
7315
9129
|
if (Object.keys(paths).length === 0) {
|
|
@@ -7323,92 +9137,98 @@ async function fulltextGet(library, options) {
|
|
|
7323
9137
|
if (!item) {
|
|
7324
9138
|
return { success: false, error: `Reference '${identifier}' not found` };
|
|
7325
9139
|
}
|
|
7326
|
-
const
|
|
9140
|
+
const attachments = item.custom?.attachments;
|
|
7327
9141
|
if (stdout2 && type2) {
|
|
7328
|
-
return
|
|
9142
|
+
return handleStdoutMode(attachments, type2, identifier, fulltextDirectory);
|
|
9143
|
+
}
|
|
9144
|
+
const fulltextFiles = findFulltextFiles(attachments);
|
|
9145
|
+
if (fulltextFiles.length === 0) {
|
|
9146
|
+
return { success: false, error: `No fulltext attached to '${identifier}'` };
|
|
7329
9147
|
}
|
|
7330
|
-
|
|
7331
|
-
|
|
9148
|
+
if (type2) {
|
|
9149
|
+
return getSingleTypePath(attachments, type2, identifier, fulltextDirectory);
|
|
9150
|
+
}
|
|
9151
|
+
if (!attachments) {
|
|
7332
9152
|
return { success: false, error: `No fulltext attached to '${identifier}'` };
|
|
7333
9153
|
}
|
|
7334
|
-
return
|
|
9154
|
+
return getAllFulltextPaths(attachments, fulltextFiles, fulltextDirectory, identifier);
|
|
9155
|
+
}
|
|
9156
|
+
function getFilesToDetach(attachments, type2) {
|
|
9157
|
+
if (type2) {
|
|
9158
|
+
const file = findFulltextFile(attachments, type2);
|
|
9159
|
+
return file ? [file] : [];
|
|
9160
|
+
}
|
|
9161
|
+
return findFulltextFiles(attachments);
|
|
7335
9162
|
}
|
|
7336
|
-
async function
|
|
9163
|
+
async function detachFiles(library, files, identifier, removeFiles, idType, fulltextDirectory) {
|
|
7337
9164
|
const detached = [];
|
|
7338
9165
|
const deleted = [];
|
|
7339
|
-
for (const
|
|
7340
|
-
const
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
9166
|
+
for (const file of files) {
|
|
9167
|
+
const result = await detachAttachment(library, {
|
|
9168
|
+
identifier,
|
|
9169
|
+
filename: file.filename,
|
|
9170
|
+
removeFiles: removeFiles ?? false,
|
|
9171
|
+
idType,
|
|
9172
|
+
attachmentsDirectory: fulltextDirectory
|
|
9173
|
+
});
|
|
9174
|
+
if (result.success) {
|
|
9175
|
+
const ext = file.filename.split(".").pop() || "";
|
|
9176
|
+
const format2 = extensionToFormat(ext);
|
|
9177
|
+
if (format2) {
|
|
9178
|
+
detached.push(format2);
|
|
9179
|
+
if (result.deleted.length > 0) {
|
|
9180
|
+
deleted.push(format2);
|
|
9181
|
+
}
|
|
9182
|
+
}
|
|
7345
9183
|
}
|
|
7346
9184
|
}
|
|
7347
9185
|
return { detached, deleted };
|
|
7348
9186
|
}
|
|
7349
|
-
function
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
newFulltext.pdf = currentFulltext.pdf;
|
|
9187
|
+
function buildResult(detached, deleted, identifier) {
|
|
9188
|
+
if (detached.length === 0) {
|
|
9189
|
+
return { success: false, error: `Failed to detach fulltext from '${identifier}'` };
|
|
7353
9190
|
}
|
|
7354
|
-
|
|
7355
|
-
|
|
9191
|
+
const result = { success: true, detached };
|
|
9192
|
+
if (deleted.length > 0) {
|
|
9193
|
+
result.deleted = deleted;
|
|
7356
9194
|
}
|
|
7357
|
-
return
|
|
7358
|
-
}
|
|
7359
|
-
function handleDetachError(error) {
|
|
7360
|
-
if (error instanceof FulltextNotAttachedError || error instanceof FulltextIOError) {
|
|
7361
|
-
return { success: false, error: error.message };
|
|
7362
|
-
}
|
|
7363
|
-
throw error;
|
|
9195
|
+
return result;
|
|
7364
9196
|
}
|
|
7365
9197
|
async function fulltextDetach(library, options) {
|
|
7366
|
-
const { identifier, type: type2,
|
|
9198
|
+
const { identifier, type: type2, removeFiles, idType = "id", fulltextDirectory } = options;
|
|
7367
9199
|
const item = await library.find(identifier, { idType });
|
|
7368
9200
|
if (!item) {
|
|
7369
9201
|
return { success: false, error: `Reference '${identifier}' not found` };
|
|
7370
9202
|
}
|
|
7371
|
-
const
|
|
7372
|
-
const
|
|
7373
|
-
if (
|
|
9203
|
+
const attachments = item.custom?.attachments;
|
|
9204
|
+
const fulltextFiles = findFulltextFiles(attachments);
|
|
9205
|
+
if (fulltextFiles.length === 0) {
|
|
7374
9206
|
return { success: false, error: `No fulltext attached to '${identifier}'` };
|
|
7375
9207
|
}
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
item,
|
|
7380
|
-
typesToDetach,
|
|
7381
|
-
deleteFile
|
|
7382
|
-
);
|
|
7383
|
-
const updatedFulltext = buildRemainingFulltext(item.custom?.fulltext ?? {}, detached);
|
|
7384
|
-
await updateReference(library, {
|
|
7385
|
-
identifier,
|
|
7386
|
-
updates: {
|
|
7387
|
-
custom: { fulltext: updatedFulltext }
|
|
7388
|
-
},
|
|
7389
|
-
idType
|
|
7390
|
-
});
|
|
7391
|
-
const resultData = { success: true, detached };
|
|
7392
|
-
if (deleted.length > 0) {
|
|
7393
|
-
resultData.deleted = deleted;
|
|
7394
|
-
}
|
|
7395
|
-
return resultData;
|
|
7396
|
-
} catch (error) {
|
|
7397
|
-
return handleDetachError(error);
|
|
9208
|
+
const filesToDetach = getFilesToDetach(attachments, type2);
|
|
9209
|
+
if (filesToDetach.length === 0) {
|
|
9210
|
+
return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
|
|
7398
9211
|
}
|
|
7399
|
-
}
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
9212
|
+
const { detached, deleted } = await detachFiles(
|
|
9213
|
+
library,
|
|
9214
|
+
filesToDetach,
|
|
9215
|
+
identifier,
|
|
9216
|
+
removeFiles,
|
|
9217
|
+
idType,
|
|
9218
|
+
fulltextDirectory
|
|
9219
|
+
);
|
|
9220
|
+
return buildResult(detached, deleted, identifier);
|
|
9221
|
+
}
|
|
9222
|
+
function buildFilePath(attachmentsDirectory, directory, filename) {
|
|
9223
|
+
return join(attachmentsDirectory, directory, filename);
|
|
9224
|
+
}
|
|
9225
|
+
function determineTypeToOpen(attachments) {
|
|
9226
|
+
const files = findFulltextFiles(attachments);
|
|
9227
|
+
if (files.length === 0) return void 0;
|
|
9228
|
+
const pdfFile = files.find((f) => f.filename.endsWith(".pdf"));
|
|
9229
|
+
if (pdfFile) return "pdf";
|
|
9230
|
+
const mdFile = files.find((f) => f.filename.endsWith(".md"));
|
|
9231
|
+
if (mdFile) return "markdown";
|
|
7412
9232
|
return void 0;
|
|
7413
9233
|
}
|
|
7414
9234
|
async function fulltextOpen(library, options) {
|
|
@@ -7417,14 +9237,16 @@ async function fulltextOpen(library, options) {
|
|
|
7417
9237
|
if (!item) {
|
|
7418
9238
|
return { success: false, error: `Reference not found: ${identifier}` };
|
|
7419
9239
|
}
|
|
7420
|
-
const
|
|
9240
|
+
const attachments = item.custom?.attachments;
|
|
9241
|
+
const typeToOpen = type2 ?? determineTypeToOpen(attachments);
|
|
7421
9242
|
if (!typeToOpen) {
|
|
7422
9243
|
return { success: false, error: `No fulltext attached to reference: ${identifier}` };
|
|
7423
9244
|
}
|
|
7424
|
-
const
|
|
7425
|
-
if (!
|
|
9245
|
+
const file = findFulltextFile(attachments, typeToOpen);
|
|
9246
|
+
if (!file || !attachments?.directory) {
|
|
7426
9247
|
return { success: false, error: `No ${typeToOpen} attached to reference: ${identifier}` };
|
|
7427
9248
|
}
|
|
9249
|
+
const filePath = buildFilePath(fulltextDirectory, attachments.directory, file.filename);
|
|
7428
9250
|
if (!existsSync(filePath)) {
|
|
7429
9251
|
return {
|
|
7430
9252
|
success: false,
|
|
@@ -7472,7 +9294,7 @@ async function executeFulltextDetach(options, context) {
|
|
|
7472
9294
|
const operationOptions = {
|
|
7473
9295
|
identifier: options.identifier,
|
|
7474
9296
|
type: options.type,
|
|
7475
|
-
|
|
9297
|
+
removeFiles: options.removeFiles,
|
|
7476
9298
|
idType: options.idType,
|
|
7477
9299
|
fulltextDirectory: options.fulltextDirectory
|
|
7478
9300
|
};
|
|
@@ -7543,12 +9365,12 @@ function getFulltextExitCode(result) {
|
|
|
7543
9365
|
return result.success ? 0 : 1;
|
|
7544
9366
|
}
|
|
7545
9367
|
async function executeInteractiveSelect(context, config2) {
|
|
7546
|
-
const { selectReferencesOrExit } = await import("./reference-select-
|
|
9368
|
+
const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
|
|
7547
9369
|
const allReferences = await context.library.getAll();
|
|
7548
9370
|
const identifiers = await selectReferencesOrExit(
|
|
7549
9371
|
allReferences,
|
|
7550
9372
|
{ multiSelect: false },
|
|
7551
|
-
config2.cli.
|
|
9373
|
+
config2.cli.tui
|
|
7552
9374
|
);
|
|
7553
9375
|
return identifiers[0];
|
|
7554
9376
|
}
|
|
@@ -7587,7 +9409,7 @@ async function handleFulltextAttachAction(identifierArg, filePathArg, options, g
|
|
|
7587
9409
|
const stdinContent = !filePath && type2 ? await readStdinBuffer() : void 0;
|
|
7588
9410
|
const attachOptions = {
|
|
7589
9411
|
identifier,
|
|
7590
|
-
fulltextDirectory: config2.
|
|
9412
|
+
fulltextDirectory: config2.attachments.directory,
|
|
7591
9413
|
...filePath && { filePath },
|
|
7592
9414
|
...type2 && { type: type2 },
|
|
7593
9415
|
...options.move && { move: options.move },
|
|
@@ -7641,7 +9463,7 @@ async function handleFulltextGetAction(identifierArg, options, globalOpts) {
|
|
|
7641
9463
|
}
|
|
7642
9464
|
const getOptions = {
|
|
7643
9465
|
identifier,
|
|
7644
|
-
fulltextDirectory: config2.
|
|
9466
|
+
fulltextDirectory: config2.attachments.directory,
|
|
7645
9467
|
...options.pdf && { type: "pdf" },
|
|
7646
9468
|
...options.markdown && { type: "markdown" },
|
|
7647
9469
|
...options.stdout && { stdout: options.stdout },
|
|
@@ -7677,10 +9499,10 @@ async function handleFulltextDetachAction(identifierArg, options, globalOpts) {
|
|
|
7677
9499
|
}
|
|
7678
9500
|
const detachOptions = {
|
|
7679
9501
|
identifier,
|
|
7680
|
-
fulltextDirectory: config2.
|
|
9502
|
+
fulltextDirectory: config2.attachments.directory,
|
|
7681
9503
|
...options.pdf && { type: "pdf" },
|
|
7682
9504
|
...options.markdown && { type: "markdown" },
|
|
7683
|
-
...options.
|
|
9505
|
+
...options.removeFiles && { removeFiles: options.removeFiles },
|
|
7684
9506
|
...options.force && { force: options.force },
|
|
7685
9507
|
...options.uuid && { idType: "uuid" }
|
|
7686
9508
|
};
|
|
@@ -7716,7 +9538,7 @@ async function handleFulltextOpenAction(identifierArg, options, globalOpts) {
|
|
|
7716
9538
|
}
|
|
7717
9539
|
const openOptions = {
|
|
7718
9540
|
identifier,
|
|
7719
|
-
fulltextDirectory: config2.
|
|
9541
|
+
fulltextDirectory: config2.attachments.directory,
|
|
7720
9542
|
...options.pdf && { type: "pdf" },
|
|
7721
9543
|
...options.markdown && { type: "markdown" },
|
|
7722
9544
|
...options.uuid && { idType: "uuid" }
|
|
@@ -7823,21 +9645,28 @@ const VALID_LIST_SORT_FIELDS = /* @__PURE__ */ new Set([
|
|
|
7823
9645
|
"pub"
|
|
7824
9646
|
]);
|
|
7825
9647
|
function getOutputFormat$1(options) {
|
|
9648
|
+
if (options.output) {
|
|
9649
|
+
if (options.output === "ids") return "ids-only";
|
|
9650
|
+
return options.output;
|
|
9651
|
+
}
|
|
7826
9652
|
if (options.json) return "json";
|
|
7827
9653
|
if (options.idsOnly) return "ids-only";
|
|
7828
|
-
if (options.
|
|
9654
|
+
if (options.uuidOnly) return "uuid";
|
|
7829
9655
|
if (options.bibtex) return "bibtex";
|
|
7830
9656
|
return "pretty";
|
|
7831
9657
|
}
|
|
7832
9658
|
function validateOptions$1(options) {
|
|
7833
|
-
const outputOptions = [options.json, options.idsOnly, options.
|
|
9659
|
+
const outputOptions = [options.json, options.idsOnly, options.uuidOnly, options.bibtex].filter(
|
|
7834
9660
|
Boolean
|
|
7835
9661
|
);
|
|
7836
9662
|
if (outputOptions.length > 1) {
|
|
7837
9663
|
throw new Error(
|
|
7838
|
-
"Multiple output formats specified. Only one of --json, --ids-only, --uuid, --bibtex can be used."
|
|
9664
|
+
"Multiple output formats specified. Only one of --json, --ids-only, --uuid-only, --bibtex can be used."
|
|
7839
9665
|
);
|
|
7840
9666
|
}
|
|
9667
|
+
if (options.output && outputOptions.length > 0) {
|
|
9668
|
+
throw new Error("Cannot combine --output with convenience flags (--json, --ids-only, etc.)");
|
|
9669
|
+
}
|
|
7841
9670
|
if (options.sort !== void 0) {
|
|
7842
9671
|
const sortStr = String(options.sort);
|
|
7843
9672
|
if (!VALID_LIST_SORT_FIELDS.has(sortStr)) {
|
|
@@ -28249,7 +30078,7 @@ function registerFulltextAttachTool(server, getLibraryOperations, getConfig) {
|
|
|
28249
30078
|
filePath: args.path,
|
|
28250
30079
|
force: true,
|
|
28251
30080
|
// MCP tools don't support interactive confirmation
|
|
28252
|
-
fulltextDirectory: config2.
|
|
30081
|
+
fulltextDirectory: config2.attachments.directory
|
|
28253
30082
|
});
|
|
28254
30083
|
if (!result.success) {
|
|
28255
30084
|
return {
|
|
@@ -28282,7 +30111,7 @@ function registerFulltextGetTool(server, getLibraryOperations, getConfig) {
|
|
|
28282
30111
|
const config2 = getConfig();
|
|
28283
30112
|
const pathResult = await fulltextGet(libraryOps, {
|
|
28284
30113
|
identifier: args.id,
|
|
28285
|
-
fulltextDirectory: config2.
|
|
30114
|
+
fulltextDirectory: config2.attachments.directory
|
|
28286
30115
|
});
|
|
28287
30116
|
if (!pathResult.success) {
|
|
28288
30117
|
return {
|
|
@@ -28296,7 +30125,7 @@ function registerFulltextGetTool(server, getLibraryOperations, getConfig) {
|
|
|
28296
30125
|
identifier: args.id,
|
|
28297
30126
|
type: "markdown",
|
|
28298
30127
|
stdout: true,
|
|
28299
|
-
fulltextDirectory: config2.
|
|
30128
|
+
fulltextDirectory: config2.attachments.directory
|
|
28300
30129
|
});
|
|
28301
30130
|
if (contentResult.success && contentResult.content) {
|
|
28302
30131
|
responses.push({
|
|
@@ -28335,7 +30164,7 @@ function registerFulltextDetachTool(server, getLibraryOperations, getConfig) {
|
|
|
28335
30164
|
const config2 = getConfig();
|
|
28336
30165
|
const result = await fulltextDetach(libraryOps, {
|
|
28337
30166
|
identifier: args.id,
|
|
28338
|
-
fulltextDirectory: config2.
|
|
30167
|
+
fulltextDirectory: config2.attachments.directory
|
|
28339
30168
|
});
|
|
28340
30169
|
if (!result.success) {
|
|
28341
30170
|
return {
|
|
@@ -28541,7 +30370,7 @@ async function mcpStart(options) {
|
|
|
28541
30370
|
async function executeRemove(options, context) {
|
|
28542
30371
|
const { identifier, idType = "id", fulltextDirectory, deleteFulltext = false } = options;
|
|
28543
30372
|
if (context.mode === "local" && deleteFulltext && fulltextDirectory) {
|
|
28544
|
-
const { removeReference } = await import("./index-
|
|
30373
|
+
const { removeReference } = await import("./index-DHgeuWGP.js").then((n) => n.r);
|
|
28545
30374
|
return removeReference(context.library, {
|
|
28546
30375
|
identifier,
|
|
28547
30376
|
idType,
|
|
@@ -28595,12 +30424,12 @@ Continue?`;
|
|
|
28595
30424
|
return readConfirmation(confirmMsg);
|
|
28596
30425
|
}
|
|
28597
30426
|
async function executeInteractiveRemove(context, config2) {
|
|
28598
|
-
const { selectReferenceItemsOrExit } = await import("./reference-select-
|
|
30427
|
+
const { selectReferenceItemsOrExit } = await import("./reference-select-B9w9CLa1.js");
|
|
28599
30428
|
const allReferences = await context.library.getAll();
|
|
28600
30429
|
const selectedItems = await selectReferenceItemsOrExit(
|
|
28601
30430
|
allReferences,
|
|
28602
30431
|
{ multiSelect: false },
|
|
28603
|
-
config2.cli.
|
|
30432
|
+
config2.cli.tui
|
|
28604
30433
|
);
|
|
28605
30434
|
const selectedItem = selectedItems[0];
|
|
28606
30435
|
return { identifier: selectedItem.id, item: selectedItem };
|
|
@@ -28682,7 +30511,7 @@ async function handleRemoveAction(identifierArg, options, globalOpts) {
|
|
|
28682
30511
|
const removeOptions = {
|
|
28683
30512
|
identifier,
|
|
28684
30513
|
idType: useUuid ? "uuid" : "id",
|
|
28685
|
-
fulltextDirectory: config2.
|
|
30514
|
+
fulltextDirectory: config2.attachments.directory,
|
|
28686
30515
|
deleteFulltext: force && hasFulltext
|
|
28687
30516
|
};
|
|
28688
30517
|
const result = await executeRemove(removeOptions, context);
|
|
@@ -28705,21 +30534,28 @@ const VALID_SEARCH_SORT_FIELDS = /* @__PURE__ */ new Set([
|
|
|
28705
30534
|
"rel"
|
|
28706
30535
|
]);
|
|
28707
30536
|
function getOutputFormat(options) {
|
|
30537
|
+
if (options.output) {
|
|
30538
|
+
if (options.output === "ids") return "ids-only";
|
|
30539
|
+
return options.output;
|
|
30540
|
+
}
|
|
28708
30541
|
if (options.json) return "json";
|
|
28709
30542
|
if (options.idsOnly) return "ids-only";
|
|
28710
|
-
if (options.
|
|
30543
|
+
if (options.uuidOnly) return "uuid";
|
|
28711
30544
|
if (options.bibtex) return "bibtex";
|
|
28712
30545
|
return "pretty";
|
|
28713
30546
|
}
|
|
28714
30547
|
function validateOptions(options) {
|
|
28715
|
-
const outputOptions = [options.json, options.idsOnly, options.
|
|
30548
|
+
const outputOptions = [options.json, options.idsOnly, options.uuidOnly, options.bibtex].filter(
|
|
28716
30549
|
Boolean
|
|
28717
30550
|
);
|
|
28718
30551
|
if (outputOptions.length > 1) {
|
|
28719
30552
|
throw new Error(
|
|
28720
|
-
"Multiple output formats specified. Only one of --json, --ids-only, --uuid, --bibtex can be used."
|
|
30553
|
+
"Multiple output formats specified. Only one of --json, --ids-only, --uuid-only, --bibtex can be used."
|
|
28721
30554
|
);
|
|
28722
30555
|
}
|
|
30556
|
+
if (options.output && outputOptions.length > 0) {
|
|
30557
|
+
throw new Error("Cannot combine --output with convenience flags (--json, --ids-only, etc.)");
|
|
30558
|
+
}
|
|
28723
30559
|
if (options.sort !== void 0) {
|
|
28724
30560
|
const sortStr = String(options.sort);
|
|
28725
30561
|
if (!VALID_SEARCH_SORT_FIELDS.has(sortStr)) {
|
|
@@ -28775,35 +30611,39 @@ function formatSearchOutput(result, options) {
|
|
|
28775
30611
|
return lines.join("\n");
|
|
28776
30612
|
}
|
|
28777
30613
|
function validateInteractiveOptions(options) {
|
|
28778
|
-
const outputOptions = [
|
|
28779
|
-
|
|
28780
|
-
|
|
30614
|
+
const outputOptions = [
|
|
30615
|
+
options.output,
|
|
30616
|
+
options.json,
|
|
30617
|
+
options.idsOnly,
|
|
30618
|
+
options.uuidOnly,
|
|
30619
|
+
options.bibtex
|
|
30620
|
+
].filter(Boolean);
|
|
28781
30621
|
if (outputOptions.length > 0) {
|
|
28782
30622
|
throw new Error(
|
|
28783
|
-
"
|
|
30623
|
+
"TUI mode cannot be combined with output format options (--output, --json, --ids-only, --uuid-only, --bibtex)"
|
|
28784
30624
|
);
|
|
28785
30625
|
}
|
|
28786
30626
|
}
|
|
28787
30627
|
async function executeInteractiveSearch(options, context, config2) {
|
|
28788
30628
|
validateInteractiveOptions(options);
|
|
28789
|
-
const { checkTTY } = await import("./tty-
|
|
30629
|
+
const { checkTTY } = await import("./tty-BMyaEOhX.js");
|
|
28790
30630
|
const { runSearchPrompt } = await import("./search-prompt-BrWpOcij.js");
|
|
28791
|
-
const { runActionMenu } = await import("./action-menu-
|
|
28792
|
-
const { search } = await import("./file-watcher-
|
|
28793
|
-
const { tokenize } = await import("./file-watcher-
|
|
30631
|
+
const { runActionMenu } = await import("./action-menu-DvwR6nMj.js");
|
|
30632
|
+
const { search } = await import("./file-watcher-B_WpVHSV.js").then((n) => n.y);
|
|
30633
|
+
const { tokenize } = await import("./file-watcher-B_WpVHSV.js").then((n) => n.x);
|
|
28794
30634
|
checkTTY();
|
|
28795
30635
|
const allReferences = await context.library.getAll();
|
|
28796
30636
|
const searchFn = (query) => {
|
|
28797
30637
|
const { tokens } = tokenize(query);
|
|
28798
30638
|
return search(allReferences, tokens);
|
|
28799
30639
|
};
|
|
28800
|
-
const
|
|
30640
|
+
const tuiConfig = config2.cli.tui;
|
|
28801
30641
|
const searchResult = await runSearchPrompt(
|
|
28802
30642
|
allReferences,
|
|
28803
30643
|
searchFn,
|
|
28804
30644
|
{
|
|
28805
|
-
limit:
|
|
28806
|
-
debounceMs:
|
|
30645
|
+
limit: tuiConfig.limit,
|
|
30646
|
+
debounceMs: tuiConfig.debounceMs
|
|
28807
30647
|
},
|
|
28808
30648
|
options.query || ""
|
|
28809
30649
|
);
|
|
@@ -29096,12 +30936,12 @@ function formatUpdateOutput(result, identifier) {
|
|
|
29096
30936
|
return parts.join("\n");
|
|
29097
30937
|
}
|
|
29098
30938
|
async function executeInteractiveUpdate(context, config2) {
|
|
29099
|
-
const { selectReferencesOrExit } = await import("./reference-select-
|
|
30939
|
+
const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
|
|
29100
30940
|
const allReferences = await context.library.getAll();
|
|
29101
30941
|
const identifiers = await selectReferencesOrExit(
|
|
29102
30942
|
allReferences,
|
|
29103
30943
|
{ multiSelect: false },
|
|
29104
|
-
config2.cli.
|
|
30944
|
+
config2.cli.tui
|
|
29105
30945
|
);
|
|
29106
30946
|
return identifiers[0];
|
|
29107
30947
|
}
|
|
@@ -29206,18 +31046,61 @@ function collectSetOption(value, previous) {
|
|
|
29206
31046
|
}
|
|
29207
31047
|
const SEARCH_SORT_FIELDS = searchSortFieldSchema.options;
|
|
29208
31048
|
const SORT_ORDERS = sortOrderSchema.options;
|
|
29209
|
-
const
|
|
31049
|
+
const CITATION_OUTPUT_FORMATS = ["text", "html", "rtf"];
|
|
31050
|
+
const EXPORT_OUTPUT_FORMATS = ["json", "yaml", "bibtex"];
|
|
31051
|
+
const LIST_OUTPUT_FORMATS = ["pretty", "json", "bibtex", "ids", "uuid"];
|
|
31052
|
+
const MUTATION_OUTPUT_FORMATS = ["json", "text"];
|
|
31053
|
+
const CONFIG_OUTPUT_FORMATS = ["text", "json"];
|
|
29210
31054
|
const LOG_LEVELS = ["silent", "info", "debug"];
|
|
31055
|
+
const ADD_INPUT_FORMATS = ["json", "bibtex", "ris", "pmid", "doi", "isbn", "auto"];
|
|
31056
|
+
const CONFIG_SECTIONS = [
|
|
31057
|
+
"backup",
|
|
31058
|
+
"citation",
|
|
31059
|
+
"cli",
|
|
31060
|
+
"fulltext",
|
|
31061
|
+
"mcp",
|
|
31062
|
+
"pubmed",
|
|
31063
|
+
"server",
|
|
31064
|
+
"watch"
|
|
31065
|
+
];
|
|
31066
|
+
const ATTACHMENT_ROLES = ["fulltext", "supplement", "notes", "draft"];
|
|
29211
31067
|
const OPTION_VALUES = {
|
|
29212
31068
|
"--sort": SEARCH_SORT_FIELDS,
|
|
29213
31069
|
// search includes 'relevance'
|
|
29214
31070
|
"--order": SORT_ORDERS,
|
|
29215
|
-
"--format": CITATION_FORMATS,
|
|
29216
31071
|
"--style": BUILTIN_STYLES,
|
|
29217
|
-
"--log-level": LOG_LEVELS
|
|
31072
|
+
"--log-level": LOG_LEVELS,
|
|
31073
|
+
"--section": CONFIG_SECTIONS,
|
|
31074
|
+
"--input": ADD_INPUT_FORMATS,
|
|
31075
|
+
"-i": ADD_INPUT_FORMATS,
|
|
31076
|
+
"--role": ATTACHMENT_ROLES,
|
|
31077
|
+
"-r": ATTACHMENT_ROLES
|
|
31078
|
+
};
|
|
31079
|
+
const OUTPUT_VALUES_BY_COMMAND = {
|
|
31080
|
+
cite: CITATION_OUTPUT_FORMATS,
|
|
31081
|
+
export: EXPORT_OUTPUT_FORMATS,
|
|
31082
|
+
list: LIST_OUTPUT_FORMATS,
|
|
31083
|
+
search: LIST_OUTPUT_FORMATS,
|
|
31084
|
+
add: MUTATION_OUTPUT_FORMATS,
|
|
31085
|
+
remove: MUTATION_OUTPUT_FORMATS,
|
|
31086
|
+
update: MUTATION_OUTPUT_FORMATS
|
|
31087
|
+
// config show uses CONFIG_OUTPUT_FORMATS, handled specially
|
|
29218
31088
|
};
|
|
31089
|
+
function getOptionValuesForCommand(option, command, subcommand) {
|
|
31090
|
+
if (option === "--output" || option === "-o") {
|
|
31091
|
+
if (command === "config" && subcommand === "show") {
|
|
31092
|
+
return CONFIG_OUTPUT_FORMATS;
|
|
31093
|
+
}
|
|
31094
|
+
if (command && OUTPUT_VALUES_BY_COMMAND[command]) {
|
|
31095
|
+
return OUTPUT_VALUES_BY_COMMAND[command];
|
|
31096
|
+
}
|
|
31097
|
+
return CITATION_OUTPUT_FORMATS;
|
|
31098
|
+
}
|
|
31099
|
+
return OPTION_VALUES[option];
|
|
31100
|
+
}
|
|
29219
31101
|
const ID_COMPLETION_COMMANDS = /* @__PURE__ */ new Set(["cite", "remove", "update"]);
|
|
29220
31102
|
const ID_COMPLETION_FULLTEXT_SUBCOMMANDS = /* @__PURE__ */ new Set(["attach", "get", "detach", "open"]);
|
|
31103
|
+
const ID_COMPLETION_ATTACH_SUBCOMMANDS = /* @__PURE__ */ new Set(["open", "add", "list", "get", "detach", "sync"]);
|
|
29221
31104
|
function toCompletionItems(values) {
|
|
29222
31105
|
return values.map((name2) => ({ name: name2 }));
|
|
29223
31106
|
}
|
|
@@ -29251,6 +31134,13 @@ function extractGlobalOptions(program) {
|
|
|
29251
31134
|
function findSubcommand(program, name2) {
|
|
29252
31135
|
return program.commands.find((cmd) => cmd.name() === name2);
|
|
29253
31136
|
}
|
|
31137
|
+
function parseCommandContext(env) {
|
|
31138
|
+
const words = env.line.trim().split(/\s+/);
|
|
31139
|
+
const args = words.slice(1);
|
|
31140
|
+
const command = args[0];
|
|
31141
|
+
const subcommand = args.length >= 2 ? args[1] : void 0;
|
|
31142
|
+
return { command, subcommand };
|
|
31143
|
+
}
|
|
29254
31144
|
function getCompletions(env, program) {
|
|
29255
31145
|
const { line, prev, last } = env;
|
|
29256
31146
|
const words = line.trim().split(/\s+/);
|
|
@@ -29262,7 +31152,8 @@ function getCompletions(env, program) {
|
|
|
29262
31152
|
}
|
|
29263
31153
|
const firstArg = args[0] ?? "";
|
|
29264
31154
|
if (prev?.startsWith("-")) {
|
|
29265
|
-
const
|
|
31155
|
+
const { command, subcommand } = parseCommandContext(env);
|
|
31156
|
+
const optionValues = getOptionValuesForCommand(prev, command, subcommand);
|
|
29266
31157
|
if (optionValues) {
|
|
29267
31158
|
return toCompletionItems(optionValues);
|
|
29268
31159
|
}
|
|
@@ -29302,11 +31193,43 @@ function needsIdCompletion(env) {
|
|
|
29302
31193
|
}
|
|
29303
31194
|
return { needs: false };
|
|
29304
31195
|
}
|
|
31196
|
+
if (command === "attach" && args.length >= 2) {
|
|
31197
|
+
const subcommand = args[1] ?? "";
|
|
31198
|
+
if (ID_COMPLETION_ATTACH_SUBCOMMANDS.has(subcommand)) {
|
|
31199
|
+
return { needs: true, command, subcommand };
|
|
31200
|
+
}
|
|
31201
|
+
return { needs: false };
|
|
31202
|
+
}
|
|
29305
31203
|
if (ID_COMPLETION_COMMANDS.has(command)) {
|
|
29306
31204
|
return { needs: true, command };
|
|
29307
31205
|
}
|
|
29308
31206
|
return { needs: false };
|
|
29309
31207
|
}
|
|
31208
|
+
const CONFIG_KEY_SUBCOMMANDS = /* @__PURE__ */ new Set(["get", "set", "unset"]);
|
|
31209
|
+
function needsConfigKeyCompletion(env) {
|
|
31210
|
+
const { line, prev } = env;
|
|
31211
|
+
const words = line.trim().split(/\s+/);
|
|
31212
|
+
const args = words.slice(1);
|
|
31213
|
+
if (args.length < 2) {
|
|
31214
|
+
return false;
|
|
31215
|
+
}
|
|
31216
|
+
const command = args[0] ?? "";
|
|
31217
|
+
const subcommand = args[1] ?? "";
|
|
31218
|
+
if (prev?.startsWith("-")) {
|
|
31219
|
+
return false;
|
|
31220
|
+
}
|
|
31221
|
+
if (command === "config" && CONFIG_KEY_SUBCOMMANDS.has(subcommand)) {
|
|
31222
|
+
if (subcommand === "set" && args.length > 3) {
|
|
31223
|
+
return false;
|
|
31224
|
+
}
|
|
31225
|
+
return true;
|
|
31226
|
+
}
|
|
31227
|
+
return false;
|
|
31228
|
+
}
|
|
31229
|
+
function getConfigKeyCompletions(prefix) {
|
|
31230
|
+
const keys = getAllConfigKeys();
|
|
31231
|
+
return keys.filter((key) => key.toLowerCase().startsWith(prefix.toLowerCase())).map((key) => ({ name: key }));
|
|
31232
|
+
}
|
|
29310
31233
|
const MAX_ID_COMPLETIONS = 100;
|
|
29311
31234
|
async function getLibraryForCompletion() {
|
|
29312
31235
|
try {
|
|
@@ -29372,6 +31295,11 @@ async function handleCompletion(program) {
|
|
|
29372
31295
|
tabtab.log(completions2);
|
|
29373
31296
|
return;
|
|
29374
31297
|
}
|
|
31298
|
+
if (needsConfigKeyCompletion(env)) {
|
|
31299
|
+
const keyCompletions = getConfigKeyCompletions(env.last);
|
|
31300
|
+
tabtab.log(keyCompletions);
|
|
31301
|
+
return;
|
|
31302
|
+
}
|
|
29375
31303
|
const idContext = needsIdCompletion(env);
|
|
29376
31304
|
if (idContext.needs) {
|
|
29377
31305
|
const library = await getLibraryForCompletion();
|
|
@@ -29411,7 +31339,9 @@ function createProgram() {
|
|
|
29411
31339
|
registerCiteCommand(program);
|
|
29412
31340
|
registerServerCommand(program);
|
|
29413
31341
|
registerFulltextCommand(program);
|
|
31342
|
+
registerAttachCommand(program);
|
|
29414
31343
|
registerMcpCommand(program);
|
|
31344
|
+
registerConfigCommand(program);
|
|
29415
31345
|
registerCompletionCommand(program);
|
|
29416
31346
|
return program;
|
|
29417
31347
|
}
|
|
@@ -29434,7 +31364,7 @@ async function handleListAction(options, program) {
|
|
|
29434
31364
|
}
|
|
29435
31365
|
}
|
|
29436
31366
|
function registerListCommand(program) {
|
|
29437
|
-
program.command("list").description("List all references in the library").option("--
|
|
31367
|
+
program.command("list").description("List all references in the library").option("-o, --output <format>", "Output format: pretty|json|bibtex|ids|uuid").option("--json", "Alias for --output json").option("--bibtex", "Alias for --output bibtex").option("--ids-only", "Alias for --output ids").option("--uuid-only", "Alias for --output uuid").option("--sort <field>", "Sort by field: created|updated|published|author|title").option("--order <order>", "Sort order: asc|desc").option("-n, --limit <n>", "Maximum number of results", Number.parseInt).option("--offset <n>", "Number of results to skip", Number.parseInt).action(async (options) => {
|
|
29438
31368
|
await handleListAction(options, program);
|
|
29439
31369
|
});
|
|
29440
31370
|
}
|
|
@@ -29463,7 +31393,7 @@ async function handleExportAction(ids, options, program) {
|
|
|
29463
31393
|
}
|
|
29464
31394
|
}
|
|
29465
31395
|
function registerExportCommand(program) {
|
|
29466
|
-
program.command("export [ids...]").description("Export raw CSL-JSON for external tool integration").option("--uuid", "Interpret identifiers as UUIDs").option("--all", "Export all references").option("--search <query>", "Export references matching search query").option("-
|
|
31396
|
+
program.command("export [ids...]").description("Export raw CSL-JSON for external tool integration").option("--uuid", "Interpret identifiers as UUIDs").option("--all", "Export all references").option("--search <query>", "Export references matching search query").option("-o, --output <format>", "Output format: json (default), yaml, bibtex").action(async (ids, options) => {
|
|
29467
31397
|
await handleExportAction(ids, options, program);
|
|
29468
31398
|
});
|
|
29469
31399
|
}
|
|
@@ -29472,7 +31402,7 @@ async function handleSearchAction(query, options, program) {
|
|
|
29472
31402
|
const globalOpts = program.opts();
|
|
29473
31403
|
const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
29474
31404
|
const context = await createExecutionContext(config2, Library.load);
|
|
29475
|
-
if (options.
|
|
31405
|
+
if (options.tui) {
|
|
29476
31406
|
const result2 = await executeInteractiveSearch({ ...options, query }, context, config2);
|
|
29477
31407
|
if (result2.output) {
|
|
29478
31408
|
process.stdout.write(`${result2.output}
|
|
@@ -29494,9 +31424,9 @@ async function handleSearchAction(query, options, program) {
|
|
|
29494
31424
|
}
|
|
29495
31425
|
}
|
|
29496
31426
|
function registerSearchCommand(program) {
|
|
29497
|
-
program.command("search").description("Search references").argument("[query]", "Search query (required unless using --
|
|
29498
|
-
if (!options.
|
|
29499
|
-
process.stderr.write("Error: Search query is required unless using --
|
|
31427
|
+
program.command("search").description("Search references").argument("[query]", "Search query (required unless using --tui)").option("-t, --tui", "Enable TUI (interactive) search mode").option("-o, --output <format>", "Output format: pretty|json|bibtex|ids|uuid").option("--json", "Alias for --output json").option("--bibtex", "Alias for --output bibtex").option("--ids-only", "Alias for --output ids").option("--uuid-only", "Alias for --output uuid").option("--sort <field>", "Sort by field: created|updated|published|author|title|relevance").option("--order <order>", "Sort order: asc|desc").option("-n, --limit <n>", "Maximum number of results", Number.parseInt).option("--offset <n>", "Number of results to skip", Number.parseInt).action(async (query, options) => {
|
|
31428
|
+
if (!options.tui && !query) {
|
|
31429
|
+
process.stderr.write("Error: Search query is required unless using --tui\n");
|
|
29500
31430
|
process.exit(1);
|
|
29501
31431
|
}
|
|
29502
31432
|
await handleSearchAction(query ?? "", options, program);
|
|
@@ -29507,8 +31437,8 @@ function buildAddOptions(inputs, options, config2, stdinContent) {
|
|
|
29507
31437
|
inputs,
|
|
29508
31438
|
force: options.force ?? false
|
|
29509
31439
|
};
|
|
29510
|
-
if (options.
|
|
29511
|
-
addOptions.format = options.
|
|
31440
|
+
if (options.input !== void 0) {
|
|
31441
|
+
addOptions.format = options.input;
|
|
29512
31442
|
}
|
|
29513
31443
|
if (options.verbose !== void 0) {
|
|
29514
31444
|
addOptions.verbose = options.verbose;
|
|
@@ -29576,11 +31506,7 @@ async function handleAddAction(inputs, options, program) {
|
|
|
29576
31506
|
}
|
|
29577
31507
|
}
|
|
29578
31508
|
function registerAddCommand(program) {
|
|
29579
|
-
program.command("add").description("Add new reference(s) to the library").argument("[input...]", "File paths or identifiers (PMID/DOI/ISBN), or use stdin").option("-f, --force", "Skip duplicate detection").option(
|
|
29580
|
-
"--format <format>",
|
|
29581
|
-
"Explicit input format: json|bibtex|ris|pmid|doi|isbn|auto",
|
|
29582
|
-
"auto"
|
|
29583
|
-
).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) => {
|
|
31509
|
+
program.command("add").description("Add new reference(s) to the library").argument("[input...]", "File paths or identifiers (PMID/DOI/ISBN), or use stdin").option("-f, --force", "Skip duplicate detection").option("-i, --input <format>", "Input format: json|bibtex|ris|pmid|doi|isbn|auto", "auto").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) => {
|
|
29584
31510
|
await handleAddAction(inputs, options, program);
|
|
29585
31511
|
});
|
|
29586
31512
|
}
|
|
@@ -29598,7 +31524,7 @@ function registerEditCommand(program) {
|
|
|
29598
31524
|
program.command("edit").description("Edit references interactively using an external editor").argument(
|
|
29599
31525
|
"[identifier...]",
|
|
29600
31526
|
"Citation keys or UUIDs to edit (interactive selection if omitted)"
|
|
29601
|
-
).option("--uuid", "Interpret identifiers as UUIDs").option("
|
|
31527
|
+
).option("--uuid", "Interpret identifiers as UUIDs").option("--format <format>", "Edit format: yaml (default), json").option("--editor <editor>", "Editor command (overrides $VISUAL/$EDITOR)").action(async (identifiers, options) => {
|
|
29602
31528
|
await handleEditAction(identifiers, options, program.opts());
|
|
29603
31529
|
});
|
|
29604
31530
|
}
|
|
@@ -29606,7 +31532,7 @@ function registerCiteCommand(program) {
|
|
|
29606
31532
|
program.command("cite").description("Generate formatted citations for references").argument(
|
|
29607
31533
|
"[id-or-uuid...]",
|
|
29608
31534
|
"Citation keys or UUIDs to cite (interactive selection if omitted)"
|
|
29609
|
-
).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("--
|
|
31535
|
+
).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("-o, --output <format>", "Output format: text|html|rtf").option("--in-text", "Generate in-text citations instead of bibliography entries").action(async (identifiers, options) => {
|
|
29610
31536
|
await handleCiteAction(identifiers, options, program.opts());
|
|
29611
31537
|
});
|
|
29612
31538
|
}
|
|
@@ -29711,6 +31637,30 @@ function registerMcpCommand(program) {
|
|
|
29711
31637
|
}
|
|
29712
31638
|
});
|
|
29713
31639
|
}
|
|
31640
|
+
function registerAttachCommand(program) {
|
|
31641
|
+
const attachCmd = program.command("attach").description("Manage file attachments for references");
|
|
31642
|
+
attachCmd.command("open").description("Open attachments directory or specific file").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").argument("[filename]", "Specific file to open").option("--role <role>", "Open file by role").option("--print", "Output path instead of opening").option("--no-sync", "Skip interactive sync prompt").option("--uuid", "Interpret identifier as UUID").action(async (identifier, filename, options) => {
|
|
31643
|
+
await handleAttachOpenAction(identifier, filename, options, program.opts());
|
|
31644
|
+
});
|
|
31645
|
+
attachCmd.command("add").description("Add a file attachment to a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").argument("<file-path>", "Path to the file to attach").requiredOption(
|
|
31646
|
+
"--role <role>",
|
|
31647
|
+
"Role for the file (fulltext, supplement, notes, draft, or custom)"
|
|
31648
|
+
).option("--label <label>", "Human-readable label").option("--move", "Move file instead of copy").option("-f, --force", "Overwrite existing attachment").option("--uuid", "Interpret identifier as UUID").action(async (identifier, filePath, options) => {
|
|
31649
|
+
await handleAttachAddAction(identifier, filePath, options, program.opts());
|
|
31650
|
+
});
|
|
31651
|
+
attachCmd.command("list").description("List attachments for a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").option("--role <role>", "Filter by role").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
|
|
31652
|
+
await handleAttachListAction(identifier, options, program.opts());
|
|
31653
|
+
});
|
|
31654
|
+
attachCmd.command("get").description("Get attachment file path or content").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").argument("[filename]", "Specific file to get").option("--role <role>", "Get file by role").option("--stdout", "Output file content to stdout").option("--uuid", "Interpret identifier as UUID").action(async (identifier, filename, options) => {
|
|
31655
|
+
await handleAttachGetAction(identifier, filename, options, program.opts());
|
|
31656
|
+
});
|
|
31657
|
+
attachCmd.command("detach").description("Detach file from a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").argument("[filename]", "Specific file to detach").option("--role <role>", "Detach files by role").option("--all", "Detach all files of specified role").option("--remove-files", "Also delete files from disk").option("--uuid", "Interpret identifier as UUID").action(async (identifier, filename, options) => {
|
|
31658
|
+
await handleAttachDetachAction(identifier, filename, options, program.opts());
|
|
31659
|
+
});
|
|
31660
|
+
attachCmd.command("sync").description("Synchronize metadata with files on disk").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").option("--yes", "Apply changes (add new files)").option("--fix", "Remove missing files from metadata").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
|
|
31661
|
+
await handleAttachSyncAction(identifier, options, program.opts());
|
|
31662
|
+
});
|
|
31663
|
+
}
|
|
29714
31664
|
function registerFulltextCommand(program) {
|
|
29715
31665
|
const fulltextCmd = program.command("fulltext").description("Manage full-text files attached to references");
|
|
29716
31666
|
fulltextCmd.command("attach").description("Attach a full-text file to a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").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) => {
|
|
@@ -29719,7 +31669,7 @@ function registerFulltextCommand(program) {
|
|
|
29719
31669
|
fulltextCmd.command("get").description("Get full-text file path or content").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").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) => {
|
|
29720
31670
|
await handleFulltextGetAction(identifier, options, program.opts());
|
|
29721
31671
|
});
|
|
29722
|
-
fulltextCmd.command("detach").description("Detach full-text file from a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").option("--pdf", "Detach PDF only").option("--markdown", "Detach Markdown only").option("--
|
|
31672
|
+
fulltextCmd.command("detach").description("Detach full-text file from a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").option("--pdf", "Detach PDF only").option("--markdown", "Detach Markdown only").option("--remove-files", "Also delete the file from disk").option("-f, --force", "Skip confirmation for file removal").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
|
|
29723
31673
|
await handleFulltextDetachAction(identifier, options, program.opts());
|
|
29724
31674
|
});
|
|
29725
31675
|
fulltextCmd.command("open").description("Open full-text file with system default application").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").option("--pdf", "Open PDF file").option("--markdown", "Open Markdown file").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
|
|
@@ -29741,8 +31691,14 @@ async function main(argv) {
|
|
|
29741
31691
|
await program.parseAsync(argv);
|
|
29742
31692
|
}
|
|
29743
31693
|
export {
|
|
31694
|
+
addAttachment as a,
|
|
29744
31695
|
createProgram as c,
|
|
31696
|
+
detachAttachment as d,
|
|
29745
31697
|
formatBibtex as f,
|
|
29746
|
-
|
|
31698
|
+
getAttachment as g,
|
|
31699
|
+
listAttachments as l,
|
|
31700
|
+
main as m,
|
|
31701
|
+
openAttachment as o,
|
|
31702
|
+
syncAttachments as s
|
|
29747
31703
|
};
|
|
29748
|
-
//# sourceMappingURL=index-
|
|
31704
|
+
//# sourceMappingURL=index-Bv5IgsL-.js.map
|