docrev 0.2.1 → 0.3.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 +11 -0
- package/bin/rev.js +474 -0
- package/lib/git.js +238 -0
- package/lib/journals.js +420 -0
- package/lib/merge.js +365 -0
- package/lib/trackchanges.js +273 -0
- package/lib/word.js +225 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -118,6 +118,7 @@ rev build docx
|
|
|
118
118
|
|---------|-------------|
|
|
119
119
|
| `rev build [formats...]` | Build PDF/DOCX/TEX from sections |
|
|
120
120
|
| `rev build --toc` | Build with table of contents |
|
|
121
|
+
| `rev build --show-changes` | Export DOCX with visible track changes |
|
|
121
122
|
| `rev new <name>` | Create new project from template |
|
|
122
123
|
| `rev new --list` | List available templates |
|
|
123
124
|
| `rev install` | Check/install dependencies (pandoc-crossref) |
|
|
@@ -176,6 +177,16 @@ rev build docx
|
|
|
176
177
|
| `rev equations from-word <docx>` | Extract equations from Word to LaTeX |
|
|
177
178
|
| `rev response [files]` | Generate response letter from comments |
|
|
178
179
|
| `rev anonymize <file>` | Prepare document for blind review |
|
|
180
|
+
| `rev validate --journal <name>` | Check manuscript against journal requirements |
|
|
181
|
+
| `rev validate --list` | List available journal profiles |
|
|
182
|
+
|
|
183
|
+
### Multi-Reviewer & Git
|
|
184
|
+
|
|
185
|
+
| Command | Description |
|
|
186
|
+
|---------|-------------|
|
|
187
|
+
| `rev merge <md> <docx...>` | Merge feedback from multiple Word documents |
|
|
188
|
+
| `rev diff [ref]` | Compare sections against git history |
|
|
189
|
+
| `rev history [file]` | Show revision history for sections |
|
|
179
190
|
|
|
180
191
|
### Configuration
|
|
181
192
|
|
package/bin/rev.js
CHANGED
|
@@ -1048,6 +1048,148 @@ program
|
|
|
1048
1048
|
}
|
|
1049
1049
|
});
|
|
1050
1050
|
|
|
1051
|
+
// ============================================================================
|
|
1052
|
+
// MERGE command - Combine feedback from multiple reviewers
|
|
1053
|
+
// ============================================================================
|
|
1054
|
+
|
|
1055
|
+
program
|
|
1056
|
+
.command('merge')
|
|
1057
|
+
.description('Merge feedback from multiple Word documents')
|
|
1058
|
+
.argument('<original>', 'Original markdown file')
|
|
1059
|
+
.argument('<docx...>', 'Word documents from reviewers')
|
|
1060
|
+
.option('-o, --output <file>', 'Output file (default: original-merged.md)')
|
|
1061
|
+
.option('--names <names>', 'Reviewer names (comma-separated, in order of docx files)')
|
|
1062
|
+
.option('--auto', 'Auto-resolve conflicts by taking first change')
|
|
1063
|
+
.option('--dry-run', 'Show conflicts without writing')
|
|
1064
|
+
.action(async (original, docxFiles, options) => {
|
|
1065
|
+
const { mergeReviewerDocs, formatConflict, applyChangesAsAnnotations, resolveConflict } = await import('../lib/merge.js');
|
|
1066
|
+
|
|
1067
|
+
if (!fs.existsSync(original)) {
|
|
1068
|
+
console.error(fmt.status('error', `Original file not found: ${original}`));
|
|
1069
|
+
process.exit(1);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Validate all docx files exist
|
|
1073
|
+
for (const docx of docxFiles) {
|
|
1074
|
+
if (!fs.existsSync(docx)) {
|
|
1075
|
+
console.error(fmt.status('error', `Reviewer file not found: ${docx}`));
|
|
1076
|
+
process.exit(1);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Parse reviewer names
|
|
1081
|
+
const names = options.names
|
|
1082
|
+
? options.names.split(',').map(n => n.trim())
|
|
1083
|
+
: docxFiles.map((f, i) => `Reviewer ${i + 1}`);
|
|
1084
|
+
|
|
1085
|
+
if (names.length < docxFiles.length) {
|
|
1086
|
+
// Pad with default names
|
|
1087
|
+
for (let i = names.length; i < docxFiles.length; i++) {
|
|
1088
|
+
names.push(`Reviewer ${i + 1}`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const reviewerDocs = docxFiles.map((p, i) => ({
|
|
1093
|
+
path: p,
|
|
1094
|
+
name: names[i],
|
|
1095
|
+
}));
|
|
1096
|
+
|
|
1097
|
+
console.log(fmt.header('Multi-Reviewer Merge'));
|
|
1098
|
+
console.log();
|
|
1099
|
+
console.log(chalk.dim(` Original: ${original}`));
|
|
1100
|
+
console.log(chalk.dim(` Reviewers: ${names.join(', ')}`));
|
|
1101
|
+
console.log();
|
|
1102
|
+
|
|
1103
|
+
const spin = fmt.spinner('Analyzing changes...').start();
|
|
1104
|
+
|
|
1105
|
+
try {
|
|
1106
|
+
const { merged, conflicts, stats, originalText } = await mergeReviewerDocs(original, reviewerDocs, {
|
|
1107
|
+
autoResolve: options.auto,
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
spin.stop();
|
|
1111
|
+
|
|
1112
|
+
// Show stats
|
|
1113
|
+
console.log(fmt.table(['Metric', 'Count'], [
|
|
1114
|
+
['Total changes', stats.totalChanges.toString()],
|
|
1115
|
+
['Non-conflicting', stats.nonConflicting.toString()],
|
|
1116
|
+
['Conflicts', stats.conflicts.toString()],
|
|
1117
|
+
['Comments', stats.comments.toString()],
|
|
1118
|
+
]));
|
|
1119
|
+
console.log();
|
|
1120
|
+
|
|
1121
|
+
// Handle conflicts
|
|
1122
|
+
if (conflicts.length > 0) {
|
|
1123
|
+
console.log(chalk.yellow(`Found ${conflicts.length} conflict(s):\n`));
|
|
1124
|
+
|
|
1125
|
+
let resolvedMerged = merged;
|
|
1126
|
+
|
|
1127
|
+
for (let i = 0; i < conflicts.length; i++) {
|
|
1128
|
+
const conflict = conflicts[i];
|
|
1129
|
+
console.log(chalk.bold(`Conflict ${i + 1}/${conflicts.length}:`));
|
|
1130
|
+
console.log(formatConflict(conflict, originalText));
|
|
1131
|
+
console.log();
|
|
1132
|
+
|
|
1133
|
+
if (options.auto) {
|
|
1134
|
+
// Auto-resolve: take first reviewer's change
|
|
1135
|
+
console.log(chalk.dim(` Auto-resolved: using ${conflict.changes[0].reviewer}'s change`));
|
|
1136
|
+
resolvedMerged = resolveConflict(resolvedMerged, conflict, 0, originalText);
|
|
1137
|
+
} else if (!options.dryRun) {
|
|
1138
|
+
// Interactive resolution
|
|
1139
|
+
const rl = await import('readline');
|
|
1140
|
+
const readline = rl.createInterface({
|
|
1141
|
+
input: process.stdin,
|
|
1142
|
+
output: process.stdout,
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
const answer = await new Promise((resolve) =>
|
|
1146
|
+
readline.question(chalk.cyan(` Choose (1-${conflict.changes.length}, s=skip): `), resolve)
|
|
1147
|
+
);
|
|
1148
|
+
readline.close();
|
|
1149
|
+
|
|
1150
|
+
if (answer.toLowerCase() !== 's' && !isNaN(parseInt(answer))) {
|
|
1151
|
+
const choice = parseInt(answer) - 1;
|
|
1152
|
+
if (choice >= 0 && choice < conflict.changes.length) {
|
|
1153
|
+
resolvedMerged = resolveConflict(resolvedMerged, conflict, choice, originalText);
|
|
1154
|
+
console.log(chalk.green(` Applied: ${conflict.changes[choice].reviewer}'s change`));
|
|
1155
|
+
}
|
|
1156
|
+
} else {
|
|
1157
|
+
console.log(chalk.dim(' Skipped'));
|
|
1158
|
+
}
|
|
1159
|
+
console.log();
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if (!options.dryRun) {
|
|
1164
|
+
const outPath = options.output || original.replace(/\.md$/, '-merged.md');
|
|
1165
|
+
fs.writeFileSync(outPath, resolvedMerged, 'utf-8');
|
|
1166
|
+
console.log(fmt.status('success', `Merged output written to ${outPath}`));
|
|
1167
|
+
}
|
|
1168
|
+
} else {
|
|
1169
|
+
// No conflicts
|
|
1170
|
+
if (!options.dryRun) {
|
|
1171
|
+
const outPath = options.output || original.replace(/\.md$/, '-merged.md');
|
|
1172
|
+
fs.writeFileSync(outPath, merged, 'utf-8');
|
|
1173
|
+
console.log(fmt.status('success', `Merged output written to ${outPath}`));
|
|
1174
|
+
} else {
|
|
1175
|
+
console.log(fmt.status('info', 'Dry run - no output written'));
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (!options.dryRun && stats.nonConflicting > 0) {
|
|
1180
|
+
console.log();
|
|
1181
|
+
console.log(chalk.dim('Next steps:'));
|
|
1182
|
+
console.log(chalk.dim(' 1. rev review <merged.md> - Review all changes'));
|
|
1183
|
+
console.log(chalk.dim(' 2. rev comments <merged.md> - Address comments'));
|
|
1184
|
+
}
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
spin.stop();
|
|
1187
|
+
console.error(fmt.status('error', err.message));
|
|
1188
|
+
if (process.env.DEBUG) console.error(err.stack);
|
|
1189
|
+
process.exit(1);
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1051
1193
|
// ============================================================================
|
|
1052
1194
|
// REFS command - Show figure/table reference status
|
|
1053
1195
|
// ============================================================================
|
|
@@ -1381,6 +1523,7 @@ program
|
|
|
1381
1523
|
.option('-d, --dir <directory>', 'Project directory', '.')
|
|
1382
1524
|
.option('--no-crossref', 'Skip pandoc-crossref filter')
|
|
1383
1525
|
.option('--toc', 'Include table of contents')
|
|
1526
|
+
.option('--show-changes', 'Export DOCX with visible track changes (audit mode)')
|
|
1384
1527
|
.action(async (formats, options) => {
|
|
1385
1528
|
const dir = path.resolve(options.dir);
|
|
1386
1529
|
|
|
@@ -1414,6 +1557,7 @@ program
|
|
|
1414
1557
|
console.log(chalk.dim(` Formats: ${targetFormats.join(', ')}`));
|
|
1415
1558
|
console.log(chalk.dim(` Crossref: ${hasPandocCrossref() && options.crossref !== false ? 'enabled' : 'disabled'}`));
|
|
1416
1559
|
if (tocEnabled) console.log(chalk.dim(` TOC: enabled`));
|
|
1560
|
+
if (options.showChanges) console.log(chalk.dim(` Track changes: visible`));
|
|
1417
1561
|
console.log('');
|
|
1418
1562
|
|
|
1419
1563
|
// Override config with CLI options
|
|
@@ -1422,6 +1566,57 @@ program
|
|
|
1422
1566
|
config.docx.toc = true;
|
|
1423
1567
|
}
|
|
1424
1568
|
|
|
1569
|
+
// Handle --show-changes mode (audit export)
|
|
1570
|
+
if (options.showChanges) {
|
|
1571
|
+
if (!targetFormats.includes('docx') && !targetFormats.includes('all')) {
|
|
1572
|
+
console.error(fmt.status('error', '--show-changes only applies to DOCX output'));
|
|
1573
|
+
process.exit(1);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
const { combineSections } = await import('../lib/build.js');
|
|
1577
|
+
const { buildWithTrackChanges } = await import('../lib/trackchanges.js');
|
|
1578
|
+
|
|
1579
|
+
const spin = fmt.spinner('Building with track changes...').start();
|
|
1580
|
+
|
|
1581
|
+
try {
|
|
1582
|
+
// Combine sections first
|
|
1583
|
+
const paperPath = combineSections(dir, config);
|
|
1584
|
+
spin.stop();
|
|
1585
|
+
console.log(chalk.cyan('Combined sections → paper.md'));
|
|
1586
|
+
console.log(chalk.dim(` ${paperPath}\n`));
|
|
1587
|
+
|
|
1588
|
+
// Build DOCX with track changes
|
|
1589
|
+
const baseName = config.title
|
|
1590
|
+
? config.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50)
|
|
1591
|
+
: 'paper';
|
|
1592
|
+
const outputPath = path.join(dir, `${baseName}-changes.docx`);
|
|
1593
|
+
|
|
1594
|
+
const spinTc = fmt.spinner('Applying track changes...').start();
|
|
1595
|
+
const result = await buildWithTrackChanges(paperPath, outputPath, {
|
|
1596
|
+
author: getUserName() || 'Author',
|
|
1597
|
+
});
|
|
1598
|
+
spinTc.stop();
|
|
1599
|
+
|
|
1600
|
+
if (result.success) {
|
|
1601
|
+
console.log(chalk.cyan('Output (with track changes):'));
|
|
1602
|
+
console.log(` DOCX: ${path.basename(outputPath)}`);
|
|
1603
|
+
if (result.stats) {
|
|
1604
|
+
console.log(chalk.dim(` ${result.stats.insertions} insertions, ${result.stats.deletions} deletions, ${result.stats.substitutions} substitutions`));
|
|
1605
|
+
}
|
|
1606
|
+
console.log(chalk.green('\nBuild complete!'));
|
|
1607
|
+
} else {
|
|
1608
|
+
console.error(fmt.status('error', result.message));
|
|
1609
|
+
process.exit(1);
|
|
1610
|
+
}
|
|
1611
|
+
} catch (err) {
|
|
1612
|
+
spin.stop();
|
|
1613
|
+
console.error(fmt.status('error', err.message));
|
|
1614
|
+
if (process.env.DEBUG) console.error(err.stack);
|
|
1615
|
+
process.exit(1);
|
|
1616
|
+
}
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1425
1620
|
const spin = fmt.spinner('Building...').start();
|
|
1426
1621
|
|
|
1427
1622
|
try {
|
|
@@ -1726,6 +1921,103 @@ program
|
|
|
1726
1921
|
console.log(fmt.status('success', `Created ${outputPath}`));
|
|
1727
1922
|
});
|
|
1728
1923
|
|
|
1924
|
+
// ============================================================================
|
|
1925
|
+
// VALIDATE command - Check manuscript against journal requirements
|
|
1926
|
+
// ============================================================================
|
|
1927
|
+
|
|
1928
|
+
program
|
|
1929
|
+
.command('validate')
|
|
1930
|
+
.description('Validate manuscript against journal requirements')
|
|
1931
|
+
.argument('[files...]', 'Markdown files to validate (default: all section files)')
|
|
1932
|
+
.option('-j, --journal <name>', 'Journal profile (e.g., nature, plos-one, science)')
|
|
1933
|
+
.option('--list', 'List available journal profiles')
|
|
1934
|
+
.action(async (files, options) => {
|
|
1935
|
+
const { listJournals, validateProject, getJournalProfile } = await import('../lib/journals.js');
|
|
1936
|
+
|
|
1937
|
+
if (options.list) {
|
|
1938
|
+
console.log(fmt.header('Available Journal Profiles'));
|
|
1939
|
+
console.log();
|
|
1940
|
+
const journals = listJournals();
|
|
1941
|
+
for (const j of journals) {
|
|
1942
|
+
console.log(` ${chalk.bold(j.id)} - ${j.name}`);
|
|
1943
|
+
console.log(chalk.dim(` ${j.url}`));
|
|
1944
|
+
}
|
|
1945
|
+
console.log();
|
|
1946
|
+
console.log(chalk.dim('Usage: rev validate --journal <name>'));
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
if (!options.journal) {
|
|
1951
|
+
console.error(fmt.status('error', 'Please specify a journal with --journal <name>'));
|
|
1952
|
+
console.error(chalk.dim('Use --list to see available profiles'));
|
|
1953
|
+
process.exit(1);
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
const profile = getJournalProfile(options.journal);
|
|
1957
|
+
if (!profile) {
|
|
1958
|
+
console.error(fmt.status('error', `Unknown journal: ${options.journal}`));
|
|
1959
|
+
console.error(chalk.dim('Use --list to see available profiles'));
|
|
1960
|
+
process.exit(1);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// Find files to validate
|
|
1964
|
+
let mdFiles = files;
|
|
1965
|
+
if (!mdFiles || mdFiles.length === 0) {
|
|
1966
|
+
mdFiles = fs.readdirSync('.').filter(f =>
|
|
1967
|
+
f.endsWith('.md') && !['README.md', 'CLAUDE.md', 'paper.md'].includes(f)
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
if (mdFiles.length === 0) {
|
|
1972
|
+
console.error(fmt.status('error', 'No markdown files found'));
|
|
1973
|
+
process.exit(1);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
console.log(fmt.header(`Validating for ${profile.name}`));
|
|
1977
|
+
console.log(chalk.dim(` ${profile.url}`));
|
|
1978
|
+
console.log();
|
|
1979
|
+
|
|
1980
|
+
const result = validateProject(mdFiles, options.journal);
|
|
1981
|
+
|
|
1982
|
+
// Show stats
|
|
1983
|
+
console.log(chalk.cyan('Manuscript Stats:'));
|
|
1984
|
+
console.log(fmt.table(['Metric', 'Value'], [
|
|
1985
|
+
['Word count', result.stats.wordCount.toString()],
|
|
1986
|
+
['Abstract', `${result.stats.abstractWords} words`],
|
|
1987
|
+
['Title', `${result.stats.titleChars} chars`],
|
|
1988
|
+
['Figures', result.stats.figures.toString()],
|
|
1989
|
+
['Tables', result.stats.tables.toString()],
|
|
1990
|
+
['References', result.stats.references.toString()],
|
|
1991
|
+
]));
|
|
1992
|
+
console.log();
|
|
1993
|
+
|
|
1994
|
+
// Show errors
|
|
1995
|
+
if (result.errors.length > 0) {
|
|
1996
|
+
console.log(chalk.red('Errors:'));
|
|
1997
|
+
for (const err of result.errors) {
|
|
1998
|
+
console.log(chalk.red(` ✗ ${err}`));
|
|
1999
|
+
}
|
|
2000
|
+
console.log();
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// Show warnings
|
|
2004
|
+
if (result.warnings.length > 0) {
|
|
2005
|
+
console.log(chalk.yellow('Warnings:'));
|
|
2006
|
+
for (const warn of result.warnings) {
|
|
2007
|
+
console.log(chalk.yellow(` ⚠ ${warn}`));
|
|
2008
|
+
}
|
|
2009
|
+
console.log();
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// Summary
|
|
2013
|
+
if (result.valid) {
|
|
2014
|
+
console.log(fmt.status('success', `Manuscript meets ${profile.name} requirements`));
|
|
2015
|
+
} else {
|
|
2016
|
+
console.log(fmt.status('error', `Manuscript has ${result.errors.length} error(s)`));
|
|
2017
|
+
process.exit(1);
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
|
|
1729
2021
|
// ============================================================================
|
|
1730
2022
|
// ANONYMIZE command - Prepare document for blind review
|
|
1731
2023
|
// ============================================================================
|
|
@@ -2497,6 +2789,188 @@ program
|
|
|
2497
2789
|
}
|
|
2498
2790
|
});
|
|
2499
2791
|
|
|
2792
|
+
// ============================================================================
|
|
2793
|
+
// DIFF command - Compare sections against git history
|
|
2794
|
+
// ============================================================================
|
|
2795
|
+
|
|
2796
|
+
program
|
|
2797
|
+
.command('diff')
|
|
2798
|
+
.description('Compare sections against git history')
|
|
2799
|
+
.argument('[ref]', 'Git reference to compare against (default: main/master)')
|
|
2800
|
+
.option('-f, --files <files>', 'Specific files to compare (comma-separated)')
|
|
2801
|
+
.option('--stat', 'Show only statistics, not full diff')
|
|
2802
|
+
.action(async (ref, options) => {
|
|
2803
|
+
const {
|
|
2804
|
+
isGitRepo,
|
|
2805
|
+
getDefaultBranch,
|
|
2806
|
+
getCurrentBranch,
|
|
2807
|
+
getChangedFiles,
|
|
2808
|
+
getWordCountDiff,
|
|
2809
|
+
compareFileVersions,
|
|
2810
|
+
} = await import('../lib/git.js');
|
|
2811
|
+
|
|
2812
|
+
if (!isGitRepo()) {
|
|
2813
|
+
console.error(fmt.status('error', 'Not a git repository'));
|
|
2814
|
+
process.exit(1);
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
const compareRef = ref || getDefaultBranch();
|
|
2818
|
+
const currentBranch = getCurrentBranch();
|
|
2819
|
+
|
|
2820
|
+
console.log(fmt.header('Git Diff'));
|
|
2821
|
+
console.log(chalk.dim(` Comparing: ${compareRef} → ${currentBranch || 'HEAD'}`));
|
|
2822
|
+
console.log();
|
|
2823
|
+
|
|
2824
|
+
// Get files to compare
|
|
2825
|
+
let filesToCompare;
|
|
2826
|
+
if (options.files) {
|
|
2827
|
+
filesToCompare = options.files.split(',').map(f => f.trim());
|
|
2828
|
+
} else {
|
|
2829
|
+
// Default to markdown section files
|
|
2830
|
+
filesToCompare = fs.readdirSync('.').filter(f =>
|
|
2831
|
+
f.endsWith('.md') && !['README.md', 'CLAUDE.md'].includes(f)
|
|
2832
|
+
);
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
if (filesToCompare.length === 0) {
|
|
2836
|
+
console.log(fmt.status('info', 'No markdown files found'));
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
// Get changed files from git
|
|
2841
|
+
const changedFiles = getChangedFiles(compareRef);
|
|
2842
|
+
const changedSet = new Set(changedFiles.map(f => f.file));
|
|
2843
|
+
|
|
2844
|
+
// Get word count differences
|
|
2845
|
+
const { total, byFile } = getWordCountDiff(filesToCompare, compareRef);
|
|
2846
|
+
|
|
2847
|
+
// Show results
|
|
2848
|
+
const rows = [];
|
|
2849
|
+
for (const file of filesToCompare) {
|
|
2850
|
+
const stats = byFile[file];
|
|
2851
|
+
if (stats && (stats.added > 0 || stats.removed > 0)) {
|
|
2852
|
+
const status = changedSet.has(file)
|
|
2853
|
+
? changedFiles.find(f => f.file === file)?.status || 'modified'
|
|
2854
|
+
: 'unchanged';
|
|
2855
|
+
rows.push([
|
|
2856
|
+
file,
|
|
2857
|
+
status,
|
|
2858
|
+
chalk.green(`+${stats.added}`),
|
|
2859
|
+
chalk.red(`-${stats.removed}`),
|
|
2860
|
+
]);
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
if (rows.length === 0) {
|
|
2865
|
+
console.log(fmt.status('success', 'No changes detected'));
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
console.log(fmt.table(['File', 'Status', 'Added', 'Removed'], rows));
|
|
2870
|
+
console.log();
|
|
2871
|
+
console.log(chalk.dim(`Total: ${chalk.green(`+${total.added}`)} words, ${chalk.red(`-${total.removed}`)} words`));
|
|
2872
|
+
|
|
2873
|
+
// Show detailed diff if not --stat
|
|
2874
|
+
if (!options.stat && rows.length > 0) {
|
|
2875
|
+
console.log();
|
|
2876
|
+
console.log(chalk.cyan('Changed sections:'));
|
|
2877
|
+
for (const file of filesToCompare) {
|
|
2878
|
+
const stats = byFile[file];
|
|
2879
|
+
if (stats && (stats.added > 0 || stats.removed > 0)) {
|
|
2880
|
+
const { changes } = compareFileVersions(file, compareRef);
|
|
2881
|
+
console.log(chalk.bold(`\n ${file}:`));
|
|
2882
|
+
|
|
2883
|
+
// Show first few significant changes
|
|
2884
|
+
let shown = 0;
|
|
2885
|
+
for (const change of changes) {
|
|
2886
|
+
if (shown >= 3) {
|
|
2887
|
+
console.log(chalk.dim(' ...'));
|
|
2888
|
+
break;
|
|
2889
|
+
}
|
|
2890
|
+
const preview = change.text.slice(0, 60).replace(/\n/g, ' ');
|
|
2891
|
+
if (change.type === 'add') {
|
|
2892
|
+
console.log(chalk.green(` + "${preview}..."`));
|
|
2893
|
+
} else {
|
|
2894
|
+
console.log(chalk.red(` - "${preview}..."`));
|
|
2895
|
+
}
|
|
2896
|
+
shown++;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
});
|
|
2902
|
+
|
|
2903
|
+
// ============================================================================
|
|
2904
|
+
// HISTORY command - Show revision history
|
|
2905
|
+
// ============================================================================
|
|
2906
|
+
|
|
2907
|
+
program
|
|
2908
|
+
.command('history')
|
|
2909
|
+
.description('Show revision history for section files')
|
|
2910
|
+
.argument('[file]', 'Specific file (default: all sections)')
|
|
2911
|
+
.option('-n, --limit <count>', 'Number of commits to show', '10')
|
|
2912
|
+
.action(async (file, options) => {
|
|
2913
|
+
const {
|
|
2914
|
+
isGitRepo,
|
|
2915
|
+
getFileHistory,
|
|
2916
|
+
getRecentCommits,
|
|
2917
|
+
hasUncommittedChanges,
|
|
2918
|
+
} = await import('../lib/git.js');
|
|
2919
|
+
|
|
2920
|
+
if (!isGitRepo()) {
|
|
2921
|
+
console.error(fmt.status('error', 'Not a git repository'));
|
|
2922
|
+
process.exit(1);
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
const limit = parseInt(options.limit) || 10;
|
|
2926
|
+
|
|
2927
|
+
console.log(fmt.header('Revision History'));
|
|
2928
|
+
console.log();
|
|
2929
|
+
|
|
2930
|
+
if (file) {
|
|
2931
|
+
// Show history for specific file
|
|
2932
|
+
if (!fs.existsSync(file)) {
|
|
2933
|
+
console.error(fmt.status('error', `File not found: ${file}`));
|
|
2934
|
+
process.exit(1);
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
const history = getFileHistory(file, limit);
|
|
2938
|
+
|
|
2939
|
+
if (history.length === 0) {
|
|
2940
|
+
console.log(fmt.status('info', 'No history found (file may not be committed)'));
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
console.log(chalk.cyan(`History for ${file}:`));
|
|
2945
|
+
console.log();
|
|
2946
|
+
|
|
2947
|
+
for (const commit of history) {
|
|
2948
|
+
const date = new Date(commit.date).toLocaleDateString();
|
|
2949
|
+
console.log(` ${chalk.yellow(commit.hash)} ${chalk.dim(date)}`);
|
|
2950
|
+
console.log(` ${commit.message}`);
|
|
2951
|
+
}
|
|
2952
|
+
} else {
|
|
2953
|
+
// Show recent commits affecting any file
|
|
2954
|
+
const commits = getRecentCommits(limit);
|
|
2955
|
+
|
|
2956
|
+
if (commits.length === 0) {
|
|
2957
|
+
console.log(fmt.status('info', 'No commits found'));
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
if (hasUncommittedChanges()) {
|
|
2962
|
+
console.log(chalk.yellow(' * Uncommitted changes'));
|
|
2963
|
+
console.log();
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
for (const commit of commits) {
|
|
2967
|
+
const date = new Date(commit.date).toLocaleDateString();
|
|
2968
|
+
console.log(` ${chalk.yellow(commit.hash)} ${chalk.dim(date)} ${chalk.blue(commit.author)}`);
|
|
2969
|
+
console.log(` ${commit.message}`);
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
});
|
|
2973
|
+
|
|
2500
2974
|
// ============================================================================
|
|
2501
2975
|
// HELP command - Comprehensive help
|
|
2502
2976
|
// ============================================================================
|