@laitszkin/apollo-toolkit 3.13.2 → 3.14.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.md +7 -7
- package/CHANGELOG.md +27 -0
- package/CLAUDE.md +8 -8
- package/analyse-app-logs/SKILL.md +3 -3
- package/bin/apollo-toolkit.ts +7 -0
- package/codex/codex-memory-manager/SKILL.md +2 -2
- package/codex/learn-skill-from-conversations/SKILL.md +3 -3
- package/dist/bin/apollo-toolkit.d.ts +2 -0
- package/dist/bin/apollo-toolkit.js +7 -0
- package/dist/lib/cli.d.ts +41 -0
- package/dist/lib/cli.js +655 -0
- package/dist/lib/installer.d.ts +59 -0
- package/dist/lib/installer.js +404 -0
- package/dist/lib/tool-runner.d.ts +19 -0
- package/dist/lib/tool-runner.js +536 -0
- package/dist/lib/tools/architecture.d.ts +2 -0
- package/dist/lib/tools/architecture.js +34 -0
- package/dist/lib/tools/create-specs.d.ts +2 -0
- package/dist/lib/tools/create-specs.js +175 -0
- package/dist/lib/tools/docs-to-voice.d.ts +2 -0
- package/dist/lib/tools/docs-to-voice.js +705 -0
- package/dist/lib/tools/enforce-video-aspect-ratio.d.ts +2 -0
- package/dist/lib/tools/enforce-video-aspect-ratio.js +312 -0
- package/dist/lib/tools/extract-conversations.d.ts +2 -0
- package/dist/lib/tools/extract-conversations.js +105 -0
- package/dist/lib/tools/extract-pdf-text.d.ts +2 -0
- package/dist/lib/tools/extract-pdf-text.js +92 -0
- package/dist/lib/tools/filter-logs.d.ts +2 -0
- package/dist/lib/tools/filter-logs.js +94 -0
- package/dist/lib/tools/find-github-issues.d.ts +2 -0
- package/dist/lib/tools/find-github-issues.js +176 -0
- package/dist/lib/tools/generate-storyboard-images.d.ts +2 -0
- package/dist/lib/tools/generate-storyboard-images.js +419 -0
- package/dist/lib/tools/log-cli-utils.d.ts +35 -0
- package/dist/lib/tools/log-cli-utils.js +233 -0
- package/dist/lib/tools/open-github-issue.d.ts +2 -0
- package/dist/lib/tools/open-github-issue.js +750 -0
- package/dist/lib/tools/read-github-issue.d.ts +2 -0
- package/dist/lib/tools/read-github-issue.js +134 -0
- package/dist/lib/tools/render-error-book.d.ts +2 -0
- package/dist/lib/tools/render-error-book.js +265 -0
- package/dist/lib/tools/render-katex.d.ts +2 -0
- package/dist/lib/tools/render-katex.js +294 -0
- package/dist/lib/tools/review-threads.d.ts +2 -0
- package/dist/lib/tools/review-threads.js +491 -0
- package/dist/lib/tools/search-logs.d.ts +2 -0
- package/dist/lib/tools/search-logs.js +164 -0
- package/dist/lib/tools/sync-memory-index.d.ts +2 -0
- package/dist/lib/tools/sync-memory-index.js +113 -0
- package/dist/lib/tools/validate-openai-agent-config.d.ts +2 -0
- package/dist/lib/tools/validate-openai-agent-config.js +184 -0
- package/dist/lib/tools/validate-skill-frontmatter.d.ts +2 -0
- package/dist/lib/tools/validate-skill-frontmatter.js +118 -0
- package/dist/lib/types.d.ts +82 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/updater.d.ts +34 -0
- package/dist/lib/updater.js +112 -0
- package/dist/lib/utils/format.d.ts +2 -0
- package/dist/lib/utils/format.js +6 -0
- package/dist/lib/utils/terminal.d.ts +12 -0
- package/dist/lib/utils/terminal.js +26 -0
- package/docs-to-voice/SKILL.md +0 -1
- package/generate-spec/SKILL.md +1 -1
- package/katex/SKILL.md +1 -2
- package/lib/cli.ts +780 -0
- package/lib/installer.ts +466 -0
- package/lib/tool-runner.ts +561 -0
- package/lib/tools/architecture.ts +34 -0
- package/lib/tools/create-specs.ts +204 -0
- package/lib/tools/docs-to-voice.ts +799 -0
- package/lib/tools/enforce-video-aspect-ratio.ts +368 -0
- package/lib/tools/extract-conversations.ts +114 -0
- package/lib/tools/extract-pdf-text.ts +99 -0
- package/lib/tools/filter-logs.ts +118 -0
- package/lib/tools/find-github-issues.ts +211 -0
- package/lib/tools/generate-storyboard-images.ts +455 -0
- package/lib/tools/log-cli-utils.ts +262 -0
- package/lib/tools/open-github-issue.ts +930 -0
- package/lib/tools/read-github-issue.ts +179 -0
- package/lib/tools/render-error-book.ts +300 -0
- package/lib/tools/render-katex.ts +325 -0
- package/lib/tools/review-threads.ts +590 -0
- package/lib/tools/search-logs.ts +200 -0
- package/lib/tools/sync-memory-index.ts +114 -0
- package/lib/tools/validate-openai-agent-config.ts +209 -0
- package/lib/tools/validate-skill-frontmatter.ts +124 -0
- package/lib/types.ts +90 -0
- package/lib/updater.ts +165 -0
- package/lib/utils/format.ts +7 -0
- package/lib/utils/terminal.ts +22 -0
- package/open-github-issue/SKILL.md +2 -2
- package/optimise-skill/SKILL.md +1 -1
- package/package.json +13 -4
- package/resources/project-architecture/assets/architecture.css +764 -0
- package/resources/project-architecture/assets/viewer.client.js +144 -0
- package/resources/project-architecture/index.html +42 -0
- package/review-spec-related-changes/SKILL.md +1 -1
- package/solve-issues-found-during-review/SKILL.md +2 -1
- package/tsconfig.json +28 -0
- package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/filter_logs_by_time.py +0 -64
- package/analyse-app-logs/scripts/log_cli_utils.py +0 -112
- package/analyse-app-logs/scripts/search_logs.py +0 -137
- package/analyse-app-logs/tests/test_filter_logs_by_time.py +0 -95
- package/analyse-app-logs/tests/test_search_logs.py +0 -100
- package/codex/codex-memory-manager/scripts/extract_recent_conversations.py +0 -369
- package/codex/codex-memory-manager/scripts/sync_memory_index.py +0 -130
- package/codex/codex-memory-manager/tests/test_extract_recent_conversations.py +0 -177
- package/codex/codex-memory-manager/tests/test_memory_template.py +0 -37
- package/codex/codex-memory-manager/tests/test_sync_memory_index.py +0 -84
- package/codex/learn-skill-from-conversations/scripts/extract_recent_conversations.py +0 -369
- package/codex/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +0 -177
- package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
- package/docs-to-voice/scripts/docs_to_voice.py +0 -1385
- package/docs-to-voice/scripts/docs_to_voice.sh +0 -11
- package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +0 -210
- package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +0 -115
- package/docs-to-voice/tests/test_docs_to_voice_settings.py +0 -43
- package/docs-to-voice/tests/test_docs_to_voice_shell_wrapper.py +0 -51
- package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +0 -57
- package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
- package/generate-spec/scripts/create-specs +0 -215
- package/generate-spec/tests/test_create_specs.py +0 -200
- package/init-project-html/scripts/architecture-bootstrap-render.js +0 -16
- package/init-project-html/scripts/architecture.js +0 -296
- package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
- package/katex/scripts/render_katex.py +0 -247
- package/katex/scripts/render_katex.sh +0 -11
- package/katex/tests/test_render_katex.py +0 -174
- package/learning-error-book/scripts/render_error_book_json_to_pdf.py +0 -590
- package/learning-error-book/tests/test_render_error_book_json_to_pdf.py +0 -134
- package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
- package/open-github-issue/scripts/open_github_issue.py +0 -705
- package/open-github-issue/tests/test_open_github_issue.py +0 -381
- package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +0 -763
- package/openai-text-to-image-storyboard/tests/test_generate_storyboard_images.py +0 -177
- package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
- package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
- package/read-github-issue/scripts/find_issues.py +0 -148
- package/read-github-issue/scripts/read_issue.py +0 -108
- package/read-github-issue/tests/test_find_issues.py +0 -127
- package/read-github-issue/tests/test_read_issue.py +0 -109
- package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
- package/resolve-review-comments/scripts/review_threads.py +0 -425
- package/resolve-review-comments/tests/test_review_threads.py +0 -74
- package/scripts/validate_openai_agent_config.py +0 -209
- package/scripts/validate_skill_frontmatter.py +0 -131
- package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
- package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +0 -350
- package/text-to-short-video/tests/test_enforce_video_aspect_ratio.py +0 -194
- package/weekly-financial-event-report/scripts/extract_pdf_text_pdfkit.swift +0 -99
- package/weekly-financial-event-report/tests/test_extract_pdf_text_pdfkit.py +0 -64
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readGitHubIssueHandler = readGitHubIssueHandler;
|
|
4
|
+
// @ts-nocheck
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const ISSUE_FIELDS = 'number,title,body,state,author,labels,assignees,comments,createdAt,updatedAt,closedAt,url';
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
const args = {
|
|
9
|
+
issue: null,
|
|
10
|
+
repo: null,
|
|
11
|
+
comments: false,
|
|
12
|
+
json: false,
|
|
13
|
+
};
|
|
14
|
+
let i = 0;
|
|
15
|
+
while (i < argv.length) {
|
|
16
|
+
const arg = argv[i];
|
|
17
|
+
switch (arg) {
|
|
18
|
+
case '--repo':
|
|
19
|
+
if (i + 1 < argv.length)
|
|
20
|
+
args.repo = argv[++i];
|
|
21
|
+
break;
|
|
22
|
+
case '--comments':
|
|
23
|
+
args.comments = true;
|
|
24
|
+
break;
|
|
25
|
+
case '--json':
|
|
26
|
+
args.json = true;
|
|
27
|
+
break;
|
|
28
|
+
default:
|
|
29
|
+
if (!arg.startsWith('-')) {
|
|
30
|
+
args.issue = arg;
|
|
31
|
+
}
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
return args;
|
|
37
|
+
}
|
|
38
|
+
function runGh(cmdArgs) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
(0, node_child_process_1.execFile)('gh', cmdArgs, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
41
|
+
if (error) {
|
|
42
|
+
resolve({
|
|
43
|
+
stdout: stdout || '',
|
|
44
|
+
stderr: stderr || '',
|
|
45
|
+
exitCode: error.status ?? 1,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
resolve({ stdout: stdout || '', stderr: stderr || '', exitCode: 0 });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function buildCommand(args) {
|
|
55
|
+
const cmd = [
|
|
56
|
+
'issue',
|
|
57
|
+
'view',
|
|
58
|
+
args.issue || '',
|
|
59
|
+
'--json',
|
|
60
|
+
ISSUE_FIELDS,
|
|
61
|
+
];
|
|
62
|
+
if (args.repo) {
|
|
63
|
+
cmd.push('--repo', args.repo);
|
|
64
|
+
}
|
|
65
|
+
return cmd;
|
|
66
|
+
}
|
|
67
|
+
function joinNames(items, key) {
|
|
68
|
+
if (!items)
|
|
69
|
+
return '-';
|
|
70
|
+
const values = items
|
|
71
|
+
.map((item) => String(item[key] || ''))
|
|
72
|
+
.filter(Boolean);
|
|
73
|
+
return values.length > 0 ? values.join(', ') : '-';
|
|
74
|
+
}
|
|
75
|
+
function printSummary(issue, includeComments, context) {
|
|
76
|
+
const { stdout } = context;
|
|
77
|
+
stdout.write(`Number: #${issue.number ?? ''}\n`);
|
|
78
|
+
stdout.write(`Title: ${issue.title ?? ''}\n`);
|
|
79
|
+
stdout.write(`State: ${issue.state ?? ''}\n`);
|
|
80
|
+
stdout.write(`URL: ${issue.url ?? ''}\n`);
|
|
81
|
+
const author = issue.author?.login ?? '-';
|
|
82
|
+
stdout.write(`Author: ${author}\n`);
|
|
83
|
+
stdout.write(`Labels: ${joinNames(issue.labels, 'name')}\n`);
|
|
84
|
+
stdout.write(`Assignees: ${joinNames(issue.assignees, 'login')}\n`);
|
|
85
|
+
stdout.write(`Created: ${issue.createdAt ?? ''}\n`);
|
|
86
|
+
stdout.write(`Updated: ${issue.updatedAt ?? ''}\n`);
|
|
87
|
+
stdout.write(`Closed: ${issue.closedAt ?? '-'}\n`);
|
|
88
|
+
stdout.write('\n');
|
|
89
|
+
stdout.write('Body:\n');
|
|
90
|
+
stdout.write(`${issue.body || '-'}\n`);
|
|
91
|
+
if (includeComments) {
|
|
92
|
+
const comments = issue.comments;
|
|
93
|
+
stdout.write('\n');
|
|
94
|
+
stdout.write(`Comments (${comments?.length ?? 0}):\n`);
|
|
95
|
+
if (!comments || comments.length === 0) {
|
|
96
|
+
stdout.write('-\n');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const comment of comments) {
|
|
100
|
+
const commentAuthor = comment.author?.login ?? '-';
|
|
101
|
+
const created = comment.createdAt ?? '';
|
|
102
|
+
const body = comment.body || '-';
|
|
103
|
+
stdout.write(`- [${created}] ${commentAuthor}: ${body}\n`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function readGitHubIssueHandler(argv, context) {
|
|
108
|
+
const { stdout, stderr } = context;
|
|
109
|
+
const args = parseArgs(argv);
|
|
110
|
+
if (!args.issue) {
|
|
111
|
+
stderr.write('Error: issue number or URL is required.\n');
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
const cmd = buildCommand(args);
|
|
115
|
+
const result = await runGh(cmd);
|
|
116
|
+
if (result.exitCode !== 0) {
|
|
117
|
+
stderr.write(result.stderr.trim() || 'gh issue view failed.\n');
|
|
118
|
+
return result.exitCode;
|
|
119
|
+
}
|
|
120
|
+
let issue;
|
|
121
|
+
try {
|
|
122
|
+
issue = JSON.parse(result.stdout);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
stderr.write('Error: unable to parse gh output as JSON.\n');
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
if (args.json) {
|
|
129
|
+
stdout.write(JSON.stringify(issue, null, 2) + '\n');
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
printSummary(issue, args.comments, context);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.renderErrorBookHandler = renderErrorBookHandler;
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
function safeText(value, defaultValue = '-') {
|
|
11
|
+
if (value === null || value === undefined)
|
|
12
|
+
return defaultValue;
|
|
13
|
+
if (typeof value === 'string')
|
|
14
|
+
return value.trim() || defaultValue;
|
|
15
|
+
if (typeof value === 'number')
|
|
16
|
+
return String(value);
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
const parts = value.map((v) => safeText(v, '')).filter(Boolean);
|
|
19
|
+
return parts.join(', ') || defaultValue;
|
|
20
|
+
}
|
|
21
|
+
return String(value);
|
|
22
|
+
}
|
|
23
|
+
function escapeHtml(text) {
|
|
24
|
+
return text
|
|
25
|
+
.replace(/&/g, '&')
|
|
26
|
+
.replace(/</g, '<')
|
|
27
|
+
.replace(/>/g, '>')
|
|
28
|
+
.replace(/"/g, '"')
|
|
29
|
+
.replace(/'/g, ''');
|
|
30
|
+
}
|
|
31
|
+
function buildHtmlContent(data) {
|
|
32
|
+
const title = safeText(data.title, 'Error Book');
|
|
33
|
+
const type = safeText(data.book_type, 'general');
|
|
34
|
+
const updated = safeText(data.last_updated);
|
|
35
|
+
const questions = Array.isArray(data.questions) ? data.questions : [];
|
|
36
|
+
let html = `<!DOCTYPE html>
|
|
37
|
+
<html lang="en">
|
|
38
|
+
<head>
|
|
39
|
+
<meta charset="utf-8">
|
|
40
|
+
<title>${escapeHtml(title)}</title>
|
|
41
|
+
<style>
|
|
42
|
+
@page { margin: 16mm; }
|
|
43
|
+
body { font-family: -apple-system, 'PingFang SC', 'Noto Sans CJK', Helvetica, Arial, sans-serif; font-size: 11pt; line-height: 1.55; color: #1F2937; }
|
|
44
|
+
h1 { font-size: 24pt; margin-bottom: 0; }
|
|
45
|
+
.subtitle { font-size: 11pt; color: #6B7280; margin-top: 4pt; }
|
|
46
|
+
h2 { font-size: 16pt; color: #0F766E; border-bottom: 1px solid #D1D5DB; padding-bottom: 4pt; margin-top: 20pt; }
|
|
47
|
+
h3 { font-size: 14pt; color: #1F2937; margin-top: 16pt; }
|
|
48
|
+
h4 { font-size: 11pt; color: #1F2937; margin-top: 12pt; margin-bottom: 4pt; }
|
|
49
|
+
table { border-collapse: collapse; width: 100%; margin: 8pt 0; font-size: 9pt; }
|
|
50
|
+
th { background: #0F766E; color: white; padding: 6pt 8pt; text-align: left; }
|
|
51
|
+
td { padding: 6pt 8pt; border: 1px solid #D1D5DB; vertical-align: top; }
|
|
52
|
+
tr:nth-child(even) td { background: #F8FAFC; }
|
|
53
|
+
.callout { background: #FFEDD5; border-left: 4px solid #9A3412; padding: 8pt 10pt; margin: 8pt 0; }
|
|
54
|
+
.meta { font-size: 10pt; color: #6B7280; }
|
|
55
|
+
.section { margin-top: 20pt; }
|
|
56
|
+
.question-block { page-break-inside: avoid; }
|
|
57
|
+
</style>
|
|
58
|
+
</head>
|
|
59
|
+
<body>
|
|
60
|
+
<h1>${escapeHtml(title)}</h1>
|
|
61
|
+
<p class="subtitle">Type: ${escapeHtml(type)} | Last updated: ${escapeHtml(updated)}</p>
|
|
62
|
+
<p class="subtitle">A structured review of mistakes, concepts, and corrections.</p>
|
|
63
|
+
`;
|
|
64
|
+
// Coverage scope table
|
|
65
|
+
html += '<h2>Coverage Scope</h2>\n<table><thead><tr><th>Source</th><th>Questions</th><th>Notes</th></tr></thead><tbody>\n';
|
|
66
|
+
const coverageScope = Array.isArray(data.coverage_scope) ? data.coverage_scope : [];
|
|
67
|
+
if (coverageScope.length === 0) {
|
|
68
|
+
html += '<tr><td>-</td><td>-</td><td>-</td></tr>\n';
|
|
69
|
+
}
|
|
70
|
+
for (const item of coverageScope) {
|
|
71
|
+
html += `<tr><td>${escapeHtml(safeText(item.source_path))}</td><td>${escapeHtml(safeText(item.included_questions))}</td><td>${escapeHtml(safeText(item.notes))}</td></tr>\n`;
|
|
72
|
+
}
|
|
73
|
+
html += '</tbody></table>\n';
|
|
74
|
+
// Mistake overview
|
|
75
|
+
html += '<h2>Common Mistake Types Overview</h2>\n';
|
|
76
|
+
const overview = Array.isArray(data.mistake_overview) ? data.mistake_overview : [];
|
|
77
|
+
if (overview.length === 0) {
|
|
78
|
+
html += '<p>No mistake overview provided.</p>\n';
|
|
79
|
+
}
|
|
80
|
+
for (const entry of overview) {
|
|
81
|
+
html += `<div style="background:#F8FAFC;border:1px solid #D1D5DB;padding:8pt 10pt;margin:6pt 0;">
|
|
82
|
+
<h4>${escapeHtml(safeText(entry.type))}</h4>
|
|
83
|
+
<p>${escapeHtml(safeText(entry.summary))}</p>
|
|
84
|
+
<p class="meta">Representative questions: ${escapeHtml(safeText(entry.representative_questions))}</p>
|
|
85
|
+
</div>\n`;
|
|
86
|
+
}
|
|
87
|
+
// Concept highlights
|
|
88
|
+
html += '<h2>Conceptual Mistake Highlights</h2>\n';
|
|
89
|
+
const concepts = Array.isArray(data.concept_highlights) ? data.concept_highlights : [];
|
|
90
|
+
if (concepts.length === 0) {
|
|
91
|
+
html += '<p>No concept highlights provided.</p>\n';
|
|
92
|
+
}
|
|
93
|
+
for (const concept of concepts) {
|
|
94
|
+
const checklist = Array.isArray(concept.checklist) ? concept.checklist : [];
|
|
95
|
+
html += `<h3>${escapeHtml(safeText(concept.name, 'Unnamed concept'))}</h3>
|
|
96
|
+
<table><tbody>
|
|
97
|
+
<tr><td style="background:#EEF2FF;width:22%;font-weight:bold;">Definition</td><td>${escapeHtml(safeText(concept.definition))}</td></tr>
|
|
98
|
+
<tr><td style="background:#EEF2FF;font-weight:bold;">Common misjudgment</td><td>${escapeHtml(safeText(concept.common_misjudgment))}</td></tr>
|
|
99
|
+
<tr><td style="background:#EEF2FF;font-weight:bold;">Checklist</td><td>${checklist.map((c) => `- ${escapeHtml(safeText(c))}`).join('<br>') || '-'}</td></tr>
|
|
100
|
+
</tbody></table>\n`;
|
|
101
|
+
}
|
|
102
|
+
// Questions
|
|
103
|
+
if (questions.length > 0) {
|
|
104
|
+
html += '<div style="page-break-before:always;"></div>\n';
|
|
105
|
+
html += '<h2>Mistake-by-Mistake Analysis & Solutions</h2>\n';
|
|
106
|
+
for (const q of questions) {
|
|
107
|
+
const qMeta = [
|
|
108
|
+
['Source', safeText(q.source_path)],
|
|
109
|
+
['Locator', safeText(q.page_or_locator)],
|
|
110
|
+
['User answer', safeText(q.user_answer)],
|
|
111
|
+
['Correct answer', safeText(q.correct_answer)],
|
|
112
|
+
['Mistake type', safeText(q.mistake_type)],
|
|
113
|
+
['Concepts', safeText(q.concepts)],
|
|
114
|
+
];
|
|
115
|
+
html += `<div class="question-block">
|
|
116
|
+
<h3>${escapeHtml(safeText(q.question_id, 'Unnamed question'))}</h3>
|
|
117
|
+
<table><tbody>
|
|
118
|
+
${qMeta.map(([label, val]) => `<tr><td style="background:#F8FAFC;width:18%;font-weight:bold;">${escapeHtml(label)}</td><td>${escapeHtml(val)}</td></tr>`).join('\n')}
|
|
119
|
+
</tbody></table>
|
|
120
|
+
<h4>Stem</h4>
|
|
121
|
+
<p>${escapeHtml(safeText(q.stem))}</p>
|
|
122
|
+
<h4>Why it was wrong</h4>
|
|
123
|
+
<p>${escapeHtml(safeText(q.why_wrong))}</p>
|
|
124
|
+
`;
|
|
125
|
+
// Correct solution steps
|
|
126
|
+
const steps = Array.isArray(q.correct_solution_steps) ? q.correct_solution_steps : [];
|
|
127
|
+
if (steps.length > 0) {
|
|
128
|
+
html += '<h4>Correct solution</h4>\n<ul>\n';
|
|
129
|
+
for (const step of steps) {
|
|
130
|
+
html += `<li>${escapeHtml(safeText(step))}</li>\n`;
|
|
131
|
+
}
|
|
132
|
+
html += '</ul>\n';
|
|
133
|
+
}
|
|
134
|
+
// Options table for MC questions
|
|
135
|
+
const options = Array.isArray(q.options) ? q.options : [];
|
|
136
|
+
if (options.length > 0) {
|
|
137
|
+
html += '<h4>Option-by-option reasoning</h4>\n<table><thead><tr><th>Option</th><th>Text</th><th>Verdict</th><th>Reason</th></tr></thead><tbody>\n';
|
|
138
|
+
for (const opt of options) {
|
|
139
|
+
html += `<tr><td>${escapeHtml(safeText(opt.label))}</td><td>${escapeHtml(safeText(opt.text))}</td><td>${escapeHtml(safeText(opt.verdict))}</td><td>${escapeHtml(safeText(opt.reason))}</td></tr>\n`;
|
|
140
|
+
}
|
|
141
|
+
html += '</tbody></table>\n';
|
|
142
|
+
}
|
|
143
|
+
// Step comparison for long questions
|
|
144
|
+
const stepComparison = Array.isArray(q.step_comparison) ? q.step_comparison : [];
|
|
145
|
+
if (stepComparison.length > 0) {
|
|
146
|
+
html += '<h4>Step-by-step comparison</h4>\n<table><thead><tr><th>Step</th><th>Expected</th><th>User</th><th>Gap</th><th>Fix</th></tr></thead><tbody>\n';
|
|
147
|
+
for (const sc of stepComparison) {
|
|
148
|
+
html += `<tr><td>${escapeHtml(safeText(sc.step_no))}</td><td>${escapeHtml(safeText(sc.expected_step))}</td><td>${escapeHtml(safeText(sc.user_step))}</td><td>${escapeHtml(safeText(sc.gap))}</td><td>${escapeHtml(safeText(sc.fix))}</td></tr>\n`;
|
|
149
|
+
}
|
|
150
|
+
html += '</tbody></table>\n';
|
|
151
|
+
}
|
|
152
|
+
html += '</div>\n';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
html += '\n</body>\n</html>\n';
|
|
156
|
+
return html;
|
|
157
|
+
}
|
|
158
|
+
async function renderErrorBookHandler(args, context) {
|
|
159
|
+
const stdout = context.stdout || process.stdout;
|
|
160
|
+
const stderr = context.stderr || process.stderr;
|
|
161
|
+
let inputFile = '';
|
|
162
|
+
let outputFile = '';
|
|
163
|
+
for (let i = 0; i < args.length; i++) {
|
|
164
|
+
const arg = args[i];
|
|
165
|
+
if (arg === '--help' || arg === '-h') {
|
|
166
|
+
stdout.write(`Usage: apltk render-error-book --input <json> --output <pdf>
|
|
167
|
+
|
|
168
|
+
Options:
|
|
169
|
+
--input Input JSON file path
|
|
170
|
+
--output Output PDF file path
|
|
171
|
+
`);
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
if (arg === '--input') {
|
|
175
|
+
inputFile = args[++i] || '';
|
|
176
|
+
}
|
|
177
|
+
else if (arg === '--output') {
|
|
178
|
+
outputFile = args[++i] || '';
|
|
179
|
+
}
|
|
180
|
+
else if (!inputFile && !arg.startsWith('-')) {
|
|
181
|
+
// positional: first is input, second is output (backward compat)
|
|
182
|
+
if (!inputFile) {
|
|
183
|
+
inputFile = arg;
|
|
184
|
+
}
|
|
185
|
+
else if (!outputFile) {
|
|
186
|
+
outputFile = arg;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!inputFile) {
|
|
191
|
+
stderr.write('Error: --input is required.\n');
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
if (!outputFile) {
|
|
195
|
+
stderr.write('Error: --output is required.\n');
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
198
|
+
const inputPath = node_path_1.default.resolve(inputFile);
|
|
199
|
+
const outputPath = node_path_1.default.resolve(outputFile);
|
|
200
|
+
if (!node_fs_1.default.existsSync(inputPath)) {
|
|
201
|
+
stderr.write(`Error: Input file not found: ${inputPath}\n`);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
let data;
|
|
205
|
+
try {
|
|
206
|
+
const raw = node_fs_1.default.readFileSync(inputPath, 'utf-8');
|
|
207
|
+
data = JSON.parse(raw);
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
const msg = err instanceof Error ? err.message : 'Invalid JSON';
|
|
211
|
+
stderr.write(`Error: Failed to parse input JSON: ${msg}\n`);
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
// Generate HTML
|
|
215
|
+
const htmlContent = buildHtmlContent(data);
|
|
216
|
+
// Try to convert to PDF
|
|
217
|
+
const tmpHtmlFile = node_path_1.default.join(node_fs_1.default.mkdtempSync('error-book-'), 'output.html');
|
|
218
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(tmpHtmlFile), { recursive: true });
|
|
219
|
+
node_fs_1.default.writeFileSync(tmpHtmlFile, htmlContent, 'utf-8');
|
|
220
|
+
let converted = false;
|
|
221
|
+
// Try pandoc first
|
|
222
|
+
try {
|
|
223
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(outputPath), { recursive: true });
|
|
224
|
+
(0, node_child_process_1.execSync)(`pandoc "${tmpHtmlFile}" -o "${outputPath}" --pdf-engine=weasyprint 2>/dev/null || ` +
|
|
225
|
+
`pandoc "${tmpHtmlFile}" -o "${outputPath}" --pdf-engine=wkhtmltopdf 2>/dev/null || ` +
|
|
226
|
+
`pandoc "${tmpHtmlFile}" -o "${outputPath}"`, { stdio: 'ignore', timeout: 60000 });
|
|
227
|
+
if (node_fs_1.default.existsSync(outputPath) && node_fs_1.default.statSync(outputPath).size > 0) {
|
|
228
|
+
converted = true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Fall through
|
|
233
|
+
}
|
|
234
|
+
// Try wkhtmltopdf
|
|
235
|
+
if (!converted) {
|
|
236
|
+
try {
|
|
237
|
+
(0, node_child_process_1.execSync)(`wkhtmltopdf "${tmpHtmlFile}" "${outputPath}"`, { stdio: 'ignore', timeout: 60000 });
|
|
238
|
+
if (node_fs_1.default.existsSync(outputPath) && node_fs_1.default.statSync(outputPath).size > 0) {
|
|
239
|
+
converted = true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// Fall through
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Clean up temp file
|
|
247
|
+
try {
|
|
248
|
+
node_fs_1.default.unlinkSync(tmpHtmlFile);
|
|
249
|
+
node_fs_1.default.rmdirSync(node_path_1.default.dirname(tmpHtmlFile));
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// ignore cleanup errors
|
|
253
|
+
}
|
|
254
|
+
if (converted) {
|
|
255
|
+
stdout.write(`${outputPath}\n`);
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
// Fallback: write HTML and report
|
|
259
|
+
const htmlOutputPath = outputPath.replace(/\.pdf$/i, '.html') + '.html';
|
|
260
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(htmlOutputPath), { recursive: true });
|
|
261
|
+
node_fs_1.default.writeFileSync(htmlOutputPath, htmlContent, 'utf-8');
|
|
262
|
+
stdout.write(`${htmlOutputPath}\n`);
|
|
263
|
+
stderr.write('Warning: No PDF converter found (pandoc/wkhtmltopdf). HTML was written instead.\n');
|
|
264
|
+
return 0;
|
|
265
|
+
}
|