@ncukondo/reference-manager 0.14.1 → 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 +64 -16
- package/bin/reference-manager.js +0 -0
- package/dist/chunks/{action-menu-CVSizwXm.js → action-menu-DvwR6nMj.js} +3 -3
- package/dist/chunks/{action-menu-CVSizwXm.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-CXoDLO8W.js → index-Bv5IgsL-.js} +1522 -409
- package/dist/chunks/index-Bv5IgsL-.js.map +1 -0
- package/dist/chunks/{index-DapYyqAC.js → index-DHgeuWGP.js} +112 -35
- 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-CYo0O7MZ.js → style-select-BNQHC79W.js} +2 -2
- package/dist/chunks/{style-select-CYo0O7MZ.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/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.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/key-parser.d.ts +1 -1
- 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/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/fulltext/manager.d.ts +1 -1
- package/dist/features/fulltext/manager.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-CXoDLO8W.js.map +0 -1
- package/dist/chunks/index-DapYyqAC.js.map +0 -1
- package/dist/chunks/loader-C1EpnyPm.js.map +0 -1
- package/dist/chunks/tty-CDBIQraQ.js.map +0 -1
|
@@ -1,23 +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
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 { l as loadConfig, e as getDefaultCurrentDirConfigFilename, h as getDefaultUserConfigPath, o as openWithSystemApp } from "./loader-C1EpnyPm.js";
|
|
12
|
-
import { mkdir, unlink, rename, copyFile, rm, readFile } from "node:fs/promises";
|
|
13
14
|
import { parse as parse$2, stringify as stringify$2 } from "@iarna/toml";
|
|
14
|
-
import { u as updateReference, B as BUILTIN_STYLES, g as getFulltextAttachmentTypes, s as startServerWithFileWatcher } from "./index-DapYyqAC.js";
|
|
15
15
|
import "@citation-js/core";
|
|
16
16
|
import "@citation-js/plugin-csl";
|
|
17
17
|
import { ZodOptional as ZodOptional$2, z } from "zod";
|
|
18
18
|
import { serve } from "@hono/node-server";
|
|
19
19
|
const name = "@ncukondo/reference-manager";
|
|
20
|
-
const version$1 = "0.
|
|
20
|
+
const version$1 = "0.15.0";
|
|
21
21
|
const description$1 = "A local reference management tool using CSL-JSON as the single source of truth";
|
|
22
22
|
const packageJson = {
|
|
23
23
|
name,
|
|
@@ -261,6 +261,598 @@ function getExitCode(result) {
|
|
|
261
261
|
}
|
|
262
262
|
return 0;
|
|
263
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
|
+
}
|
|
264
856
|
class OperationsLibrary {
|
|
265
857
|
constructor(library, citationConfig) {
|
|
266
858
|
this.library = library;
|
|
@@ -287,15 +879,15 @@ class OperationsLibrary {
|
|
|
287
879
|
}
|
|
288
880
|
// High-level operations
|
|
289
881
|
async search(options) {
|
|
290
|
-
const { searchReferences } = await import("./index-
|
|
882
|
+
const { searchReferences } = await import("./index-DHgeuWGP.js").then((n) => n.n);
|
|
291
883
|
return searchReferences(this.library, options);
|
|
292
884
|
}
|
|
293
885
|
async list(options) {
|
|
294
|
-
const { listReferences } = await import("./index-
|
|
886
|
+
const { listReferences } = await import("./index-DHgeuWGP.js").then((n) => n.m);
|
|
295
887
|
return listReferences(this.library, options ?? {});
|
|
296
888
|
}
|
|
297
889
|
async cite(options) {
|
|
298
|
-
const { citeReferences } = await import("./index-
|
|
890
|
+
const { citeReferences } = await import("./index-DHgeuWGP.js").then((n) => n.l);
|
|
299
891
|
const defaultStyle = options.defaultStyle ?? this.citationConfig?.defaultStyle;
|
|
300
892
|
const cslDirectory = options.cslDirectory ?? this.citationConfig?.cslDirectory;
|
|
301
893
|
const mergedOptions = {
|
|
@@ -306,9 +898,34 @@ class OperationsLibrary {
|
|
|
306
898
|
return citeReferences(this.library, mergedOptions);
|
|
307
899
|
}
|
|
308
900
|
async import(inputs, options) {
|
|
309
|
-
const { addReferences } = await import("./index-
|
|
901
|
+
const { addReferences } = await import("./index-DHgeuWGP.js").then((n) => n.k);
|
|
310
902
|
return addReferences(inputs, this.library, options ?? {});
|
|
311
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
|
+
}
|
|
312
929
|
}
|
|
313
930
|
class ServerClient {
|
|
314
931
|
constructor(baseUrl) {
|
|
@@ -495,6 +1112,111 @@ class ServerClient {
|
|
|
495
1112
|
}
|
|
496
1113
|
return await response.json();
|
|
497
1114
|
}
|
|
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());
|
|
1132
|
+
}
|
|
1133
|
+
return await response.json();
|
|
1134
|
+
}
|
|
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
|
+
}
|
|
498
1220
|
}
|
|
499
1221
|
async function getServerConnection(libraryPath, config2) {
|
|
500
1222
|
const portfilePath = getPortfilePath();
|
|
@@ -622,40 +1344,494 @@ async function readIdentifierFromStdin() {
|
|
|
622
1344
|
const firstLine = content.split("\n")[0]?.trim();
|
|
623
1345
|
return firstLine || void 0;
|
|
624
1346
|
}
|
|
625
|
-
async function readConfirmation(prompt) {
|
|
626
|
-
if (!isTTY()) {
|
|
627
|
-
return true;
|
|
1347
|
+
async function readConfirmation(prompt) {
|
|
1348
|
+
if (!isTTY()) {
|
|
1349
|
+
return true;
|
|
1350
|
+
}
|
|
1351
|
+
const enquirer = await import("enquirer");
|
|
1352
|
+
const Confirm = enquirer.default.Confirm;
|
|
1353
|
+
const confirmPrompt = new Confirm({
|
|
1354
|
+
name: "confirm",
|
|
1355
|
+
message: prompt,
|
|
1356
|
+
initial: false
|
|
1357
|
+
});
|
|
1358
|
+
try {
|
|
1359
|
+
return await confirmPrompt.run();
|
|
1360
|
+
} catch {
|
|
1361
|
+
return false;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
async function readStdinContent() {
|
|
1365
|
+
const chunks = [];
|
|
1366
|
+
for await (const chunk of stdin) {
|
|
1367
|
+
chunks.push(chunk);
|
|
1368
|
+
}
|
|
1369
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
1370
|
+
}
|
|
1371
|
+
async function readStdinBuffer() {
|
|
1372
|
+
const chunks = [];
|
|
1373
|
+
for await (const chunk of stdin) {
|
|
1374
|
+
chunks.push(chunk);
|
|
1375
|
+
}
|
|
1376
|
+
return Buffer.concat(chunks);
|
|
1377
|
+
}
|
|
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);
|
|
1388
|
+
}
|
|
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 }
|
|
1399
|
+
};
|
|
1400
|
+
return addAttachment(context.library, operationOptions);
|
|
1401
|
+
}
|
|
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);
|
|
1409
|
+
}
|
|
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
|
+
}
|
|
1478
|
+
const lines = [];
|
|
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
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
lines.push("");
|
|
1491
|
+
}
|
|
1492
|
+
return lines.join("\n").trimEnd();
|
|
1493
|
+
}
|
|
1494
|
+
function formatAttachDetachOutput(result) {
|
|
1495
|
+
if (!result.success) {
|
|
1496
|
+
return `Error: ${result.error}`;
|
|
1497
|
+
}
|
|
1498
|
+
const lines = [];
|
|
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}`);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
if (result.directoryDeleted) {
|
|
1507
|
+
lines.push("Directory removed.");
|
|
1508
|
+
}
|
|
1509
|
+
return lines.join("\n");
|
|
1510
|
+
}
|
|
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}`);
|
|
1533
|
+
}
|
|
1534
|
+
lines.push("");
|
|
1535
|
+
}
|
|
1536
|
+
function formatAttachSyncOutput(result) {
|
|
1537
|
+
if (!result.success) {
|
|
1538
|
+
return `Error: ${result.error}`;
|
|
1539
|
+
}
|
|
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();
|
|
1557
|
+
}
|
|
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");
|
|
1563
|
+
const allReferences = await context.library.getAll();
|
|
1564
|
+
const identifiers = await selectReferencesOrExit(
|
|
1565
|
+
allReferences,
|
|
1566
|
+
{ multiSelect: false },
|
|
1567
|
+
config2.cli.tui
|
|
1568
|
+
);
|
|
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();
|
|
1612
|
+
});
|
|
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
|
+
`);
|
|
1685
|
+
process.exit(0);
|
|
1686
|
+
}
|
|
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);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
async function handleAttachAddAction(identifierArg, filePathArg, options, globalOpts) {
|
|
1707
|
+
try {
|
|
1708
|
+
const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
1709
|
+
const context = await createExecutionContext(config2, Library.load);
|
|
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);
|
|
628
1752
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const confirmPrompt = new Confirm({
|
|
632
|
-
name: "confirm",
|
|
633
|
-
message: prompt,
|
|
634
|
-
initial: false
|
|
635
|
-
});
|
|
1753
|
+
}
|
|
1754
|
+
async function handleAttachGetAction(identifierArg, filenameArg, options, globalOpts) {
|
|
636
1755
|
try {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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);
|
|
640
1782
|
}
|
|
641
1783
|
}
|
|
642
|
-
async function
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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);
|
|
646
1807
|
}
|
|
647
|
-
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
648
1808
|
}
|
|
649
|
-
async function
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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);
|
|
653
1830
|
}
|
|
654
|
-
return Buffer.concat(chunks);
|
|
655
1831
|
}
|
|
656
1832
|
async function validateOptions$2(options) {
|
|
657
|
-
if (options.
|
|
658
|
-
throw new Error(`Invalid format '${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`);
|
|
659
1835
|
}
|
|
660
1836
|
if (options.cslFile) {
|
|
661
1837
|
const fs2 = await import("node:fs");
|
|
@@ -671,7 +1847,7 @@ function buildCiteOptions(options) {
|
|
|
671
1847
|
...options.style !== void 0 && { style: options.style },
|
|
672
1848
|
...options.cslFile !== void 0 && { cslFile: options.cslFile },
|
|
673
1849
|
...options.locale !== void 0 && { locale: options.locale },
|
|
674
|
-
...options.
|
|
1850
|
+
...options.output !== void 0 && { format: options.output },
|
|
675
1851
|
...options.inText !== void 0 && { inText: options.inText }
|
|
676
1852
|
};
|
|
677
1853
|
}
|
|
@@ -709,13 +1885,13 @@ function getCiteExitCode(result) {
|
|
|
709
1885
|
return 0;
|
|
710
1886
|
}
|
|
711
1887
|
async function executeInteractiveCite(options, context, config2) {
|
|
712
|
-
const { selectReferencesOrExit } = await import("./reference-select-
|
|
713
|
-
const { runStyleSelect } = await import("./style-select-
|
|
1888
|
+
const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
|
|
1889
|
+
const { runStyleSelect } = await import("./style-select-BNQHC79W.js");
|
|
714
1890
|
const allReferences = await context.library.getAll();
|
|
715
1891
|
const identifiers = await selectReferencesOrExit(
|
|
716
1892
|
allReferences,
|
|
717
1893
|
{ multiSelect: true },
|
|
718
|
-
config2.cli.
|
|
1894
|
+
config2.cli.tui
|
|
719
1895
|
);
|
|
720
1896
|
let style = options.style;
|
|
721
1897
|
if (!style && !options.cslFile) {
|
|
@@ -770,7 +1946,7 @@ async function handleCiteAction(identifiers, options, globalOpts) {
|
|
|
770
1946
|
}
|
|
771
1947
|
const ENV_OVERRIDE_MAP = {
|
|
772
1948
|
REFERENCE_MANAGER_LIBRARY: "library",
|
|
773
|
-
|
|
1949
|
+
REFERENCE_MANAGER_ATTACHMENTS_DIR: "attachments.directory",
|
|
774
1950
|
REFERENCE_MANAGER_CLI_DEFAULT_LIMIT: "cli.default_limit",
|
|
775
1951
|
REFERENCE_MANAGER_MCP_DEFAULT_LIMIT: "mcp.default_limit",
|
|
776
1952
|
PUBMED_EMAIL: "pubmed.email",
|
|
@@ -852,14 +2028,14 @@ const CONFIG_KEY_REGISTRY = [
|
|
|
852
2028
|
description: "Default sort order",
|
|
853
2029
|
enumValues: ["asc", "desc"]
|
|
854
2030
|
},
|
|
855
|
-
// cli.
|
|
2031
|
+
// cli.tui section
|
|
856
2032
|
{
|
|
857
|
-
key: "cli.
|
|
2033
|
+
key: "cli.tui.limit",
|
|
858
2034
|
type: "integer",
|
|
859
|
-
description: "Result limit in
|
|
2035
|
+
description: "Result limit in TUI mode"
|
|
860
2036
|
},
|
|
861
2037
|
{
|
|
862
|
-
key: "cli.
|
|
2038
|
+
key: "cli.tui.debounce_ms",
|
|
863
2039
|
type: "integer",
|
|
864
2040
|
description: "Search debounce delay (ms)"
|
|
865
2041
|
},
|
|
@@ -1064,7 +2240,7 @@ function createConfigTemplate() {
|
|
|
1064
2240
|
# default_sort = "updated" # created, updated, published, author, title
|
|
1065
2241
|
# default_order = "desc" # asc, desc
|
|
1066
2242
|
|
|
1067
|
-
[cli.
|
|
2243
|
+
[cli.tui]
|
|
1068
2244
|
# limit = 20
|
|
1069
2245
|
# debounce_ms = 200
|
|
1070
2246
|
|
|
@@ -1307,9 +2483,9 @@ function toSnakeCaseConfig(config2) {
|
|
|
1307
2483
|
default_limit: config2.cli.defaultLimit,
|
|
1308
2484
|
default_sort: config2.cli.defaultSort,
|
|
1309
2485
|
default_order: config2.cli.defaultOrder,
|
|
1310
|
-
|
|
1311
|
-
limit: config2.cli.
|
|
1312
|
-
debounce_ms: config2.cli.
|
|
2486
|
+
tui: {
|
|
2487
|
+
limit: config2.cli.tui.limit,
|
|
2488
|
+
debounce_ms: config2.cli.tui.debounceMs
|
|
1313
2489
|
},
|
|
1314
2490
|
edit: {
|
|
1315
2491
|
default_format: config2.cli.edit.defaultFormat
|
|
@@ -1466,11 +2642,11 @@ function resolveEditor(platform) {
|
|
|
1466
2642
|
}
|
|
1467
2643
|
function registerConfigCommand(program) {
|
|
1468
2644
|
const configCmd = program.command("config").description("Manage configuration settings");
|
|
1469
|
-
configCmd.command("show").description("Display effective configuration").option("--
|
|
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) => {
|
|
1470
2646
|
try {
|
|
1471
2647
|
const config2 = loadConfig();
|
|
1472
2648
|
const output = showConfig(config2, {
|
|
1473
|
-
json: options.json,
|
|
2649
|
+
json: options.output === "json",
|
|
1474
2650
|
section: options.section,
|
|
1475
2651
|
sources: options.sources
|
|
1476
2652
|
});
|
|
@@ -1557,7 +2733,7 @@ function registerConfigCommand(program) {
|
|
|
1557
2733
|
process.exit(1);
|
|
1558
2734
|
}
|
|
1559
2735
|
});
|
|
1560
|
-
configCmd.command("
|
|
2736
|
+
configCmd.command("keys").description("List all available configuration keys").option("--section <name>", "List keys only in a specific section").action(async (options) => {
|
|
1561
2737
|
try {
|
|
1562
2738
|
const output = listConfigKeys({ section: options.section });
|
|
1563
2739
|
if (output) {
|
|
@@ -4542,12 +5718,12 @@ function formatEditOutput(result) {
|
|
|
4542
5718
|
return lines.join("\n");
|
|
4543
5719
|
}
|
|
4544
5720
|
async function executeInteractiveEdit(options, context, config2) {
|
|
4545
|
-
const { selectReferencesOrExit } = await import("./reference-select-
|
|
5721
|
+
const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
|
|
4546
5722
|
const allReferences = await context.library.getAll();
|
|
4547
5723
|
const identifiers = await selectReferencesOrExit(
|
|
4548
5724
|
allReferences,
|
|
4549
5725
|
{ multiSelect: true },
|
|
4550
|
-
config2.cli.
|
|
5726
|
+
config2.cli.tui
|
|
4551
5727
|
);
|
|
4552
5728
|
const format2 = options.format ?? config2.cli.edit.defaultFormat;
|
|
4553
5729
|
return executeEditCommand(
|
|
@@ -7778,7 +8954,7 @@ async function executeExport(options, context) {
|
|
|
7778
8954
|
return { items: items2, notFound };
|
|
7779
8955
|
}
|
|
7780
8956
|
function formatExportOutput(result, options) {
|
|
7781
|
-
const format2 = options.
|
|
8957
|
+
const format2 = options.output ?? "json";
|
|
7782
8958
|
const singleIdRequest = (options.ids?.length ?? 0) === 1 && !options.all && !options.search;
|
|
7783
8959
|
const data = result.items.length === 1 && singleIdRequest ? result.items[0] : result.items;
|
|
7784
8960
|
if (format2 === "json") {
|
|
@@ -7795,183 +8971,6 @@ function formatExportOutput(result, options) {
|
|
|
7795
8971
|
function getExportExitCode(result) {
|
|
7796
8972
|
return result.notFound.length > 0 ? 1 : 0;
|
|
7797
8973
|
}
|
|
7798
|
-
const FULLTEXT_EXTENSIONS = {
|
|
7799
|
-
pdf: ".pdf",
|
|
7800
|
-
markdown: ".md"
|
|
7801
|
-
};
|
|
7802
|
-
function generateFulltextFilename(item, type2) {
|
|
7803
|
-
const uuid2 = item.custom?.uuid;
|
|
7804
|
-
if (!uuid2) {
|
|
7805
|
-
throw new Error("Missing uuid in custom field");
|
|
7806
|
-
}
|
|
7807
|
-
const parts = [item.id];
|
|
7808
|
-
if (item.PMID && item.PMID.length > 0) {
|
|
7809
|
-
parts.push(`PMID${item.PMID}`);
|
|
7810
|
-
}
|
|
7811
|
-
parts.push(uuid2);
|
|
7812
|
-
return parts.join("-") + FULLTEXT_EXTENSIONS[type2];
|
|
7813
|
-
}
|
|
7814
|
-
class FulltextIOError extends Error {
|
|
7815
|
-
constructor(message, cause) {
|
|
7816
|
-
super(message);
|
|
7817
|
-
this.cause = cause;
|
|
7818
|
-
this.name = "FulltextIOError";
|
|
7819
|
-
}
|
|
7820
|
-
}
|
|
7821
|
-
class FulltextNotAttachedError extends Error {
|
|
7822
|
-
constructor(itemId, type2) {
|
|
7823
|
-
super(`No ${type2} attached to reference ${itemId}`);
|
|
7824
|
-
this.itemId = itemId;
|
|
7825
|
-
this.type = type2;
|
|
7826
|
-
this.name = "FulltextNotAttachedError";
|
|
7827
|
-
}
|
|
7828
|
-
}
|
|
7829
|
-
class FulltextManager {
|
|
7830
|
-
constructor(fulltextDirectory) {
|
|
7831
|
-
this.fulltextDirectory = fulltextDirectory;
|
|
7832
|
-
}
|
|
7833
|
-
/**
|
|
7834
|
-
* Ensure the fulltext directory exists
|
|
7835
|
-
*/
|
|
7836
|
-
async ensureDirectory() {
|
|
7837
|
-
await mkdir(this.fulltextDirectory, { recursive: true });
|
|
7838
|
-
}
|
|
7839
|
-
/**
|
|
7840
|
-
* Attach a file to a reference
|
|
7841
|
-
*/
|
|
7842
|
-
async attachFile(item, sourcePath, type2, options) {
|
|
7843
|
-
const { move = false, force = false } = options ?? {};
|
|
7844
|
-
const newFilename = generateFulltextFilename(item, type2);
|
|
7845
|
-
this.validateSourceFile(sourcePath);
|
|
7846
|
-
const existingFilename = this.getExistingFilename(item, type2);
|
|
7847
|
-
if (existingFilename && !force) {
|
|
7848
|
-
return {
|
|
7849
|
-
filename: newFilename,
|
|
7850
|
-
existingFile: existingFilename,
|
|
7851
|
-
overwritten: false
|
|
7852
|
-
};
|
|
7853
|
-
}
|
|
7854
|
-
await this.ensureDirectory();
|
|
7855
|
-
const deletedOldFile = await this.deleteOldFileIfNeeded(existingFilename, newFilename, force);
|
|
7856
|
-
const destPath = join(this.fulltextDirectory, newFilename);
|
|
7857
|
-
await this.copyOrMoveFile(sourcePath, destPath, move);
|
|
7858
|
-
const result = {
|
|
7859
|
-
filename: newFilename,
|
|
7860
|
-
overwritten: existingFilename !== void 0
|
|
7861
|
-
};
|
|
7862
|
-
if (deletedOldFile) {
|
|
7863
|
-
result.deletedOldFile = deletedOldFile;
|
|
7864
|
-
}
|
|
7865
|
-
return result;
|
|
7866
|
-
}
|
|
7867
|
-
/**
|
|
7868
|
-
* Validate that source file exists
|
|
7869
|
-
*/
|
|
7870
|
-
validateSourceFile(sourcePath) {
|
|
7871
|
-
if (!existsSync(sourcePath)) {
|
|
7872
|
-
throw new FulltextIOError(`Source file not found: ${sourcePath}`);
|
|
7873
|
-
}
|
|
7874
|
-
}
|
|
7875
|
-
/**
|
|
7876
|
-
* Delete old file if force mode and filename changed
|
|
7877
|
-
* @returns Deleted filename or undefined
|
|
7878
|
-
*/
|
|
7879
|
-
async deleteOldFileIfNeeded(existingFilename, newFilename, force) {
|
|
7880
|
-
if (!force || !existingFilename || existingFilename === newFilename) {
|
|
7881
|
-
return void 0;
|
|
7882
|
-
}
|
|
7883
|
-
const oldPath = join(this.fulltextDirectory, existingFilename);
|
|
7884
|
-
try {
|
|
7885
|
-
await unlink(oldPath);
|
|
7886
|
-
} catch {
|
|
7887
|
-
}
|
|
7888
|
-
return existingFilename;
|
|
7889
|
-
}
|
|
7890
|
-
/**
|
|
7891
|
-
* Copy or move file to destination
|
|
7892
|
-
*/
|
|
7893
|
-
async copyOrMoveFile(sourcePath, destPath, move) {
|
|
7894
|
-
try {
|
|
7895
|
-
if (move) {
|
|
7896
|
-
await rename(sourcePath, destPath);
|
|
7897
|
-
} else {
|
|
7898
|
-
await copyFile(sourcePath, destPath);
|
|
7899
|
-
}
|
|
7900
|
-
} catch (error) {
|
|
7901
|
-
const operation = move ? "move" : "copy";
|
|
7902
|
-
throw new FulltextIOError(
|
|
7903
|
-
`Failed to ${operation} file to ${destPath}`,
|
|
7904
|
-
error instanceof Error ? error : void 0
|
|
7905
|
-
);
|
|
7906
|
-
}
|
|
7907
|
-
}
|
|
7908
|
-
/**
|
|
7909
|
-
* Get the full path for an attached file
|
|
7910
|
-
* @returns Full path or null if not attached
|
|
7911
|
-
*/
|
|
7912
|
-
getFilePath(item, type2) {
|
|
7913
|
-
const filename = this.getExistingFilename(item, type2);
|
|
7914
|
-
if (!filename) {
|
|
7915
|
-
return null;
|
|
7916
|
-
}
|
|
7917
|
-
return join(this.fulltextDirectory, filename);
|
|
7918
|
-
}
|
|
7919
|
-
/**
|
|
7920
|
-
* Detach a file from a reference
|
|
7921
|
-
*/
|
|
7922
|
-
async detachFile(item, type2, options) {
|
|
7923
|
-
const { delete: deleteFile = false } = options ?? {};
|
|
7924
|
-
const filename = this.getExistingFilename(item, type2);
|
|
7925
|
-
if (!filename) {
|
|
7926
|
-
throw new FulltextNotAttachedError(item.id, type2);
|
|
7927
|
-
}
|
|
7928
|
-
if (deleteFile) {
|
|
7929
|
-
const filePath = join(this.fulltextDirectory, filename);
|
|
7930
|
-
try {
|
|
7931
|
-
await unlink(filePath);
|
|
7932
|
-
} catch {
|
|
7933
|
-
}
|
|
7934
|
-
}
|
|
7935
|
-
return {
|
|
7936
|
-
filename,
|
|
7937
|
-
deleted: deleteFile
|
|
7938
|
-
};
|
|
7939
|
-
}
|
|
7940
|
-
/**
|
|
7941
|
-
* Get list of attached fulltext types
|
|
7942
|
-
*/
|
|
7943
|
-
getAttachedTypes(item) {
|
|
7944
|
-
const types2 = [];
|
|
7945
|
-
const fulltext = item.custom?.fulltext;
|
|
7946
|
-
if (fulltext?.pdf) {
|
|
7947
|
-
types2.push("pdf");
|
|
7948
|
-
}
|
|
7949
|
-
if (fulltext?.markdown) {
|
|
7950
|
-
types2.push("markdown");
|
|
7951
|
-
}
|
|
7952
|
-
return types2;
|
|
7953
|
-
}
|
|
7954
|
-
/**
|
|
7955
|
-
* Check if item has attachment
|
|
7956
|
-
* @param type Optional type to check; if omitted, checks for any attachment
|
|
7957
|
-
*/
|
|
7958
|
-
hasAttachment(item, type2) {
|
|
7959
|
-
if (type2) {
|
|
7960
|
-
return this.getExistingFilename(item, type2) !== void 0;
|
|
7961
|
-
}
|
|
7962
|
-
return this.getAttachedTypes(item).length > 0;
|
|
7963
|
-
}
|
|
7964
|
-
/**
|
|
7965
|
-
* Get existing filename from item metadata
|
|
7966
|
-
*/
|
|
7967
|
-
getExistingFilename(item, type2) {
|
|
7968
|
-
const fulltext = item.custom?.fulltext;
|
|
7969
|
-
if (!fulltext) {
|
|
7970
|
-
return void 0;
|
|
7971
|
-
}
|
|
7972
|
-
return fulltext[type2];
|
|
7973
|
-
}
|
|
7974
|
-
}
|
|
7975
8974
|
function detectType(filePath) {
|
|
7976
8975
|
const ext = extname(filePath).toLowerCase();
|
|
7977
8976
|
if (ext === ".pdf") return "pdf";
|
|
@@ -7996,8 +8995,8 @@ function resolveFileType(explicitType, filePath, stdinContent) {
|
|
|
7996
8995
|
function prepareStdinSource(stdinContent, fileType) {
|
|
7997
8996
|
try {
|
|
7998
8997
|
const tempDir = mkdtempSync(join(tmpdir(), "refmgr-"));
|
|
7999
|
-
const ext = fileType
|
|
8000
|
-
const sourcePath = join(tempDir, `stdin
|
|
8998
|
+
const ext = formatToExtension(fileType);
|
|
8999
|
+
const sourcePath = join(tempDir, `stdin.${ext}`);
|
|
8001
9000
|
writeFileSync(sourcePath, stdinContent);
|
|
8002
9001
|
return { sourcePath, tempDir };
|
|
8003
9002
|
} catch (error) {
|
|
@@ -8012,13 +9011,6 @@ async function cleanupTempDir(tempDir) {
|
|
|
8012
9011
|
});
|
|
8013
9012
|
}
|
|
8014
9013
|
}
|
|
8015
|
-
function buildNewFulltext(currentFulltext, fileType, filename) {
|
|
8016
|
-
const newFulltext = {};
|
|
8017
|
-
if (currentFulltext.pdf) newFulltext.pdf = currentFulltext.pdf;
|
|
8018
|
-
if (currentFulltext.markdown) newFulltext.markdown = currentFulltext.markdown;
|
|
8019
|
-
newFulltext[fileType] = filename;
|
|
8020
|
-
return newFulltext;
|
|
8021
|
-
}
|
|
8022
9014
|
function prepareSourcePath(filePath, stdinContent, fileType) {
|
|
8023
9015
|
if (stdinContent) {
|
|
8024
9016
|
return prepareStdinSource(stdinContent, fileType);
|
|
@@ -8028,12 +9020,26 @@ function prepareSourcePath(filePath, stdinContent, fileType) {
|
|
|
8028
9020
|
}
|
|
8029
9021
|
return { sourcePath: filePath };
|
|
8030
9022
|
}
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
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
|
|
8035
9042
|
};
|
|
8036
|
-
return manager.attachFile(item, sourcePath, fileType, attachOptions);
|
|
8037
9043
|
}
|
|
8038
9044
|
async function fulltextAttach(library, options) {
|
|
8039
9045
|
const {
|
|
@@ -8046,71 +9052,78 @@ async function fulltextAttach(library, options) {
|
|
|
8046
9052
|
fulltextDirectory,
|
|
8047
9053
|
stdinContent
|
|
8048
9054
|
} = options;
|
|
8049
|
-
const item = await library.find(identifier, { idType });
|
|
8050
|
-
if (!item) {
|
|
8051
|
-
return { success: false, error: `Reference '${identifier}' not found` };
|
|
8052
|
-
}
|
|
8053
9055
|
const fileTypeResult = resolveFileType(explicitType, filePath, stdinContent);
|
|
8054
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
|
+
}
|
|
8055
9061
|
return { success: false, error: fileTypeResult.error };
|
|
8056
9062
|
}
|
|
8057
9063
|
const fileType = fileTypeResult;
|
|
8058
9064
|
const sourceResult = prepareSourcePath(filePath, stdinContent, fileType);
|
|
8059
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
|
+
}
|
|
8060
9070
|
return { success: false, error: sourceResult.error };
|
|
8061
9071
|
}
|
|
8062
9072
|
const { sourcePath, tempDir } = sourceResult;
|
|
8063
|
-
const manager = new FulltextManager(fulltextDirectory);
|
|
8064
9073
|
try {
|
|
8065
|
-
const result = await
|
|
8066
|
-
if (result.existingFile && !result.overwritten) {
|
|
8067
|
-
await cleanupTempDir(tempDir);
|
|
8068
|
-
return { success: false, existingFile: result.existingFile, requiresConfirmation: true };
|
|
8069
|
-
}
|
|
8070
|
-
const newFulltext = buildNewFulltext(item.custom?.fulltext ?? {}, fileType, result.filename);
|
|
8071
|
-
await updateReference(library, {
|
|
9074
|
+
const result = await addAttachment(library, {
|
|
8072
9075
|
identifier,
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
9076
|
+
filePath: sourcePath,
|
|
9077
|
+
role: FULLTEXT_ROLE,
|
|
9078
|
+
move: move ?? false,
|
|
9079
|
+
force: force ?? false,
|
|
9080
|
+
idType,
|
|
9081
|
+
attachmentsDirectory: fulltextDirectory
|
|
8077
9082
|
});
|
|
8078
9083
|
await cleanupTempDir(tempDir);
|
|
8079
|
-
return
|
|
8080
|
-
success: true,
|
|
8081
|
-
filename: result.filename,
|
|
8082
|
-
type: fileType,
|
|
8083
|
-
overwritten: result.overwritten
|
|
8084
|
-
};
|
|
9084
|
+
return convertResult(result, fileType);
|
|
8085
9085
|
} catch (error) {
|
|
8086
9086
|
await cleanupTempDir(tempDir);
|
|
8087
|
-
if (error instanceof FulltextIOError) {
|
|
8088
|
-
return { success: false, error: error.message };
|
|
8089
|
-
}
|
|
8090
9087
|
throw error;
|
|
8091
9088
|
}
|
|
8092
9089
|
}
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
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) {
|
|
8096
9100
|
return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
|
|
8097
9101
|
}
|
|
9102
|
+
const filePath = buildFilePath$1(fulltextDirectory, attachments.directory, file.filename);
|
|
8098
9103
|
try {
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
return {
|
|
8103
|
-
success: false,
|
|
8104
|
-
error: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`
|
|
8105
|
-
};
|
|
9104
|
+
return await getFileContent(filePath);
|
|
9105
|
+
} catch {
|
|
9106
|
+
return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
|
|
8106
9107
|
}
|
|
8107
9108
|
}
|
|
8108
|
-
function
|
|
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}'` };
|
|
9113
|
+
}
|
|
9114
|
+
const filePath = buildFilePath$1(fulltextDirectory, attachments.directory, file.filename);
|
|
9115
|
+
const paths = {};
|
|
9116
|
+
paths[type2] = filePath;
|
|
9117
|
+
return { success: true, paths };
|
|
9118
|
+
}
|
|
9119
|
+
function getAllFulltextPaths(attachments, fulltextFiles, fulltextDirectory, identifier) {
|
|
8109
9120
|
const paths = {};
|
|
8110
|
-
for (const
|
|
8111
|
-
const
|
|
8112
|
-
|
|
8113
|
-
|
|
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;
|
|
8114
9127
|
}
|
|
8115
9128
|
}
|
|
8116
9129
|
if (Object.keys(paths).length === 0) {
|
|
@@ -8124,92 +9137,98 @@ async function fulltextGet(library, options) {
|
|
|
8124
9137
|
if (!item) {
|
|
8125
9138
|
return { success: false, error: `Reference '${identifier}' not found` };
|
|
8126
9139
|
}
|
|
8127
|
-
const
|
|
9140
|
+
const attachments = item.custom?.attachments;
|
|
8128
9141
|
if (stdout2 && type2) {
|
|
8129
|
-
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}'` };
|
|
9147
|
+
}
|
|
9148
|
+
if (type2) {
|
|
9149
|
+
return getSingleTypePath(attachments, type2, identifier, fulltextDirectory);
|
|
8130
9150
|
}
|
|
8131
|
-
|
|
8132
|
-
if (attachedTypes.length === 0) {
|
|
9151
|
+
if (!attachments) {
|
|
8133
9152
|
return { success: false, error: `No fulltext attached to '${identifier}'` };
|
|
8134
9153
|
}
|
|
8135
|
-
return
|
|
9154
|
+
return getAllFulltextPaths(attachments, fulltextFiles, fulltextDirectory, identifier);
|
|
8136
9155
|
}
|
|
8137
|
-
|
|
9156
|
+
function getFilesToDetach(attachments, type2) {
|
|
9157
|
+
if (type2) {
|
|
9158
|
+
const file = findFulltextFile(attachments, type2);
|
|
9159
|
+
return file ? [file] : [];
|
|
9160
|
+
}
|
|
9161
|
+
return findFulltextFiles(attachments);
|
|
9162
|
+
}
|
|
9163
|
+
async function detachFiles(library, files, identifier, removeFiles, idType, fulltextDirectory) {
|
|
8138
9164
|
const detached = [];
|
|
8139
9165
|
const deleted = [];
|
|
8140
|
-
for (const
|
|
8141
|
-
const
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
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
|
+
}
|
|
8146
9183
|
}
|
|
8147
9184
|
}
|
|
8148
9185
|
return { detached, deleted };
|
|
8149
9186
|
}
|
|
8150
|
-
function
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
newFulltext.pdf = currentFulltext.pdf;
|
|
8154
|
-
}
|
|
8155
|
-
if (currentFulltext.markdown && !detached.includes("markdown")) {
|
|
8156
|
-
newFulltext.markdown = currentFulltext.markdown;
|
|
9187
|
+
function buildResult(detached, deleted, identifier) {
|
|
9188
|
+
if (detached.length === 0) {
|
|
9189
|
+
return { success: false, error: `Failed to detach fulltext from '${identifier}'` };
|
|
8157
9190
|
}
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
if (error instanceof FulltextNotAttachedError || error instanceof FulltextIOError) {
|
|
8162
|
-
return { success: false, error: error.message };
|
|
9191
|
+
const result = { success: true, detached };
|
|
9192
|
+
if (deleted.length > 0) {
|
|
9193
|
+
result.deleted = deleted;
|
|
8163
9194
|
}
|
|
8164
|
-
|
|
9195
|
+
return result;
|
|
8165
9196
|
}
|
|
8166
9197
|
async function fulltextDetach(library, options) {
|
|
8167
|
-
const { identifier, type: type2,
|
|
9198
|
+
const { identifier, type: type2, removeFiles, idType = "id", fulltextDirectory } = options;
|
|
8168
9199
|
const item = await library.find(identifier, { idType });
|
|
8169
9200
|
if (!item) {
|
|
8170
9201
|
return { success: false, error: `Reference '${identifier}' not found` };
|
|
8171
9202
|
}
|
|
8172
|
-
const
|
|
8173
|
-
const
|
|
8174
|
-
if (
|
|
9203
|
+
const attachments = item.custom?.attachments;
|
|
9204
|
+
const fulltextFiles = findFulltextFiles(attachments);
|
|
9205
|
+
if (fulltextFiles.length === 0) {
|
|
8175
9206
|
return { success: false, error: `No fulltext attached to '${identifier}'` };
|
|
8176
9207
|
}
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
item,
|
|
8181
|
-
typesToDetach,
|
|
8182
|
-
deleteFile
|
|
8183
|
-
);
|
|
8184
|
-
const updatedFulltext = buildRemainingFulltext(item.custom?.fulltext ?? {}, detached);
|
|
8185
|
-
await updateReference(library, {
|
|
8186
|
-
identifier,
|
|
8187
|
-
updates: {
|
|
8188
|
-
custom: { fulltext: updatedFulltext }
|
|
8189
|
-
},
|
|
8190
|
-
idType
|
|
8191
|
-
});
|
|
8192
|
-
const resultData = { success: true, detached };
|
|
8193
|
-
if (deleted.length > 0) {
|
|
8194
|
-
resultData.deleted = deleted;
|
|
8195
|
-
}
|
|
8196
|
-
return resultData;
|
|
8197
|
-
} catch (error) {
|
|
8198
|
-
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}'` };
|
|
8199
9211
|
}
|
|
8200
|
-
}
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
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";
|
|
8213
9232
|
return void 0;
|
|
8214
9233
|
}
|
|
8215
9234
|
async function fulltextOpen(library, options) {
|
|
@@ -8218,14 +9237,16 @@ async function fulltextOpen(library, options) {
|
|
|
8218
9237
|
if (!item) {
|
|
8219
9238
|
return { success: false, error: `Reference not found: ${identifier}` };
|
|
8220
9239
|
}
|
|
8221
|
-
const
|
|
9240
|
+
const attachments = item.custom?.attachments;
|
|
9241
|
+
const typeToOpen = type2 ?? determineTypeToOpen(attachments);
|
|
8222
9242
|
if (!typeToOpen) {
|
|
8223
9243
|
return { success: false, error: `No fulltext attached to reference: ${identifier}` };
|
|
8224
9244
|
}
|
|
8225
|
-
const
|
|
8226
|
-
if (!
|
|
9245
|
+
const file = findFulltextFile(attachments, typeToOpen);
|
|
9246
|
+
if (!file || !attachments?.directory) {
|
|
8227
9247
|
return { success: false, error: `No ${typeToOpen} attached to reference: ${identifier}` };
|
|
8228
9248
|
}
|
|
9249
|
+
const filePath = buildFilePath(fulltextDirectory, attachments.directory, file.filename);
|
|
8229
9250
|
if (!existsSync(filePath)) {
|
|
8230
9251
|
return {
|
|
8231
9252
|
success: false,
|
|
@@ -8273,7 +9294,7 @@ async function executeFulltextDetach(options, context) {
|
|
|
8273
9294
|
const operationOptions = {
|
|
8274
9295
|
identifier: options.identifier,
|
|
8275
9296
|
type: options.type,
|
|
8276
|
-
|
|
9297
|
+
removeFiles: options.removeFiles,
|
|
8277
9298
|
idType: options.idType,
|
|
8278
9299
|
fulltextDirectory: options.fulltextDirectory
|
|
8279
9300
|
};
|
|
@@ -8344,12 +9365,12 @@ function getFulltextExitCode(result) {
|
|
|
8344
9365
|
return result.success ? 0 : 1;
|
|
8345
9366
|
}
|
|
8346
9367
|
async function executeInteractiveSelect(context, config2) {
|
|
8347
|
-
const { selectReferencesOrExit } = await import("./reference-select-
|
|
9368
|
+
const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
|
|
8348
9369
|
const allReferences = await context.library.getAll();
|
|
8349
9370
|
const identifiers = await selectReferencesOrExit(
|
|
8350
9371
|
allReferences,
|
|
8351
9372
|
{ multiSelect: false },
|
|
8352
|
-
config2.cli.
|
|
9373
|
+
config2.cli.tui
|
|
8353
9374
|
);
|
|
8354
9375
|
return identifiers[0];
|
|
8355
9376
|
}
|
|
@@ -8388,7 +9409,7 @@ async function handleFulltextAttachAction(identifierArg, filePathArg, options, g
|
|
|
8388
9409
|
const stdinContent = !filePath && type2 ? await readStdinBuffer() : void 0;
|
|
8389
9410
|
const attachOptions = {
|
|
8390
9411
|
identifier,
|
|
8391
|
-
fulltextDirectory: config2.
|
|
9412
|
+
fulltextDirectory: config2.attachments.directory,
|
|
8392
9413
|
...filePath && { filePath },
|
|
8393
9414
|
...type2 && { type: type2 },
|
|
8394
9415
|
...options.move && { move: options.move },
|
|
@@ -8442,7 +9463,7 @@ async function handleFulltextGetAction(identifierArg, options, globalOpts) {
|
|
|
8442
9463
|
}
|
|
8443
9464
|
const getOptions = {
|
|
8444
9465
|
identifier,
|
|
8445
|
-
fulltextDirectory: config2.
|
|
9466
|
+
fulltextDirectory: config2.attachments.directory,
|
|
8446
9467
|
...options.pdf && { type: "pdf" },
|
|
8447
9468
|
...options.markdown && { type: "markdown" },
|
|
8448
9469
|
...options.stdout && { stdout: options.stdout },
|
|
@@ -8478,10 +9499,10 @@ async function handleFulltextDetachAction(identifierArg, options, globalOpts) {
|
|
|
8478
9499
|
}
|
|
8479
9500
|
const detachOptions = {
|
|
8480
9501
|
identifier,
|
|
8481
|
-
fulltextDirectory: config2.
|
|
9502
|
+
fulltextDirectory: config2.attachments.directory,
|
|
8482
9503
|
...options.pdf && { type: "pdf" },
|
|
8483
9504
|
...options.markdown && { type: "markdown" },
|
|
8484
|
-
...options.
|
|
9505
|
+
...options.removeFiles && { removeFiles: options.removeFiles },
|
|
8485
9506
|
...options.force && { force: options.force },
|
|
8486
9507
|
...options.uuid && { idType: "uuid" }
|
|
8487
9508
|
};
|
|
@@ -8517,7 +9538,7 @@ async function handleFulltextOpenAction(identifierArg, options, globalOpts) {
|
|
|
8517
9538
|
}
|
|
8518
9539
|
const openOptions = {
|
|
8519
9540
|
identifier,
|
|
8520
|
-
fulltextDirectory: config2.
|
|
9541
|
+
fulltextDirectory: config2.attachments.directory,
|
|
8521
9542
|
...options.pdf && { type: "pdf" },
|
|
8522
9543
|
...options.markdown && { type: "markdown" },
|
|
8523
9544
|
...options.uuid && { idType: "uuid" }
|
|
@@ -8624,21 +9645,28 @@ const VALID_LIST_SORT_FIELDS = /* @__PURE__ */ new Set([
|
|
|
8624
9645
|
"pub"
|
|
8625
9646
|
]);
|
|
8626
9647
|
function getOutputFormat$1(options) {
|
|
9648
|
+
if (options.output) {
|
|
9649
|
+
if (options.output === "ids") return "ids-only";
|
|
9650
|
+
return options.output;
|
|
9651
|
+
}
|
|
8627
9652
|
if (options.json) return "json";
|
|
8628
9653
|
if (options.idsOnly) return "ids-only";
|
|
8629
|
-
if (options.
|
|
9654
|
+
if (options.uuidOnly) return "uuid";
|
|
8630
9655
|
if (options.bibtex) return "bibtex";
|
|
8631
9656
|
return "pretty";
|
|
8632
9657
|
}
|
|
8633
9658
|
function validateOptions$1(options) {
|
|
8634
|
-
const outputOptions = [options.json, options.idsOnly, options.
|
|
9659
|
+
const outputOptions = [options.json, options.idsOnly, options.uuidOnly, options.bibtex].filter(
|
|
8635
9660
|
Boolean
|
|
8636
9661
|
);
|
|
8637
9662
|
if (outputOptions.length > 1) {
|
|
8638
9663
|
throw new Error(
|
|
8639
|
-
"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."
|
|
8640
9665
|
);
|
|
8641
9666
|
}
|
|
9667
|
+
if (options.output && outputOptions.length > 0) {
|
|
9668
|
+
throw new Error("Cannot combine --output with convenience flags (--json, --ids-only, etc.)");
|
|
9669
|
+
}
|
|
8642
9670
|
if (options.sort !== void 0) {
|
|
8643
9671
|
const sortStr = String(options.sort);
|
|
8644
9672
|
if (!VALID_LIST_SORT_FIELDS.has(sortStr)) {
|
|
@@ -29050,7 +30078,7 @@ function registerFulltextAttachTool(server, getLibraryOperations, getConfig) {
|
|
|
29050
30078
|
filePath: args.path,
|
|
29051
30079
|
force: true,
|
|
29052
30080
|
// MCP tools don't support interactive confirmation
|
|
29053
|
-
fulltextDirectory: config2.
|
|
30081
|
+
fulltextDirectory: config2.attachments.directory
|
|
29054
30082
|
});
|
|
29055
30083
|
if (!result.success) {
|
|
29056
30084
|
return {
|
|
@@ -29083,7 +30111,7 @@ function registerFulltextGetTool(server, getLibraryOperations, getConfig) {
|
|
|
29083
30111
|
const config2 = getConfig();
|
|
29084
30112
|
const pathResult = await fulltextGet(libraryOps, {
|
|
29085
30113
|
identifier: args.id,
|
|
29086
|
-
fulltextDirectory: config2.
|
|
30114
|
+
fulltextDirectory: config2.attachments.directory
|
|
29087
30115
|
});
|
|
29088
30116
|
if (!pathResult.success) {
|
|
29089
30117
|
return {
|
|
@@ -29097,7 +30125,7 @@ function registerFulltextGetTool(server, getLibraryOperations, getConfig) {
|
|
|
29097
30125
|
identifier: args.id,
|
|
29098
30126
|
type: "markdown",
|
|
29099
30127
|
stdout: true,
|
|
29100
|
-
fulltextDirectory: config2.
|
|
30128
|
+
fulltextDirectory: config2.attachments.directory
|
|
29101
30129
|
});
|
|
29102
30130
|
if (contentResult.success && contentResult.content) {
|
|
29103
30131
|
responses.push({
|
|
@@ -29136,7 +30164,7 @@ function registerFulltextDetachTool(server, getLibraryOperations, getConfig) {
|
|
|
29136
30164
|
const config2 = getConfig();
|
|
29137
30165
|
const result = await fulltextDetach(libraryOps, {
|
|
29138
30166
|
identifier: args.id,
|
|
29139
|
-
fulltextDirectory: config2.
|
|
30167
|
+
fulltextDirectory: config2.attachments.directory
|
|
29140
30168
|
});
|
|
29141
30169
|
if (!result.success) {
|
|
29142
30170
|
return {
|
|
@@ -29342,7 +30370,7 @@ async function mcpStart(options) {
|
|
|
29342
30370
|
async function executeRemove(options, context) {
|
|
29343
30371
|
const { identifier, idType = "id", fulltextDirectory, deleteFulltext = false } = options;
|
|
29344
30372
|
if (context.mode === "local" && deleteFulltext && fulltextDirectory) {
|
|
29345
|
-
const { removeReference } = await import("./index-
|
|
30373
|
+
const { removeReference } = await import("./index-DHgeuWGP.js").then((n) => n.r);
|
|
29346
30374
|
return removeReference(context.library, {
|
|
29347
30375
|
identifier,
|
|
29348
30376
|
idType,
|
|
@@ -29396,12 +30424,12 @@ Continue?`;
|
|
|
29396
30424
|
return readConfirmation(confirmMsg);
|
|
29397
30425
|
}
|
|
29398
30426
|
async function executeInteractiveRemove(context, config2) {
|
|
29399
|
-
const { selectReferenceItemsOrExit } = await import("./reference-select-
|
|
30427
|
+
const { selectReferenceItemsOrExit } = await import("./reference-select-B9w9CLa1.js");
|
|
29400
30428
|
const allReferences = await context.library.getAll();
|
|
29401
30429
|
const selectedItems = await selectReferenceItemsOrExit(
|
|
29402
30430
|
allReferences,
|
|
29403
30431
|
{ multiSelect: false },
|
|
29404
|
-
config2.cli.
|
|
30432
|
+
config2.cli.tui
|
|
29405
30433
|
);
|
|
29406
30434
|
const selectedItem = selectedItems[0];
|
|
29407
30435
|
return { identifier: selectedItem.id, item: selectedItem };
|
|
@@ -29483,7 +30511,7 @@ async function handleRemoveAction(identifierArg, options, globalOpts) {
|
|
|
29483
30511
|
const removeOptions = {
|
|
29484
30512
|
identifier,
|
|
29485
30513
|
idType: useUuid ? "uuid" : "id",
|
|
29486
|
-
fulltextDirectory: config2.
|
|
30514
|
+
fulltextDirectory: config2.attachments.directory,
|
|
29487
30515
|
deleteFulltext: force && hasFulltext
|
|
29488
30516
|
};
|
|
29489
30517
|
const result = await executeRemove(removeOptions, context);
|
|
@@ -29506,21 +30534,28 @@ const VALID_SEARCH_SORT_FIELDS = /* @__PURE__ */ new Set([
|
|
|
29506
30534
|
"rel"
|
|
29507
30535
|
]);
|
|
29508
30536
|
function getOutputFormat(options) {
|
|
30537
|
+
if (options.output) {
|
|
30538
|
+
if (options.output === "ids") return "ids-only";
|
|
30539
|
+
return options.output;
|
|
30540
|
+
}
|
|
29509
30541
|
if (options.json) return "json";
|
|
29510
30542
|
if (options.idsOnly) return "ids-only";
|
|
29511
|
-
if (options.
|
|
30543
|
+
if (options.uuidOnly) return "uuid";
|
|
29512
30544
|
if (options.bibtex) return "bibtex";
|
|
29513
30545
|
return "pretty";
|
|
29514
30546
|
}
|
|
29515
30547
|
function validateOptions(options) {
|
|
29516
|
-
const outputOptions = [options.json, options.idsOnly, options.
|
|
30548
|
+
const outputOptions = [options.json, options.idsOnly, options.uuidOnly, options.bibtex].filter(
|
|
29517
30549
|
Boolean
|
|
29518
30550
|
);
|
|
29519
30551
|
if (outputOptions.length > 1) {
|
|
29520
30552
|
throw new Error(
|
|
29521
|
-
"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."
|
|
29522
30554
|
);
|
|
29523
30555
|
}
|
|
30556
|
+
if (options.output && outputOptions.length > 0) {
|
|
30557
|
+
throw new Error("Cannot combine --output with convenience flags (--json, --ids-only, etc.)");
|
|
30558
|
+
}
|
|
29524
30559
|
if (options.sort !== void 0) {
|
|
29525
30560
|
const sortStr = String(options.sort);
|
|
29526
30561
|
if (!VALID_SEARCH_SORT_FIELDS.has(sortStr)) {
|
|
@@ -29576,35 +30611,39 @@ function formatSearchOutput(result, options) {
|
|
|
29576
30611
|
return lines.join("\n");
|
|
29577
30612
|
}
|
|
29578
30613
|
function validateInteractiveOptions(options) {
|
|
29579
|
-
const outputOptions = [
|
|
29580
|
-
|
|
29581
|
-
|
|
30614
|
+
const outputOptions = [
|
|
30615
|
+
options.output,
|
|
30616
|
+
options.json,
|
|
30617
|
+
options.idsOnly,
|
|
30618
|
+
options.uuidOnly,
|
|
30619
|
+
options.bibtex
|
|
30620
|
+
].filter(Boolean);
|
|
29582
30621
|
if (outputOptions.length > 0) {
|
|
29583
30622
|
throw new Error(
|
|
29584
|
-
"
|
|
30623
|
+
"TUI mode cannot be combined with output format options (--output, --json, --ids-only, --uuid-only, --bibtex)"
|
|
29585
30624
|
);
|
|
29586
30625
|
}
|
|
29587
30626
|
}
|
|
29588
30627
|
async function executeInteractiveSearch(options, context, config2) {
|
|
29589
30628
|
validateInteractiveOptions(options);
|
|
29590
|
-
const { checkTTY } = await import("./tty-
|
|
30629
|
+
const { checkTTY } = await import("./tty-BMyaEOhX.js");
|
|
29591
30630
|
const { runSearchPrompt } = await import("./search-prompt-BrWpOcij.js");
|
|
29592
|
-
const { runActionMenu } = await import("./action-menu-
|
|
29593
|
-
const { search } = await import("./file-watcher-
|
|
29594
|
-
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);
|
|
29595
30634
|
checkTTY();
|
|
29596
30635
|
const allReferences = await context.library.getAll();
|
|
29597
30636
|
const searchFn = (query) => {
|
|
29598
30637
|
const { tokens } = tokenize(query);
|
|
29599
30638
|
return search(allReferences, tokens);
|
|
29600
30639
|
};
|
|
29601
|
-
const
|
|
30640
|
+
const tuiConfig = config2.cli.tui;
|
|
29602
30641
|
const searchResult = await runSearchPrompt(
|
|
29603
30642
|
allReferences,
|
|
29604
30643
|
searchFn,
|
|
29605
30644
|
{
|
|
29606
|
-
limit:
|
|
29607
|
-
debounceMs:
|
|
30645
|
+
limit: tuiConfig.limit,
|
|
30646
|
+
debounceMs: tuiConfig.debounceMs
|
|
29608
30647
|
},
|
|
29609
30648
|
options.query || ""
|
|
29610
30649
|
);
|
|
@@ -29897,12 +30936,12 @@ function formatUpdateOutput(result, identifier) {
|
|
|
29897
30936
|
return parts.join("\n");
|
|
29898
30937
|
}
|
|
29899
30938
|
async function executeInteractiveUpdate(context, config2) {
|
|
29900
|
-
const { selectReferencesOrExit } = await import("./reference-select-
|
|
30939
|
+
const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
|
|
29901
30940
|
const allReferences = await context.library.getAll();
|
|
29902
30941
|
const identifiers = await selectReferencesOrExit(
|
|
29903
30942
|
allReferences,
|
|
29904
30943
|
{ multiSelect: false },
|
|
29905
|
-
config2.cli.
|
|
30944
|
+
config2.cli.tui
|
|
29906
30945
|
);
|
|
29907
30946
|
return identifiers[0];
|
|
29908
30947
|
}
|
|
@@ -30007,8 +31046,13 @@ function collectSetOption(value, previous) {
|
|
|
30007
31046
|
}
|
|
30008
31047
|
const SEARCH_SORT_FIELDS = searchSortFieldSchema.options;
|
|
30009
31048
|
const SORT_ORDERS = sortOrderSchema.options;
|
|
30010
|
-
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"];
|
|
30011
31054
|
const LOG_LEVELS = ["silent", "info", "debug"];
|
|
31055
|
+
const ADD_INPUT_FORMATS = ["json", "bibtex", "ris", "pmid", "doi", "isbn", "auto"];
|
|
30012
31056
|
const CONFIG_SECTIONS = [
|
|
30013
31057
|
"backup",
|
|
30014
31058
|
"citation",
|
|
@@ -30019,17 +31063,44 @@ const CONFIG_SECTIONS = [
|
|
|
30019
31063
|
"server",
|
|
30020
31064
|
"watch"
|
|
30021
31065
|
];
|
|
31066
|
+
const ATTACHMENT_ROLES = ["fulltext", "supplement", "notes", "draft"];
|
|
30022
31067
|
const OPTION_VALUES = {
|
|
30023
31068
|
"--sort": SEARCH_SORT_FIELDS,
|
|
30024
31069
|
// search includes 'relevance'
|
|
30025
31070
|
"--order": SORT_ORDERS,
|
|
30026
|
-
"--format": CITATION_FORMATS,
|
|
30027
31071
|
"--style": BUILTIN_STYLES,
|
|
30028
31072
|
"--log-level": LOG_LEVELS,
|
|
30029
|
-
"--section": CONFIG_SECTIONS
|
|
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
|
|
30030
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
|
+
}
|
|
30031
31101
|
const ID_COMPLETION_COMMANDS = /* @__PURE__ */ new Set(["cite", "remove", "update"]);
|
|
30032
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"]);
|
|
30033
31104
|
function toCompletionItems(values) {
|
|
30034
31105
|
return values.map((name2) => ({ name: name2 }));
|
|
30035
31106
|
}
|
|
@@ -30063,6 +31134,13 @@ function extractGlobalOptions(program) {
|
|
|
30063
31134
|
function findSubcommand(program, name2) {
|
|
30064
31135
|
return program.commands.find((cmd) => cmd.name() === name2);
|
|
30065
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
|
+
}
|
|
30066
31144
|
function getCompletions(env, program) {
|
|
30067
31145
|
const { line, prev, last } = env;
|
|
30068
31146
|
const words = line.trim().split(/\s+/);
|
|
@@ -30074,7 +31152,8 @@ function getCompletions(env, program) {
|
|
|
30074
31152
|
}
|
|
30075
31153
|
const firstArg = args[0] ?? "";
|
|
30076
31154
|
if (prev?.startsWith("-")) {
|
|
30077
|
-
const
|
|
31155
|
+
const { command, subcommand } = parseCommandContext(env);
|
|
31156
|
+
const optionValues = getOptionValuesForCommand(prev, command, subcommand);
|
|
30078
31157
|
if (optionValues) {
|
|
30079
31158
|
return toCompletionItems(optionValues);
|
|
30080
31159
|
}
|
|
@@ -30114,6 +31193,13 @@ function needsIdCompletion(env) {
|
|
|
30114
31193
|
}
|
|
30115
31194
|
return { needs: false };
|
|
30116
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
|
+
}
|
|
30117
31203
|
if (ID_COMPLETION_COMMANDS.has(command)) {
|
|
30118
31204
|
return { needs: true, command };
|
|
30119
31205
|
}
|
|
@@ -30253,6 +31339,7 @@ function createProgram() {
|
|
|
30253
31339
|
registerCiteCommand(program);
|
|
30254
31340
|
registerServerCommand(program);
|
|
30255
31341
|
registerFulltextCommand(program);
|
|
31342
|
+
registerAttachCommand(program);
|
|
30256
31343
|
registerMcpCommand(program);
|
|
30257
31344
|
registerConfigCommand(program);
|
|
30258
31345
|
registerCompletionCommand(program);
|
|
@@ -30277,7 +31364,7 @@ async function handleListAction(options, program) {
|
|
|
30277
31364
|
}
|
|
30278
31365
|
}
|
|
30279
31366
|
function registerListCommand(program) {
|
|
30280
|
-
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) => {
|
|
30281
31368
|
await handleListAction(options, program);
|
|
30282
31369
|
});
|
|
30283
31370
|
}
|
|
@@ -30306,7 +31393,7 @@ async function handleExportAction(ids, options, program) {
|
|
|
30306
31393
|
}
|
|
30307
31394
|
}
|
|
30308
31395
|
function registerExportCommand(program) {
|
|
30309
|
-
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) => {
|
|
30310
31397
|
await handleExportAction(ids, options, program);
|
|
30311
31398
|
});
|
|
30312
31399
|
}
|
|
@@ -30315,7 +31402,7 @@ async function handleSearchAction(query, options, program) {
|
|
|
30315
31402
|
const globalOpts = program.opts();
|
|
30316
31403
|
const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
|
|
30317
31404
|
const context = await createExecutionContext(config2, Library.load);
|
|
30318
|
-
if (options.
|
|
31405
|
+
if (options.tui) {
|
|
30319
31406
|
const result2 = await executeInteractiveSearch({ ...options, query }, context, config2);
|
|
30320
31407
|
if (result2.output) {
|
|
30321
31408
|
process.stdout.write(`${result2.output}
|
|
@@ -30337,9 +31424,9 @@ async function handleSearchAction(query, options, program) {
|
|
|
30337
31424
|
}
|
|
30338
31425
|
}
|
|
30339
31426
|
function registerSearchCommand(program) {
|
|
30340
|
-
program.command("search").description("Search references").argument("[query]", "Search query (required unless using --
|
|
30341
|
-
if (!options.
|
|
30342
|
-
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");
|
|
30343
31430
|
process.exit(1);
|
|
30344
31431
|
}
|
|
30345
31432
|
await handleSearchAction(query ?? "", options, program);
|
|
@@ -30350,8 +31437,8 @@ function buildAddOptions(inputs, options, config2, stdinContent) {
|
|
|
30350
31437
|
inputs,
|
|
30351
31438
|
force: options.force ?? false
|
|
30352
31439
|
};
|
|
30353
|
-
if (options.
|
|
30354
|
-
addOptions.format = options.
|
|
31440
|
+
if (options.input !== void 0) {
|
|
31441
|
+
addOptions.format = options.input;
|
|
30355
31442
|
}
|
|
30356
31443
|
if (options.verbose !== void 0) {
|
|
30357
31444
|
addOptions.verbose = options.verbose;
|
|
@@ -30419,11 +31506,7 @@ async function handleAddAction(inputs, options, program) {
|
|
|
30419
31506
|
}
|
|
30420
31507
|
}
|
|
30421
31508
|
function registerAddCommand(program) {
|
|
30422
|
-
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(
|
|
30423
|
-
"--format <format>",
|
|
30424
|
-
"Explicit input format: json|bibtex|ris|pmid|doi|isbn|auto",
|
|
30425
|
-
"auto"
|
|
30426
|
-
).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) => {
|
|
30427
31510
|
await handleAddAction(inputs, options, program);
|
|
30428
31511
|
});
|
|
30429
31512
|
}
|
|
@@ -30441,7 +31524,7 @@ function registerEditCommand(program) {
|
|
|
30441
31524
|
program.command("edit").description("Edit references interactively using an external editor").argument(
|
|
30442
31525
|
"[identifier...]",
|
|
30443
31526
|
"Citation keys or UUIDs to edit (interactive selection if omitted)"
|
|
30444
|
-
).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) => {
|
|
30445
31528
|
await handleEditAction(identifiers, options, program.opts());
|
|
30446
31529
|
});
|
|
30447
31530
|
}
|
|
@@ -30449,7 +31532,7 @@ function registerCiteCommand(program) {
|
|
|
30449
31532
|
program.command("cite").description("Generate formatted citations for references").argument(
|
|
30450
31533
|
"[id-or-uuid...]",
|
|
30451
31534
|
"Citation keys or UUIDs to cite (interactive selection if omitted)"
|
|
30452
|
-
).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) => {
|
|
30453
31536
|
await handleCiteAction(identifiers, options, program.opts());
|
|
30454
31537
|
});
|
|
30455
31538
|
}
|
|
@@ -30554,6 +31637,30 @@ function registerMcpCommand(program) {
|
|
|
30554
31637
|
}
|
|
30555
31638
|
});
|
|
30556
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
|
+
}
|
|
30557
31664
|
function registerFulltextCommand(program) {
|
|
30558
31665
|
const fulltextCmd = program.command("fulltext").description("Manage full-text files attached to references");
|
|
30559
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) => {
|
|
@@ -30562,7 +31669,7 @@ function registerFulltextCommand(program) {
|
|
|
30562
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) => {
|
|
30563
31670
|
await handleFulltextGetAction(identifier, options, program.opts());
|
|
30564
31671
|
});
|
|
30565
|
-
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) => {
|
|
30566
31673
|
await handleFulltextDetachAction(identifier, options, program.opts());
|
|
30567
31674
|
});
|
|
30568
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) => {
|
|
@@ -30584,8 +31691,14 @@ async function main(argv) {
|
|
|
30584
31691
|
await program.parseAsync(argv);
|
|
30585
31692
|
}
|
|
30586
31693
|
export {
|
|
31694
|
+
addAttachment as a,
|
|
30587
31695
|
createProgram as c,
|
|
31696
|
+
detachAttachment as d,
|
|
30588
31697
|
formatBibtex as f,
|
|
30589
|
-
|
|
31698
|
+
getAttachment as g,
|
|
31699
|
+
listAttachments as l,
|
|
31700
|
+
main as m,
|
|
31701
|
+
openAttachment as o,
|
|
31702
|
+
syncAttachments as s
|
|
30590
31703
|
};
|
|
30591
|
-
//# sourceMappingURL=index-
|
|
31704
|
+
//# sourceMappingURL=index-Bv5IgsL-.js.map
|