@ncukondo/reference-manager 0.19.0 → 0.20.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.
Files changed (28) hide show
  1. package/dist/chunks/{action-menu-BoJkH1yr.js → action-menu-Brg5Lmz7.js} +3 -3
  2. package/dist/chunks/{action-menu-BoJkH1yr.js.map → action-menu-Brg5Lmz7.js.map} +1 -1
  3. package/dist/chunks/{format-QgF4pmp6.js → format-CYO99JV-.js} +2 -2
  4. package/dist/chunks/{format-QgF4pmp6.js.map → format-CYO99JV-.js.map} +1 -1
  5. package/dist/chunks/{index-BJf01yMW.js → index-BR5tlrNU.js} +2 -2
  6. package/dist/chunks/index-BR5tlrNU.js.map +1 -0
  7. package/dist/chunks/{index-Di6yhlFH.js → index-Cno7_aWr.js} +434 -96
  8. package/dist/chunks/index-Cno7_aWr.js.map +1 -0
  9. package/dist/chunks/{index-C49EfSAl.js → index-D--7n1SB.js} +4 -4
  10. package/dist/chunks/{index-C49EfSAl.js.map → index-D--7n1SB.js.map} +1 -1
  11. package/dist/chunks/{index-B4gr0P83.js → index-DrZawbND.js} +2 -1
  12. package/dist/chunks/{index-B4gr0P83.js.map → index-DrZawbND.js.map} +1 -1
  13. package/dist/chunks/{reference-select-D4iGJ4kA.js → reference-select-DrINWBuP.js} +3 -3
  14. package/dist/chunks/{reference-select-D4iGJ4kA.js.map → reference-select-DrINWBuP.js.map} +1 -1
  15. package/dist/chunks/{style-select-UWYScO8e.js → style-select-DxcSWBSF.js} +3 -3
  16. package/dist/chunks/{style-select-UWYScO8e.js.map → style-select-DxcSWBSF.js.map} +1 -1
  17. package/dist/cli/commands/attach.d.ts +44 -1
  18. package/dist/cli/commands/attach.d.ts.map +1 -1
  19. package/dist/cli/helpers.d.ts +18 -0
  20. package/dist/cli/helpers.d.ts.map +1 -1
  21. package/dist/cli/index.d.ts.map +1 -1
  22. package/dist/cli.js +1 -1
  23. package/dist/features/operations/attachments/sync.d.ts +14 -0
  24. package/dist/features/operations/attachments/sync.d.ts.map +1 -1
  25. package/dist/server.js +1 -1
  26. package/package.json +1 -1
  27. package/dist/chunks/index-BJf01yMW.js.map +0 -1
  28. package/dist/chunks/index-Di6yhlFH.js.map +0 -1
@@ -6,8 +6,8 @@ import * as os from "node:os";
6
6
  import { tmpdir } from "node:os";
7
7
  import * as path from "node:path";
8
8
  import path__default, { extname, join, basename, dirname } from "node:path";
9
+ import { g as getExtension, i as isValidFulltextFiles, a as isReservedRole, R as RESERVED_ROLES, 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-DrZawbND.js";
9
10
  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-B4gr0P83.js";
11
11
  import { o as openWithSystemApp, l as loadConfig, e as getDefaultCurrentDirConfigFilename, h as getDefaultUserConfigPath } from "./loader-BtW20O32.js";
12
12
  import { spawn, spawnSync } from "node:child_process";
13
13
  import process$1, { stdin, stdout } from "node:process";
@@ -20,7 +20,7 @@ import "@citation-js/plugin-csl";
20
20
  import { ZodOptional as ZodOptional$2, z } from "zod";
21
21
  import { serve } from "@hono/node-server";
22
22
  const name = "@ncukondo/reference-manager";
23
- const version$1 = "0.19.0";
23
+ const version$1 = "0.20.0";
24
24
  const description$1 = "A local reference management tool using CSL-JSON as the single source of truth";
25
25
  const packageJson = {
26
26
  name,
@@ -414,6 +414,44 @@ function getExitCode(result) {
414
414
  }
415
415
  return 0;
416
416
  }
417
+ function slugifyLabel(label) {
418
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
419
+ }
420
+ function generateFilename(role, ext, label) {
421
+ if (label) {
422
+ const slug = slugifyLabel(label);
423
+ return `${role}-${slug}.${ext}`;
424
+ }
425
+ return `${role}.${ext}`;
426
+ }
427
+ function parseFilename(filename) {
428
+ if (!filename) {
429
+ return null;
430
+ }
431
+ const ext = path__default.extname(filename);
432
+ const extWithoutDot = ext.startsWith(".") ? ext.slice(1) : ext;
433
+ const basename2 = ext ? filename.slice(0, -ext.length) : filename;
434
+ const firstHyphenIndex = basename2.indexOf("-");
435
+ if (firstHyphenIndex === -1) {
436
+ return {
437
+ role: basename2,
438
+ ext: extWithoutDot
439
+ };
440
+ }
441
+ const role = basename2.slice(0, firstHyphenIndex);
442
+ const label = basename2.slice(firstHyphenIndex + 1);
443
+ if (label) {
444
+ return {
445
+ role,
446
+ ext: extWithoutDot,
447
+ label
448
+ };
449
+ }
450
+ return {
451
+ role,
452
+ ext: extWithoutDot
453
+ };
454
+ }
417
455
  function normalizePathForOutput(p) {
418
456
  return p.replace(/\\/g, "/");
419
457
  }
@@ -464,44 +502,6 @@ async function deleteDirectoryIfEmpty(dirPath) {
464
502
  }
465
503
  }
466
504
  }
467
- function slugifyLabel(label) {
468
- return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
469
- }
470
- function generateFilename(role, ext, label) {
471
- if (label) {
472
- const slug = slugifyLabel(label);
473
- return `${role}-${slug}.${ext}`;
474
- }
475
- return `${role}.${ext}`;
476
- }
477
- function parseFilename(filename) {
478
- if (!filename) {
479
- return null;
480
- }
481
- const ext = path__default.extname(filename);
482
- const extWithoutDot = ext.startsWith(".") ? ext.slice(1) : ext;
483
- const basename2 = ext ? filename.slice(0, -ext.length) : filename;
484
- const firstHyphenIndex = basename2.indexOf("-");
485
- if (firstHyphenIndex === -1) {
486
- return {
487
- role: basename2,
488
- ext: extWithoutDot
489
- };
490
- }
491
- const role = basename2.slice(0, firstHyphenIndex);
492
- const label = basename2.slice(firstHyphenIndex + 1);
493
- if (label) {
494
- return {
495
- role,
496
- ext: extWithoutDot,
497
- label
498
- };
499
- }
500
- return {
501
- role,
502
- ext: extWithoutDot
503
- };
504
- }
505
505
  async function checkSourceFile(filePath) {
506
506
  try {
507
507
  await stat(filePath);
@@ -794,6 +794,19 @@ async function detachAttachment(library, options) {
794
794
  function errorResult(error) {
795
795
  return { success: false, newFiles: [], missingFiles: [], applied: false, error };
796
796
  }
797
+ function suggestRoleFromContext(filename, existingFiles) {
798
+ const ext = filename.toLowerCase();
799
+ const dataExtensions = [".xlsx", ".csv", ".tsv", ".zip"];
800
+ if (dataExtensions.some((de) => ext.endsWith(de)) || ext.endsWith(".tar.gz")) {
801
+ return "supplement";
802
+ }
803
+ const isDocumentLike = ext.endsWith(".pdf") || ext.endsWith(".md");
804
+ if (isDocumentLike) {
805
+ const hasFulltext = existingFiles.some((f) => f.role === "fulltext");
806
+ return hasFulltext ? "supplement" : "fulltext";
807
+ }
808
+ return null;
809
+ }
797
810
  function inferFromFilename(filename) {
798
811
  const parsed = parseFilename(filename);
799
812
  if (!parsed) {
@@ -836,14 +849,17 @@ function findNewFiles(diskFiles, metadataFilenames) {
836
849
  function findMissingFiles(metadataFiles, diskFilenames) {
837
850
  return metadataFiles.filter((f) => !diskFilenames.has(f.filename)).map((f) => f.filename);
838
851
  }
839
- function buildUpdatedFiles(metadataFiles, newFiles, missingFiles, shouldApplyNew, shouldApplyFix) {
852
+ function buildUpdatedFiles(metadataFiles, newFiles, missingFiles, shouldApplyNew, shouldApplyFix, roleOverrides) {
840
853
  let updatedFiles = [...metadataFiles];
841
854
  if (shouldApplyNew) {
842
855
  for (const newFile of newFiles) {
856
+ const override = roleOverrides?.[newFile.filename];
857
+ const role = override?.role ?? newFile.role;
858
+ const label = override ? override.label : newFile.label;
843
859
  const attachmentFile = {
844
860
  filename: newFile.filename,
845
- role: newFile.role,
846
- ...newFile.label && { label: newFile.label }
861
+ role,
862
+ ...label && { label }
847
863
  };
848
864
  updatedFiles.push(attachmentFile);
849
865
  }
@@ -854,6 +870,36 @@ function buildUpdatedFiles(metadataFiles, newFiles, missingFiles, shouldApplyNew
854
870
  }
855
871
  return updatedFiles;
856
872
  }
873
+ async function applyRenames(dirPath, renames) {
874
+ const applied = {};
875
+ for (const [oldName, newName] of Object.entries(renames)) {
876
+ const oldPath = join(dirPath, oldName);
877
+ const newPath = join(dirPath, newName);
878
+ try {
879
+ await stat(newPath);
880
+ process.stderr.write(
881
+ `Warning: Cannot rename ${oldName} → ${newName}: target already exists
882
+ `
883
+ );
884
+ } catch {
885
+ try {
886
+ await rename(oldPath, newPath);
887
+ applied[oldName] = newName;
888
+ } catch {
889
+ }
890
+ }
891
+ }
892
+ return applied;
893
+ }
894
+ function applyRenamesInMetadata(files, appliedRenames) {
895
+ return files.map((f) => {
896
+ const newName = appliedRenames[f.filename];
897
+ if (newName) {
898
+ return { ...f, filename: newName };
899
+ }
900
+ return f;
901
+ });
902
+ }
857
903
  async function updateAttachmentMetadata(library, item, attachments, updatedFiles) {
858
904
  await library.update(item.id, {
859
905
  custom: {
@@ -866,7 +912,15 @@ async function updateAttachmentMetadata(library, item, attachments, updatedFiles
866
912
  });
867
913
  }
868
914
  async function syncAttachments(library, options) {
869
- const { identifier, yes = false, fix = false, idType = "id", attachmentsDirectory } = options;
915
+ const {
916
+ identifier,
917
+ yes = false,
918
+ fix = false,
919
+ idType = "id",
920
+ attachmentsDirectory,
921
+ roleOverrides,
922
+ renames
923
+ } = options;
870
924
  const item = await library.find(identifier, { idType });
871
925
  if (!item) {
872
926
  return errorResult(`Reference '${identifier}' not found`);
@@ -889,13 +943,20 @@ async function syncAttachments(library, options) {
889
943
  const shouldApplyFix = fix && missingFiles.length > 0;
890
944
  const shouldApply = shouldApplyNew || shouldApplyFix;
891
945
  if (shouldApply) {
892
- const updatedFiles = buildUpdatedFiles(
946
+ let updatedFiles = buildUpdatedFiles(
893
947
  metadataFiles,
894
948
  newFiles,
895
949
  missingFiles,
896
950
  shouldApplyNew,
897
- shouldApplyFix
951
+ shouldApplyFix,
952
+ roleOverrides
898
953
  );
954
+ if (shouldApplyNew && renames && Object.keys(renames).length > 0) {
955
+ const appliedRenames = await applyRenames(dirPath, renames);
956
+ if (Object.keys(appliedRenames).length > 0) {
957
+ updatedFiles = applyRenamesInMetadata(updatedFiles, appliedRenames);
958
+ }
959
+ }
899
960
  await updateAttachmentMetadata(library, item, attachments, updatedFiles);
900
961
  await library.save();
901
962
  }
@@ -1032,15 +1093,15 @@ class OperationsLibrary {
1032
1093
  }
1033
1094
  // High-level operations
1034
1095
  async search(options) {
1035
- const { searchReferences } = await import("./index-B4gr0P83.js").then((n) => n.n);
1096
+ const { searchReferences } = await import("./index-DrZawbND.js").then((n) => n.n);
1036
1097
  return searchReferences(this.library, options);
1037
1098
  }
1038
1099
  async list(options) {
1039
- const { listReferences } = await import("./index-B4gr0P83.js").then((n) => n.m);
1100
+ const { listReferences } = await import("./index-DrZawbND.js").then((n) => n.m);
1040
1101
  return listReferences(this.library, options ?? {});
1041
1102
  }
1042
1103
  async cite(options) {
1043
- const { citeReferences } = await import("./index-B4gr0P83.js").then((n) => n.l);
1104
+ const { citeReferences } = await import("./index-DrZawbND.js").then((n) => n.l);
1044
1105
  const defaultStyle = options.defaultStyle ?? this.citationConfig?.defaultStyle;
1045
1106
  const cslDirectory = options.cslDirectory ?? this.citationConfig?.cslDirectory;
1046
1107
  const mergedOptions = {
@@ -1051,32 +1112,32 @@ class OperationsLibrary {
1051
1112
  return citeReferences(this.library, mergedOptions);
1052
1113
  }
1053
1114
  async import(inputs, options) {
1054
- const { addReferences } = await import("./index-B4gr0P83.js").then((n) => n.k);
1115
+ const { addReferences } = await import("./index-DrZawbND.js").then((n) => n.k);
1055
1116
  return addReferences(inputs, this.library, options ?? {});
1056
1117
  }
1057
1118
  // Attachment operations
1058
1119
  async attachAdd(options) {
1059
- const { addAttachment: addAttachment2 } = await import("./index-BJf01yMW.js");
1120
+ const { addAttachment: addAttachment2 } = await import("./index-BR5tlrNU.js");
1060
1121
  return addAttachment2(this.library, options);
1061
1122
  }
1062
1123
  async attachList(options) {
1063
- const { listAttachments: listAttachments2 } = await import("./index-BJf01yMW.js");
1124
+ const { listAttachments: listAttachments2 } = await import("./index-BR5tlrNU.js");
1064
1125
  return listAttachments2(this.library, options);
1065
1126
  }
1066
1127
  async attachGet(options) {
1067
- const { getAttachment: getAttachment2 } = await import("./index-BJf01yMW.js");
1128
+ const { getAttachment: getAttachment2 } = await import("./index-BR5tlrNU.js");
1068
1129
  return getAttachment2(this.library, options);
1069
1130
  }
1070
1131
  async attachDetach(options) {
1071
- const { detachAttachment: detachAttachment2 } = await import("./index-BJf01yMW.js");
1132
+ const { detachAttachment: detachAttachment2 } = await import("./index-BR5tlrNU.js");
1072
1133
  return detachAttachment2(this.library, options);
1073
1134
  }
1074
1135
  async attachSync(options) {
1075
- const { syncAttachments: syncAttachments2 } = await import("./index-BJf01yMW.js");
1136
+ const { syncAttachments: syncAttachments2 } = await import("./index-BR5tlrNU.js");
1076
1137
  return syncAttachments2(this.library, options);
1077
1138
  }
1078
1139
  async attachOpen(options) {
1079
- const { openAttachment: openAttachment2 } = await import("./index-BJf01yMW.js");
1140
+ const { openAttachment: openAttachment2 } = await import("./index-BR5tlrNU.js");
1080
1141
  return openAttachment2(this.library, options);
1081
1142
  }
1082
1143
  }
@@ -1536,6 +1597,62 @@ async function readConfirmation(prompt) {
1536
1597
  });
1537
1598
  });
1538
1599
  }
1600
+ async function readChoice(prompt, choices, defaultIndex) {
1601
+ if (choices.length === 0) {
1602
+ throw new Error("readChoice requires at least one choice");
1603
+ }
1604
+ const effectiveDefault = defaultIndex !== void 0 && defaultIndex >= 0 && defaultIndex < choices.length ? defaultIndex : 0;
1605
+ const getDefault = () => {
1606
+ const choice = choices[effectiveDefault];
1607
+ if (!choice) throw new Error("Invalid default index");
1608
+ return choice.value;
1609
+ };
1610
+ const parseAnswer = (answer) => {
1611
+ const trimmed = answer.trim();
1612
+ if (trimmed === "") return getDefault();
1613
+ const num = Number.parseInt(trimmed, 10);
1614
+ if (Number.isNaN(num) || num < 1 || num > choices.length) return getDefault();
1615
+ const selected = choices[num - 1];
1616
+ return selected ? selected.value : getDefault();
1617
+ };
1618
+ if (!isTTY()) {
1619
+ return getDefault();
1620
+ }
1621
+ if (process.stdin.isTTY && process.stdin.isRaw) {
1622
+ process.stdin.setRawMode(false);
1623
+ }
1624
+ process.stdin.resume();
1625
+ process.stdin.ref();
1626
+ const rl = readline.createInterface({
1627
+ input: process.stdin,
1628
+ output: process.stderr
1629
+ });
1630
+ process.stderr.write(`${prompt}
1631
+ `);
1632
+ for (let i = 0; i < choices.length; i++) {
1633
+ const choice = choices[i];
1634
+ if (!choice) continue;
1635
+ const marker = i === effectiveDefault ? " (default)" : "";
1636
+ process.stderr.write(` ${i + 1}) ${choice.label}${marker}
1637
+ `);
1638
+ }
1639
+ return new Promise((resolve2) => {
1640
+ let resolved = false;
1641
+ rl.question("Enter number: ", (answer) => {
1642
+ if (!resolved) {
1643
+ resolved = true;
1644
+ rl.close();
1645
+ resolve2(parseAnswer(answer));
1646
+ }
1647
+ });
1648
+ rl.on("close", () => {
1649
+ if (!resolved) {
1650
+ resolved = true;
1651
+ resolve2(getDefault());
1652
+ }
1653
+ });
1654
+ });
1655
+ }
1539
1656
  async function readStdinContent() {
1540
1657
  const chunks = [];
1541
1658
  for await (const chunk of stdin) {
@@ -1658,7 +1775,9 @@ async function executeAttachSync(options, context) {
1658
1775
  attachmentsDirectory: options.attachmentsDirectory,
1659
1776
  ...options.yes !== void 0 && { yes: options.yes },
1660
1777
  ...options.fix !== void 0 && { fix: options.fix },
1661
- ...options.idType !== void 0 && { idType: options.idType }
1778
+ ...options.idType !== void 0 && { idType: options.idType },
1779
+ ...options.roleOverrides !== void 0 && { roleOverrides: options.roleOverrides },
1780
+ ...options.renames !== void 0 && { renames: options.renames }
1662
1781
  };
1663
1782
  return syncAttachments(context.library, operationOptions);
1664
1783
  }
@@ -1755,6 +1874,73 @@ function formatMissingFilesSection(result, lines) {
1755
1874
  }
1756
1875
  lines.push("");
1757
1876
  }
1877
+ function buildRoleOverridesFromSuggestions(newFiles) {
1878
+ const overrides = {};
1879
+ for (const file of newFiles) {
1880
+ if (file.role !== "other") continue;
1881
+ const suggested = suggestRoleFromContext(file.filename, newFiles);
1882
+ if (suggested) {
1883
+ overrides[file.filename] = { role: suggested };
1884
+ }
1885
+ }
1886
+ return overrides;
1887
+ }
1888
+ function generateRenameMap(newFiles, overrides) {
1889
+ const renames = {};
1890
+ for (const file of newFiles) {
1891
+ const override = overrides[file.filename];
1892
+ if (!override) continue;
1893
+ const ext = path__default.extname(file.filename);
1894
+ const extWithoutDot = ext.startsWith(".") ? ext.slice(1) : ext;
1895
+ const baseName = ext ? file.filename.slice(0, -ext.length) : file.filename;
1896
+ const canonical = generateFilename(override.role, extWithoutDot, override.label);
1897
+ if (canonical !== file.filename) {
1898
+ const newName = override.label ? canonical : `${override.role}-${baseName}${ext}`;
1899
+ renames[file.filename] = newName;
1900
+ }
1901
+ }
1902
+ return renames;
1903
+ }
1904
+ function formatSuggestedFileEntry(file, suggestions, renames, lines) {
1905
+ lines.push(` ${file.filename}`);
1906
+ const suggestion = suggestions[file.filename];
1907
+ if (suggestion) {
1908
+ lines.push(` role: ${suggestion.role} (suggested, inferred: ${file.role})`);
1909
+ } else {
1910
+ const labelPart = file.label ? `, label: "${file.label}"` : "";
1911
+ lines.push(` role: ${file.role}${labelPart}`);
1912
+ }
1913
+ const rename2 = renames[file.filename];
1914
+ if (rename2) {
1915
+ lines.push(` rename: ${rename2}`);
1916
+ }
1917
+ }
1918
+ function formatSyncPreviewWithSuggestions(result, suggestions, renames, identifier) {
1919
+ if (!result.success) {
1920
+ return `Error: ${result.error}`;
1921
+ }
1922
+ const hasNewFiles = result.newFiles.length > 0;
1923
+ const hasMissingFiles = result.missingFiles.length > 0;
1924
+ const hasRenames = Object.keys(renames).length > 0;
1925
+ if (!hasNewFiles && !hasMissingFiles) {
1926
+ return "Already in sync.";
1927
+ }
1928
+ const lines = [];
1929
+ lines.push(`Sync preview for ${identifier}:`);
1930
+ if (hasNewFiles) {
1931
+ lines.push(" New files:");
1932
+ for (const file of result.newFiles) {
1933
+ formatSuggestedFileEntry(file, suggestions, renames, lines);
1934
+ }
1935
+ lines.push("");
1936
+ }
1937
+ formatMissingFilesSection(result, lines);
1938
+ lines.push(`To apply: ref attach sync ${identifier} --yes`);
1939
+ if (hasRenames) {
1940
+ lines.push(`To apply without renaming: ref attach sync ${identifier} --yes --no-rename`);
1941
+ }
1942
+ return lines.join("\n").trimEnd();
1943
+ }
1758
1944
  function formatAttachSyncOutput(result) {
1759
1945
  if (!result.success) {
1760
1946
  return `Error: ${result.error}`;
@@ -1795,7 +1981,7 @@ function getAttachExitCode(result) {
1795
1981
  }
1796
1982
  async function executeInteractiveSelect$2(context, config2) {
1797
1983
  const { withAlternateScreen: withAlternateScreen2 } = await Promise.resolve().then(() => alternateScreen);
1798
- const { selectReferencesOrExit } = await import("./reference-select-D4iGJ4kA.js");
1984
+ const { selectReferencesOrExit } = await import("./reference-select-DrINWBuP.js");
1799
1985
  const allReferences = await context.library.getAll();
1800
1986
  const identifiers = await withAlternateScreen2(
1801
1987
  () => selectReferencesOrExit(allReferences, { multiSelect: false }, config2.cli.tui)
@@ -1853,44 +2039,95 @@ async function waitForEnter() {
1853
2039
  });
1854
2040
  });
1855
2041
  }
1856
- function displayInteractiveSyncResult(result, identifier) {
1857
- if (result.newFiles.length === 0) {
1858
- process.stderr.write("No new files detected.\n");
1859
- return;
2042
+ async function promptForRenames(renameMap) {
2043
+ const accepted = {};
2044
+ for (const [oldName, newName] of Object.entries(renameMap)) {
2045
+ const shouldRename = await readConfirmation(`Rename ${oldName} → ${newName}?`);
2046
+ if (shouldRename) {
2047
+ accepted[oldName] = newName;
2048
+ }
1860
2049
  }
1861
- process.stderr.write("Scanning directory...\n\n");
2050
+ return accepted;
2051
+ }
2052
+ function showFilePreview(previewFiles, acceptedRenames) {
2053
+ process.stderr.write("\nScanning directory...\n\n");
1862
2054
  process.stderr.write(
1863
- `Found ${result.newFiles.length} new file${result.newFiles.length > 1 ? "s" : ""}:
2055
+ `Found ${previewFiles.length} new file${previewFiles.length > 1 ? "s" : ""}:
1864
2056
  `
1865
2057
  );
1866
- for (const file of result.newFiles) {
2058
+ for (const file of previewFiles) {
1867
2059
  const labelPart = file.label ? `, label: "${file.label}"` : "";
1868
- process.stderr.write(` ${file.filename} → role: ${file.role}${labelPart}
1869
- `);
2060
+ const renamePart = acceptedRenames[file.filename] ? ` (rename → ${acceptedRenames[file.filename]})` : "";
2061
+ process.stderr.write(
2062
+ ` ✓ ${file.filename} → role: ${file.role}${labelPart}${renamePart}
2063
+ `
2064
+ );
1870
2065
  }
1871
- process.stderr.write(`
1872
- Updated metadata for ${identifier}.
1873
- `);
1874
2066
  }
1875
- async function runInteractiveMode(identifier, dirPath, attachmentsDirectory, idType, context) {
1876
- displayNamingConvention(identifier, dirPath);
1877
- await waitForEnter();
1878
- const syncResult = await executeAttachSync(
2067
+ function buildPreviewFiles(newFiles, roleOverrides) {
2068
+ return newFiles.map((file) => {
2069
+ const override = roleOverrides[file.filename];
2070
+ if (override) {
2071
+ return { ...file, role: override.role, ...override.label && { label: override.label } };
2072
+ }
2073
+ return file;
2074
+ });
2075
+ }
2076
+ async function syncNewFilesWithRolePrompt(identifier, attachmentsDirectory, idType, context) {
2077
+ const dryRunResult = await executeAttachSync(
1879
2078
  {
1880
2079
  identifier,
1881
2080
  attachmentsDirectory,
1882
- yes: true,
1883
2081
  ...idType && { idType }
1884
2082
  },
1885
2083
  context
1886
2084
  );
1887
- if (syncResult.success) {
1888
- displayInteractiveSyncResult(syncResult, identifier);
2085
+ if (!dryRunResult.success) {
2086
+ process.stderr.write(`Sync error: ${dryRunResult.error}
2087
+ `);
2088
+ return;
2089
+ }
2090
+ if (dryRunResult.newFiles.length === 0) {
2091
+ process.stderr.write("No new files detected.\n");
2092
+ return;
2093
+ }
2094
+ const roleOverrides = await promptForUnknownRoles(dryRunResult.newFiles);
2095
+ const renameMap = generateRenameMap(dryRunResult.newFiles, roleOverrides);
2096
+ const acceptedRenames = await promptForRenames(renameMap);
2097
+ const previewFiles = buildPreviewFiles(dryRunResult.newFiles, roleOverrides);
2098
+ showFilePreview(previewFiles, acceptedRenames);
2099
+ const shouldApply = await readConfirmation("\nAdd new files to metadata?");
2100
+ if (!shouldApply) {
2101
+ process.stderr.write("No changes applied.\n");
2102
+ return;
2103
+ }
2104
+ const hasOverrides = Object.keys(roleOverrides).length > 0;
2105
+ const hasRenames = Object.keys(acceptedRenames).length > 0;
2106
+ const applyResult = await executeAttachSync(
2107
+ {
2108
+ identifier,
2109
+ attachmentsDirectory,
2110
+ yes: true,
2111
+ ...idType && { idType },
2112
+ ...hasOverrides && { roleOverrides },
2113
+ ...hasRenames && { renames: acceptedRenames }
2114
+ },
2115
+ context
2116
+ );
2117
+ if (applyResult.success) {
2118
+ process.stderr.write(`
2119
+ Updated metadata for ${identifier}.
2120
+ `);
1889
2121
  } else {
1890
- process.stderr.write(`Sync error: ${syncResult.error}
2122
+ process.stderr.write(`Sync error: ${applyResult.error}
1891
2123
  `);
1892
2124
  }
1893
2125
  }
2126
+ async function runInteractiveMode(identifier, dirPath, attachmentsDirectory, idType, context) {
2127
+ displayNamingConvention(identifier, dirPath);
2128
+ await waitForEnter();
2129
+ await syncNewFilesWithRolePrompt(identifier, attachmentsDirectory, idType, context);
2130
+ }
1894
2131
  function buildOpenOptions(identifier, filenameArg, options, attachmentsDirectory) {
1895
2132
  return {
1896
2133
  identifier,
@@ -2049,6 +2286,30 @@ async function handleAttachDetachAction(identifierArg, filenameArg, options, glo
2049
2286
  setExitCode(ExitCode.INTERNAL_ERROR);
2050
2287
  }
2051
2288
  }
2289
+ async function promptForUnknownRoles(newFiles) {
2290
+ const overrides = {};
2291
+ const unknownFiles = newFiles.filter((f) => f.role === "other");
2292
+ if (unknownFiles.length === 0) return overrides;
2293
+ process.stderr.write("\nSome files could not be classified by filename:\n");
2294
+ const roleChoices = [
2295
+ ...RESERVED_ROLES.map((r) => ({ label: r, value: r })),
2296
+ { label: "other (keep as-is)", value: "other" }
2297
+ ];
2298
+ for (const file of unknownFiles) {
2299
+ const suggested = suggestRoleFromContext(file.filename, newFiles);
2300
+ const defaultIndex = suggested ? roleChoices.findIndex((c) => c.value === suggested) : void 0;
2301
+ const selectedRole = await readChoice(
2302
+ `
2303
+ Role for "${file.filename}"?`,
2304
+ roleChoices,
2305
+ defaultIndex !== void 0 && defaultIndex >= 0 ? defaultIndex : void 0
2306
+ );
2307
+ if (selectedRole !== "other") {
2308
+ overrides[file.filename] = { role: selectedRole };
2309
+ }
2310
+ }
2311
+ return overrides;
2312
+ }
2052
2313
  async function runInteractiveSyncMode(identifier, attachmentsDirectory, idType, context) {
2053
2314
  const dryRunOptions = {
2054
2315
  identifier,
@@ -2063,7 +2324,12 @@ async function runInteractiveSyncMode(identifier, attachmentsDirectory, idType,
2063
2324
  `);
2064
2325
  return;
2065
2326
  }
2066
- process.stderr.write(`${formatSyncPreview(dryRunResult)}
2327
+ const roleOverrides = await promptForUnknownRoles(dryRunResult.newFiles);
2328
+ const renameMap = generateRenameMap(dryRunResult.newFiles, roleOverrides);
2329
+ const acceptedRenames = await promptForRenames(renameMap);
2330
+ const previewFiles = buildPreviewFiles(dryRunResult.newFiles, roleOverrides);
2331
+ const previewResult = { ...dryRunResult, newFiles: previewFiles };
2332
+ process.stderr.write(`${formatSyncPreview(previewResult)}
2067
2333
  `);
2068
2334
  const shouldApplyNew = hasNewFiles && await readConfirmation("Add new files to metadata?");
2069
2335
  const shouldApplyFix = hasMissingFiles && await readConfirmation("Remove missing files from metadata?");
@@ -2071,16 +2337,70 @@ async function runInteractiveSyncMode(identifier, attachmentsDirectory, idType,
2071
2337
  process.stderr.write("No changes applied.\n");
2072
2338
  return;
2073
2339
  }
2340
+ const hasRenames = Object.keys(acceptedRenames).length > 0;
2074
2341
  const applyOptions = {
2075
2342
  identifier,
2076
2343
  attachmentsDirectory,
2077
2344
  ...shouldApplyNew && { yes: true },
2078
2345
  ...shouldApplyFix && { fix: true },
2346
+ ...idType && { idType },
2347
+ ...shouldApplyNew && Object.keys(roleOverrides).length > 0 && { roleOverrides },
2348
+ ...shouldApplyNew && hasRenames && { renames: acceptedRenames }
2349
+ };
2350
+ const result = await executeAttachSync(applyOptions, context);
2351
+ process.stderr.write(`${formatAttachSyncOutput(result)}
2352
+ `);
2353
+ }
2354
+ async function handleSyncApplyWithSuggestions(identifier, attachmentsDirectory, idType, context, fix, noRename) {
2355
+ const dryRunOptions = {
2356
+ identifier,
2357
+ attachmentsDirectory,
2079
2358
  ...idType && { idType }
2080
2359
  };
2360
+ const dryRunResult = await executeAttachSync(dryRunOptions, context);
2361
+ if (!dryRunResult.success) {
2362
+ process.stderr.write(`${formatAttachSyncOutput(dryRunResult)}
2363
+ `);
2364
+ setExitCode(getAttachExitCode(dryRunResult));
2365
+ return;
2366
+ }
2367
+ const suggestions = buildRoleOverridesFromSuggestions(dryRunResult.newFiles);
2368
+ const renames = noRename ? {} : generateRenameMap(dryRunResult.newFiles, suggestions);
2369
+ const hasRenames = Object.keys(renames).length > 0;
2370
+ const applyOptions = {
2371
+ identifier,
2372
+ attachmentsDirectory,
2373
+ yes: true,
2374
+ ...fix && { fix: true },
2375
+ ...idType && { idType },
2376
+ ...Object.keys(suggestions).length > 0 && { roleOverrides: suggestions },
2377
+ ...hasRenames && { renames }
2378
+ };
2081
2379
  const result = await executeAttachSync(applyOptions, context);
2082
2380
  process.stderr.write(`${formatAttachSyncOutput(result)}
2083
2381
  `);
2382
+ setExitCode(getAttachExitCode(result));
2383
+ }
2384
+ async function handleSyncDryRunPreview(identifier, attachmentsDirectory, idType, context) {
2385
+ const dryRunOptions = {
2386
+ identifier,
2387
+ attachmentsDirectory,
2388
+ ...idType && { idType }
2389
+ };
2390
+ const dryRunResult = await executeAttachSync(dryRunOptions, context);
2391
+ if (!dryRunResult.success) {
2392
+ process.stderr.write(`${formatAttachSyncOutput(dryRunResult)}
2393
+ `);
2394
+ setExitCode(getAttachExitCode(dryRunResult));
2395
+ return;
2396
+ }
2397
+ const suggestions = buildRoleOverridesFromSuggestions(dryRunResult.newFiles);
2398
+ const renames = generateRenameMap(dryRunResult.newFiles, suggestions);
2399
+ process.stderr.write(
2400
+ `${formatSyncPreviewWithSuggestions(dryRunResult, suggestions, renames, identifier)}
2401
+ `
2402
+ );
2403
+ setExitCode(getAttachExitCode(dryRunResult));
2084
2404
  }
2085
2405
  async function handleAttachSyncAction(identifierArg, options, globalOpts) {
2086
2406
  try {
@@ -2089,17 +2409,31 @@ async function handleAttachSyncAction(identifierArg, options, globalOpts) {
2089
2409
  const identifier = await resolveIdentifier(identifierArg, context, config2);
2090
2410
  const attachmentsDirectory = config2.attachments.directory;
2091
2411
  const idType = options.uuid ? "uuid" : void 0;
2092
- const shouldUseInteractive = isTTY() && !options.yes && !options.fix;
2093
- if (shouldUseInteractive) {
2412
+ if (isTTY() && !options.yes && !options.fix) {
2094
2413
  await runInteractiveSyncMode(identifier, attachmentsDirectory, idType, context);
2095
2414
  setExitCode(ExitCode.SUCCESS);
2096
2415
  return;
2097
2416
  }
2417
+ const noRename = options.noRename || options.rename === false;
2418
+ if (options.yes) {
2419
+ await handleSyncApplyWithSuggestions(
2420
+ identifier,
2421
+ attachmentsDirectory,
2422
+ idType,
2423
+ context,
2424
+ options.fix,
2425
+ noRename
2426
+ );
2427
+ return;
2428
+ }
2429
+ if (!options.fix) {
2430
+ await handleSyncDryRunPreview(identifier, attachmentsDirectory, idType, context);
2431
+ return;
2432
+ }
2098
2433
  const syncOptions = {
2099
2434
  identifier,
2100
2435
  attachmentsDirectory,
2101
- ...options.yes && { yes: true },
2102
- ...options.fix && { fix: true },
2436
+ fix: true,
2103
2437
  ...idType && { idType }
2104
2438
  };
2105
2439
  const result = await executeAttachSync(syncOptions, context);
@@ -2114,6 +2448,7 @@ async function handleAttachSyncAction(identifierArg, options, globalOpts) {
2114
2448
  }
2115
2449
  const attach = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
2116
2450
  __proto__: null,
2451
+ buildRoleOverridesFromSuggestions,
2117
2452
  executeAttachAdd,
2118
2453
  executeAttachDetach,
2119
2454
  executeAttachGet,
@@ -2125,6 +2460,8 @@ const attach = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
2125
2460
  formatAttachListOutput,
2126
2461
  formatAttachOpenOutput,
2127
2462
  formatAttachSyncOutput,
2463
+ formatSyncPreviewWithSuggestions,
2464
+ generateRenameMap,
2128
2465
  getAttachExitCode,
2129
2466
  handleAttachAddAction,
2130
2467
  handleAttachDetachAction,
@@ -2132,7 +2469,8 @@ const attach = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
2132
2469
  handleAttachListAction,
2133
2470
  handleAttachOpenAction,
2134
2471
  handleAttachSyncAction,
2135
- runInteractiveMode
2472
+ runInteractiveMode,
2473
+ syncNewFilesWithRolePrompt
2136
2474
  }, Symbol.toStringTag, { value: "Module" }));
2137
2475
  async function validateOptions$2(options) {
2138
2476
  if (options.output && !["text", "html", "rtf"].includes(options.output)) {
@@ -2191,8 +2529,8 @@ function getCiteExitCode(result) {
2191
2529
  }
2192
2530
  async function executeInteractiveCite(options, context, config2) {
2193
2531
  const { withAlternateScreen: withAlternateScreen2 } = await Promise.resolve().then(() => alternateScreen);
2194
- const { runCiteFlow } = await import("./index-C49EfSAl.js");
2195
- const { buildStyleChoices, listCustomStyles } = await import("./style-select-UWYScO8e.js");
2532
+ const { runCiteFlow } = await import("./index-D--7n1SB.js");
2533
+ const { buildStyleChoices, listCustomStyles } = await import("./style-select-DxcSWBSF.js");
2196
2534
  const { search } = await import("./file-watcher-CrsNHUpz.js").then((n) => n.z);
2197
2535
  const { tokenize } = await import("./file-watcher-CrsNHUpz.js").then((n) => n.y);
2198
2536
  const { checkTTY } = await import("./tty-BMyaEOhX.js");
@@ -6749,7 +7087,7 @@ function formatEditOutput(result) {
6749
7087
  }
6750
7088
  async function executeInteractiveEdit(options, context, config2) {
6751
7089
  const { withAlternateScreen: withAlternateScreen2 } = await Promise.resolve().then(() => alternateScreen);
6752
- const { selectReferencesOrExit } = await import("./reference-select-D4iGJ4kA.js");
7090
+ const { selectReferencesOrExit } = await import("./reference-select-DrINWBuP.js");
6753
7091
  const allReferences = await context.library.getAll();
6754
7092
  const identifiers = await withAlternateScreen2(
6755
7093
  () => selectReferencesOrExit(allReferences, { multiSelect: true }, config2.cli.tui)
@@ -10403,7 +10741,7 @@ function getFulltextExitCode(result) {
10403
10741
  }
10404
10742
  async function executeInteractiveSelect$1(context, config2) {
10405
10743
  const { withAlternateScreen: withAlternateScreen2 } = await Promise.resolve().then(() => alternateScreen);
10406
- const { selectReferencesOrExit } = await import("./reference-select-D4iGJ4kA.js");
10744
+ const { selectReferencesOrExit } = await import("./reference-select-DrINWBuP.js");
10407
10745
  const allReferences = await context.library.getAll();
10408
10746
  const identifiers = await withAlternateScreen2(
10409
10747
  () => selectReferencesOrExit(allReferences, { multiSelect: false }, config2.cli.tui)
@@ -31436,7 +31774,7 @@ async function mcpStart(options) {
31436
31774
  async function executeRemove(options, context) {
31437
31775
  const { identifier, idType = "id", fulltextDirectory, deleteFulltext = false } = options;
31438
31776
  if (context.mode === "local" && deleteFulltext && fulltextDirectory) {
31439
- const { removeReference } = await import("./index-B4gr0P83.js").then((n) => n.r);
31777
+ const { removeReference } = await import("./index-DrZawbND.js").then((n) => n.r);
31440
31778
  return removeReference(context.library, {
31441
31779
  identifier,
31442
31780
  idType,
@@ -31491,7 +31829,7 @@ Continue?`;
31491
31829
  }
31492
31830
  async function executeInteractiveRemove(context, config2) {
31493
31831
  const { withAlternateScreen: withAlternateScreen2 } = await Promise.resolve().then(() => alternateScreen);
31494
- const { selectReferenceItemsOrExit } = await import("./reference-select-D4iGJ4kA.js");
31832
+ const { selectReferenceItemsOrExit } = await import("./reference-select-DrINWBuP.js");
31495
31833
  const allReferences = await context.library.getAll();
31496
31834
  const selectedItems = await withAlternateScreen2(
31497
31835
  () => selectReferenceItemsOrExit(allReferences, { multiSelect: false }, config2.cli.tui)
@@ -31716,7 +32054,7 @@ async function executeInteractiveSearch(options, context, config2) {
31716
32054
  validateInteractiveOptions(options);
31717
32055
  const { checkTTY } = await import("./tty-BMyaEOhX.js");
31718
32056
  const { withAlternateScreen: withAlternateScreen2 } = await Promise.resolve().then(() => alternateScreen);
31719
- const { runSearchFlow } = await import("./index-C49EfSAl.js");
32057
+ const { runSearchFlow } = await import("./index-D--7n1SB.js");
31720
32058
  const { search } = await import("./file-watcher-CrsNHUpz.js").then((n) => n.z);
31721
32059
  const { tokenize } = await import("./file-watcher-CrsNHUpz.js").then((n) => n.y);
31722
32060
  checkTTY();
@@ -31735,7 +32073,7 @@ async function executeInteractiveSearch(options, context, config2) {
31735
32073
  })
31736
32074
  );
31737
32075
  if (result.selectedItems && !result.cancelled) {
31738
- const { isSideEffectAction } = await import("./action-menu-BoJkH1yr.js");
32076
+ const { isSideEffectAction } = await import("./action-menu-Brg5Lmz7.js");
31739
32077
  if (isSideEffectAction(result.action)) {
31740
32078
  await executeSideEffectAction(result.action, result.selectedItems, context, config2);
31741
32079
  return { output: "", cancelled: false, action: result.action };
@@ -32138,7 +32476,7 @@ function formatUpdateOutput(result, identifier) {
32138
32476
  }
32139
32477
  async function executeInteractiveUpdate(context, config2) {
32140
32478
  const { withAlternateScreen: withAlternateScreen2 } = await Promise.resolve().then(() => alternateScreen);
32141
- const { selectReferencesOrExit } = await import("./reference-select-D4iGJ4kA.js");
32479
+ const { selectReferencesOrExit } = await import("./reference-select-DrINWBuP.js");
32142
32480
  const allReferences = await context.library.getAll();
32143
32481
  const identifiers = await withAlternateScreen2(
32144
32482
  () => selectReferencesOrExit(allReferences, { multiSelect: false }, config2.cli.tui)
@@ -32433,7 +32771,7 @@ function getUrlExitCode(result) {
32433
32771
  }
32434
32772
  async function executeInteractiveSelect(context, config2) {
32435
32773
  const { withAlternateScreen: withAlternateScreen2 } = await Promise.resolve().then(() => alternateScreen);
32436
- const { selectReferencesOrExit } = await import("./reference-select-D4iGJ4kA.js");
32774
+ const { selectReferencesOrExit } = await import("./reference-select-DrINWBuP.js");
32437
32775
  const allReferences = await context.library.getAll();
32438
32776
  const identifiers = await withAlternateScreen2(
32439
32777
  () => selectReferencesOrExit(allReferences, { multiSelect: false }, config2.cli.tui)
@@ -33116,7 +33454,7 @@ function registerAttachCommand(program) {
33116
33454
  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) => {
33117
33455
  await handleAttachDetachAction(identifier, filename, options, program.opts());
33118
33456
  });
33119
- 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) => {
33457
+ 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("--no-rename", "Skip file renaming (keep original filenames)").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
33120
33458
  await handleAttachSyncAction(identifier, options, program.opts());
33121
33459
  });
33122
33460
  }
@@ -33170,4 +33508,4 @@ export {
33170
33508
  restoreStdinAfterInk as r,
33171
33509
  syncAttachments as s
33172
33510
  };
33173
- //# sourceMappingURL=index-Di6yhlFH.js.map
33511
+ //# sourceMappingURL=index-Cno7_aWr.js.map