docrev 0.9.13 → 0.9.15

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 (126) hide show
  1. package/.claude/settings.local.json +9 -9
  2. package/.gitattributes +1 -1
  3. package/CHANGELOG.md +149 -149
  4. package/PLAN-tables-and-postprocess.md +850 -850
  5. package/README.md +411 -391
  6. package/bin/rev.js +11 -11
  7. package/bin/rev.ts +145 -145
  8. package/completions/rev.bash +127 -127
  9. package/completions/rev.ps1 +210 -210
  10. package/completions/rev.zsh +207 -207
  11. package/dev_notes/stress2/build_adversarial.ts +186 -186
  12. package/dev_notes/stress2/drift_matcher.ts +62 -62
  13. package/dev_notes/stress2/probe_anchors.ts +35 -35
  14. package/dev_notes/stress2/project/discussion.before.md +3 -3
  15. package/dev_notes/stress2/project/discussion.md +3 -3
  16. package/dev_notes/stress2/project/methods.before.md +20 -20
  17. package/dev_notes/stress2/project/methods.md +20 -20
  18. package/dev_notes/stress2/project/rev.yaml +5 -5
  19. package/dev_notes/stress2/project/sections.yaml +4 -4
  20. package/dev_notes/stress2/sections.yaml +5 -5
  21. package/dev_notes/stress2/trace_placement.ts +50 -50
  22. package/dev_notes/stresstest_boundaries.ts +27 -27
  23. package/dev_notes/stresstest_drift_apply.ts +43 -43
  24. package/dev_notes/stresstest_drift_compare.ts +43 -43
  25. package/dev_notes/stresstest_drift_v2.ts +54 -54
  26. package/dev_notes/stresstest_inspect.ts +54 -54
  27. package/dev_notes/stresstest_pstyle.ts +55 -55
  28. package/dev_notes/stresstest_section_debug.ts +23 -23
  29. package/dev_notes/stresstest_split.ts +70 -70
  30. package/dev_notes/stresstest_trace.ts +19 -19
  31. package/dev_notes/stresstest_verify_no_overwrite.ts +40 -40
  32. package/dist/lib/build.d.ts +38 -1
  33. package/dist/lib/build.d.ts.map +1 -1
  34. package/dist/lib/build.js +68 -30
  35. package/dist/lib/build.js.map +1 -1
  36. package/dist/lib/commands/build.d.ts.map +1 -1
  37. package/dist/lib/commands/build.js +38 -5
  38. package/dist/lib/commands/build.js.map +1 -1
  39. package/dist/lib/commands/utilities.js +164 -164
  40. package/dist/lib/commands/word-tools.js +8 -8
  41. package/dist/lib/grammar.js +3 -3
  42. package/dist/lib/pdf-comments.js +44 -44
  43. package/dist/lib/plugins.js +57 -57
  44. package/dist/lib/pptx-themes.js +115 -115
  45. package/dist/lib/spelling.js +2 -2
  46. package/dist/lib/templates.js +387 -387
  47. package/dist/lib/themes.js +51 -51
  48. package/eslint.config.js +27 -27
  49. package/lib/anchor-match.ts +276 -276
  50. package/lib/annotations.ts +644 -644
  51. package/lib/build.ts +1300 -1251
  52. package/lib/citations.ts +160 -160
  53. package/lib/commands/build.ts +833 -801
  54. package/lib/commands/citations.ts +515 -515
  55. package/lib/commands/comments.ts +1050 -1050
  56. package/lib/commands/context.ts +174 -174
  57. package/lib/commands/core.ts +309 -309
  58. package/lib/commands/doi.ts +435 -435
  59. package/lib/commands/file-ops.ts +372 -372
  60. package/lib/commands/history.ts +320 -320
  61. package/lib/commands/index.ts +87 -87
  62. package/lib/commands/init.ts +259 -259
  63. package/lib/commands/merge-resolve.ts +378 -378
  64. package/lib/commands/preview.ts +178 -178
  65. package/lib/commands/project-info.ts +244 -244
  66. package/lib/commands/quality.ts +517 -517
  67. package/lib/commands/response.ts +454 -454
  68. package/lib/commands/section-boundaries.ts +82 -82
  69. package/lib/commands/sections.ts +451 -451
  70. package/lib/commands/sync.ts +706 -706
  71. package/lib/commands/text-ops.ts +449 -449
  72. package/lib/commands/utilities.ts +448 -448
  73. package/lib/commands/verify-anchors.ts +272 -272
  74. package/lib/commands/word-tools.ts +340 -340
  75. package/lib/comment-realign.ts +517 -517
  76. package/lib/config.ts +84 -84
  77. package/lib/crossref.ts +781 -781
  78. package/lib/csl.ts +191 -191
  79. package/lib/dependencies.ts +98 -98
  80. package/lib/diff-engine.ts +465 -465
  81. package/lib/doi-cache.ts +115 -115
  82. package/lib/doi.ts +897 -897
  83. package/lib/equations.ts +506 -506
  84. package/lib/errors.ts +346 -346
  85. package/lib/format.ts +541 -541
  86. package/lib/git.ts +326 -326
  87. package/lib/grammar.ts +303 -303
  88. package/lib/image-registry.ts +180 -180
  89. package/lib/import.ts +911 -911
  90. package/lib/journals.ts +543 -543
  91. package/lib/merge.ts +633 -633
  92. package/lib/orcid.ts +144 -144
  93. package/lib/pdf-comments.ts +263 -263
  94. package/lib/pdf-import.ts +524 -524
  95. package/lib/plugins.ts +362 -362
  96. package/lib/postprocess.ts +188 -188
  97. package/lib/pptx-color-filter.lua +37 -37
  98. package/lib/pptx-template.ts +469 -469
  99. package/lib/pptx-themes.ts +483 -483
  100. package/lib/protect-restore.ts +520 -520
  101. package/lib/rate-limiter.ts +94 -94
  102. package/lib/response.ts +197 -197
  103. package/lib/restore-references.ts +240 -240
  104. package/lib/review.ts +327 -327
  105. package/lib/schema.ts +417 -417
  106. package/lib/scientific-words.ts +73 -73
  107. package/lib/sections.ts +335 -335
  108. package/lib/slides.ts +756 -756
  109. package/lib/spelling.ts +334 -334
  110. package/lib/templates.ts +526 -526
  111. package/lib/themes.ts +742 -742
  112. package/lib/trackchanges.ts +247 -247
  113. package/lib/tui.ts +450 -450
  114. package/lib/types.ts +550 -550
  115. package/lib/undo.ts +250 -250
  116. package/lib/utils.ts +69 -69
  117. package/lib/variables.ts +179 -179
  118. package/lib/word-extraction.ts +806 -806
  119. package/lib/word.ts +643 -643
  120. package/lib/wordcomments.ts +817 -817
  121. package/package.json +137 -137
  122. package/scripts/postbuild.js +28 -28
  123. package/skill/REFERENCE.md +473 -431
  124. package/skill/SKILL.md +274 -258
  125. package/tsconfig.json +26 -26
  126. package/types/index.d.ts +525 -525
package/lib/csl.ts CHANGED
@@ -1,191 +1,191 @@
1
- /**
2
- * CSL citation style resolution and caching
3
- *
4
- * Resolves short CSL names (e.g. "nature") to local file paths,
5
- * downloading from the CSL repository if needed.
6
- */
7
-
8
- import * as fs from 'fs';
9
- import * as path from 'path';
10
- import * as os from 'os';
11
- import * as https from 'https';
12
-
13
- // =============================================================================
14
- // Constants
15
- // =============================================================================
16
-
17
- /** Cache directory for downloaded CSL files */
18
- const CSL_CACHE_DIR = path.join(os.homedir(), '.rev', 'csl');
19
-
20
- /** GitHub raw URL for the CSL styles repository */
21
- const CSL_REPO_BASE = 'https://raw.githubusercontent.com/citation-style-language/styles/master';
22
-
23
- /**
24
- * Short name → CSL filename mapping for common styles.
25
- * Names that match their filename exactly don't need an entry here.
26
- */
27
- const CSL_ALIASES: Record<string, string> = {
28
- 'apa': 'apa',
29
- 'chicago': 'chicago-author-date',
30
- 'vancouver': 'vancouver',
31
- 'ieee': 'ieee',
32
- 'nature': 'nature',
33
- 'science': 'science',
34
- 'cell': 'cell',
35
- 'pnas': 'pnas',
36
- 'plos': 'plos',
37
- 'elife': 'elife',
38
- 'ecology-letters': 'ecology-letters',
39
- 'ecology': 'ecology',
40
- 'ama': 'american-medical-association',
41
- 'acs': 'american-chemical-society',
42
- 'rsc': 'royal-society-of-chemistry',
43
- 'harvard': 'harvard-cite-them-right',
44
- 'mla': 'modern-language-association',
45
- 'elsevier': 'elsevier-harvard',
46
- 'springer': 'springer-basic-author-date',
47
- 'biomed-central': 'biomed-central',
48
- };
49
-
50
- // =============================================================================
51
- // Public API
52
- // =============================================================================
53
-
54
- /**
55
- * Get the CSL cache directory path
56
- */
57
- export function getCSLCacheDir(): string {
58
- return CSL_CACHE_DIR;
59
- }
60
-
61
- /**
62
- * Resolve a CSL name or path to a local file path.
63
- *
64
- * Resolution order:
65
- * 1. If it's an absolute path or relative path that exists, return it
66
- * 2. Check project directory for <name>.csl
67
- * 3. Check ~/.rev/csl/ cache
68
- * 4. Return null (caller can then use fetchCSL to download)
69
- */
70
- export function resolveCSL(nameOrPath: string, projectDir?: string): string | null {
71
- // Already a file path that exists
72
- if (path.isAbsolute(nameOrPath) && fs.existsSync(nameOrPath)) {
73
- return nameOrPath;
74
- }
75
-
76
- // Relative path in project directory
77
- if (projectDir) {
78
- const projectPath = path.join(projectDir, nameOrPath);
79
- if (fs.existsSync(projectPath)) {
80
- return projectPath;
81
- }
82
- // Try with .csl extension
83
- const projectPathCsl = projectPath.endsWith('.csl') ? projectPath : `${projectPath}.csl`;
84
- if (fs.existsSync(projectPathCsl)) {
85
- return projectPathCsl;
86
- }
87
- }
88
-
89
- // Resolve short name to filename
90
- const baseName = resolveCSLName(nameOrPath);
91
- const fileName = baseName.endsWith('.csl') ? baseName : `${baseName}.csl`;
92
-
93
- // Check cache
94
- const cachePath = path.join(CSL_CACHE_DIR, fileName);
95
- if (fs.existsSync(cachePath)) {
96
- return cachePath;
97
- }
98
-
99
- return null;
100
- }
101
-
102
- /**
103
- * Download a CSL style from the CSL repository to the local cache.
104
- *
105
- * @returns Path to the cached file, or null on failure
106
- */
107
- export async function fetchCSL(name: string): Promise<string | null> {
108
- const baseName = resolveCSLName(name);
109
- const fileName = baseName.endsWith('.csl') ? baseName : `${baseName}.csl`;
110
- const url = `${CSL_REPO_BASE}/${fileName}`;
111
- const cachePath = path.join(CSL_CACHE_DIR, fileName);
112
-
113
- // Ensure cache directory exists
114
- if (!fs.existsSync(CSL_CACHE_DIR)) {
115
- fs.mkdirSync(CSL_CACHE_DIR, { recursive: true });
116
- }
117
-
118
- try {
119
- const content = await httpGet(url);
120
- if (content) {
121
- fs.writeFileSync(cachePath, content, 'utf-8');
122
- return cachePath;
123
- }
124
- return null;
125
- } catch {
126
- return null;
127
- }
128
- }
129
-
130
- /**
131
- * List all cached CSL files
132
- */
133
- export function listCachedCSL(): Array<{ name: string; path: string }> {
134
- if (!fs.existsSync(CSL_CACHE_DIR)) {
135
- return [];
136
- }
137
-
138
- return fs.readdirSync(CSL_CACHE_DIR)
139
- .filter(f => f.endsWith('.csl'))
140
- .sort()
141
- .map(f => ({
142
- name: path.basename(f, '.csl'),
143
- path: path.join(CSL_CACHE_DIR, f),
144
- }));
145
- }
146
-
147
- /**
148
- * Get the list of known CSL short name aliases
149
- */
150
- export function getCSLAliases(): Record<string, string> {
151
- return { ...CSL_ALIASES };
152
- }
153
-
154
- // =============================================================================
155
- // Internal helpers
156
- // =============================================================================
157
-
158
- /**
159
- * Resolve a short name to a CSL filename (without extension)
160
- */
161
- function resolveCSLName(name: string): string {
162
- const normalized = name.toLowerCase().replace(/\.csl$/, '');
163
- return CSL_ALIASES[normalized] || normalized;
164
- }
165
-
166
- /**
167
- * Simple HTTPS GET that follows redirects
168
- */
169
- function httpGet(url: string, redirectCount = 0): Promise<string | null> {
170
- if (redirectCount > 5) return Promise.resolve(null);
171
-
172
- return new Promise((resolve) => {
173
- https.get(url, (res) => {
174
- // Follow redirects
175
- if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
176
- resolve(httpGet(res.headers.location, redirectCount + 1));
177
- return;
178
- }
179
-
180
- if (res.statusCode !== 200) {
181
- resolve(null);
182
- return;
183
- }
184
-
185
- let data = '';
186
- res.on('data', chunk => { data += chunk; });
187
- res.on('end', () => resolve(data));
188
- res.on('error', () => resolve(null));
189
- }).on('error', () => resolve(null));
190
- });
191
- }
1
+ /**
2
+ * CSL citation style resolution and caching
3
+ *
4
+ * Resolves short CSL names (e.g. "nature") to local file paths,
5
+ * downloading from the CSL repository if needed.
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import * as os from 'os';
11
+ import * as https from 'https';
12
+
13
+ // =============================================================================
14
+ // Constants
15
+ // =============================================================================
16
+
17
+ /** Cache directory for downloaded CSL files */
18
+ const CSL_CACHE_DIR = path.join(os.homedir(), '.rev', 'csl');
19
+
20
+ /** GitHub raw URL for the CSL styles repository */
21
+ const CSL_REPO_BASE = 'https://raw.githubusercontent.com/citation-style-language/styles/master';
22
+
23
+ /**
24
+ * Short name → CSL filename mapping for common styles.
25
+ * Names that match their filename exactly don't need an entry here.
26
+ */
27
+ const CSL_ALIASES: Record<string, string> = {
28
+ 'apa': 'apa',
29
+ 'chicago': 'chicago-author-date',
30
+ 'vancouver': 'vancouver',
31
+ 'ieee': 'ieee',
32
+ 'nature': 'nature',
33
+ 'science': 'science',
34
+ 'cell': 'cell',
35
+ 'pnas': 'pnas',
36
+ 'plos': 'plos',
37
+ 'elife': 'elife',
38
+ 'ecology-letters': 'ecology-letters',
39
+ 'ecology': 'ecology',
40
+ 'ama': 'american-medical-association',
41
+ 'acs': 'american-chemical-society',
42
+ 'rsc': 'royal-society-of-chemistry',
43
+ 'harvard': 'harvard-cite-them-right',
44
+ 'mla': 'modern-language-association',
45
+ 'elsevier': 'elsevier-harvard',
46
+ 'springer': 'springer-basic-author-date',
47
+ 'biomed-central': 'biomed-central',
48
+ };
49
+
50
+ // =============================================================================
51
+ // Public API
52
+ // =============================================================================
53
+
54
+ /**
55
+ * Get the CSL cache directory path
56
+ */
57
+ export function getCSLCacheDir(): string {
58
+ return CSL_CACHE_DIR;
59
+ }
60
+
61
+ /**
62
+ * Resolve a CSL name or path to a local file path.
63
+ *
64
+ * Resolution order:
65
+ * 1. If it's an absolute path or relative path that exists, return it
66
+ * 2. Check project directory for <name>.csl
67
+ * 3. Check ~/.rev/csl/ cache
68
+ * 4. Return null (caller can then use fetchCSL to download)
69
+ */
70
+ export function resolveCSL(nameOrPath: string, projectDir?: string): string | null {
71
+ // Already a file path that exists
72
+ if (path.isAbsolute(nameOrPath) && fs.existsSync(nameOrPath)) {
73
+ return nameOrPath;
74
+ }
75
+
76
+ // Relative path in project directory
77
+ if (projectDir) {
78
+ const projectPath = path.join(projectDir, nameOrPath);
79
+ if (fs.existsSync(projectPath)) {
80
+ return projectPath;
81
+ }
82
+ // Try with .csl extension
83
+ const projectPathCsl = projectPath.endsWith('.csl') ? projectPath : `${projectPath}.csl`;
84
+ if (fs.existsSync(projectPathCsl)) {
85
+ return projectPathCsl;
86
+ }
87
+ }
88
+
89
+ // Resolve short name to filename
90
+ const baseName = resolveCSLName(nameOrPath);
91
+ const fileName = baseName.endsWith('.csl') ? baseName : `${baseName}.csl`;
92
+
93
+ // Check cache
94
+ const cachePath = path.join(CSL_CACHE_DIR, fileName);
95
+ if (fs.existsSync(cachePath)) {
96
+ return cachePath;
97
+ }
98
+
99
+ return null;
100
+ }
101
+
102
+ /**
103
+ * Download a CSL style from the CSL repository to the local cache.
104
+ *
105
+ * @returns Path to the cached file, or null on failure
106
+ */
107
+ export async function fetchCSL(name: string): Promise<string | null> {
108
+ const baseName = resolveCSLName(name);
109
+ const fileName = baseName.endsWith('.csl') ? baseName : `${baseName}.csl`;
110
+ const url = `${CSL_REPO_BASE}/${fileName}`;
111
+ const cachePath = path.join(CSL_CACHE_DIR, fileName);
112
+
113
+ // Ensure cache directory exists
114
+ if (!fs.existsSync(CSL_CACHE_DIR)) {
115
+ fs.mkdirSync(CSL_CACHE_DIR, { recursive: true });
116
+ }
117
+
118
+ try {
119
+ const content = await httpGet(url);
120
+ if (content) {
121
+ fs.writeFileSync(cachePath, content, 'utf-8');
122
+ return cachePath;
123
+ }
124
+ return null;
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * List all cached CSL files
132
+ */
133
+ export function listCachedCSL(): Array<{ name: string; path: string }> {
134
+ if (!fs.existsSync(CSL_CACHE_DIR)) {
135
+ return [];
136
+ }
137
+
138
+ return fs.readdirSync(CSL_CACHE_DIR)
139
+ .filter(f => f.endsWith('.csl'))
140
+ .sort()
141
+ .map(f => ({
142
+ name: path.basename(f, '.csl'),
143
+ path: path.join(CSL_CACHE_DIR, f),
144
+ }));
145
+ }
146
+
147
+ /**
148
+ * Get the list of known CSL short name aliases
149
+ */
150
+ export function getCSLAliases(): Record<string, string> {
151
+ return { ...CSL_ALIASES };
152
+ }
153
+
154
+ // =============================================================================
155
+ // Internal helpers
156
+ // =============================================================================
157
+
158
+ /**
159
+ * Resolve a short name to a CSL filename (without extension)
160
+ */
161
+ function resolveCSLName(name: string): string {
162
+ const normalized = name.toLowerCase().replace(/\.csl$/, '');
163
+ return CSL_ALIASES[normalized] || normalized;
164
+ }
165
+
166
+ /**
167
+ * Simple HTTPS GET that follows redirects
168
+ */
169
+ function httpGet(url: string, redirectCount = 0): Promise<string | null> {
170
+ if (redirectCount > 5) return Promise.resolve(null);
171
+
172
+ return new Promise((resolve) => {
173
+ https.get(url, (res) => {
174
+ // Follow redirects
175
+ if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
176
+ resolve(httpGet(res.headers.location, redirectCount + 1));
177
+ return;
178
+ }
179
+
180
+ if (res.statusCode !== 200) {
181
+ resolve(null);
182
+ return;
183
+ }
184
+
185
+ let data = '';
186
+ res.on('data', chunk => { data += chunk; });
187
+ res.on('end', () => resolve(data));
188
+ res.on('error', () => resolve(null));
189
+ }).on('error', () => resolve(null));
190
+ });
191
+ }
@@ -1,98 +1,98 @@
1
- /**
2
- * Dependency checking utilities for pandoc, LaTeX, and related tools
3
- */
4
-
5
- import { execSync } from 'child_process';
6
-
7
- /**
8
- * Check if a command is available by running it silently
9
- */
10
- function commandExists(cmd: string): boolean {
11
- try {
12
- execSync(cmd, { stdio: 'ignore' });
13
- return true;
14
- } catch {
15
- return false;
16
- }
17
- }
18
-
19
- /**
20
- * Check if pandoc-crossref is available
21
- */
22
- export function hasPandocCrossref(): boolean {
23
- return commandExists('pandoc-crossref --version');
24
- }
25
-
26
- /**
27
- * Check if pandoc is available
28
- */
29
- export function hasPandoc(): boolean {
30
- return commandExists('pandoc --version');
31
- }
32
-
33
- /**
34
- * Check if LaTeX is available (for PDF generation)
35
- */
36
- export function hasLatex(): boolean {
37
- return commandExists('pdflatex --version') || commandExists('xelatex --version');
38
- }
39
-
40
- /**
41
- * Get installation instructions for missing dependencies
42
- */
43
- export function getInstallInstructions(dependency: string): string {
44
- const platform = process.platform;
45
- const instructions: Record<string, Record<string, string>> = {
46
- pandoc: {
47
- darwin: 'brew install pandoc',
48
- win32: 'winget install JohnMacFarlane.Pandoc',
49
- linux: 'sudo apt install pandoc',
50
- },
51
- latex: {
52
- darwin: 'brew install --cask mactex-no-gui',
53
- win32: 'Install MiKTeX from https://miktex.org/download',
54
- linux: 'sudo apt install texlive-latex-base texlive-fonts-recommended',
55
- },
56
- 'pandoc-crossref': {
57
- darwin: 'brew install pandoc-crossref',
58
- win32: 'Download from https://github.com/lierdakil/pandoc-crossref/releases',
59
- linux: 'Download from https://github.com/lierdakil/pandoc-crossref/releases',
60
- },
61
- };
62
-
63
- const platformInstructions = instructions[dependency];
64
- if (!platformInstructions) return '';
65
-
66
- return platformInstructions[platform] || platformInstructions.linux || '';
67
- }
68
-
69
- export interface DependencyStatus {
70
- pandoc: boolean;
71
- latex: boolean;
72
- crossref: boolean;
73
- messages: string[];
74
- }
75
-
76
- /**
77
- * Check dependencies and return status
78
- */
79
- export function checkDependencies(): DependencyStatus {
80
- const status: DependencyStatus = {
81
- pandoc: hasPandoc(),
82
- latex: hasLatex(),
83
- crossref: hasPandocCrossref(),
84
- messages: [],
85
- };
86
-
87
- if (!status.pandoc) {
88
- status.messages.push(`Pandoc not found. Install with: ${getInstallInstructions('pandoc')}`);
89
- }
90
- if (!status.latex) {
91
- status.messages.push(`LaTeX not found (required for PDF). Install with: ${getInstallInstructions('latex')}`);
92
- }
93
- if (!status.crossref) {
94
- status.messages.push(`pandoc-crossref not found (optional, for figure/table refs). Install with: ${getInstallInstructions('pandoc-crossref')}`);
95
- }
96
-
97
- return status;
98
- }
1
+ /**
2
+ * Dependency checking utilities for pandoc, LaTeX, and related tools
3
+ */
4
+
5
+ import { execSync } from 'child_process';
6
+
7
+ /**
8
+ * Check if a command is available by running it silently
9
+ */
10
+ function commandExists(cmd: string): boolean {
11
+ try {
12
+ execSync(cmd, { stdio: 'ignore' });
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Check if pandoc-crossref is available
21
+ */
22
+ export function hasPandocCrossref(): boolean {
23
+ return commandExists('pandoc-crossref --version');
24
+ }
25
+
26
+ /**
27
+ * Check if pandoc is available
28
+ */
29
+ export function hasPandoc(): boolean {
30
+ return commandExists('pandoc --version');
31
+ }
32
+
33
+ /**
34
+ * Check if LaTeX is available (for PDF generation)
35
+ */
36
+ export function hasLatex(): boolean {
37
+ return commandExists('pdflatex --version') || commandExists('xelatex --version');
38
+ }
39
+
40
+ /**
41
+ * Get installation instructions for missing dependencies
42
+ */
43
+ export function getInstallInstructions(dependency: string): string {
44
+ const platform = process.platform;
45
+ const instructions: Record<string, Record<string, string>> = {
46
+ pandoc: {
47
+ darwin: 'brew install pandoc',
48
+ win32: 'winget install JohnMacFarlane.Pandoc',
49
+ linux: 'sudo apt install pandoc',
50
+ },
51
+ latex: {
52
+ darwin: 'brew install --cask mactex-no-gui',
53
+ win32: 'Install MiKTeX from https://miktex.org/download',
54
+ linux: 'sudo apt install texlive-latex-base texlive-fonts-recommended',
55
+ },
56
+ 'pandoc-crossref': {
57
+ darwin: 'brew install pandoc-crossref',
58
+ win32: 'Download from https://github.com/lierdakil/pandoc-crossref/releases',
59
+ linux: 'Download from https://github.com/lierdakil/pandoc-crossref/releases',
60
+ },
61
+ };
62
+
63
+ const platformInstructions = instructions[dependency];
64
+ if (!platformInstructions) return '';
65
+
66
+ return platformInstructions[platform] || platformInstructions.linux || '';
67
+ }
68
+
69
+ export interface DependencyStatus {
70
+ pandoc: boolean;
71
+ latex: boolean;
72
+ crossref: boolean;
73
+ messages: string[];
74
+ }
75
+
76
+ /**
77
+ * Check dependencies and return status
78
+ */
79
+ export function checkDependencies(): DependencyStatus {
80
+ const status: DependencyStatus = {
81
+ pandoc: hasPandoc(),
82
+ latex: hasLatex(),
83
+ crossref: hasPandocCrossref(),
84
+ messages: [],
85
+ };
86
+
87
+ if (!status.pandoc) {
88
+ status.messages.push(`Pandoc not found. Install with: ${getInstallInstructions('pandoc')}`);
89
+ }
90
+ if (!status.latex) {
91
+ status.messages.push(`LaTeX not found (required for PDF). Install with: ${getInstallInstructions('latex')}`);
92
+ }
93
+ if (!status.crossref) {
94
+ status.messages.push(`pandoc-crossref not found (optional, for figure/table refs). Install with: ${getInstallInstructions('pandoc-crossref')}`);
95
+ }
96
+
97
+ return status;
98
+ }