docrev 0.6.13 → 0.7.6

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.
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Shared context for command modules
3
+ *
4
+ * This module provides shared utilities and state that command modules need.
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import * as fmt from '../format.js';
11
+
12
+ // Global flags (set by main CLI)
13
+ export let quietMode = false;
14
+ export let jsonMode = false;
15
+
16
+ export function setQuietMode(value) {
17
+ quietMode = value;
18
+ }
19
+
20
+ export function setJsonMode(value) {
21
+ jsonMode = value;
22
+ if (value) {
23
+ chalk.level = 0;
24
+ }
25
+ }
26
+
27
+ // JSON output helper
28
+ export function jsonOutput(data) {
29
+ console.log(JSON.stringify(data, null, 2));
30
+ }
31
+
32
+ // Find files by extension
33
+ export function findFiles(ext, cwd = process.cwd()) {
34
+ try {
35
+ return fs.readdirSync(cwd)
36
+ .filter(f => f.endsWith(ext) && !f.startsWith('.'));
37
+ } catch {
38
+ return [];
39
+ }
40
+ }
41
+
42
+ // Re-export common dependencies
43
+ export { chalk, fs, path, fmt };
44
+
45
+ // Re-export from lib modules
46
+ export {
47
+ parseAnnotations,
48
+ stripAnnotations,
49
+ countAnnotations,
50
+ getComments,
51
+ setCommentStatus,
52
+ hasAnnotations,
53
+ getTrackChanges,
54
+ applyDecision,
55
+ } from '../annotations.js';
56
+
57
+ export {
58
+ interactiveReview,
59
+ listComments,
60
+ interactiveCommentReview,
61
+ } from '../review.js';
62
+
63
+ export {
64
+ generateConfig,
65
+ loadConfig,
66
+ saveConfig,
67
+ matchHeading,
68
+ extractSectionsFromText,
69
+ splitAnnotatedPaper,
70
+ getOrderedSections,
71
+ } from '../sections.js';
72
+
73
+ export {
74
+ buildRegistry,
75
+ detectHardcodedRefs,
76
+ convertHardcodedRefs,
77
+ getRefStatus,
78
+ formatRegistry,
79
+ } from '../crossref.js';
80
+
81
+ export {
82
+ build,
83
+ loadConfig as loadBuildConfig,
84
+ hasPandoc,
85
+ hasPandocCrossref,
86
+ formatBuildResults,
87
+ hasLatex,
88
+ checkDependencies,
89
+ getInstallInstructions,
90
+ } from '../build.js';
91
+
92
+ export {
93
+ getTemplate,
94
+ listTemplates,
95
+ generateCustomTemplate,
96
+ } from '../templates.js';
97
+
98
+ export {
99
+ getUserName,
100
+ setUserName,
101
+ getConfigPath,
102
+ getDefaultSections,
103
+ setDefaultSections,
104
+ loadUserConfig,
105
+ saveUserConfig,
106
+ } from '../config.js';
107
+
108
+ export { inlineDiffPreview } from '../format.js';
109
+
110
+ export {
111
+ parseCommentsWithReplies,
112
+ collectComments,
113
+ generateResponseLetter,
114
+ groupByReviewer,
115
+ } from '../response.js';
116
+
117
+ export {
118
+ validateCitations,
119
+ getCitationStats,
120
+ } from '../citations.js';
121
+
122
+ export {
123
+ extractEquations,
124
+ getEquationStats,
125
+ createEquationsDoc,
126
+ extractEquationsFromWord,
127
+ getWordEquationStats,
128
+ } from '../equations.js';
129
+
130
+ export {
131
+ parseBibEntries,
132
+ checkBibDois,
133
+ fetchBibtex,
134
+ addToBib,
135
+ isValidDoiFormat,
136
+ lookupDoi,
137
+ lookupMissingDois,
138
+ clearDoiCache,
139
+ getDoiCacheStats,
140
+ } from '../doi.js';
141
+
142
+ export {
143
+ formatError,
144
+ getFileNotFoundSuggestions,
145
+ getDependencySuggestions,
146
+ getAnnotationSuggestions,
147
+ getBuildSuggestions,
148
+ exitWithError,
149
+ requireFile,
150
+ } from '../errors.js';
151
+
152
+ export {
153
+ listJournals,
154
+ getJournalProfile,
155
+ validateManuscript,
156
+ validateProject,
157
+ } from '../journals.js';
158
+
159
+ export {
160
+ listCustomProfiles,
161
+ saveProfileTemplate,
162
+ getPluginDirs,
163
+ } from '../plugins.js';
164
+
165
+ export { tuiCommentReview } from '../tui.js';
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Core commands: review, strip, status
3
+ *
4
+ * Basic annotation operations for track changes workflow.
5
+ */
6
+
7
+ import {
8
+ chalk,
9
+ fs,
10
+ path,
11
+ fmt,
12
+ quietMode,
13
+ jsonMode,
14
+ jsonOutput,
15
+ findFiles,
16
+ parseAnnotations,
17
+ stripAnnotations,
18
+ countAnnotations,
19
+ getComments,
20
+ interactiveReview,
21
+ exitWithError,
22
+ getFileNotFoundSuggestions,
23
+ requireFile,
24
+ } from './context.js';
25
+
26
+ /**
27
+ * Register core commands with the program
28
+ * @param {import('commander').Command} program
29
+ */
30
+ export function register(program) {
31
+ // ==========================================================================
32
+ // REVIEW command - Interactive track change review
33
+ // ==========================================================================
34
+
35
+ program
36
+ .command('review')
37
+ .description('Interactively review and accept/reject track changes')
38
+ .argument('<file>', 'Markdown file to review')
39
+ .action(async (file) => {
40
+ requireFile(file, 'Markdown file');
41
+
42
+ const text = fs.readFileSync(file, 'utf-8');
43
+ const result = await interactiveReview(text);
44
+
45
+ if (result.accepted > 0 || result.rejected > 0) {
46
+ // Confirm save
47
+ const rl = await import('readline');
48
+ const readline = rl.createInterface({
49
+ input: process.stdin,
50
+ output: process.stdout,
51
+ });
52
+
53
+ readline.question(chalk.cyan(`\nSave changes to ${file}? [y/N] `), (answer) => {
54
+ readline.close();
55
+ if (answer.toLowerCase() === 'y') {
56
+ fs.writeFileSync(file, result.text, 'utf-8');
57
+ console.log(chalk.green(`Saved ${file}`));
58
+ } else {
59
+ console.log(chalk.yellow('Changes not saved.'));
60
+ }
61
+ });
62
+ }
63
+ });
64
+
65
+ // ==========================================================================
66
+ // STRIP command - Remove annotations
67
+ // ==========================================================================
68
+
69
+ program
70
+ .command('strip')
71
+ .description('Strip annotations, outputting clean Markdown')
72
+ .argument('<file>', 'Markdown file to strip')
73
+ .option('-o, --output <file>', 'Output file (default: stdout)')
74
+ .option('-c, --keep-comments', 'Keep comment annotations')
75
+ .action((file, options) => {
76
+ requireFile(file, 'Markdown file');
77
+
78
+ const text = fs.readFileSync(file, 'utf-8');
79
+ const clean = stripAnnotations(text, { keepComments: options.keepComments });
80
+
81
+ if (options.output) {
82
+ fs.writeFileSync(options.output, clean, 'utf-8');
83
+ console.error(chalk.green(`Written to ${options.output}`));
84
+ } else {
85
+ process.stdout.write(clean);
86
+ }
87
+ });
88
+
89
+ // ==========================================================================
90
+ // STATUS command - Show annotation statistics
91
+ // ==========================================================================
92
+
93
+ program
94
+ .command('status')
95
+ .alias('s')
96
+ .description('Show project overview or file annotation statistics')
97
+ .argument('[file]', 'Markdown file to analyze (default: project overview)')
98
+ .action(async (file) => {
99
+ // If a specific file is given, show its annotations
100
+ if (file) {
101
+ if (!fs.existsSync(file)) {
102
+ if (jsonMode) {
103
+ jsonOutput({ error: `File not found: ${file}` });
104
+ } else {
105
+ exitWithError(`File not found: ${file}`, getFileNotFoundSuggestions(file));
106
+ }
107
+ }
108
+
109
+ const text = fs.readFileSync(file, 'utf-8');
110
+ const counts = countAnnotations(text);
111
+ const comments = getComments(text);
112
+
113
+ if (jsonMode) {
114
+ jsonOutput({
115
+ file: path.basename(file),
116
+ annotations: counts,
117
+ comments: comments.map(c => ({
118
+ author: c.author || null,
119
+ content: c.content,
120
+ line: c.line,
121
+ resolved: c.resolved || false,
122
+ })),
123
+ });
124
+ return;
125
+ }
126
+
127
+ if (counts.total === 0) {
128
+ console.log(fmt.status('success', 'No annotations found.'));
129
+ return;
130
+ }
131
+
132
+ console.log(fmt.header(`Annotations in ${path.basename(file)}`));
133
+ console.log();
134
+
135
+ // Build stats table
136
+ const rows = [];
137
+ if (counts.inserts > 0) rows.push([chalk.green('+'), 'Insertions', chalk.green(counts.inserts)]);
138
+ if (counts.deletes > 0) rows.push([chalk.red('-'), 'Deletions', chalk.red(counts.deletes)]);
139
+ if (counts.substitutes > 0) rows.push([chalk.yellow('~'), 'Substitutions', chalk.yellow(counts.substitutes)]);
140
+ if (counts.comments > 0) rows.push([chalk.blue('#'), 'Comments', chalk.blue(counts.comments)]);
141
+ rows.push([chalk.dim('Σ'), chalk.dim('Total'), chalk.dim(counts.total)]);
142
+
143
+ console.log(fmt.table(['', 'Type', 'Count'], rows, { align: ['center', 'left', 'right'] }));
144
+
145
+ // List comments with authors in a table
146
+ if (comments.length > 0) {
147
+ console.log();
148
+ console.log(fmt.header('Comments'));
149
+ console.log();
150
+
151
+ const commentRows = comments.map((c, i) => [
152
+ chalk.dim(i + 1),
153
+ c.author ? chalk.blue(c.author) : chalk.dim('Anonymous'),
154
+ c.content.length > 45 ? c.content.slice(0, 45) + '...' : c.content,
155
+ chalk.dim(`L${c.line}`),
156
+ ]);
157
+
158
+ console.log(fmt.table(['#', 'Author', 'Comment', 'Line'], commentRows, {
159
+ align: ['right', 'left', 'left', 'right'],
160
+ }));
161
+ }
162
+ return;
163
+ }
164
+
165
+ // Project overview mode
166
+ // Find all markdown files
167
+ const mdFiles = findFiles('.md');
168
+ if (mdFiles.length === 0) {
169
+ if (jsonMode) {
170
+ jsonOutput({ error: 'No markdown files found', files: [] });
171
+ } else {
172
+ console.log(fmt.status('warning', 'No markdown files found in current directory.'));
173
+ }
174
+ return;
175
+ }
176
+
177
+ // Gather stats across all files
178
+ let totalWords = 0;
179
+ let totalComments = 0;
180
+ let pendingComments = 0;
181
+ let totalInserts = 0;
182
+ let totalDeletes = 0;
183
+ let totalSubstitutes = 0;
184
+ const fileStats = [];
185
+
186
+ for (const f of mdFiles) {
187
+ const text = fs.readFileSync(f, 'utf-8');
188
+ const counts = countAnnotations(text);
189
+ const comments = getComments(text);
190
+ const pending = comments.filter(c => !c.resolved).length;
191
+
192
+ // Simple word count (excluding annotations)
193
+ const stripped = stripAnnotations(text);
194
+ const words = stripped.split(/\s+/).filter(w => w.length > 0).length;
195
+
196
+ totalWords += words;
197
+ totalComments += comments.length;
198
+ pendingComments += pending;
199
+ totalInserts += counts.inserts;
200
+ totalDeletes += counts.deletes;
201
+ totalSubstitutes += counts.substitutes;
202
+
203
+ if (counts.total > 0 || words > 0) {
204
+ fileStats.push({
205
+ file: f,
206
+ words,
207
+ inserts: counts.inserts,
208
+ deletes: counts.deletes,
209
+ substitutions: counts.substitutes,
210
+ comments: comments.length,
211
+ pending,
212
+ });
213
+ }
214
+ }
215
+
216
+ // JSON output
217
+ if (jsonMode) {
218
+ const docxFiles = findFiles('.docx');
219
+ const latestDocx = docxFiles.length > 0
220
+ ? docxFiles
221
+ .map(f => ({ name: f, mtime: fs.statSync(f).mtime }))
222
+ .sort((a, b) => b.mtime - a.mtime)[0]
223
+ : null;
224
+
225
+ jsonOutput({
226
+ summary: {
227
+ words: totalWords,
228
+ files: mdFiles.length,
229
+ comments: totalComments,
230
+ pendingComments,
231
+ insertions: totalInserts,
232
+ deletions: totalDeletes,
233
+ substitutions: totalSubstitutes,
234
+ },
235
+ files: fileStats,
236
+ latestDocx: latestDocx ? { name: latestDocx.name, mtime: latestDocx.mtime.toISOString() } : null,
237
+ });
238
+ return;
239
+ }
240
+
241
+ // Normal output
242
+ console.log(fmt.header('Project Status'));
243
+ console.log();
244
+
245
+ // Summary
246
+ console.log(` ${chalk.bold(totalWords.toLocaleString())} words across ${mdFiles.length} files`);
247
+
248
+ if (totalComments > 0) {
249
+ console.log(` ${chalk.blue(totalComments)} comments (${chalk.yellow(pendingComments)} pending)`);
250
+ }
251
+
252
+ const totalChanges = totalInserts + totalDeletes + totalSubstitutes;
253
+ if (totalChanges > 0) {
254
+ console.log(` ${chalk.green(`+${totalInserts}`)} insertions, ${chalk.red(`-${totalDeletes}`)} deletions, ${chalk.yellow(`~${totalSubstitutes}`)} substitutions`);
255
+ }
256
+
257
+ // Per-file breakdown if there are annotations
258
+ if (totalChanges > 0 || totalComments > 0) {
259
+ console.log();
260
+ const rows = fileStats
261
+ .filter(f => f.inserts + f.deletes + f.substitutions + f.comments > 0)
262
+ .map(f => [
263
+ f.file,
264
+ f.words.toLocaleString(),
265
+ f.inserts > 0 ? chalk.green(`+${f.inserts}`) : chalk.dim('-'),
266
+ f.deletes > 0 ? chalk.red(`-${f.deletes}`) : chalk.dim('-'),
267
+ f.substitutions > 0 ? chalk.yellow(`~${f.substitutions}`) : chalk.dim('-'),
268
+ f.pending > 0 ? chalk.yellow(f.pending) : (f.comments > 0 ? chalk.dim(f.comments) : chalk.dim('-')),
269
+ ]);
270
+
271
+ if (rows.length > 0) {
272
+ console.log(fmt.table(
273
+ ['File', 'Words', 'Ins', 'Del', 'Sub', 'Cmt'],
274
+ rows,
275
+ { align: ['left', 'right', 'right', 'right', 'right', 'right'] }
276
+ ));
277
+ }
278
+ }
279
+
280
+ // Check for recent docx files
281
+ const docxFiles = findFiles('.docx');
282
+ if (docxFiles.length > 0) {
283
+ const sorted = docxFiles
284
+ .map(f => ({ name: f, mtime: fs.statSync(f).mtime }))
285
+ .sort((a, b) => b.mtime - a.mtime);
286
+ const latest = sorted[0];
287
+ const age = Date.now() - latest.mtime.getTime();
288
+ const ageStr = age < 3600000 ? `${Math.round(age / 60000)}m ago` :
289
+ age < 86400000 ? `${Math.round(age / 3600000)}h ago` :
290
+ `${Math.round(age / 86400000)}d ago`;
291
+ console.log();
292
+ console.log(chalk.dim(` Latest DOCX: ${latest.name} (${ageStr})`));
293
+ }
294
+ });
295
+ }