docrev 0.11.2 → 0.12.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 (205) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +21 -0
  3. package/dist/bin/rev.js +29 -21
  4. package/dist/bin/rev.js.map +1 -1
  5. package/dist/lib/annotations.d.ts.map +1 -1
  6. package/dist/lib/annotations.js +28 -0
  7. package/dist/lib/annotations.js.map +1 -1
  8. package/dist/lib/build.d.ts +43 -0
  9. package/dist/lib/build.d.ts.map +1 -1
  10. package/dist/lib/build.js +175 -5
  11. package/dist/lib/build.js.map +1 -1
  12. package/dist/lib/commands/build.d.ts.map +1 -1
  13. package/dist/lib/commands/build.js +39 -125
  14. package/dist/lib/commands/build.js.map +1 -1
  15. package/dist/lib/commands/comments.d.ts.map +1 -1
  16. package/dist/lib/commands/comments.js +61 -31
  17. package/dist/lib/commands/comments.js.map +1 -1
  18. package/dist/lib/commands/context.d.ts +12 -0
  19. package/dist/lib/commands/context.d.ts.map +1 -1
  20. package/dist/lib/commands/context.js +34 -0
  21. package/dist/lib/commands/context.js.map +1 -1
  22. package/dist/lib/commands/core.d.ts.map +1 -1
  23. package/dist/lib/commands/core.js +25 -8
  24. package/dist/lib/commands/core.js.map +1 -1
  25. package/dist/lib/commands/merge-resolve.js +1 -1
  26. package/dist/lib/commands/merge-resolve.js.map +1 -1
  27. package/dist/lib/commands/quality.js +2 -2
  28. package/dist/lib/commands/quality.js.map +1 -1
  29. package/dist/lib/commands/response.js +1 -1
  30. package/dist/lib/commands/response.js.map +1 -1
  31. package/dist/lib/commands/sections.js +1 -1
  32. package/dist/lib/commands/sections.js.map +1 -1
  33. package/dist/lib/commands/text-ops.js +1 -1
  34. package/dist/lib/commands/text-ops.js.map +1 -1
  35. package/dist/lib/commands/utilities.d.ts.map +1 -1
  36. package/dist/lib/commands/utilities.js +5 -2
  37. package/dist/lib/commands/utilities.js.map +1 -1
  38. package/dist/lib/commands/word-tools.js +2 -2
  39. package/dist/lib/commands/word-tools.js.map +1 -1
  40. package/dist/lib/crossref.d.ts.map +1 -1
  41. package/dist/lib/crossref.js +12 -41
  42. package/dist/lib/crossref.js.map +1 -1
  43. package/dist/lib/csl.d.ts.map +1 -1
  44. package/dist/lib/csl.js +14 -22
  45. package/dist/lib/csl.js.map +1 -1
  46. package/dist/lib/diff-engine.js +1 -1
  47. package/dist/lib/diff-engine.js.map +1 -1
  48. package/dist/lib/equations.d.ts.map +1 -1
  49. package/dist/lib/equations.js +12 -3
  50. package/dist/lib/equations.js.map +1 -1
  51. package/dist/lib/errors.d.ts.map +1 -1
  52. package/dist/lib/errors.js +1 -30
  53. package/dist/lib/errors.js.map +1 -1
  54. package/dist/lib/image-registry.d.ts.map +1 -1
  55. package/dist/lib/image-registry.js +4 -0
  56. package/dist/lib/image-registry.js.map +1 -1
  57. package/dist/lib/import.d.ts +15 -1
  58. package/dist/lib/import.d.ts.map +1 -1
  59. package/dist/lib/import.js +24 -1
  60. package/dist/lib/import.js.map +1 -1
  61. package/dist/lib/input.d.ts +56 -0
  62. package/dist/lib/input.d.ts.map +1 -0
  63. package/dist/lib/input.js +129 -0
  64. package/dist/lib/input.js.map +1 -0
  65. package/dist/lib/merge.d.ts.map +1 -1
  66. package/dist/lib/merge.js +9 -1
  67. package/dist/lib/merge.js.map +1 -1
  68. package/dist/lib/ooxml.d.ts +22 -0
  69. package/dist/lib/ooxml.d.ts.map +1 -1
  70. package/dist/lib/ooxml.js +130 -0
  71. package/dist/lib/ooxml.js.map +1 -1
  72. package/dist/lib/orcid.d.ts.map +1 -1
  73. package/dist/lib/orcid.js +16 -7
  74. package/dist/lib/orcid.js.map +1 -1
  75. package/dist/lib/plugins.d.ts.map +1 -1
  76. package/dist/lib/plugins.js +8 -4
  77. package/dist/lib/plugins.js.map +1 -1
  78. package/dist/lib/protect-restore.d.ts.map +1 -1
  79. package/dist/lib/protect-restore.js +7 -5
  80. package/dist/lib/protect-restore.js.map +1 -1
  81. package/dist/lib/response.d.ts +1 -1
  82. package/dist/lib/response.d.ts.map +1 -1
  83. package/dist/lib/response.js +5 -2
  84. package/dist/lib/response.js.map +1 -1
  85. package/dist/lib/restore-references.d.ts.map +1 -1
  86. package/dist/lib/restore-references.js +25 -3
  87. package/dist/lib/restore-references.js.map +1 -1
  88. package/dist/lib/schema.d.ts.map +1 -1
  89. package/dist/lib/schema.js +1 -19
  90. package/dist/lib/schema.js.map +1 -1
  91. package/dist/lib/slides.js +1 -1
  92. package/dist/lib/slides.js.map +1 -1
  93. package/dist/lib/spelling.js +1 -1
  94. package/dist/lib/spelling.js.map +1 -1
  95. package/dist/lib/tui.js +0 -1
  96. package/dist/lib/tui.js.map +1 -1
  97. package/dist/lib/utils.d.ts +14 -0
  98. package/dist/lib/utils.d.ts.map +1 -1
  99. package/dist/lib/utils.js +41 -0
  100. package/dist/lib/utils.js.map +1 -1
  101. package/dist/lib/word-extraction.d.ts +5 -5
  102. package/dist/lib/word-extraction.d.ts.map +1 -1
  103. package/dist/lib/word-extraction.js +25 -106
  104. package/dist/lib/word-extraction.js.map +1 -1
  105. package/dist/lib/word.d.ts +14 -38
  106. package/dist/lib/word.d.ts.map +1 -1
  107. package/dist/lib/word.js +24 -179
  108. package/dist/lib/word.js.map +1 -1
  109. package/dist/lib/wordcomments.d.ts.map +1 -1
  110. package/dist/lib/wordcomments.js +9 -7
  111. package/dist/lib/wordcomments.js.map +1 -1
  112. package/package.json +17 -6
  113. package/.gitattributes +0 -1
  114. package/PLAN-tables-and-postprocess.md +0 -850
  115. package/bin/rev.ts +0 -145
  116. package/docs-src/build.py +0 -113
  117. package/docs-src/extra.css +0 -208
  118. package/docs-src/md-to-html.lua +0 -6
  119. package/docs-src/template.html +0 -116
  120. package/eslint.config.js +0 -27
  121. package/lib/anchor-match.ts +0 -307
  122. package/lib/annotations.ts +0 -664
  123. package/lib/build.ts +0 -2047
  124. package/lib/citations.ts +0 -160
  125. package/lib/commands/build.ts +0 -885
  126. package/lib/commands/citations.ts +0 -515
  127. package/lib/commands/comments.ts +0 -1050
  128. package/lib/commands/context.ts +0 -176
  129. package/lib/commands/core.ts +0 -309
  130. package/lib/commands/doi.ts +0 -451
  131. package/lib/commands/file-ops.ts +0 -372
  132. package/lib/commands/history.ts +0 -320
  133. package/lib/commands/index.ts +0 -87
  134. package/lib/commands/init.ts +0 -259
  135. package/lib/commands/merge-resolve.ts +0 -378
  136. package/lib/commands/preview.ts +0 -178
  137. package/lib/commands/project-info.ts +0 -244
  138. package/lib/commands/quality.ts +0 -517
  139. package/lib/commands/response.ts +0 -454
  140. package/lib/commands/section-boundaries.ts +0 -82
  141. package/lib/commands/sections.ts +0 -451
  142. package/lib/commands/sync.ts +0 -689
  143. package/lib/commands/text-ops.ts +0 -449
  144. package/lib/commands/utilities.ts +0 -448
  145. package/lib/commands/verify-anchors.ts +0 -272
  146. package/lib/commands/word-tools.ts +0 -340
  147. package/lib/comment-realign.ts +0 -99
  148. package/lib/config.ts +0 -84
  149. package/lib/crossref.ts +0 -781
  150. package/lib/csl.ts +0 -191
  151. package/lib/dependencies.ts +0 -132
  152. package/lib/diff-engine.ts +0 -465
  153. package/lib/doi-cache.ts +0 -115
  154. package/lib/doi.ts +0 -879
  155. package/lib/equations.ts +0 -506
  156. package/lib/errors.ts +0 -350
  157. package/lib/format.ts +0 -541
  158. package/lib/git.ts +0 -326
  159. package/lib/grammar.ts +0 -303
  160. package/lib/image-registry.ts +0 -180
  161. package/lib/import.ts +0 -906
  162. package/lib/journals.ts +0 -543
  163. package/lib/macro-filter.lua +0 -201
  164. package/lib/macros.ts +0 -273
  165. package/lib/merge.ts +0 -633
  166. package/lib/ooxml.ts +0 -768
  167. package/lib/orcid.ts +0 -144
  168. package/lib/pdf-comments.ts +0 -263
  169. package/lib/pdf-import.ts +0 -524
  170. package/lib/plugins.ts +0 -362
  171. package/lib/postprocess.ts +0 -188
  172. package/lib/pptx-color-filter.lua +0 -37
  173. package/lib/pptx-template.ts +0 -469
  174. package/lib/pptx-themes/academic.pptx +0 -0
  175. package/lib/pptx-themes/corporate.pptx +0 -0
  176. package/lib/pptx-themes/dark.pptx +0 -0
  177. package/lib/pptx-themes/default.pptx +0 -0
  178. package/lib/pptx-themes/minimal.pptx +0 -0
  179. package/lib/pptx-themes/plant.pptx +0 -0
  180. package/lib/pptx-themes.ts +0 -483
  181. package/lib/protect-restore.ts +0 -520
  182. package/lib/rate-limiter.ts +0 -127
  183. package/lib/response.ts +0 -197
  184. package/lib/restore-references.ts +0 -240
  185. package/lib/review.ts +0 -327
  186. package/lib/schema.ts +0 -494
  187. package/lib/scientific-words.ts +0 -73
  188. package/lib/sections.ts +0 -428
  189. package/lib/slides.ts +0 -756
  190. package/lib/spelling.ts +0 -334
  191. package/lib/templates.ts +0 -526
  192. package/lib/themes.ts +0 -742
  193. package/lib/trackchanges.ts +0 -166
  194. package/lib/tui.ts +0 -450
  195. package/lib/types.ts +0 -546
  196. package/lib/undo.ts +0 -250
  197. package/lib/utils.ts +0 -69
  198. package/lib/variables.ts +0 -179
  199. package/lib/word-extraction.ts +0 -525
  200. package/lib/word.ts +0 -526
  201. package/lib/wordcomments.ts +0 -789
  202. package/mkdocs.yml +0 -64
  203. package/scripts/postbuild.js +0 -47
  204. package/tsconfig.json +0 -26
  205. package/types/index.d.ts +0 -531
@@ -1,372 +0,0 @@
1
- /**
2
- * File operation commands: backup, archive, export, clean
3
- *
4
- * Commands that create, move, or delete project files.
5
- */
6
-
7
- import type { Command } from 'commander';
8
- import {
9
- chalk,
10
- fs,
11
- path,
12
- fmt,
13
- findFiles,
14
- loadBuildConfig,
15
- } from './context.js';
16
-
17
- // Use the actual BuildConfig from build.ts which allows string|Author[]
18
- type BuildConfig = ReturnType<typeof loadBuildConfig>;
19
-
20
- interface ZipLike {
21
- addLocalFile(localPath: string, zipPath?: string): void;
22
- }
23
-
24
- /**
25
- * Recursively add directory contents to a zip archive, filtering by predicate
26
- */
27
- function addDirToZip(
28
- zip: ZipLike,
29
- dir: string,
30
- shouldInclude: (name: string) => boolean,
31
- zipPath = '',
32
- ): void {
33
- const entries = fs.readdirSync(dir, { withFileTypes: true });
34
- for (const entry of entries) {
35
- const fullPath = path.join(dir, entry.name);
36
- const entryZipPath = path.join(zipPath, entry.name);
37
-
38
- if (!shouldInclude(entry.name)) continue;
39
-
40
- if (entry.isDirectory()) {
41
- addDirToZip(zip, fullPath, shouldInclude, entryZipPath);
42
- } else {
43
- zip.addLocalFile(fullPath, zipPath || undefined);
44
- }
45
- }
46
- }
47
-
48
- // Options interfaces
49
- interface BackupOptions {
50
- name?: string;
51
- output?: string;
52
- }
53
-
54
- interface ArchiveOptions {
55
- dir?: string;
56
- by?: string;
57
- rename?: boolean;
58
- dryRun?: boolean;
59
- }
60
-
61
- interface ExportOptions {
62
- output?: string;
63
- includeOutput?: boolean;
64
- }
65
-
66
- interface CleanOptions {
67
- dryRun?: boolean;
68
- all?: boolean;
69
- }
70
-
71
- /**
72
- * Register file-ops commands with the program
73
- */
74
- export function register(program: Command): void {
75
- // ==========================================================================
76
- // BACKUP command - Timestamped project backup
77
- // ==========================================================================
78
-
79
- program
80
- .command('backup')
81
- .description('Create timestamped project backup')
82
- .option('-n, --name <name>', 'Custom backup name')
83
- .option('-o, --output <dir>', 'Output directory', '.')
84
- .action(async (options: BackupOptions) => {
85
- const { default: AdmZip } = await import('adm-zip');
86
- const zip = new AdmZip();
87
-
88
- const date = new Date().toISOString().slice(0, 10);
89
- const name = options.name || `backup-${date}`;
90
- const outputPath = path.join(options.output || '.', `${name}.zip`);
91
-
92
- // Files to exclude
93
- const excludePatterns = [
94
- 'node_modules', '.git', '.DS_Store', '*.zip',
95
- 'paper.md' // Generated file
96
- ];
97
-
98
- const shouldInclude = (file: string): boolean => {
99
- for (const pattern of excludePatterns) {
100
- if (file.includes(pattern.replace('*', ''))) return false;
101
- }
102
- return true;
103
- };
104
-
105
- addDirToZip(zip, '.', shouldInclude);
106
-
107
- zip.writeZip(outputPath);
108
- console.log(fmt.status('success', `Backup created: ${outputPath}`));
109
- });
110
-
111
- // ==========================================================================
112
- // ARCHIVE command - Archive reviewer docx files
113
- // ==========================================================================
114
-
115
- program
116
- .command('archive')
117
- .description('Move reviewer .docx files to archive folder')
118
- .argument('[files...]', 'Specific files to archive (default: all .docx)')
119
- .option('-d, --dir <folder>', 'Archive folder name', 'archive')
120
- .option('--by <name>', 'Reviewer name (auto-detected if single commenter)')
121
- .option('--no-rename', 'Keep original filenames')
122
- .option('--dry-run', 'Preview without moving files')
123
- .action(async (files: string[] | undefined, options: ArchiveOptions) => {
124
- const { extractWordComments } = await import('../import.js');
125
- const { default: YAML } = await import('yaml');
126
-
127
- // Find docx files to archive
128
- let docxFiles = files && files.length > 0
129
- ? files.filter(f => f.endsWith('.docx') && fs.existsSync(f))
130
- : findFiles('.docx');
131
-
132
- // Exclude our own build outputs
133
- let projectSlug: string | null = null;
134
- const configPath = path.join(process.cwd(), 'rev.yaml');
135
- if (fs.existsSync(configPath)) {
136
- try {
137
- const config = YAML.parse(fs.readFileSync(configPath, 'utf-8'));
138
- if (config.title) {
139
- projectSlug = config.title
140
- .toLowerCase()
141
- .replace(/[^a-z0-9]+/g, '-')
142
- .replace(/^-|-$/g, '')
143
- .slice(0, 50);
144
- }
145
- } catch (e) {
146
- // Ignore config errors
147
- }
148
- }
149
-
150
- // Filter out build outputs
151
- if (projectSlug && (!files || files.length === 0)) {
152
- const buildPatterns = [
153
- `${projectSlug}.docx`,
154
- `${projectSlug}_comments.docx`,
155
- `${projectSlug}-changes.docx`,
156
- 'paper.docx',
157
- 'paper_comments.docx',
158
- 'paper-changes.docx',
159
- ];
160
- const excluded: string[] = [];
161
- docxFiles = docxFiles.filter(f => {
162
- const base = path.basename(f).toLowerCase();
163
- const isBuilt = buildPatterns.includes(base);
164
- if (isBuilt) excluded.push(f);
165
- return !isBuilt;
166
- });
167
- if (excluded.length > 0) {
168
- console.log(chalk.dim(` Skipping build outputs: ${excluded.join(', ')}`));
169
- console.log();
170
- }
171
- }
172
-
173
- if (docxFiles.length === 0) {
174
- console.log(fmt.status('info', 'No .docx files to archive.'));
175
- return;
176
- }
177
-
178
- const projectTitle = projectSlug;
179
-
180
- // Create archive folder
181
- const archiveDir = path.resolve(options.dir || 'archive');
182
- if (!options.dryRun && !fs.existsSync(archiveDir)) {
183
- fs.mkdirSync(archiveDir, { recursive: true });
184
- }
185
-
186
- console.log(fmt.header('Archive'));
187
- console.log();
188
-
189
- const moved: string[] = [];
190
- for (const file of docxFiles) {
191
- const stat = fs.statSync(file);
192
- const mtime = stat.mtime;
193
- const timestamp = mtime.toISOString().slice(0, 19).replace(/[-:]/g, '').replace('T', '_');
194
-
195
- // Determine reviewer name
196
- let reviewer: string | null = options.by || null;
197
- if (!reviewer && options.rename !== false) {
198
- try {
199
- const comments = await extractWordComments(file);
200
- const authors = [...new Set(comments.map(c => c.author).filter(a => a && a !== 'Unknown'))];
201
- if (authors.length === 1) {
202
- reviewer = authors[0].replace(/[^a-zA-Z0-9]/g, '-').replace(/^-|-$/g, '');
203
- }
204
- } catch (e) {
205
- // Ignore extraction errors
206
- }
207
- }
208
-
209
- // Generate new name
210
- let newName: string;
211
- if (options.rename === false) {
212
- newName = path.basename(file);
213
- } else {
214
- const base = path.basename(file, '.docx');
215
- if (/^\d{8}_\d{6}_/.test(base)) {
216
- newName = path.basename(file);
217
- } else {
218
- const namePart = projectTitle || base;
219
- if (reviewer) {
220
- newName = `${timestamp}_${reviewer}_${namePart}.docx`;
221
- } else {
222
- newName = `${timestamp}_${namePart}.docx`;
223
- }
224
- }
225
- }
226
-
227
- const destPath = path.join(archiveDir, newName);
228
-
229
- if (options.dryRun) {
230
- console.log(` ${chalk.dim(file)} → ${chalk.cyan(path.join(options.dir || 'archive', newName))}`);
231
- } else {
232
- // Handle name collision
233
- let finalPath = destPath;
234
- let counter = 1;
235
- while (fs.existsSync(finalPath)) {
236
- const ext = path.extname(newName);
237
- const base = path.basename(newName, ext);
238
- finalPath = path.join(archiveDir, `${base}_${counter}${ext}`);
239
- counter++;
240
- }
241
- fs.renameSync(file, finalPath);
242
- console.log(` ${chalk.dim(file)} → ${chalk.green(path.relative(process.cwd(), finalPath))}`);
243
- }
244
- moved.push(file);
245
- }
246
-
247
- console.log();
248
- if (options.dryRun) {
249
- console.log(fmt.status('info', `Would archive ${moved.length} file(s). Run without --dry-run to proceed.`));
250
- } else {
251
- console.log(fmt.status('success', `Archived ${moved.length} file(s) to ${options.dir || 'archive'}/`));
252
- }
253
- });
254
-
255
- // ==========================================================================
256
- // EXPORT command - Export project as distributable zip
257
- // ==========================================================================
258
-
259
- program
260
- .command('export')
261
- .description('Export project as distributable zip')
262
- .option('-o, --output <file>', 'Output filename')
263
- .option('--include-output', 'Include built PDF/DOCX files')
264
- .action(async (options: ExportOptions) => {
265
- const { default: AdmZip } = await import('adm-zip');
266
- const { build } = await import('../build.js');
267
-
268
- let config: Partial<BuildConfig> = {};
269
- try {
270
- config = loadBuildConfig('.') || {};
271
- } catch {
272
- // Not in a rev project, that's ok
273
- }
274
-
275
- // Build first if including output
276
- if (options.includeOutput) {
277
- console.log(chalk.dim('Building documents...'));
278
- await build('.', ['pdf', 'docx']);
279
- }
280
-
281
- const zip = new AdmZip();
282
- const projectName = config.title?.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase() || 'project';
283
- const outputPath = options.output || `${projectName}-export.zip`;
284
-
285
- const exclude = ['node_modules', '.git', '.DS_Store', '*.zip'];
286
-
287
- const shouldInclude = (name: string): boolean => {
288
- if (!options.includeOutput && (name.endsWith('.pdf') || name.endsWith('.docx'))) {
289
- return false;
290
- }
291
- for (const pattern of exclude) {
292
- if (name === pattern || name.includes(pattern.replace('*', ''))) return false;
293
- }
294
- return true;
295
- };
296
-
297
- addDirToZip(zip, '.', shouldInclude);
298
-
299
- zip.writeZip(outputPath);
300
- console.log(fmt.status('success', `Exported: ${outputPath}`));
301
- });
302
-
303
- // ==========================================================================
304
- // CLEAN command - Remove generated files
305
- // ==========================================================================
306
-
307
- program
308
- .command('clean')
309
- .description('Remove generated files (paper.md, PDFs, DOCXs)')
310
- .option('-n, --dry-run', 'Show what would be deleted without deleting')
311
- .option('--all', 'Also remove backup and export zips')
312
- .action((options: CleanOptions) => {
313
- let config: Partial<BuildConfig> = {};
314
- try {
315
- config = loadBuildConfig('.') || {};
316
- } catch {
317
- // Not in a rev project, that's ok
318
- }
319
-
320
- const projectName = config.title?.toLowerCase().replace(/\s+/g, '-') || 'paper';
321
-
322
- // Files to clean
323
- const patterns = [
324
- 'paper.md',
325
- '*.pdf',
326
- `${projectName}.docx`,
327
- `${projectName}.pdf`,
328
- `${projectName}.tex`,
329
- '.paper-*.md', // Temp build files
330
- ];
331
-
332
- if (options.all) {
333
- patterns.push('*.zip', 'backup-*.zip', '*-export.zip');
334
- }
335
-
336
- const toDelete: string[] = [];
337
-
338
- for (const pattern of patterns) {
339
- if (pattern.includes('*')) {
340
- const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
341
- const files = fs.readdirSync('.').filter(f => regex.test(f));
342
- toDelete.push(...files);
343
- } else if (fs.existsSync(pattern)) {
344
- toDelete.push(pattern);
345
- }
346
- }
347
-
348
- if (toDelete.length === 0) {
349
- console.log(chalk.dim('No generated files to clean.'));
350
- return;
351
- }
352
-
353
- console.log(fmt.header('Clean'));
354
- console.log();
355
-
356
- for (const file of toDelete) {
357
- if (options.dryRun) {
358
- console.log(chalk.dim(` Would delete: ${file}`));
359
- } else {
360
- fs.unlinkSync(file);
361
- console.log(chalk.red(` Deleted: ${file}`));
362
- }
363
- }
364
-
365
- console.log();
366
- if (options.dryRun) {
367
- console.log(chalk.dim(`Would delete ${toDelete.length} file(s). Run without --dry-run to delete.`));
368
- } else {
369
- console.log(fmt.status('success', `Cleaned ${toDelete.length} file(s)`));
370
- }
371
- });
372
- }
@@ -1,320 +0,0 @@
1
- /**
2
- * History commands: diff, history, contributors
3
- *
4
- * Commands for git-based revision tracking and author statistics.
5
- */
6
-
7
- import type { Command } from 'commander';
8
- import {
9
- chalk,
10
- fs,
11
- path,
12
- fmt,
13
- loadBuildConfig,
14
- } from './context.js';
15
-
16
- interface DiffOptions {
17
- files?: string;
18
- stat?: boolean;
19
- }
20
-
21
- interface HistoryOptions {
22
- limit: string;
23
- }
24
-
25
- interface ContributorsOptions {
26
- blame?: boolean;
27
- }
28
-
29
- /**
30
- * Register history commands with the program
31
- */
32
- export function register(program: Command): void {
33
- // ==========================================================================
34
- // DIFF command - Compare sections against git history
35
- // ==========================================================================
36
-
37
- program
38
- .command('diff')
39
- .description('Compare sections against git history')
40
- .argument('[ref]', 'Git reference to compare against (default: main/master)')
41
- .option('-f, --files <files>', 'Specific files to compare (comma-separated)')
42
- .option('--stat', 'Show only statistics, not full diff')
43
- .action(async (ref: string | undefined, options: DiffOptions) => {
44
- const {
45
- isGitRepo,
46
- getDefaultBranch,
47
- getCurrentBranch,
48
- getChangedFiles,
49
- getWordCountDiff,
50
- compareFileVersions,
51
- } = await import('../git.js');
52
-
53
- if (!isGitRepo()) {
54
- console.error(fmt.status('error', 'Not a git repository'));
55
- process.exit(1);
56
- }
57
-
58
- const compareRef = ref || getDefaultBranch();
59
- const currentBranch = getCurrentBranch();
60
-
61
- console.log(fmt.header('Git Diff'));
62
- console.log(chalk.dim(` Comparing: ${compareRef} → ${currentBranch || 'HEAD'}`));
63
- console.log();
64
-
65
- // Get files to compare
66
- let filesToCompare: string[];
67
- if (options.files) {
68
- filesToCompare = options.files.split(',').map(f => f.trim());
69
- } else {
70
- // Default to markdown section files
71
- filesToCompare = fs.readdirSync('.').filter(f =>
72
- f.endsWith('.md') && !['README.md', 'CLAUDE.md'].includes(f)
73
- );
74
- }
75
-
76
- if (filesToCompare.length === 0) {
77
- console.log(fmt.status('info', 'No markdown files found'));
78
- return;
79
- }
80
-
81
- // Get changed files from git
82
- const changedFiles = getChangedFiles(compareRef);
83
- const changedSet = new Set(changedFiles.map(f => f.file));
84
-
85
- // Get word count differences
86
- const { total, byFile } = getWordCountDiff(filesToCompare, compareRef);
87
-
88
- // Show results
89
- const rows: string[][] = [];
90
- for (const file of filesToCompare) {
91
- const stats = byFile[file];
92
- if (stats && (stats.added > 0 || stats.removed > 0)) {
93
- const status = changedSet.has(file)
94
- ? changedFiles.find(f => f.file === file)?.status || 'modified'
95
- : 'unchanged';
96
- rows.push([
97
- file,
98
- status,
99
- chalk.green(`+${stats.added}`),
100
- chalk.red(`-${stats.removed}`),
101
- ]);
102
- }
103
- }
104
-
105
- if (rows.length === 0) {
106
- console.log(fmt.status('success', 'No changes detected'));
107
- return;
108
- }
109
-
110
- console.log(fmt.table(['File', 'Status', 'Added', 'Removed'], rows));
111
- console.log();
112
- console.log(chalk.dim(`Total: ${chalk.green(`+${total.added}`)} words, ${chalk.red(`-${total.removed}`)} words`));
113
-
114
- // Show detailed diff if not --stat
115
- if (!options.stat && rows.length > 0) {
116
- console.log();
117
- console.log(chalk.cyan('Changed sections:'));
118
- for (const file of filesToCompare) {
119
- const stats = byFile[file];
120
- if (stats && (stats.added > 0 || stats.removed > 0)) {
121
- const { changes } = compareFileVersions(file, compareRef);
122
- console.log(chalk.bold(`\n ${file}:`));
123
-
124
- // Show first few significant changes
125
- let shown = 0;
126
- for (const change of changes) {
127
- if (shown >= 3) {
128
- console.log(chalk.dim(' ...'));
129
- break;
130
- }
131
- const preview = change.value.slice(0, 60).replace(/\n/g, ' ');
132
- if (change.added) {
133
- console.log(chalk.green(` + "${preview}..."`));
134
- } else if (change.removed) {
135
- console.log(chalk.red(` - "${preview}..."`));
136
- }
137
- shown++;
138
- }
139
- }
140
- }
141
- }
142
- });
143
-
144
- // ==========================================================================
145
- // HISTORY command - Show revision history
146
- // ==========================================================================
147
-
148
- program
149
- .command('history')
150
- .description('Show revision history for section files')
151
- .argument('[file]', 'Specific file (default: all sections)')
152
- .option('-n, --limit <count>', 'Number of commits to show', '10')
153
- .action(async (file: string | undefined, options: HistoryOptions) => {
154
- const {
155
- isGitRepo,
156
- getFileHistory,
157
- getRecentCommits,
158
- hasUncommittedChanges,
159
- } = await import('../git.js');
160
-
161
- if (!isGitRepo()) {
162
- console.error(fmt.status('error', 'Not a git repository'));
163
- process.exit(1);
164
- }
165
-
166
- const limit = parseInt(options.limit) || 10;
167
-
168
- console.log(fmt.header('Revision History'));
169
- console.log();
170
-
171
- if (file) {
172
- // Show history for specific file
173
- if (!fs.existsSync(file)) {
174
- console.error(fmt.status('error', `File not found: ${file}`));
175
- process.exit(1);
176
- }
177
-
178
- const history = getFileHistory(file, limit);
179
-
180
- if (history.length === 0) {
181
- console.log(fmt.status('info', 'No history found (file may not be committed)'));
182
- return;
183
- }
184
-
185
- console.log(chalk.cyan(`History for ${file}:`));
186
- console.log();
187
-
188
- for (const commit of history) {
189
- const date = new Date(commit.date).toLocaleDateString();
190
- console.log(` ${chalk.yellow(commit.hash)} ${chalk.dim(date)}`);
191
- console.log(` ${commit.message}`);
192
- }
193
- } else {
194
- // Show recent commits affecting any file
195
- const commits = getRecentCommits(limit);
196
-
197
- if (commits.length === 0) {
198
- console.log(fmt.status('info', 'No commits found'));
199
- return;
200
- }
201
-
202
- if (hasUncommittedChanges()) {
203
- console.log(chalk.yellow(' * Uncommitted changes'));
204
- console.log();
205
- }
206
-
207
- for (const commit of commits) {
208
- const date = new Date(commit.date).toLocaleDateString();
209
- console.log(` ${chalk.yellow(commit.hash)} ${chalk.dim(date)} ${chalk.blue(commit.author)}`);
210
- console.log(` ${commit.message}`);
211
- }
212
- }
213
- });
214
-
215
- // ==========================================================================
216
- // CONTRIBUTORS command - Show who wrote what
217
- // ==========================================================================
218
-
219
- program
220
- .command('contributors')
221
- .alias('authors')
222
- .description('Show author contributions across section files')
223
- .argument('[file]', 'Specific file (default: all sections)')
224
- .option('--blame', 'Show detailed line-by-line blame for a file')
225
- .action(async (file: string | undefined, options: ContributorsOptions) => {
226
- const { isGitRepo, getAuthorStats, getContributors, getFileBlame } = await import('../git.js');
227
-
228
- if (!isGitRepo()) {
229
- console.error(fmt.status('error', 'Not a git repository'));
230
- process.exit(1);
231
- }
232
-
233
- console.log(fmt.header('Contributors'));
234
- console.log();
235
-
236
- if (file) {
237
- // Show stats for specific file
238
- if (!fs.existsSync(file)) {
239
- console.error(fmt.status('error', `File not found: ${file}`));
240
- process.exit(1);
241
- }
242
-
243
- if (options.blame) {
244
- // Detailed blame output
245
- const blame = getFileBlame(file);
246
- if (blame.length === 0) {
247
- console.log(fmt.status('info', 'No git history (file may not be committed)'));
248
- return;
249
- }
250
-
251
- console.log(chalk.cyan(`Blame for ${file}:`));
252
- console.log();
253
-
254
- for (const entry of blame) {
255
- const authorShort = entry.author.slice(0, 15).padEnd(15);
256
- const content = entry.content.length > 60 ? entry.content.slice(0, 60) + '...' : entry.content;
257
- console.log(` ${chalk.dim(entry.hash)} ${chalk.blue(authorShort)} ${chalk.dim(`L${String(entry.line).padStart(3)}`)} ${content}`);
258
- }
259
- } else {
260
- // Summary stats
261
- const stats = getAuthorStats(file);
262
- if (Object.keys(stats).length === 0) {
263
- console.log(fmt.status('info', 'No git history (file may not be committed)'));
264
- return;
265
- }
266
-
267
- console.log(chalk.cyan(`Authors for ${file}:`));
268
- console.log();
269
-
270
- const sorted = Object.entries(stats).sort((a, b) => b[1].lines - a[1].lines);
271
- for (const [author, data] of sorted) {
272
- const bar = '█'.repeat(Math.ceil(data.percentage / 5));
273
- console.log(` ${chalk.blue(author.padEnd(25))} ${chalk.dim(String(data.lines).padStart(4))} lines ${chalk.green(bar)} ${data.percentage}%`);
274
- }
275
- }
276
- } else {
277
- // Show contributors across all sections
278
- let config: { sections?: string[] } = {};
279
- try {
280
- config = loadBuildConfig(process.cwd()) || {};
281
- } catch {
282
- // Not in a rev project
283
- }
284
-
285
- let sections = config.sections || [];
286
- if (sections.length === 0) {
287
- sections = fs.readdirSync('.').filter(f =>
288
- f.endsWith('.md') && !['README.md', 'CLAUDE.md', 'paper.md'].includes(f)
289
- );
290
- }
291
-
292
- if (sections.length === 0) {
293
- console.error(fmt.status('error', 'No section files found'));
294
- process.exit(1);
295
- }
296
-
297
- const contributors = getContributors(sections);
298
-
299
- if (Object.keys(contributors).length === 0) {
300
- console.log(fmt.status('info', 'No git history found'));
301
- return;
302
- }
303
-
304
- const sorted = Object.entries(contributors).sort((a, b) => b[1].lines - a[1].lines);
305
- const totalLines = sorted.reduce((sum, [, data]) => sum + data.lines, 0);
306
-
307
- console.log(chalk.cyan('Project contributors:'));
308
- console.log();
309
-
310
- for (const [author, data] of sorted) {
311
- const pct = Math.round((data.lines / totalLines) * 100);
312
- const bar = '█'.repeat(Math.ceil(pct / 5));
313
- console.log(` ${chalk.blue(author.padEnd(25))} ${chalk.dim(String(data.lines).padStart(5))} lines ${chalk.dim(String(data.files))} files ${chalk.green(bar)} ${pct}%`);
314
- }
315
-
316
- console.log();
317
- console.log(chalk.dim(` Total: ${totalLines} lines across ${sections.length} files`));
318
- }
319
- });
320
- }