laporan-generator 2.6.2 → 2.7.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/.agents/skills/laporan-generator/SKILL.md +99 -109
- package/.agents/skills/laporan-generator/resources/laporan +37 -4
- package/.agents/skills/laporan-generator/resources/laporan.ps1 +74 -28
- package/.agents/skills/laporan-generator/resources/scripts/bundle.py +11 -2
- package/.agents/skills/laporan-generator/resources/scripts/report-doctor.py +121 -48
- package/.agents/skills/laporan-generator/resources/scripts/report-stats.py +53 -2
- package/.agents/skills/laporan-generator/resources/scripts/test_scripts.py +63 -2
- package/.agents/skills/laporan-generator/scripts/bundle.py +11 -2
- package/.agents/skills/laporan-generator/scripts/finalize-docx.py +140 -1
- package/.agents/skills/laporan-generator/scripts/make-reference-docx.py +42 -2
- package/.agents/skills/laporan-generator/scripts/report-doctor.py +121 -48
- package/.agents/skills/laporan-generator/scripts/report-stats.py +53 -2
- package/.agents/skills/laporan-generator/scripts/test_scripts.py +88 -2
- package/README.md +44 -0
- package/bin/laporan-generator.js +15 -3
- package/bin/mcp-server.js +4 -0
- package/laporan +37 -4
- package/laporan.ps1 +50 -11
- package/lib/init.js +115 -44
- package/lib/mcp.js +569 -0
- package/lib/sync-hosts.js +196 -68
- package/lib/uninstall.js +21 -0
- package/package.json +9 -3
- package/scripts/bundle.py +11 -2
- package/scripts/report-doctor.py +121 -48
- package/scripts/report-stats.py +46 -2
- package/scripts/test_scripts.py +50 -3
package/scripts/report-doctor.py
CHANGED
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
"""report-doctor.py: Audit komprehensif kesehatan dan integritas proyek laporan."""
|
|
3
3
|
|
|
4
4
|
import glob
|
|
5
|
+
import json
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
8
|
import shutil
|
|
8
9
|
import sys
|
|
9
10
|
|
|
11
|
+
if sys.platform == "win32":
|
|
12
|
+
try:
|
|
13
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
14
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
15
|
+
except Exception:
|
|
16
|
+
pass
|
|
17
|
+
|
|
10
18
|
GREEN = "\033[0;32m"
|
|
11
19
|
BLUE = "\033[0;34m"
|
|
12
20
|
CYAN = "\033[0;36m"
|
|
@@ -98,101 +106,166 @@ def check_style_anomalies(markdown_files):
|
|
|
98
106
|
|
|
99
107
|
|
|
100
108
|
def main():
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
json_mode = "--json" in sys.argv
|
|
110
|
+
pos_args = [a for a in sys.argv[1:] if not a.startswith("-")]
|
|
111
|
+
|
|
112
|
+
target_dir = os.getcwd()
|
|
113
|
+
if pos_args:
|
|
114
|
+
target_dir = os.path.abspath(pos_args[0])
|
|
115
|
+
elif not (os.path.isfile(os.path.join(target_dir, "metadata.yml")) or os.path.isdir(os.path.join(target_dir, "chapters"))):
|
|
116
|
+
pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
117
|
+
if os.path.isfile(os.path.join(pkg_root, "metadata.yml")):
|
|
118
|
+
target_dir = pkg_root
|
|
119
|
+
|
|
120
|
+
os.chdir(target_dir)
|
|
121
|
+
|
|
122
|
+
if not json_mode:
|
|
123
|
+
print(f"{CYAN}{BOLD}")
|
|
124
|
+
print(" ========================================================")
|
|
125
|
+
print(" LAPORAN GENERATOR - DOCTOR & AUDIT SUITE ")
|
|
126
|
+
print(" ========================================================")
|
|
127
|
+
print(f"{NC}")
|
|
109
128
|
|
|
110
129
|
errors = 0
|
|
111
130
|
warnings = 0
|
|
112
131
|
|
|
113
132
|
# 1. Dependensi
|
|
114
|
-
|
|
133
|
+
if not json_mode:
|
|
134
|
+
print(f" {BLUE}{BOLD}[1/4] Audit Toolchain & Dependensi Sistem{NC}")
|
|
115
135
|
deps, has_im = check_dependencies()
|
|
136
|
+
dep_list = []
|
|
116
137
|
for cmd, name, req, found in deps:
|
|
117
138
|
if "ImageMagick" in name:
|
|
118
139
|
continue
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
print(f" {RED}[FAIL]{NC} {name:<24}: TIDAK DITEMUKAN (Wajib)")
|
|
124
|
-
errors += 1
|
|
140
|
+
dep_list.append({"command": cmd, "name": name, "required": req, "found": found})
|
|
141
|
+
if not json_mode:
|
|
142
|
+
if found:
|
|
143
|
+
print(f" {GREEN}[OK]{NC} {name:<26}: Terpasang ({cmd})")
|
|
125
144
|
else:
|
|
126
|
-
|
|
145
|
+
if req:
|
|
146
|
+
print(f" {RED}[FAIL]{NC} {name:<24}: TIDAK DITEMUKAN (Wajib)")
|
|
147
|
+
else:
|
|
148
|
+
print(f" {YELLOW}[INFO]{NC} {name:<24}: Tidak ditemukan (Opsional)")
|
|
149
|
+
if not found and req:
|
|
150
|
+
errors += 1
|
|
127
151
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
152
|
+
dep_list.append({"command": "magick/convert", "name": "ImageMagick", "required": False, "found": has_im})
|
|
153
|
+
if not json_mode:
|
|
154
|
+
if has_im:
|
|
155
|
+
print(f" {GREEN}[OK]{NC} {'ImageMagick':<26}: Terpasang")
|
|
156
|
+
else:
|
|
157
|
+
print(f" {YELLOW}[INFO]{NC} {'ImageMagick':<24}: Tidak ditemukan (Opsional untuk kompresi gambar)")
|
|
158
|
+
print("")
|
|
133
159
|
|
|
134
160
|
# 2. Berkas & Struktur
|
|
135
|
-
|
|
161
|
+
if not json_mode:
|
|
162
|
+
print(f" {BLUE}{BOLD}[2/4] Audit Struktur Direktori & Berkas Wajib{NC}")
|
|
136
163
|
req_files = [
|
|
137
164
|
"metadata.yml", "template.typ", "reference.docx",
|
|
138
165
|
"references.bib", "docx.lua", "cover.md"
|
|
139
166
|
]
|
|
167
|
+
file_status = []
|
|
140
168
|
for rf in req_files:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
169
|
+
exists = os.path.isfile(rf)
|
|
170
|
+
file_status.append({"file": rf, "found": exists})
|
|
171
|
+
if not json_mode:
|
|
172
|
+
if exists:
|
|
173
|
+
print(f" {GREEN}[OK]{NC} Berkas {rf}")
|
|
174
|
+
else:
|
|
175
|
+
print(f" {RED}[FAIL]{NC} Berkas {rf} TIDAK DITEMUKAN")
|
|
176
|
+
if not exists:
|
|
145
177
|
errors += 1
|
|
146
178
|
|
|
147
179
|
chapter_files = sorted(glob.glob("chapters/*.md"))
|
|
148
|
-
if
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
180
|
+
if not json_mode:
|
|
181
|
+
if chapter_files:
|
|
182
|
+
print(f" {GREEN}[OK]{NC} Direktori chapters/ ({len(chapter_files)} bab ditemukan)")
|
|
183
|
+
else:
|
|
184
|
+
print(f" {RED}[FAIL]{NC} Tidak ada berkas markdown di chapters/")
|
|
185
|
+
if not chapter_files:
|
|
152
186
|
errors += 1
|
|
153
|
-
|
|
187
|
+
if not json_mode:
|
|
188
|
+
print("")
|
|
154
189
|
|
|
155
190
|
# 3. Sitasi & Referensi
|
|
156
|
-
|
|
191
|
+
if not json_mode:
|
|
192
|
+
print(f" {BLUE}{BOLD}[3/4] Audit Sitasi & Integritas Bibliografi{NC}")
|
|
157
193
|
bib_keys, cited_keys, missing_keys, orphan_keys = check_citations("references.bib", chapter_files)
|
|
158
|
-
|
|
159
|
-
|
|
194
|
+
if not json_mode:
|
|
195
|
+
print(f" * Total entri .bib : {len(bib_keys)}")
|
|
196
|
+
print(f" * Total sitasi di teks : {len(cited_keys)}")
|
|
160
197
|
|
|
161
198
|
if missing_keys:
|
|
162
199
|
for k, locs in missing_keys.items():
|
|
163
200
|
loc_str = ", ".join([f"{f}:{l}" for f, l in locs])
|
|
164
|
-
|
|
201
|
+
if not json_mode:
|
|
202
|
+
print(f" {RED}[ERROR]{NC} Sitasi '@{k}' TIDAK DITEMUKAN di references.bib ({loc_str})")
|
|
165
203
|
errors += 1
|
|
166
|
-
|
|
204
|
+
elif not json_mode:
|
|
167
205
|
print(f" {GREEN}[OK]{NC} Semua sitasi dalam teks valid terdaftar di references.bib")
|
|
168
206
|
|
|
169
207
|
if orphan_keys:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
208
|
+
if not json_mode:
|
|
209
|
+
print(f" {YELLOW}[WARN]{NC} {len(orphan_keys)} entri di references.bib belum disitir dalam teks:")
|
|
210
|
+
for ok in list(orphan_keys)[:5]:
|
|
211
|
+
print(f" - @{ok}")
|
|
212
|
+
if len(orphan_keys) > 5:
|
|
213
|
+
print(f" - ... dan {len(orphan_keys) - 5} lainnya")
|
|
175
214
|
warnings += 1
|
|
176
|
-
|
|
215
|
+
if not json_mode:
|
|
216
|
+
print("")
|
|
177
217
|
|
|
178
218
|
# 4. Gambar & Format Teks
|
|
179
|
-
|
|
219
|
+
if not json_mode:
|
|
220
|
+
print(f" {BLUE}{BOLD}[4/4] Audit Media Citra & Anomali Tipografi{NC}")
|
|
180
221
|
total_imgs, broken_imgs = check_images(chapter_files)
|
|
181
222
|
if broken_imgs:
|
|
182
223
|
for p, f, l in broken_imgs:
|
|
183
|
-
|
|
224
|
+
if not json_mode:
|
|
225
|
+
print(f" {RED}[ERROR]{NC} Berkas gambar tidak ditemukan: '{p}' (di {f}:{l})")
|
|
184
226
|
errors += 1
|
|
185
|
-
|
|
227
|
+
elif not json_mode:
|
|
186
228
|
print(f" {GREEN}[OK]{NC} Semua tautan gambar ({total_imgs} gambar) valid dan ada di disk")
|
|
187
229
|
|
|
188
230
|
anomalies = check_style_anomalies(chapter_files)
|
|
189
231
|
if anomalies:
|
|
190
232
|
for f, l, msg in anomalies:
|
|
191
|
-
|
|
233
|
+
if not json_mode:
|
|
234
|
+
print(f" {YELLOW}[WARN]{NC} {f}:{l} - {msg}")
|
|
192
235
|
warnings += 1
|
|
193
|
-
|
|
236
|
+
elif not json_mode:
|
|
194
237
|
print(f" {GREEN}[OK]{NC} Bebas dari manual heading numbering dan box-drawing characters")
|
|
195
|
-
|
|
238
|
+
if not json_mode:
|
|
239
|
+
print("")
|
|
240
|
+
|
|
241
|
+
score = 100 if (errors == 0 and warnings == 0) else (95 if errors == 0 else max(0, 90 - errors * 15 - warnings * 5))
|
|
242
|
+
status_label = "HEALTHY" if errors == 0 and warnings == 0 else ("WARNING" if errors == 0 else "ERROR")
|
|
243
|
+
|
|
244
|
+
if json_mode:
|
|
245
|
+
result = {
|
|
246
|
+
"status": status_label,
|
|
247
|
+
"score": score,
|
|
248
|
+
"errors_count": errors,
|
|
249
|
+
"warnings_count": warnings,
|
|
250
|
+
"target_dir": os.getcwd(),
|
|
251
|
+
"dependencies": dep_list,
|
|
252
|
+
"required_files": file_status,
|
|
253
|
+
"chapters_count": len(chapter_files),
|
|
254
|
+
"chapters": [os.path.basename(c) for c in chapter_files],
|
|
255
|
+
"citations": {
|
|
256
|
+
"bib_count": len(bib_keys),
|
|
257
|
+
"cited_count": len(cited_keys),
|
|
258
|
+
"missing_keys": {k: [f"{f}:{l}" for f, l in locs] for k, locs in missing_keys.items()},
|
|
259
|
+
"orphan_keys": list(orphan_keys),
|
|
260
|
+
},
|
|
261
|
+
"images": {
|
|
262
|
+
"total": total_imgs,
|
|
263
|
+
"broken": [{"path": p, "file": f, "line": l} for p, f, l in broken_imgs],
|
|
264
|
+
},
|
|
265
|
+
"anomalies": [{"file": f, "line": l, "message": msg} for f, l, msg in anomalies],
|
|
266
|
+
}
|
|
267
|
+
print(json.dumps(result, indent=2))
|
|
268
|
+
return 1 if errors > 0 else 0
|
|
196
269
|
|
|
197
270
|
# Skor Kesehatan
|
|
198
271
|
print(" ========================================================")
|
package/scripts/report-stats.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"""report-stats.py: Menghitung statistik komprehensif dokumen laporan akademik."""
|
|
3
3
|
|
|
4
4
|
import glob
|
|
5
|
+
import json
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
8
|
import sys
|
|
@@ -55,8 +56,18 @@ def count_file_stats(filepath):
|
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
def main():
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
json_mode = "--json" in sys.argv
|
|
60
|
+
pos_args = [a for a in sys.argv[1:] if not a.startswith("-")]
|
|
61
|
+
|
|
62
|
+
target_dir = os.getcwd()
|
|
63
|
+
if pos_args:
|
|
64
|
+
target_dir = os.path.abspath(pos_args[0])
|
|
65
|
+
elif not (os.path.isfile(os.path.join(target_dir, "metadata.yml")) or os.path.isdir(os.path.join(target_dir, "chapters"))):
|
|
66
|
+
pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
67
|
+
if os.path.isfile(os.path.join(pkg_root, "metadata.yml")):
|
|
68
|
+
target_dir = pkg_root
|
|
69
|
+
|
|
70
|
+
os.chdir(target_dir)
|
|
60
71
|
|
|
61
72
|
title = "Laporan Akademik"
|
|
62
73
|
preset = "standard"
|
|
@@ -116,6 +127,39 @@ def main():
|
|
|
116
127
|
sz = os.path.getsize("Laporan.docx")
|
|
117
128
|
docx_size = f"{sz / 1024:.1f} KB"
|
|
118
129
|
|
|
130
|
+
if json_mode:
|
|
131
|
+
result = {
|
|
132
|
+
"title": title,
|
|
133
|
+
"preset": preset,
|
|
134
|
+
"target_dir": os.getcwd(),
|
|
135
|
+
"chapters_count": len(chapter_files),
|
|
136
|
+
"total_words": total_words,
|
|
137
|
+
"total_chars": total_chars,
|
|
138
|
+
"est_pages": est_pages,
|
|
139
|
+
"read_mins": read_mins,
|
|
140
|
+
"headings": {
|
|
141
|
+
"h1": total_h1,
|
|
142
|
+
"h2": total_h2,
|
|
143
|
+
"h3": total_h3,
|
|
144
|
+
},
|
|
145
|
+
"elements": {
|
|
146
|
+
"images": total_images,
|
|
147
|
+
"tables": total_tables,
|
|
148
|
+
"equations": total_equations,
|
|
149
|
+
},
|
|
150
|
+
"citations": {
|
|
151
|
+
"unique_cited": len(all_citations),
|
|
152
|
+
"bib_entries": bib_count,
|
|
153
|
+
},
|
|
154
|
+
"compiled_files": {
|
|
155
|
+
"pdf": pdf_size,
|
|
156
|
+
"docx": docx_size,
|
|
157
|
+
},
|
|
158
|
+
"file_breakdown": [{"filename": f, "words": c} for f, c in file_breakdown],
|
|
159
|
+
}
|
|
160
|
+
print(json.dumps(result, indent=2))
|
|
161
|
+
return
|
|
162
|
+
|
|
119
163
|
print(f"{CYAN}{BOLD}")
|
|
120
164
|
print(" ========================================================")
|
|
121
165
|
print(" LAPORAN GENERATOR - STATISTIK DOKUMEN ")
|
package/scripts/test_scripts.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""test_scripts.py: Automated unit tests for Laporan Generator Python helper scripts."""
|
|
3
3
|
|
|
4
|
+
import json
|
|
4
5
|
import os
|
|
6
|
+
import subprocess
|
|
5
7
|
import sys
|
|
6
8
|
import unittest
|
|
7
9
|
|
|
@@ -36,16 +38,39 @@ class TestScanPreset(unittest.TestCase):
|
|
|
36
38
|
self.assertIn("UNIVERSITAS INDONESIA", inst)
|
|
37
39
|
|
|
38
40
|
|
|
41
|
+
def get_presets_dir():
|
|
42
|
+
for c in [
|
|
43
|
+
os.path.join(ROOT_DIR, "presets"),
|
|
44
|
+
os.path.join(ROOT_DIR, "resources", "presets"),
|
|
45
|
+
os.path.join(ROOT_DIR, "..", "..", "..", "presets"),
|
|
46
|
+
]:
|
|
47
|
+
if os.path.isdir(c):
|
|
48
|
+
return os.path.abspath(c)
|
|
49
|
+
return os.path.join(ROOT_DIR, "presets")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_project_root():
|
|
53
|
+
for c in [
|
|
54
|
+
ROOT_DIR,
|
|
55
|
+
os.path.join(ROOT_DIR, "resources"),
|
|
56
|
+
os.path.join(ROOT_DIR, "..", "..", ".."),
|
|
57
|
+
]:
|
|
58
|
+
if os.path.isfile(os.path.join(c, "metadata.yml")):
|
|
59
|
+
return os.path.abspath(c)
|
|
60
|
+
return ROOT_DIR
|
|
61
|
+
|
|
62
|
+
|
|
39
63
|
class TestValidatePreset(unittest.TestCase):
|
|
40
64
|
def test_parse_simple_yaml(self):
|
|
41
|
-
|
|
65
|
+
presets_dir = get_presets_dir()
|
|
66
|
+
test_yaml = os.path.join(presets_dir, "standard.yml")
|
|
42
67
|
self.assertTrue(os.path.exists(test_yaml))
|
|
43
68
|
data = validate_preset.parse_simple_yaml(test_yaml)
|
|
44
69
|
self.assertEqual(data.get("preset_id"), "standard")
|
|
45
70
|
self.assertEqual(data.get("margin_top"), "2cm")
|
|
46
71
|
|
|
47
72
|
def test_validate_all_presets(self):
|
|
48
|
-
presets_dir =
|
|
73
|
+
presets_dir = get_presets_dir()
|
|
49
74
|
for f in os.listdir(presets_dir):
|
|
50
75
|
if f.endswith(".yml"):
|
|
51
76
|
path = os.path.join(presets_dir, f)
|
|
@@ -128,7 +153,7 @@ report_stats = __import__("report-stats")
|
|
|
128
153
|
|
|
129
154
|
class TestReportStats(unittest.TestCase):
|
|
130
155
|
def test_count_file_stats_structure(self):
|
|
131
|
-
sample_md = os.path.join(
|
|
156
|
+
sample_md = os.path.join(get_project_root(), "cover.md")
|
|
132
157
|
if os.path.exists(sample_md):
|
|
133
158
|
stats = report_stats.count_file_stats(sample_md)
|
|
134
159
|
self.assertIsInstance(stats, dict)
|
|
@@ -136,6 +161,28 @@ class TestReportStats(unittest.TestCase):
|
|
|
136
161
|
self.assertIsInstance(stats["chars"], int)
|
|
137
162
|
self.assertGreaterEqual(stats["words"], 0)
|
|
138
163
|
|
|
164
|
+
def test_json_stats_execution(self):
|
|
165
|
+
proj_root = get_project_root()
|
|
166
|
+
cmd = [sys.executable, os.path.join(SCRIPTS_DIR, "report-stats.py"), proj_root, "--json"]
|
|
167
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, cwd=proj_root)
|
|
168
|
+
self.assertEqual(proc.returncode, 0)
|
|
169
|
+
data = json.loads(proc.stdout)
|
|
170
|
+
self.assertIn("total_words", data)
|
|
171
|
+
self.assertIn("preset", data)
|
|
172
|
+
self.assertIn("target_dir", data)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class TestReportDoctor(unittest.TestCase):
|
|
176
|
+
def test_json_doctor_execution(self):
|
|
177
|
+
proj_root = get_project_root()
|
|
178
|
+
cmd = [sys.executable, os.path.join(SCRIPTS_DIR, "report-doctor.py"), proj_root, "--json"]
|
|
179
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, cwd=proj_root)
|
|
180
|
+
data = json.loads(proc.stdout)
|
|
181
|
+
self.assertIn("status", data)
|
|
182
|
+
self.assertIn("dependencies", data)
|
|
183
|
+
self.assertIn("required_files", data)
|
|
184
|
+
self.assertIn("citations", data)
|
|
185
|
+
|
|
139
186
|
|
|
140
187
|
if __name__ == "__main__":
|
|
141
188
|
unittest.main(verbosity=2)
|