@laitszkin/apollo-toolkit 2.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.
- package/AGENTS.md +62 -0
- package/CHANGELOG.md +100 -0
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/align-project-documents/SKILL.md +94 -0
- package/align-project-documents/agents/openai.yaml +4 -0
- package/analyse-app-logs/LICENSE +21 -0
- package/analyse-app-logs/README.md +126 -0
- package/analyse-app-logs/SKILL.md +121 -0
- package/analyse-app-logs/agents/openai.yaml +4 -0
- package/analyse-app-logs/references/investigation-checklist.md +58 -0
- package/analyse-app-logs/references/log-signal-patterns.md +52 -0
- package/answering-questions-with-research/SKILL.md +46 -0
- package/answering-questions-with-research/agents/openai.yaml +4 -0
- package/bin/apollo-toolkit.js +7 -0
- package/commit-and-push/LICENSE +21 -0
- package/commit-and-push/README.md +26 -0
- package/commit-and-push/SKILL.md +70 -0
- package/commit-and-push/agents/openai.yaml +4 -0
- package/commit-and-push/references/branch-naming.md +15 -0
- package/commit-and-push/references/commit-messages.md +19 -0
- package/deep-research-topics/LICENSE +21 -0
- package/deep-research-topics/README.md +43 -0
- package/deep-research-topics/SKILL.md +84 -0
- package/deep-research-topics/agents/openai.yaml +4 -0
- package/develop-new-features/LICENSE +21 -0
- package/develop-new-features/README.md +52 -0
- package/develop-new-features/SKILL.md +105 -0
- package/develop-new-features/agents/openai.yaml +4 -0
- package/develop-new-features/references/testing-e2e.md +35 -0
- package/develop-new-features/references/testing-integration.md +42 -0
- package/develop-new-features/references/testing-property-based.md +44 -0
- package/develop-new-features/references/testing-unit.md +37 -0
- package/discover-edge-cases/CHANGELOG.md +19 -0
- package/discover-edge-cases/LICENSE +21 -0
- package/discover-edge-cases/README.md +87 -0
- package/discover-edge-cases/SKILL.md +124 -0
- package/discover-edge-cases/agents/openai.yaml +4 -0
- package/discover-edge-cases/references/architecture-edge-cases.md +41 -0
- package/discover-edge-cases/references/code-edge-cases.md +46 -0
- package/docs-to-voice/.env.example +106 -0
- package/docs-to-voice/CHANGELOG.md +71 -0
- package/docs-to-voice/LICENSE +21 -0
- package/docs-to-voice/README.md +118 -0
- package/docs-to-voice/SKILL.md +107 -0
- package/docs-to-voice/agents/openai.yaml +4 -0
- package/docs-to-voice/scripts/docs_to_voice.py +1385 -0
- package/docs-to-voice/scripts/docs_to_voice.sh +11 -0
- package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +210 -0
- package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +115 -0
- package/docs-to-voice/tests/test_docs_to_voice_settings.py +43 -0
- package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +57 -0
- package/enhance-existing-features/CHANGELOG.md +35 -0
- package/enhance-existing-features/LICENSE +21 -0
- package/enhance-existing-features/README.md +54 -0
- package/enhance-existing-features/SKILL.md +120 -0
- package/enhance-existing-features/agents/openai.yaml +4 -0
- package/enhance-existing-features/references/e2e-tests.md +25 -0
- package/enhance-existing-features/references/integration-tests.md +30 -0
- package/enhance-existing-features/references/property-based-tests.md +33 -0
- package/enhance-existing-features/references/unit-tests.md +29 -0
- package/feature-propose/LICENSE +21 -0
- package/feature-propose/README.md +23 -0
- package/feature-propose/SKILL.md +107 -0
- package/feature-propose/agents/openai.yaml +4 -0
- package/feature-propose/references/enhancement-features.md +25 -0
- package/feature-propose/references/important-features.md +25 -0
- package/feature-propose/references/mvp-features.md +25 -0
- package/feature-propose/references/performance-features.md +25 -0
- package/financial-research/SKILL.md +208 -0
- package/financial-research/agents/openai.yaml +4 -0
- package/financial-research/assets/weekly_market_report_template.md +45 -0
- package/fix-github-issues/SKILL.md +98 -0
- package/fix-github-issues/agents/openai.yaml +4 -0
- package/fix-github-issues/scripts/list_issues.py +148 -0
- package/fix-github-issues/tests/test_list_issues.py +127 -0
- package/generate-spec/LICENSE +21 -0
- package/generate-spec/README.md +61 -0
- package/generate-spec/SKILL.md +96 -0
- package/generate-spec/agents/openai.yaml +4 -0
- package/generate-spec/references/templates/checklist.md +78 -0
- package/generate-spec/references/templates/spec.md +55 -0
- package/generate-spec/references/templates/tasks.md +35 -0
- package/generate-spec/scripts/create-specs +123 -0
- package/harden-app-security/CHANGELOG.md +27 -0
- package/harden-app-security/LICENSE +21 -0
- package/harden-app-security/README.md +46 -0
- package/harden-app-security/SKILL.md +127 -0
- package/harden-app-security/agents/openai.yaml +4 -0
- package/harden-app-security/references/agent-attack-catalog.md +117 -0
- package/harden-app-security/references/common-software-attack-catalog.md +168 -0
- package/harden-app-security/references/red-team-extreme-scenarios.md +81 -0
- package/harden-app-security/references/risk-checklist.md +78 -0
- package/harden-app-security/references/security-test-patterns-agent.md +101 -0
- package/harden-app-security/references/security-test-patterns-finance.md +88 -0
- package/harden-app-security/references/test-snippets.md +73 -0
- package/improve-observability/SKILL.md +114 -0
- package/improve-observability/agents/openai.yaml +4 -0
- package/learn-skill-from-conversations/CHANGELOG.md +15 -0
- package/learn-skill-from-conversations/LICENSE +22 -0
- package/learn-skill-from-conversations/README.md +47 -0
- package/learn-skill-from-conversations/SKILL.md +85 -0
- package/learn-skill-from-conversations/agents/openai.yaml +4 -0
- package/learn-skill-from-conversations/scripts/extract_recent_conversations.py +369 -0
- package/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +176 -0
- package/learning-error-book/SKILL.md +112 -0
- package/learning-error-book/agents/openai.yaml +4 -0
- package/learning-error-book/assets/error_book_template.md +66 -0
- package/learning-error-book/scripts/render_markdown_to_pdf.py +367 -0
- package/lib/cli.js +338 -0
- package/lib/installer.js +225 -0
- package/maintain-project-constraints/SKILL.md +109 -0
- package/maintain-project-constraints/agents/openai.yaml +4 -0
- package/maintain-skill-catalog/README.md +18 -0
- package/maintain-skill-catalog/SKILL.md +66 -0
- package/maintain-skill-catalog/agents/openai.yaml +4 -0
- package/novel-to-short-video/CHANGELOG.md +53 -0
- package/novel-to-short-video/LICENSE +21 -0
- package/novel-to-short-video/README.md +63 -0
- package/novel-to-short-video/SKILL.md +233 -0
- package/novel-to-short-video/agents/openai.yaml +4 -0
- package/novel-to-short-video/references/plan-template.md +71 -0
- package/novel-to-short-video/references/roles-json.md +41 -0
- package/open-github-issue/LICENSE +21 -0
- package/open-github-issue/README.md +97 -0
- package/open-github-issue/SKILL.md +119 -0
- package/open-github-issue/agents/openai.yaml +4 -0
- package/open-github-issue/scripts/open_github_issue.py +380 -0
- package/open-github-issue/tests/test_open_github_issue.py +159 -0
- package/open-source-pr-workflow/CHANGELOG.md +32 -0
- package/open-source-pr-workflow/LICENSE +21 -0
- package/open-source-pr-workflow/README.md +23 -0
- package/open-source-pr-workflow/SKILL.md +123 -0
- package/open-source-pr-workflow/agents/openai.yaml +4 -0
- package/openai-text-to-image-storyboard/.env.example +10 -0
- package/openai-text-to-image-storyboard/CHANGELOG.md +49 -0
- package/openai-text-to-image-storyboard/LICENSE +21 -0
- package/openai-text-to-image-storyboard/README.md +99 -0
- package/openai-text-to-image-storyboard/SKILL.md +107 -0
- package/openai-text-to-image-storyboard/agents/openai.yaml +4 -0
- package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +763 -0
- package/package.json +36 -0
- package/record-spending/SKILL.md +113 -0
- package/record-spending/agents/openai.yaml +4 -0
- package/record-spending/references/account-format.md +33 -0
- package/record-spending/references/workbook-layout.md +84 -0
- package/resolve-review-comments/SKILL.md +122 -0
- package/resolve-review-comments/agents/openai.yaml +4 -0
- package/resolve-review-comments/references/adoption-criteria.md +23 -0
- package/resolve-review-comments/scripts/review_threads.py +425 -0
- package/resolve-review-comments/tests/test_review_threads.py +74 -0
- package/review-change-set/LICENSE +21 -0
- package/review-change-set/README.md +55 -0
- package/review-change-set/SKILL.md +103 -0
- package/review-change-set/agents/openai.yaml +4 -0
- package/review-codebases/LICENSE +21 -0
- package/review-codebases/README.md +67 -0
- package/review-codebases/SKILL.md +109 -0
- package/review-codebases/agents/openai.yaml +4 -0
- package/scripts/install_skills.ps1 +283 -0
- package/scripts/install_skills.sh +262 -0
- package/scripts/validate_openai_agent_config.py +194 -0
- package/scripts/validate_skill_frontmatter.py +110 -0
- package/specs-to-project-docs/LICENSE +21 -0
- package/specs-to-project-docs/README.md +57 -0
- package/specs-to-project-docs/SKILL.md +111 -0
- package/specs-to-project-docs/agents/openai.yaml +4 -0
- package/specs-to-project-docs/references/templates/architecture.md +29 -0
- package/specs-to-project-docs/references/templates/configuration.md +29 -0
- package/specs-to-project-docs/references/templates/developer-guide.md +33 -0
- package/specs-to-project-docs/references/templates/docs-index.md +39 -0
- package/specs-to-project-docs/references/templates/features.md +25 -0
- package/specs-to-project-docs/references/templates/getting-started.md +38 -0
- package/specs-to-project-docs/references/templates/readme.md +49 -0
- package/systematic-debug/LICENSE +21 -0
- package/systematic-debug/README.md +81 -0
- package/systematic-debug/SKILL.md +59 -0
- package/systematic-debug/agents/openai.yaml +4 -0
- package/text-to-short-video/.env.example +36 -0
- package/text-to-short-video/LICENSE +21 -0
- package/text-to-short-video/README.md +82 -0
- package/text-to-short-video/SKILL.md +221 -0
- package/text-to-short-video/agents/openai.yaml +4 -0
- package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +350 -0
- package/version-release/CHANGELOG.md +53 -0
- package/version-release/LICENSE +21 -0
- package/version-release/README.md +28 -0
- package/version-release/SKILL.md +94 -0
- package/version-release/agents/openai.yaml +4 -0
- package/version-release/references/branch-naming.md +15 -0
- package/version-release/references/changelog-writing.md +8 -0
- package/version-release/references/commit-messages.md +19 -0
- package/version-release/references/readme-writing.md +12 -0
- package/version-release/references/semantic-versioning.md +12 -0
- package/video-production/CHANGELOG.md +104 -0
- package/video-production/LICENSE +18 -0
- package/video-production/README.md +68 -0
- package/video-production/SKILL.md +213 -0
- package/video-production/agents/openai.yaml +4 -0
- package/video-production/references/plan-template.md +54 -0
- package/video-production/references/roles-json.md +41 -0
- package/weekly-financial-event-report/SKILL.md +195 -0
- package/weekly-financial-event-report/agents/openai.yaml +4 -0
- package/weekly-financial-event-report/assets/financial_event_report_template.md +53 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Render a small Markdown subset to PDF using ReportLab.
|
|
4
|
+
|
|
5
|
+
Why not use pandoc/markdown/weasyprint?
|
|
6
|
+
- This repo environment may not have those installed.
|
|
7
|
+
- ReportLab is already available and works offline.
|
|
8
|
+
|
|
9
|
+
Supported Markdown subset:
|
|
10
|
+
- Headings: # / ## / ###
|
|
11
|
+
- Unordered lists: "- " or "* "
|
|
12
|
+
- Ordered lists: "1. "
|
|
13
|
+
- Fenced code blocks: ``` (optional language ignored)
|
|
14
|
+
- Inline: **bold**, *italic*, `code`
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Iterable, List, Optional, Tuple
|
|
24
|
+
|
|
25
|
+
from reportlab.lib.pagesizes import A4, letter
|
|
26
|
+
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
|
27
|
+
from reportlab.lib.units import mm
|
|
28
|
+
from reportlab.pdfbase import pdfmetrics
|
|
29
|
+
from reportlab.pdfbase.ttfonts import TTFont
|
|
30
|
+
from reportlab.platypus import (
|
|
31
|
+
SimpleDocTemplate,
|
|
32
|
+
Spacer,
|
|
33
|
+
Paragraph,
|
|
34
|
+
Preformatted,
|
|
35
|
+
ListFlowable,
|
|
36
|
+
ListItem,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _read_text(path: str) -> str:
|
|
41
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
42
|
+
return f.read()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _ensure_parent_dir(path: str) -> None:
|
|
46
|
+
parent = os.path.dirname(os.path.abspath(path))
|
|
47
|
+
if parent:
|
|
48
|
+
os.makedirs(parent, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _detect_cjk_font_path(user_font_path: Optional[str]) -> Tuple[str, int]:
|
|
52
|
+
"""
|
|
53
|
+
Returns (font_path, subfont_index).
|
|
54
|
+
|
|
55
|
+
ReportLab can load TTC via subfontIndex. We default to 0.
|
|
56
|
+
"""
|
|
57
|
+
if user_font_path:
|
|
58
|
+
return user_font_path, 0
|
|
59
|
+
|
|
60
|
+
candidates = [
|
|
61
|
+
# macOS common CJK fonts
|
|
62
|
+
"/System/Library/Fonts/PingFang.ttc",
|
|
63
|
+
"/System/Library/Fonts/STHeiti Light.ttc",
|
|
64
|
+
"/System/Library/Fonts/STHeiti Medium.ttc",
|
|
65
|
+
"/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc",
|
|
66
|
+
"/System/Library/Fonts/ヒラギノ角ゴシック W4.ttc",
|
|
67
|
+
"/System/Library/Fonts/ヒラギノ角ゴシック W5.ttc",
|
|
68
|
+
"/System/Library/Fonts/ヒラギノ明朝 ProN.ttc",
|
|
69
|
+
# Some systems may have these
|
|
70
|
+
"/Library/Fonts/Arial Unicode.ttf",
|
|
71
|
+
]
|
|
72
|
+
for p in candidates:
|
|
73
|
+
if os.path.exists(p):
|
|
74
|
+
return p, 0
|
|
75
|
+
raise SystemExit(
|
|
76
|
+
"No CJK font found. Re-run with --font-path pointing to a .ttf/.otf/.ttc font file."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _register_fonts(font_path: str, subfont_index: int) -> Tuple[str, str]:
|
|
81
|
+
"""
|
|
82
|
+
Registers a CJK-capable font for body text. Returns (body_font_name, code_font_name).
|
|
83
|
+
"""
|
|
84
|
+
body_font_name = "ErrorBookBodyFont"
|
|
85
|
+
# Avoid double registration errors when the script is invoked multiple times.
|
|
86
|
+
if body_font_name not in pdfmetrics.getRegisteredFontNames():
|
|
87
|
+
pdfmetrics.registerFont(TTFont(body_font_name, font_path, subfontIndex=subfont_index))
|
|
88
|
+
|
|
89
|
+
# Code blocks: built-in Courier is fine for ASCII code and Markdown symbols.
|
|
90
|
+
code_font_name = "Courier"
|
|
91
|
+
return body_font_name, code_font_name
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _xml_escape(s: str) -> str:
|
|
95
|
+
return (
|
|
96
|
+
s.replace("&", "&")
|
|
97
|
+
.replace("<", "<")
|
|
98
|
+
.replace(">", ">")
|
|
99
|
+
.replace('"', """)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
_RE_BOLD = re.compile(r"\*\*(.+?)\*\*")
|
|
104
|
+
_RE_ITALIC = re.compile(r"(?<!\*)\*(?!\s)(.+?)(?<!\s)\*(?!\*)")
|
|
105
|
+
_RE_CODE = re.compile(r"`([^`]+?)`")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _inline_to_rl_markup(text: str, code_font: str) -> str:
|
|
109
|
+
"""
|
|
110
|
+
Convert a subset of inline Markdown to ReportLab Paragraph markup.
|
|
111
|
+
"""
|
|
112
|
+
text = _xml_escape(text)
|
|
113
|
+
|
|
114
|
+
# Inline code first to avoid formatting inside code spans.
|
|
115
|
+
def repl_code(m: re.Match) -> str:
|
|
116
|
+
# `text` has already been XML-escaped, so do not escape again here;
|
|
117
|
+
# otherwise sequences like "<" become "&lt;" inside code spans.
|
|
118
|
+
return f'<font face="{code_font}">{m.group(1)}</font>'
|
|
119
|
+
|
|
120
|
+
text = _RE_CODE.sub(repl_code, text)
|
|
121
|
+
text = _RE_BOLD.sub(r"<b>\1</b>", text)
|
|
122
|
+
text = _RE_ITALIC.sub(r"<i>\1</i>", text)
|
|
123
|
+
return text
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class Block:
|
|
128
|
+
kind: str
|
|
129
|
+
lines: Tuple[str, ...]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _parse_blocks(md: str) -> List[Block]:
|
|
133
|
+
"""
|
|
134
|
+
Parse Markdown into coarse blocks to keep rendering predictable.
|
|
135
|
+
"""
|
|
136
|
+
lines = md.splitlines()
|
|
137
|
+
blocks: List[Block] = []
|
|
138
|
+
|
|
139
|
+
i = 0
|
|
140
|
+
in_code = False
|
|
141
|
+
code_buf: List[str] = []
|
|
142
|
+
para_buf: List[str] = []
|
|
143
|
+
list_buf: List[str] = []
|
|
144
|
+
list_kind: Optional[str] = None # "ul" or "ol"
|
|
145
|
+
|
|
146
|
+
def flush_para() -> None:
|
|
147
|
+
nonlocal para_buf
|
|
148
|
+
if para_buf:
|
|
149
|
+
blocks.append(Block("para", tuple(para_buf)))
|
|
150
|
+
para_buf = []
|
|
151
|
+
|
|
152
|
+
def flush_list() -> None:
|
|
153
|
+
nonlocal list_buf, list_kind
|
|
154
|
+
if list_buf and list_kind:
|
|
155
|
+
blocks.append(Block(list_kind, tuple(list_buf)))
|
|
156
|
+
list_buf = []
|
|
157
|
+
list_kind = None
|
|
158
|
+
|
|
159
|
+
while i < len(lines):
|
|
160
|
+
line = lines[i]
|
|
161
|
+
|
|
162
|
+
if line.strip().startswith("```"):
|
|
163
|
+
if in_code:
|
|
164
|
+
# end code
|
|
165
|
+
blocks.append(Block("code", tuple(code_buf)))
|
|
166
|
+
code_buf = []
|
|
167
|
+
in_code = False
|
|
168
|
+
else:
|
|
169
|
+
# start code
|
|
170
|
+
flush_para()
|
|
171
|
+
flush_list()
|
|
172
|
+
in_code = True
|
|
173
|
+
code_buf = []
|
|
174
|
+
i += 1
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
if in_code:
|
|
178
|
+
code_buf.append(line.rstrip("\n"))
|
|
179
|
+
i += 1
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
# Blank line breaks paragraphs/lists
|
|
183
|
+
if not line.strip():
|
|
184
|
+
flush_para()
|
|
185
|
+
flush_list()
|
|
186
|
+
i += 1
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
# Headings
|
|
190
|
+
if line.startswith("#"):
|
|
191
|
+
flush_para()
|
|
192
|
+
flush_list()
|
|
193
|
+
m = re.match(r"^(#{1,6})\s+(.*)$", line)
|
|
194
|
+
if m:
|
|
195
|
+
level = len(m.group(1))
|
|
196
|
+
blocks.append(Block(f"h{level}", (m.group(2).strip(),)))
|
|
197
|
+
i += 1
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
# Lists (simple, non-nested)
|
|
201
|
+
m_ul = re.match(r"^\s*[-\*]\s+(.*)$", line)
|
|
202
|
+
m_ol = re.match(r"^\s*\d+\.\s+(.*)$", line)
|
|
203
|
+
if m_ul:
|
|
204
|
+
flush_para()
|
|
205
|
+
if list_kind not in (None, "ul"):
|
|
206
|
+
flush_list()
|
|
207
|
+
list_kind = "ul"
|
|
208
|
+
list_buf.append(m_ul.group(1))
|
|
209
|
+
i += 1
|
|
210
|
+
continue
|
|
211
|
+
if m_ol:
|
|
212
|
+
flush_para()
|
|
213
|
+
if list_kind not in (None, "ol"):
|
|
214
|
+
flush_list()
|
|
215
|
+
list_kind = "ol"
|
|
216
|
+
list_buf.append(m_ol.group(1))
|
|
217
|
+
i += 1
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
# Default: paragraph line (we keep soft-wrapping by joining with spaces)
|
|
221
|
+
flush_list()
|
|
222
|
+
para_buf.append(line.strip())
|
|
223
|
+
i += 1
|
|
224
|
+
|
|
225
|
+
# EOF flush
|
|
226
|
+
if in_code:
|
|
227
|
+
blocks.append(Block("code", tuple(code_buf)))
|
|
228
|
+
flush_para()
|
|
229
|
+
flush_list()
|
|
230
|
+
|
|
231
|
+
return blocks
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _build_story(
|
|
235
|
+
blocks: Iterable[Block],
|
|
236
|
+
body_font: str,
|
|
237
|
+
code_font: str,
|
|
238
|
+
base_font_size: int,
|
|
239
|
+
) -> List[object]:
|
|
240
|
+
styles = getSampleStyleSheet()
|
|
241
|
+
|
|
242
|
+
normal = ParagraphStyle(
|
|
243
|
+
"ErrorBookNormal",
|
|
244
|
+
parent=styles["Normal"],
|
|
245
|
+
fontName=body_font,
|
|
246
|
+
fontSize=base_font_size,
|
|
247
|
+
leading=int(base_font_size * 1.45),
|
|
248
|
+
spaceAfter=6,
|
|
249
|
+
)
|
|
250
|
+
h1 = ParagraphStyle(
|
|
251
|
+
"ErrorBookH1",
|
|
252
|
+
parent=styles["Heading1"],
|
|
253
|
+
fontName=body_font,
|
|
254
|
+
fontSize=base_font_size + 8,
|
|
255
|
+
leading=int((base_font_size + 8) * 1.2),
|
|
256
|
+
spaceBefore=10,
|
|
257
|
+
spaceAfter=8,
|
|
258
|
+
)
|
|
259
|
+
h2 = ParagraphStyle(
|
|
260
|
+
"ErrorBookH2",
|
|
261
|
+
parent=styles["Heading2"],
|
|
262
|
+
fontName=body_font,
|
|
263
|
+
fontSize=base_font_size + 4,
|
|
264
|
+
leading=int((base_font_size + 4) * 1.2),
|
|
265
|
+
spaceBefore=10,
|
|
266
|
+
spaceAfter=6,
|
|
267
|
+
)
|
|
268
|
+
h3 = ParagraphStyle(
|
|
269
|
+
"ErrorBookH3",
|
|
270
|
+
parent=styles["Heading3"],
|
|
271
|
+
fontName=body_font,
|
|
272
|
+
fontSize=base_font_size + 2,
|
|
273
|
+
leading=int((base_font_size + 2) * 1.2),
|
|
274
|
+
spaceBefore=8,
|
|
275
|
+
spaceAfter=4,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
story: List[object] = []
|
|
279
|
+
for b in blocks:
|
|
280
|
+
if b.kind == "para":
|
|
281
|
+
text = " ".join(b.lines)
|
|
282
|
+
story.append(Paragraph(_inline_to_rl_markup(text, code_font), normal))
|
|
283
|
+
elif b.kind in ("h1", "h2", "h3"):
|
|
284
|
+
text = b.lines[0] if b.lines else ""
|
|
285
|
+
style = {"h1": h1, "h2": h2, "h3": h3}[b.kind]
|
|
286
|
+
story.append(Paragraph(_inline_to_rl_markup(text, code_font), style))
|
|
287
|
+
elif b.kind.startswith("h"):
|
|
288
|
+
# Fallback: treat other heading levels as h3
|
|
289
|
+
text = b.lines[0] if b.lines else ""
|
|
290
|
+
story.append(Paragraph(_inline_to_rl_markup(text, code_font), h3))
|
|
291
|
+
elif b.kind == "code":
|
|
292
|
+
code_text = "\n".join(b.lines)
|
|
293
|
+
story.append(
|
|
294
|
+
Preformatted(
|
|
295
|
+
code_text,
|
|
296
|
+
ParagraphStyle(
|
|
297
|
+
"ErrorBookCode",
|
|
298
|
+
fontName=code_font,
|
|
299
|
+
fontSize=max(9, base_font_size - 1),
|
|
300
|
+
leading=int(max(9, base_font_size - 1) * 1.25),
|
|
301
|
+
backColor=None,
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
story.append(Spacer(1, 4))
|
|
306
|
+
elif b.kind in ("ul", "ol"):
|
|
307
|
+
is_ordered = b.kind == "ol"
|
|
308
|
+
items: List[ListItem] = []
|
|
309
|
+
for item in b.lines:
|
|
310
|
+
items.append(ListItem(Paragraph(_inline_to_rl_markup(item, code_font), normal)))
|
|
311
|
+
story.append(
|
|
312
|
+
ListFlowable(
|
|
313
|
+
items,
|
|
314
|
+
bulletType="1" if is_ordered else "bullet",
|
|
315
|
+
start="1",
|
|
316
|
+
leftIndent=14,
|
|
317
|
+
bulletFontName=body_font,
|
|
318
|
+
bulletFontSize=base_font_size,
|
|
319
|
+
bulletOffsetY=0,
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
story.append(Spacer(1, 6))
|
|
323
|
+
else:
|
|
324
|
+
# Unknown block type: render as plain text.
|
|
325
|
+
text = " ".join(b.lines)
|
|
326
|
+
story.append(Paragraph(_inline_to_rl_markup(text, code_font), normal))
|
|
327
|
+
|
|
328
|
+
return story
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def main() -> int:
|
|
332
|
+
parser = argparse.ArgumentParser(description="Render Markdown to PDF (CJK-friendly) via ReportLab.")
|
|
333
|
+
parser.add_argument("input_md", help="Input Markdown file path")
|
|
334
|
+
parser.add_argument("output_pdf", help="Output PDF file path")
|
|
335
|
+
parser.add_argument("--font-path", default=None, help="Path to a CJK-capable font file (.ttf/.otf/.ttc)")
|
|
336
|
+
parser.add_argument("--font-size", type=int, default=12, help="Base font size (default: 12)")
|
|
337
|
+
parser.add_argument("--pagesize", choices=["a4", "letter"], default="a4", help="Page size (default: a4)")
|
|
338
|
+
parser.add_argument("--margin-mm", type=float, default=18.0, help="Page margin in mm (default: 18)")
|
|
339
|
+
args = parser.parse_args()
|
|
340
|
+
|
|
341
|
+
md = _read_text(args.input_md)
|
|
342
|
+
blocks = _parse_blocks(md)
|
|
343
|
+
|
|
344
|
+
font_path, subfont_index = _detect_cjk_font_path(args.font_path)
|
|
345
|
+
body_font, code_font = _register_fonts(font_path, subfont_index)
|
|
346
|
+
|
|
347
|
+
page_size = A4 if args.pagesize == "a4" else letter
|
|
348
|
+
margin = args.margin_mm * mm
|
|
349
|
+
|
|
350
|
+
_ensure_parent_dir(args.output_pdf)
|
|
351
|
+
doc = SimpleDocTemplate(
|
|
352
|
+
args.output_pdf,
|
|
353
|
+
pagesize=page_size,
|
|
354
|
+
leftMargin=margin,
|
|
355
|
+
rightMargin=margin,
|
|
356
|
+
topMargin=margin,
|
|
357
|
+
bottomMargin=margin,
|
|
358
|
+
title=os.path.basename(args.output_pdf),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
story = _build_story(blocks, body_font, code_font, args.font_size)
|
|
362
|
+
doc.build(story)
|
|
363
|
+
return 0
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
if __name__ == "__main__":
|
|
367
|
+
raise SystemExit(main())
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
const { createInterface } = require('node:readline/promises');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
VALID_MODES,
|
|
6
|
+
installLinks,
|
|
7
|
+
normalizeModes,
|
|
8
|
+
resolveToolkitHome,
|
|
9
|
+
syncToolkitHome,
|
|
10
|
+
getTargetRoots,
|
|
11
|
+
} = require('./installer');
|
|
12
|
+
|
|
13
|
+
const TARGET_OPTIONS = [
|
|
14
|
+
{ id: 'all', label: 'All', description: 'Install every supported target below' },
|
|
15
|
+
{ id: 'codex', label: 'Codex', description: '~/.codex/skills' },
|
|
16
|
+
{ id: 'openclaw', label: 'OpenClaw', description: '~/.openclaw/workspace*/skills' },
|
|
17
|
+
{ id: 'trae', label: 'Trae', description: '~/.trae/skills' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function supportsColor(stream, env = process.env) {
|
|
21
|
+
return Boolean(stream && stream.isTTY && !env.NO_COLOR);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function color(text, code, enabled) {
|
|
25
|
+
if (!enabled) {
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return `\u001b[${code}m${text}\u001b[0m`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildBanner({ version, colorEnabled }) {
|
|
33
|
+
const lines = [
|
|
34
|
+
'+------------------------------------------+',
|
|
35
|
+
'| Apollo Toolkit |',
|
|
36
|
+
'| npm installer and skill linker |',
|
|
37
|
+
'+------------------------------------------+',
|
|
38
|
+
`Version ${version}`,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
return lines
|
|
42
|
+
.map((line, index) => {
|
|
43
|
+
if (index <= 2) {
|
|
44
|
+
return color(line, '1;36', colorEnabled);
|
|
45
|
+
}
|
|
46
|
+
return color(line, '2', colorEnabled);
|
|
47
|
+
})
|
|
48
|
+
.join('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildHelpText({ version, colorEnabled }) {
|
|
52
|
+
return [
|
|
53
|
+
buildBanner({ version, colorEnabled }),
|
|
54
|
+
'',
|
|
55
|
+
'Usage:',
|
|
56
|
+
' apollo-toolkit [install] [codex|openclaw|trae|all]...',
|
|
57
|
+
' apollo-toolkit --help',
|
|
58
|
+
'',
|
|
59
|
+
'Examples:',
|
|
60
|
+
' npx @laitszkin/apollo-toolkit',
|
|
61
|
+
' npx @laitszkin/apollo-toolkit codex openclaw',
|
|
62
|
+
' npm i -g @laitszkin/apollo-toolkit',
|
|
63
|
+
' apollo-toolkit all',
|
|
64
|
+
'',
|
|
65
|
+
'Options:',
|
|
66
|
+
' --home <path> Override Apollo Toolkit home directory',
|
|
67
|
+
' --help Show this help text',
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseArguments(argv) {
|
|
72
|
+
const args = [...argv];
|
|
73
|
+
const result = {
|
|
74
|
+
modes: [],
|
|
75
|
+
showHelp: false,
|
|
76
|
+
toolkitHome: null,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
while (args.length > 0) {
|
|
80
|
+
const arg = args.shift();
|
|
81
|
+
|
|
82
|
+
if (arg === '--help' || arg === '-h') {
|
|
83
|
+
result.showHelp = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (arg === '--home') {
|
|
88
|
+
const toolkitHome = args.shift();
|
|
89
|
+
if (!toolkitHome) {
|
|
90
|
+
throw new Error('Missing value for --home');
|
|
91
|
+
}
|
|
92
|
+
result.toolkitHome = path.resolve(toolkitHome);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (arg === 'install') {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
result.modes.push(arg);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function clearScreen(output) {
|
|
107
|
+
if (output.isTTY) {
|
|
108
|
+
output.write('\u001b[2J\u001b[H');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function renderSelectionScreen({ output, version, cursor, selected, message, env }) {
|
|
113
|
+
const colorEnabled = supportsColor(output, env);
|
|
114
|
+
const allSelected = VALID_MODES.every((mode) => selected.has(mode));
|
|
115
|
+
|
|
116
|
+
clearScreen(output);
|
|
117
|
+
output.write(`${buildBanner({ version, colorEnabled })}\n\n`);
|
|
118
|
+
output.write('Choose where Apollo Toolkit should create symlinked skills.\n');
|
|
119
|
+
output.write('Use Up/Down (or j/k) to move, Space to toggle, Enter to continue.\n');
|
|
120
|
+
output.write('Press a to toggle all, q to cancel.\n\n');
|
|
121
|
+
|
|
122
|
+
TARGET_OPTIONS.forEach((option, index) => {
|
|
123
|
+
const isFocused = index === cursor;
|
|
124
|
+
const isChecked = option.id === 'all' ? allSelected : selected.has(option.id);
|
|
125
|
+
const prefix = isFocused ? color('>', '1;33', colorEnabled) : ' ';
|
|
126
|
+
const checkbox = isChecked ? color('[x]', '1;32', colorEnabled) : '[ ]';
|
|
127
|
+
const label = isFocused ? color(option.label, '1', colorEnabled) : option.label;
|
|
128
|
+
output.write(`${prefix} ${checkbox} ${label} ${color(option.description, '2', colorEnabled)}\n`);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const selectedModes = allSelected ? [...VALID_MODES] : [...selected].sort();
|
|
132
|
+
output.write('\n');
|
|
133
|
+
output.write(`Selected: ${selectedModes.length > 0 ? selectedModes.join(', ') : 'none'}\n`);
|
|
134
|
+
if (message) {
|
|
135
|
+
output.write(`${color(message, '1;31', colorEnabled)}\n`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function promptForModes({ stdin, stdout, version, env }) {
|
|
140
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
141
|
+
throw new Error('Interactive install requires a TTY. Re-run with targets like `codex`, `openclaw`, `trae`, or `all`.');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
let cursor = 0;
|
|
146
|
+
let message = '';
|
|
147
|
+
const selected = new Set();
|
|
148
|
+
|
|
149
|
+
const cleanup = () => {
|
|
150
|
+
stdin.setRawMode(false);
|
|
151
|
+
stdin.pause();
|
|
152
|
+
stdin.removeListener('data', onData);
|
|
153
|
+
stdout.write('\n');
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const toggleMode = (mode) => {
|
|
157
|
+
if (mode === 'all') {
|
|
158
|
+
const shouldSelectAll = !VALID_MODES.every((candidate) => selected.has(candidate));
|
|
159
|
+
selected.clear();
|
|
160
|
+
if (shouldSelectAll) {
|
|
161
|
+
VALID_MODES.forEach((candidate) => selected.add(candidate));
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (selected.has(mode)) {
|
|
167
|
+
selected.delete(mode);
|
|
168
|
+
} else {
|
|
169
|
+
selected.add(mode);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const onData = (chunk) => {
|
|
174
|
+
const value = chunk.toString('utf8');
|
|
175
|
+
if (value === '\u0003') {
|
|
176
|
+
cleanup();
|
|
177
|
+
reject(new Error('Installation cancelled.'));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (value === '\u001b[A' || value === 'k') {
|
|
182
|
+
cursor = (cursor - 1 + TARGET_OPTIONS.length) % TARGET_OPTIONS.length;
|
|
183
|
+
message = '';
|
|
184
|
+
renderSelectionScreen({ output: stdout, version, cursor, selected, message, env });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (value === '\u001b[B' || value === 'j') {
|
|
189
|
+
cursor = (cursor + 1) % TARGET_OPTIONS.length;
|
|
190
|
+
message = '';
|
|
191
|
+
renderSelectionScreen({ output: stdout, version, cursor, selected, message, env });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (value === ' ') {
|
|
196
|
+
toggleMode(TARGET_OPTIONS[cursor].id);
|
|
197
|
+
message = '';
|
|
198
|
+
renderSelectionScreen({ output: stdout, version, cursor, selected, message, env });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (value.toLowerCase() === 'a') {
|
|
203
|
+
toggleMode('all');
|
|
204
|
+
message = '';
|
|
205
|
+
renderSelectionScreen({ output: stdout, version, cursor, selected, message, env });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (value.toLowerCase() === 'q' || value === '\u001b') {
|
|
210
|
+
cleanup();
|
|
211
|
+
reject(new Error('Installation cancelled.'));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (value === '\r') {
|
|
216
|
+
if (selected.size === 0) {
|
|
217
|
+
message = 'Select at least one target before continuing.';
|
|
218
|
+
renderSelectionScreen({ output: stdout, version, cursor, selected, message, env });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
cleanup();
|
|
223
|
+
resolve([...selected]);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
stdin.setRawMode(true);
|
|
228
|
+
stdin.resume();
|
|
229
|
+
stdin.on('data', onData);
|
|
230
|
+
renderSelectionScreen({ output: stdout, version, cursor, selected, message, env });
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function confirmInstall({ stdin, stdout, version, toolkitHome, modes, env }) {
|
|
235
|
+
const colorEnabled = supportsColor(stdout, env);
|
|
236
|
+
stdout.write(`${buildBanner({ version, colorEnabled })}\n\n`);
|
|
237
|
+
stdout.write(`Apollo Toolkit home: ${toolkitHome}\n`);
|
|
238
|
+
stdout.write(`Targets: ${modes.join(', ')}\n\n`);
|
|
239
|
+
|
|
240
|
+
const targets = await getTargetRoots(modes, env).catch((error) => {
|
|
241
|
+
throw error;
|
|
242
|
+
});
|
|
243
|
+
for (const target of targets) {
|
|
244
|
+
stdout.write(`- ${target.label}: ${target.root}\n`);
|
|
245
|
+
}
|
|
246
|
+
stdout.write('\n');
|
|
247
|
+
|
|
248
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
253
|
+
try {
|
|
254
|
+
const answer = await rl.question('Install Apollo Toolkit to these targets? [Y/n] ');
|
|
255
|
+
return answer.trim() === '' || answer.trim().toLowerCase() === 'y';
|
|
256
|
+
} finally {
|
|
257
|
+
rl.close();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function printSummary({ stdout, version, toolkitHome, modes, installResult, env }) {
|
|
262
|
+
const colorEnabled = supportsColor(stdout, env);
|
|
263
|
+
stdout.write(`\n${buildBanner({ version, colorEnabled })}\n\n`);
|
|
264
|
+
stdout.write(color('Installation complete.', '1;32', colorEnabled));
|
|
265
|
+
stdout.write('\n');
|
|
266
|
+
stdout.write(`Apollo Toolkit home: ${toolkitHome}\n`);
|
|
267
|
+
stdout.write(`Linked skills: ${installResult.skillNames.length}\n`);
|
|
268
|
+
stdout.write(`Targets: ${modes.join(', ')}\n\n`);
|
|
269
|
+
|
|
270
|
+
for (const target of installResult.targets) {
|
|
271
|
+
stdout.write(`- ${target.label}: ${target.root}\n`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function run(argv, context = {}) {
|
|
276
|
+
const sourceRoot = context.sourceRoot || path.resolve(__dirname, '..');
|
|
277
|
+
const stdout = context.stdout || process.stdout;
|
|
278
|
+
const stderr = context.stderr || process.stderr;
|
|
279
|
+
const stdin = context.stdin || process.stdin;
|
|
280
|
+
const env = context.env || process.env;
|
|
281
|
+
const packageJson = require(path.join(sourceRoot, 'package.json'));
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const parsed = parseArguments(argv);
|
|
285
|
+
if (parsed.showHelp) {
|
|
286
|
+
stdout.write(`${buildHelpText({ version: packageJson.version, colorEnabled: supportsColor(stdout, env) })}\n`);
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const toolkitHome = parsed.toolkitHome || resolveToolkitHome(env);
|
|
291
|
+
const modes = parsed.modes.length > 0
|
|
292
|
+
? normalizeModes(parsed.modes)
|
|
293
|
+
: normalizeModes(await promptForModes({ stdin, stdout, version: packageJson.version, env }));
|
|
294
|
+
|
|
295
|
+
const confirmed = await confirmInstall({
|
|
296
|
+
stdin,
|
|
297
|
+
stdout,
|
|
298
|
+
version: packageJson.version,
|
|
299
|
+
toolkitHome,
|
|
300
|
+
modes,
|
|
301
|
+
env,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (!confirmed) {
|
|
305
|
+
stdout.write('Installation cancelled.\n');
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
await syncToolkitHome({
|
|
310
|
+
sourceRoot,
|
|
311
|
+
toolkitHome,
|
|
312
|
+
version: packageJson.version,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const installResult = await installLinks({
|
|
316
|
+
toolkitHome,
|
|
317
|
+
modes,
|
|
318
|
+
env: {
|
|
319
|
+
...env,
|
|
320
|
+
APOLLO_TOOLKIT_HOME: toolkitHome,
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
printSummary({ stdout, version: packageJson.version, toolkitHome, modes, installResult, env });
|
|
325
|
+
return 0;
|
|
326
|
+
} catch (error) {
|
|
327
|
+
stderr.write(`Error: ${error.message}\n`);
|
|
328
|
+
return 1;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = {
|
|
333
|
+
buildBanner,
|
|
334
|
+
buildHelpText,
|
|
335
|
+
parseArguments,
|
|
336
|
+
promptForModes,
|
|
337
|
+
run,
|
|
338
|
+
};
|