paperfit-cli 1.0.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 (65) hide show
  1. package/.claude/commands/adjust-length.md +21 -0
  2. package/.claude/commands/check-visual.md +27 -0
  3. package/.claude/commands/fix-layout.md +31 -0
  4. package/.claude/commands/migrate-template.md +23 -0
  5. package/.claude/commands/repair-table.md +21 -0
  6. package/.claude/commands/show-status.md +32 -0
  7. package/.claude-plugin/README.md +77 -0
  8. package/.claude-plugin/marketplace.json +41 -0
  9. package/.claude-plugin/plugin.json +39 -0
  10. package/CLAUDE.md +266 -0
  11. package/CONTRIBUTING.md +131 -0
  12. package/LICENSE +21 -0
  13. package/README.md +164 -0
  14. package/agents/code-surgeon-agent.md +214 -0
  15. package/agents/layout-detective-agent.md +229 -0
  16. package/agents/orchestrator-agent.md +254 -0
  17. package/agents/quality-gatekeeper-agent.md +270 -0
  18. package/agents/rule-engine-agent.md +224 -0
  19. package/agents/semantic-polish-agent.md +250 -0
  20. package/bin/paperfit.js +176 -0
  21. package/config/agent_roles.yaml +56 -0
  22. package/config/layout_rules.yaml +54 -0
  23. package/config/templates.yaml +241 -0
  24. package/config/vto_taxonomy.yaml +489 -0
  25. package/config/writing_rules.yaml +64 -0
  26. package/install.sh +30 -0
  27. package/package.json +52 -0
  28. package/requirements.txt +5 -0
  29. package/scripts/benchmark_runner.py +629 -0
  30. package/scripts/compile.sh +244 -0
  31. package/scripts/config_validator.py +339 -0
  32. package/scripts/cv_detector.py +600 -0
  33. package/scripts/evidence_collector.py +167 -0
  34. package/scripts/float_fixers.py +861 -0
  35. package/scripts/inject_defects.py +549 -0
  36. package/scripts/install-claude-global.js +148 -0
  37. package/scripts/install.js +66 -0
  38. package/scripts/install.sh +106 -0
  39. package/scripts/overflow_fixers.py +656 -0
  40. package/scripts/package-for-opensource.sh +138 -0
  41. package/scripts/parse_log.py +260 -0
  42. package/scripts/postinstall.js +38 -0
  43. package/scripts/pre_tool_use.py +265 -0
  44. package/scripts/render_pages.py +244 -0
  45. package/scripts/session_logger.py +329 -0
  46. package/scripts/space_util_fixers.py +773 -0
  47. package/scripts/state_manager.py +352 -0
  48. package/scripts/test_commands.py +187 -0
  49. package/scripts/test_cv_detector.py +214 -0
  50. package/scripts/test_integration.py +290 -0
  51. package/skills/consistency-polisher/SKILL.md +337 -0
  52. package/skills/float-optimizer/SKILL.md +284 -0
  53. package/skills/latex_fixers/__init__.py +82 -0
  54. package/skills/latex_fixers/float_fixers.py +392 -0
  55. package/skills/latex_fixers/fullwidth_fixers.py +375 -0
  56. package/skills/latex_fixers/overflow_fixers.py +250 -0
  57. package/skills/latex_fixers/semantic_micro_tuning.py +362 -0
  58. package/skills/latex_fixers/space_util_fixers.py +389 -0
  59. package/skills/latex_fixers/utils.py +55 -0
  60. package/skills/overflow-repair/SKILL.md +304 -0
  61. package/skills/space-util-fixer/SKILL.md +307 -0
  62. package/skills/taxonomy-vto/SKILL.md +486 -0
  63. package/skills/template-migrator/SKILL.md +251 -0
  64. package/skills/visual-inspector/SKILL.md +217 -0
  65. package/skills/writing-polish/SKILL.md +289 -0
@@ -0,0 +1,549 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 缺陷注入脚本 - 用于构建 VTO Benchmark 测试集
4
+
5
+ 向干净的 LaTeX 论文中注入已知的、可检测的排版缺陷,用于:
6
+ 1. 测试缺陷检测算法的准确性
7
+ 2. 评估修复策略的有效性
8
+ 3. 建立回归测试基准
9
+
10
+ 支持的缺陷类型(按 VTO 分类体系):
11
+ - Category A: 空间利用缺陷(孤行寡行、末页留白、页数预算、双栏不齐)
12
+ - Category B: 浮动体缺陷(远离引用、尺寸不适配、连续堆叠、跨页分裂)
13
+ - Category C: 一致性缺陷(表格字号不一、图片风格不一致)
14
+ - Category D: 溢出缺陷(overfull hbox、长公式溢出、URL 溢出)
15
+ - Category E: 跨模板缺陷(单双栏失配)
16
+ """
17
+
18
+ import argparse
19
+ import json
20
+ import random
21
+ import re
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
24
+ from typing import Dict, List, Optional, Tuple
25
+
26
+
27
+ # ============================================================
28
+ # 缺陷注入配置
29
+ # ============================================================
30
+
31
+ @dataclass
32
+ class DefectConfig:
33
+ """缺陷配置"""
34
+ defect_id: str
35
+ category: str
36
+ name: str
37
+ description: str
38
+ severity: str # "minor" | "major" | "critical"
39
+ injection_method: str # 注入方法描述
40
+
41
+
42
+ DEFECT_CATALOG = [
43
+ # Category A: 空间利用缺陷
44
+ DefectConfig(
45
+ defect_id="A1-widow-orphan",
46
+ category="A",
47
+ name="孤行寡行",
48
+ description="在段落末尾添加短行模拟孤行",
49
+ severity="major",
50
+ injection_method="add short last line to paragraph"
51
+ ),
52
+ DefectConfig(
53
+ defect_id="A2-trailing-whitespace",
54
+ category="A",
55
+ name="末页留白",
56
+ description="在文档末尾添加\\vspace 制造大面积留白",
57
+ severity="minor",
58
+ injection_method="add \\\\vspace before end"
59
+ ),
60
+
61
+ # Category B: 浮动体缺陷
62
+ DefectConfig(
63
+ defect_id="B1-float-placement",
64
+ category="B",
65
+ name="浮动体远离引用",
66
+ description="将浮动体位置参数改为 [p] 强制独立页面",
67
+ severity="major",
68
+ injection_method="change float placement to [p]"
69
+ ),
70
+ DefectConfig(
71
+ defect_id="B2-float-width",
72
+ category="B",
73
+ name="浮动体尺寸不适配",
74
+ description="将图片宽度设为 1.5\\linewidth 超出栏宽",
75
+ severity="major",
76
+ injection_method="increase figure width to 1.5\\\\linewidth"
77
+ ),
78
+ DefectConfig(
79
+ defect_id="B3-float-clustering",
80
+ category="B",
81
+ name="浮动体堆叠",
82
+ description="连续插入多个浮动体无正文间隔",
83
+ severity="major",
84
+ injection_method="add consecutive floats without text"
85
+ ),
86
+
87
+ # Category D: 溢出缺陷
88
+ DefectConfig(
89
+ defect_id="D1-overfull-hbox",
90
+ category="D",
91
+ name="Overfull hbox",
92
+ description="添加超长无断点单词导致溢出",
93
+ severity="major",
94
+ injection_method="add very long word without hyphenation"
95
+ ),
96
+ DefectConfig(
97
+ defect_id="D2-long-formula",
98
+ category="D",
99
+ name="长公式溢出",
100
+ description="添加超宽公式环境",
101
+ severity="major",
102
+ injection_method="add wide formula"
103
+ ),
104
+ DefectConfig(
105
+ defect_id="D3-url-overflow",
106
+ category="D",
107
+ name="URL 溢出",
108
+ description="添加裸 URL(不用\\url{}包裹)",
109
+ severity="minor",
110
+ injection_method="add bare URL without \\\\url wrapper"
111
+ ),
112
+ ]
113
+
114
+
115
+ # ============================================================
116
+ # 缺陷注入器
117
+ # ============================================================
118
+
119
+ class DefectInjector:
120
+ """缺陷注入器"""
121
+
122
+ def __init__(self, seed: int = 42):
123
+ self.seed = seed
124
+ random.seed(seed)
125
+ self.injected_defects: List[Dict] = []
126
+
127
+ def inject_all(
128
+ self,
129
+ tex_content: str,
130
+ defect_types: Optional[List[str]] = None,
131
+ ) -> Tuple[str, List[Dict]]:
132
+ """
133
+ 向 TeX 内容注入指定类型的缺陷
134
+
135
+ Args:
136
+ tex_content: 原始 TeX 内容
137
+ defect_types: 要注入的缺陷类型列表(如 ["A1", "B2"]),None 表示全部
138
+
139
+ Returns:
140
+ (modified_content, injected_defects_list)
141
+ """
142
+ self.injected_defects = []
143
+ modified_content = tex_content
144
+
145
+ # 选择要注入的缺陷类型
146
+ if defect_types is None:
147
+ selected_defects = DEFECT_CATALOG
148
+ else:
149
+ selected_defects = [
150
+ d for d in DEFECT_CATALOG
151
+ if any(d.defect_id.startswith(t) for t in defect_types)
152
+ ]
153
+
154
+ # 按类别分组注入,累积修改
155
+ for defect in selected_defects:
156
+ modified_content = self._inject_defect(modified_content, defect)
157
+
158
+ return modified_content, self.injected_defects
159
+
160
+ def _inject_defect(self, tex_content: str, config: DefectConfig) -> str:
161
+ """注入单个缺陷,返回修改后的内容"""
162
+ # defect_id 如 "A1-widow-orphan" -> "a1_widow_orphan" (小写)
163
+ method_name = f"_inject_{config.defect_id.replace('-', '_').lower()}"
164
+ if hasattr(self, method_name):
165
+ method = getattr(self, method_name)
166
+ return method(tex_content, config)
167
+ return tex_content
168
+
169
+ def _inject_a1_widow_orphan(self, tex_content: str, config: DefectConfig) -> str:
170
+ """注入孤行寡行缺陷 - 在段落末尾添加短行"""
171
+ # 查找\lipsum 调用,在其后添加短行
172
+ lipsum_pattern = r'(\\lipsum\[\d+\])'
173
+
174
+ def add_widow(match):
175
+ return match.group(1) + "\n\n% INJECTED: Widow/Orphan test\nShort line."
176
+
177
+ modified, count = re.subn(lipsum_pattern, add_widow, tex_content, count=2)
178
+
179
+ if count > 0:
180
+ self.injected_defects.append({
181
+ "defect_id": config.defect_id,
182
+ "action": config.injection_method,
183
+ "count": count,
184
+ })
185
+
186
+ return modified
187
+
188
+ def _inject_a2_trailing_whitespace(self, tex_content: str, config: DefectConfig) -> str:
189
+ """注入末页留白缺陷"""
190
+ # 在\end{document}前添加大间距
191
+ end_pattern = r'(\\end\{document\})'
192
+ replacement = r'\\vspace{5cm}\n% INJECTED: Trailing whitespace\n\1'
193
+
194
+ modified = re.sub(end_pattern, replacement, tex_content, count=1)
195
+
196
+ if modified != tex_content:
197
+ self.injected_defects.append({
198
+ "defect_id": config.defect_id,
199
+ "action": config.injection_method,
200
+ "count": 1,
201
+ })
202
+
203
+ return modified
204
+
205
+ def _inject_b1_float_placement(self, tex_content: str, config: DefectConfig) -> str:
206
+ """注入浮动体远离引用缺陷"""
207
+ # 将 [htbp] 改为 [p]
208
+ float_pattern = r'\\begin\{(figure|table)\}\[htbp\]'
209
+
210
+ def worsen_placement(match):
211
+ env_type = match.group(1)
212
+ return f"\\begin{{{env_type}}}[p]"
213
+
214
+ modified, count = re.subn(float_pattern, worsen_placement, tex_content)
215
+
216
+ if count > 0:
217
+ self.injected_defects.append({
218
+ "defect_id": config.defect_id,
219
+ "action": config.injection_method,
220
+ "count": count,
221
+ })
222
+
223
+ return modified
224
+
225
+ def _inject_b2_float_width(self, tex_content: str, config: DefectConfig) -> str:
226
+ """注入浮动体尺寸不适配缺陷"""
227
+ # 将 0.8\\linewidth 改为 1.5\\linewidth
228
+ width_pattern = r'width\s*=\s*0\.8\\linewidth'
229
+ replacement = r'width=1.5\\linewidth'
230
+
231
+ modified, count = re.subn(width_pattern, replacement, tex_content)
232
+
233
+ if count > 0:
234
+ self.injected_defects.append({
235
+ "defect_id": config.defect_id,
236
+ "action": config.injection_method,
237
+ "count": count,
238
+ })
239
+
240
+ return modified
241
+
242
+ def _inject_b3_float_clustering(self, tex_content: str, config: DefectConfig) -> str:
243
+ """注入浮动体堆叠缺陷"""
244
+ # 在现有 figure 后添加额外的 figure
245
+ end_figure_pattern = r'(\\end\{figure\})'
246
+ # 注意:替换字符串中 \\\\ 会被解释为单个 \,\1 是反向引用
247
+ extra_figure = (
248
+ r'\1'
249
+ '\n% INJECTED: Float clustering - extra figure without text separation'
250
+ '\n\\\\begin{figure}[h]'
251
+ '\n\\\\centering'
252
+ '\n\\\\includegraphics[width=0.5\\\\linewidth]{example-image-a}'
253
+ '\n\\\\caption{Injected figure causing clustering.}'
254
+ '\n\\\\label{fig:injected}'
255
+ '\n\\\\end{figure}'
256
+ )
257
+
258
+ modified, count = re.subn(end_figure_pattern, extra_figure, tex_content, count=1)
259
+
260
+ if count > 0:
261
+ self.injected_defects.append({
262
+ "defect_id": config.defect_id,
263
+ "action": config.injection_method,
264
+ "count": count,
265
+ })
266
+
267
+ return modified
268
+
269
+ def _inject_d1_overfull_hbox(self, tex_content: str, config: DefectConfig) -> str:
270
+ """注入 overfull hbox 缺陷"""
271
+ # 添加超长单词
272
+ long_word = "Supercalifragilisticexpialidocious" * 3 # 非常长的单词
273
+
274
+ # 在段落中插入
275
+ lipsum_pattern = r'(\\lipsum\[\d+\])'
276
+ replacement = f'\\1\n\n% INJECTED: Overfull hbox test\n{long_word}'
277
+
278
+ modified, count = re.subn(lipsum_pattern, replacement, tex_content, count=1)
279
+
280
+ if count > 0:
281
+ self.injected_defects.append({
282
+ "defect_id": config.defect_id,
283
+ "action": config.injection_method,
284
+ "count": count,
285
+ })
286
+
287
+ return modified
288
+
289
+ def _inject_d2_long_formula(self, tex_content: str, config: DefectConfig) -> str:
290
+ """注入长公式溢出缺陷"""
291
+ # 添加超宽公式 - 使用双反斜杠避免转义问题
292
+ long_formula = r'''
293
+ % INJECTED: Long formula overflow
294
+ \\begin{equation}
295
+ f(x) = a_0 + a_1 x + a_2 x^2 + a_3 x^3 + a_4 x^4 + a_5 x^5 + a_6 x^6 + a_7 x^7 + a_8 x^8 + a_9 x^9 + a_{10} x^{10}
296
+ \\end{equation}
297
+ '''
298
+ # 在现有 equation 后添加
299
+ end_equation_pattern = r'(\\end\{equation\})'
300
+ modified = re.sub(end_equation_pattern, r'\1' + long_formula, tex_content, count=1)
301
+
302
+ if modified != tex_content:
303
+ self.injected_defects.append({
304
+ "defect_id": config.defect_id,
305
+ "action": config.injection_method,
306
+ "count": 1,
307
+ })
308
+
309
+ return modified
310
+
311
+ def _inject_d3_url_overflow(self, tex_content: str, config: DefectConfig) -> str:
312
+ """注入 URL 溢出缺陷"""
313
+ # 添加裸 URL
314
+ bare_url = "https://www.example.com/this/is/a/very/long/path/that/will/cause/overflow/in/the/document/and/create/a/line/that/exceeds/the/page/width/and/causes/an/error"
315
+
316
+ # 在现有 URL 附近添加
317
+ url_pattern = r'(\\url\{[^}]+\})'
318
+ replacement = f'\\1\n\n% INJECTED: URL overflow test\n{bare_url}'
319
+
320
+ modified, count = re.subn(url_pattern, replacement, tex_content, count=1)
321
+
322
+ if count > 0:
323
+ self.injected_defects.append({
324
+ "defect_id": config.defect_id,
325
+ "action": config.injection_method,
326
+ "count": count,
327
+ })
328
+
329
+ return modified
330
+
331
+
332
+ # ============================================================
333
+ # 样本生成器
334
+ # ============================================================
335
+
336
+ class SampleGenerator:
337
+ """生成包含特定缺陷的测试样本"""
338
+
339
+ def __init__(self, output_dir: Path):
340
+ self.output_dir = output_dir
341
+ self.output_dir.mkdir(parents=True, exist_ok=True)
342
+
343
+ def generate_clean_sample(self, name: str = "clean_sample") -> Path:
344
+ """生成干净的测试样本(无缺陷)"""
345
+ content = self._create_minimal_latex_sample()
346
+ output_path = self.output_dir / f"{name}.tex"
347
+ output_path.write_text(content, encoding="utf-8")
348
+ return output_path
349
+
350
+ def generate_defective_sample(
351
+ self,
352
+ name: str,
353
+ defect_types: List[str],
354
+ seed: int = 42,
355
+ ) -> Tuple[Path, List[Dict]]:
356
+ """生成包含指定缺陷的样本"""
357
+ # 先生成干净样本
358
+ clean_path = self.generate_clean_sample(f"{name}_base")
359
+
360
+ # 读取并注入缺陷
361
+ content = clean_path.read_text(encoding="utf-8")
362
+ injector = DefectInjector(seed=seed)
363
+ modified_content, defects = injector.inject_all(content, defect_types)
364
+
365
+ # 保存缺陷样本
366
+ output_path = self.output_dir / f"{name}.tex"
367
+ output_path.write_text(modified_content, encoding="utf-8")
368
+
369
+ # 保存缺陷清单
370
+ manifest_path = self.output_dir / f"{name}_defects.json"
371
+ manifest_path.write_text(
372
+ json.dumps(defects, indent=2, ensure_ascii=False),
373
+ encoding="utf-8"
374
+ )
375
+
376
+ return output_path, defects
377
+
378
+ def _create_minimal_latex_sample(self) -> str:
379
+ """创建最小化 LaTeX 论文样本"""
380
+ return r"""\documentclass[12pt,a4paper]{article}
381
+ \usepackage{graphicx}
382
+ \usepackage{booktabs}
383
+ \usepackage{amsmath}
384
+ \usepackage{hyperref}
385
+ \usepackage{lipsum}
386
+
387
+ \title{Test Paper for VTO Benchmark}
388
+ \author{Test Author}
389
+ \date{\today}
390
+
391
+ \begin{document}
392
+
393
+ \maketitle
394
+
395
+ \begin{abstract}
396
+ This is a test document for VTO (Visual Typesetting Optimization) benchmark.
397
+ It contains various LaTeX elements that can be used to test defect detection.
398
+ \end{abstract}
399
+
400
+ \section{Introduction}
401
+ \label{sec:intro}
402
+ \lipsum[1]
403
+
404
+ \section{Methodology}
405
+ \label{sec:method}
406
+
407
+ \lipsum[2]
408
+
409
+ \begin{figure}[htbp]
410
+ \centering
411
+ \includegraphics[width=0.8\linewidth]{example-image}
412
+ \caption{A test figure.}
413
+ \label{fig:test}
414
+ \end{figure}
415
+
416
+ As shown in Figure~\ref{fig:test}, the methodology is straightforward.
417
+
418
+ \lipsum[3]
419
+
420
+ \begin{table}[htbp]
421
+ \centering
422
+ \begin{tabular}{lll}
423
+ \toprule
424
+ Method & Accuracy & Speed \\
425
+ \midrule
426
+ Baseline & 85.2\% & Fast \\
427
+ Ours & 92.1\% & Medium \\
428
+ \bottomrule
429
+ \end{tabular}
430
+ \caption{Comparison results.}
431
+ \label{tab:results}
432
+ \end{table}
433
+
434
+ Table~\ref{tab:results} shows the comparison results.
435
+
436
+ \section{Experiments}
437
+ \label{sec:experiments}
438
+
439
+ \lipsum[4-5]
440
+
441
+ The long equation below demonstrates formula handling:
442
+ \begin{equation}
443
+ f(x) = \sum_{n=0}^{\infty} \frac{f^{(n)}(0)}{n!} x^n
444
+ \end{equation}
445
+
446
+ \lipsum[6]
447
+
448
+ For more information, visit \url{https://www.example.com/very/long/path}
449
+
450
+ \section{Conclusion}
451
+ \label{sec:conclusion}
452
+ \lipsum[7]
453
+
454
+ \bibliographystyle{plain}
455
+ \begin{thebibliography}{9}
456
+ \bibitem{test} Test Reference. Example Paper Title. Journal, 2024.
457
+ \end{thebibliography}
458
+
459
+ \end{document}
460
+ """
461
+
462
+
463
+ # ============================================================
464
+ # 主函数
465
+ # ============================================================
466
+
467
+ def main():
468
+ """主函数"""
469
+ parser = argparse.ArgumentParser(
470
+ description="缺陷注入脚本 - 生成 VTO Benchmark 测试集"
471
+ )
472
+ parser.add_argument(
473
+ "--output-dir",
474
+ type=str,
475
+ default="data/benchmarks/samples",
476
+ help="输出目录"
477
+ )
478
+ parser.add_argument(
479
+ "--defect-types",
480
+ nargs="+",
481
+ default=None,
482
+ help="要注入的缺陷类型(如 A B1 D3),默认全部"
483
+ )
484
+ parser.add_argument(
485
+ "--seed",
486
+ type=int,
487
+ default=42,
488
+ help="随机种子"
489
+ )
490
+ parser.add_argument(
491
+ "--list-defects",
492
+ action="store_true",
493
+ help="列出所有支持的缺陷类型"
494
+ )
495
+
496
+ args = parser.parse_args()
497
+
498
+ # 列出缺陷类型
499
+ if args.list_defects:
500
+ print("\n支持的缺陷类型:")
501
+ print("-" * 80)
502
+ for defect in DEFECT_CATALOG:
503
+ print(f" {defect.defect_id:25} [{defect.severity:8}] - {defect.name}")
504
+ print(f" {defect.description}")
505
+ print(f" 注入方法:{defect.injection_method}")
506
+ print("-" * 80)
507
+ return
508
+
509
+ # 生成测试样本
510
+ output_dir = Path(args.output_dir)
511
+ generator = SampleGenerator(output_dir)
512
+
513
+ print(f"生成测试样本到:{output_dir}")
514
+ print("-" * 50)
515
+
516
+ # 生成干净样本
517
+ clean_path = generator.generate_clean_sample("clean_sample")
518
+ print(f"[生成] 干净样本:{clean_path.name}")
519
+
520
+ # 生成包含所有缺陷的样本
521
+ if args.defect_types:
522
+ defective_path, defects = generator.generate_defective_sample(
523
+ name="defective_sample",
524
+ defect_types=args.defect_types,
525
+ seed=args.seed
526
+ )
527
+ print(f"[生成] 缺陷样本:{defective_path.name}")
528
+ print(f" 注入缺陷数:{len(defects)}")
529
+ for d in defects:
530
+ print(f" - {d['defect_id']}: {d['action']}")
531
+ else:
532
+ # 生成按类别分组的样本
533
+ for category in ["A", "B", "D"]:
534
+ defective_path, defects = generator.generate_defective_sample(
535
+ name=f"defective_cat_{category}",
536
+ defect_types=[category],
537
+ seed=args.seed
538
+ )
539
+ print(f"[生成] Category {category} 缺陷样本:{defective_path.name}")
540
+ print(f" 注入缺陷数:{len(defects)}")
541
+ for d in defects:
542
+ print(f" - {d['defect_id']}: {d['action']}")
543
+
544
+ print("-" * 50)
545
+ print(f"样本生成完成。使用 --list-defects 查看所有支持的缺陷类型。")
546
+
547
+
548
+ if __name__ == "__main__":
549
+ main()
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Install PaperFit into the user's global Claude Code home (~/.claude).
4
+ * Usage: paperfit-install [--force] [--dry-run]
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ const HOME = os.homedir();
12
+ const CLAUDE_DIR = path.join(HOME, '.claude');
13
+ const PAPERFIT_HOME = path.join(HOME, '.paperfit');
14
+ const PKG_ROOT = path.join(__dirname, '..');
15
+
16
+ function parseArgs() {
17
+ const argv = process.argv.slice(2);
18
+ return {
19
+ force: argv.includes('--force'),
20
+ dryRun: argv.includes('--dry-run'),
21
+ help: argv.includes('--help') || argv.includes('-h'),
22
+ };
23
+ }
24
+
25
+ function ensureDir(p, dryRun) {
26
+ if (dryRun) return;
27
+ fs.mkdirSync(p, { recursive: true });
28
+ }
29
+
30
+ function copyFile(src, dest, { force, dryRun }) {
31
+ if (dryRun) {
32
+ console.log(` [dry-run] ${src} -> ${dest}`);
33
+ return;
34
+ }
35
+ if (fs.existsSync(dest) && !force) {
36
+ console.log(` skip (exists): ${path.basename(dest)}`);
37
+ return;
38
+ }
39
+ ensureDir(path.dirname(dest), false);
40
+ fs.copyFileSync(src, dest);
41
+ console.log(` ok: ${path.basename(dest)}`);
42
+ }
43
+
44
+ function copyTree(srcDir, destDir, { force, dryRun }, filter = () => true) {
45
+ if (!fs.existsSync(srcDir)) return;
46
+ const walk = (rel = '') => {
47
+ const cur = path.join(srcDir, rel);
48
+ if (!fs.existsSync(cur)) return;
49
+ const st = fs.lstatSync(cur);
50
+ if (st.isDirectory()) {
51
+ for (const name of fs.readdirSync(cur)) {
52
+ const sub = rel ? `${rel}/${name}` : name;
53
+ if (filter(sub)) walk(sub);
54
+ }
55
+ return;
56
+ }
57
+ const dest = path.join(destDir, rel);
58
+ copyFile(cur, dest, { force, dryRun });
59
+ };
60
+ walk();
61
+ }
62
+
63
+ function showHelp() {
64
+ console.log(`
65
+ paperfit-install — copy PaperFit into ~/.claude for Claude Code (global)
66
+
67
+ Usage:
68
+ paperfit-install merge-copy commands, skills, agents, rules, config
69
+ paperfit-install --force overwrite existing same-named files
70
+ paperfit-install --dry-run print planned copies only
71
+
72
+ After install, open Claude Code and use /fix-layout in your LaTeX project.
73
+ `);
74
+ }
75
+
76
+ function main() {
77
+ const opts = parseArgs();
78
+ if (opts.help) {
79
+ showHelp();
80
+ process.exit(0);
81
+ }
82
+
83
+ console.log('\nPaperFit → global Claude Code\n');
84
+ console.log(`Package: ${PKG_ROOT}`);
85
+ console.log(`Target: ${CLAUDE_DIR}`);
86
+ console.log(`Data: ${PAPERFIT_HOME}\n`);
87
+
88
+ const { force, dryRun } = opts;
89
+
90
+ ensureDir(CLAUDE_DIR, dryRun);
91
+ ensureDir(path.join(CLAUDE_DIR, 'commands'), dryRun);
92
+ ensureDir(path.join(CLAUDE_DIR, 'skills'), dryRun);
93
+ ensureDir(path.join(CLAUDE_DIR, 'agents'), dryRun);
94
+ ensureDir(path.join(CLAUDE_DIR, 'rules'), dryRun);
95
+ ensureDir(path.join(PAPERFIT_HOME, 'config'), dryRun);
96
+
97
+ console.log('Commands (.claude/commands → ~/.claude/commands):');
98
+ const cmdSrc = path.join(PKG_ROOT, '.claude', 'commands');
99
+ copyTree(cmdSrc, path.join(CLAUDE_DIR, 'commands'), opts);
100
+
101
+ console.log('\nSkills (skills/ → ~/.claude/skills):');
102
+ const skillSrc = path.join(PKG_ROOT, 'skills');
103
+ copyTree(skillSrc, path.join(CLAUDE_DIR, 'skills'), opts);
104
+
105
+ console.log('\nAgents (agents/ → ~/.claude/agents):');
106
+ const agentSrc = path.join(PKG_ROOT, 'agents');
107
+ copyTree(agentSrc, path.join(CLAUDE_DIR, 'agents'), opts);
108
+
109
+ console.log('\nRules (CLAUDE.md → ~/.claude/rules/paperfit.md):');
110
+ const claudeMd = path.join(PKG_ROOT, 'CLAUDE.md');
111
+ const ruleDest = path.join(CLAUDE_DIR, 'rules', 'paperfit.md');
112
+ if (fs.existsSync(claudeMd)) {
113
+ copyFile(claudeMd, ruleDest, opts);
114
+ }
115
+
116
+ console.log('\nConfig (config/ → ~/.paperfit/config):');
117
+ const cfgSrc = path.join(PKG_ROOT, 'config');
118
+ copyTree(cfgSrc, path.join(PAPERFIT_HOME, 'config'), opts);
119
+
120
+ const manifest = {
121
+ name: 'paperfit',
122
+ version: require(path.join(PKG_ROOT, 'package.json')).version,
123
+ packageRoot: PKG_ROOT,
124
+ claudeDir: CLAUDE_DIR,
125
+ paperfitHome: PAPERFIT_HOME,
126
+ installedAt: new Date().toISOString(),
127
+ };
128
+ const manifestPath = path.join(PAPERFIT_HOME, 'install-manifest.json');
129
+ if (!dryRun) {
130
+ ensureDir(PAPERFIT_HOME, false);
131
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
132
+ console.log(`\nWrote ${manifestPath}`);
133
+ } else {
134
+ console.log(`\n[dry-run] would write ${manifestPath}`);
135
+ }
136
+
137
+ console.log(`
138
+ Done.${dryRun ? ' (dry-run; no files written)' : ''}
139
+
140
+ Optional: add to your shell profile for scripts that resolve bundled assets:
141
+ export PAPERFIT_HOME="${PAPERFIT_HOME}"
142
+
143
+ Python / LaTeX tools: pip3 install -r "${path.join(PKG_ROOT, 'requirements.txt')}"
144
+ System: brew install poppler # and TeX distribution for latexmk
145
+ `);
146
+ }
147
+
148
+ main();