@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.
Files changed (204) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +100 -0
  3. package/LICENSE +21 -0
  4. package/README.md +144 -0
  5. package/align-project-documents/SKILL.md +94 -0
  6. package/align-project-documents/agents/openai.yaml +4 -0
  7. package/analyse-app-logs/LICENSE +21 -0
  8. package/analyse-app-logs/README.md +126 -0
  9. package/analyse-app-logs/SKILL.md +121 -0
  10. package/analyse-app-logs/agents/openai.yaml +4 -0
  11. package/analyse-app-logs/references/investigation-checklist.md +58 -0
  12. package/analyse-app-logs/references/log-signal-patterns.md +52 -0
  13. package/answering-questions-with-research/SKILL.md +46 -0
  14. package/answering-questions-with-research/agents/openai.yaml +4 -0
  15. package/bin/apollo-toolkit.js +7 -0
  16. package/commit-and-push/LICENSE +21 -0
  17. package/commit-and-push/README.md +26 -0
  18. package/commit-and-push/SKILL.md +70 -0
  19. package/commit-and-push/agents/openai.yaml +4 -0
  20. package/commit-and-push/references/branch-naming.md +15 -0
  21. package/commit-and-push/references/commit-messages.md +19 -0
  22. package/deep-research-topics/LICENSE +21 -0
  23. package/deep-research-topics/README.md +43 -0
  24. package/deep-research-topics/SKILL.md +84 -0
  25. package/deep-research-topics/agents/openai.yaml +4 -0
  26. package/develop-new-features/LICENSE +21 -0
  27. package/develop-new-features/README.md +52 -0
  28. package/develop-new-features/SKILL.md +105 -0
  29. package/develop-new-features/agents/openai.yaml +4 -0
  30. package/develop-new-features/references/testing-e2e.md +35 -0
  31. package/develop-new-features/references/testing-integration.md +42 -0
  32. package/develop-new-features/references/testing-property-based.md +44 -0
  33. package/develop-new-features/references/testing-unit.md +37 -0
  34. package/discover-edge-cases/CHANGELOG.md +19 -0
  35. package/discover-edge-cases/LICENSE +21 -0
  36. package/discover-edge-cases/README.md +87 -0
  37. package/discover-edge-cases/SKILL.md +124 -0
  38. package/discover-edge-cases/agents/openai.yaml +4 -0
  39. package/discover-edge-cases/references/architecture-edge-cases.md +41 -0
  40. package/discover-edge-cases/references/code-edge-cases.md +46 -0
  41. package/docs-to-voice/.env.example +106 -0
  42. package/docs-to-voice/CHANGELOG.md +71 -0
  43. package/docs-to-voice/LICENSE +21 -0
  44. package/docs-to-voice/README.md +118 -0
  45. package/docs-to-voice/SKILL.md +107 -0
  46. package/docs-to-voice/agents/openai.yaml +4 -0
  47. package/docs-to-voice/scripts/docs_to_voice.py +1385 -0
  48. package/docs-to-voice/scripts/docs_to_voice.sh +11 -0
  49. package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +210 -0
  50. package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +115 -0
  51. package/docs-to-voice/tests/test_docs_to_voice_settings.py +43 -0
  52. package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +57 -0
  53. package/enhance-existing-features/CHANGELOG.md +35 -0
  54. package/enhance-existing-features/LICENSE +21 -0
  55. package/enhance-existing-features/README.md +54 -0
  56. package/enhance-existing-features/SKILL.md +120 -0
  57. package/enhance-existing-features/agents/openai.yaml +4 -0
  58. package/enhance-existing-features/references/e2e-tests.md +25 -0
  59. package/enhance-existing-features/references/integration-tests.md +30 -0
  60. package/enhance-existing-features/references/property-based-tests.md +33 -0
  61. package/enhance-existing-features/references/unit-tests.md +29 -0
  62. package/feature-propose/LICENSE +21 -0
  63. package/feature-propose/README.md +23 -0
  64. package/feature-propose/SKILL.md +107 -0
  65. package/feature-propose/agents/openai.yaml +4 -0
  66. package/feature-propose/references/enhancement-features.md +25 -0
  67. package/feature-propose/references/important-features.md +25 -0
  68. package/feature-propose/references/mvp-features.md +25 -0
  69. package/feature-propose/references/performance-features.md +25 -0
  70. package/financial-research/SKILL.md +208 -0
  71. package/financial-research/agents/openai.yaml +4 -0
  72. package/financial-research/assets/weekly_market_report_template.md +45 -0
  73. package/fix-github-issues/SKILL.md +98 -0
  74. package/fix-github-issues/agents/openai.yaml +4 -0
  75. package/fix-github-issues/scripts/list_issues.py +148 -0
  76. package/fix-github-issues/tests/test_list_issues.py +127 -0
  77. package/generate-spec/LICENSE +21 -0
  78. package/generate-spec/README.md +61 -0
  79. package/generate-spec/SKILL.md +96 -0
  80. package/generate-spec/agents/openai.yaml +4 -0
  81. package/generate-spec/references/templates/checklist.md +78 -0
  82. package/generate-spec/references/templates/spec.md +55 -0
  83. package/generate-spec/references/templates/tasks.md +35 -0
  84. package/generate-spec/scripts/create-specs +123 -0
  85. package/harden-app-security/CHANGELOG.md +27 -0
  86. package/harden-app-security/LICENSE +21 -0
  87. package/harden-app-security/README.md +46 -0
  88. package/harden-app-security/SKILL.md +127 -0
  89. package/harden-app-security/agents/openai.yaml +4 -0
  90. package/harden-app-security/references/agent-attack-catalog.md +117 -0
  91. package/harden-app-security/references/common-software-attack-catalog.md +168 -0
  92. package/harden-app-security/references/red-team-extreme-scenarios.md +81 -0
  93. package/harden-app-security/references/risk-checklist.md +78 -0
  94. package/harden-app-security/references/security-test-patterns-agent.md +101 -0
  95. package/harden-app-security/references/security-test-patterns-finance.md +88 -0
  96. package/harden-app-security/references/test-snippets.md +73 -0
  97. package/improve-observability/SKILL.md +114 -0
  98. package/improve-observability/agents/openai.yaml +4 -0
  99. package/learn-skill-from-conversations/CHANGELOG.md +15 -0
  100. package/learn-skill-from-conversations/LICENSE +22 -0
  101. package/learn-skill-from-conversations/README.md +47 -0
  102. package/learn-skill-from-conversations/SKILL.md +85 -0
  103. package/learn-skill-from-conversations/agents/openai.yaml +4 -0
  104. package/learn-skill-from-conversations/scripts/extract_recent_conversations.py +369 -0
  105. package/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +176 -0
  106. package/learning-error-book/SKILL.md +112 -0
  107. package/learning-error-book/agents/openai.yaml +4 -0
  108. package/learning-error-book/assets/error_book_template.md +66 -0
  109. package/learning-error-book/scripts/render_markdown_to_pdf.py +367 -0
  110. package/lib/cli.js +338 -0
  111. package/lib/installer.js +225 -0
  112. package/maintain-project-constraints/SKILL.md +109 -0
  113. package/maintain-project-constraints/agents/openai.yaml +4 -0
  114. package/maintain-skill-catalog/README.md +18 -0
  115. package/maintain-skill-catalog/SKILL.md +66 -0
  116. package/maintain-skill-catalog/agents/openai.yaml +4 -0
  117. package/novel-to-short-video/CHANGELOG.md +53 -0
  118. package/novel-to-short-video/LICENSE +21 -0
  119. package/novel-to-short-video/README.md +63 -0
  120. package/novel-to-short-video/SKILL.md +233 -0
  121. package/novel-to-short-video/agents/openai.yaml +4 -0
  122. package/novel-to-short-video/references/plan-template.md +71 -0
  123. package/novel-to-short-video/references/roles-json.md +41 -0
  124. package/open-github-issue/LICENSE +21 -0
  125. package/open-github-issue/README.md +97 -0
  126. package/open-github-issue/SKILL.md +119 -0
  127. package/open-github-issue/agents/openai.yaml +4 -0
  128. package/open-github-issue/scripts/open_github_issue.py +380 -0
  129. package/open-github-issue/tests/test_open_github_issue.py +159 -0
  130. package/open-source-pr-workflow/CHANGELOG.md +32 -0
  131. package/open-source-pr-workflow/LICENSE +21 -0
  132. package/open-source-pr-workflow/README.md +23 -0
  133. package/open-source-pr-workflow/SKILL.md +123 -0
  134. package/open-source-pr-workflow/agents/openai.yaml +4 -0
  135. package/openai-text-to-image-storyboard/.env.example +10 -0
  136. package/openai-text-to-image-storyboard/CHANGELOG.md +49 -0
  137. package/openai-text-to-image-storyboard/LICENSE +21 -0
  138. package/openai-text-to-image-storyboard/README.md +99 -0
  139. package/openai-text-to-image-storyboard/SKILL.md +107 -0
  140. package/openai-text-to-image-storyboard/agents/openai.yaml +4 -0
  141. package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +763 -0
  142. package/package.json +36 -0
  143. package/record-spending/SKILL.md +113 -0
  144. package/record-spending/agents/openai.yaml +4 -0
  145. package/record-spending/references/account-format.md +33 -0
  146. package/record-spending/references/workbook-layout.md +84 -0
  147. package/resolve-review-comments/SKILL.md +122 -0
  148. package/resolve-review-comments/agents/openai.yaml +4 -0
  149. package/resolve-review-comments/references/adoption-criteria.md +23 -0
  150. package/resolve-review-comments/scripts/review_threads.py +425 -0
  151. package/resolve-review-comments/tests/test_review_threads.py +74 -0
  152. package/review-change-set/LICENSE +21 -0
  153. package/review-change-set/README.md +55 -0
  154. package/review-change-set/SKILL.md +103 -0
  155. package/review-change-set/agents/openai.yaml +4 -0
  156. package/review-codebases/LICENSE +21 -0
  157. package/review-codebases/README.md +67 -0
  158. package/review-codebases/SKILL.md +109 -0
  159. package/review-codebases/agents/openai.yaml +4 -0
  160. package/scripts/install_skills.ps1 +283 -0
  161. package/scripts/install_skills.sh +262 -0
  162. package/scripts/validate_openai_agent_config.py +194 -0
  163. package/scripts/validate_skill_frontmatter.py +110 -0
  164. package/specs-to-project-docs/LICENSE +21 -0
  165. package/specs-to-project-docs/README.md +57 -0
  166. package/specs-to-project-docs/SKILL.md +111 -0
  167. package/specs-to-project-docs/agents/openai.yaml +4 -0
  168. package/specs-to-project-docs/references/templates/architecture.md +29 -0
  169. package/specs-to-project-docs/references/templates/configuration.md +29 -0
  170. package/specs-to-project-docs/references/templates/developer-guide.md +33 -0
  171. package/specs-to-project-docs/references/templates/docs-index.md +39 -0
  172. package/specs-to-project-docs/references/templates/features.md +25 -0
  173. package/specs-to-project-docs/references/templates/getting-started.md +38 -0
  174. package/specs-to-project-docs/references/templates/readme.md +49 -0
  175. package/systematic-debug/LICENSE +21 -0
  176. package/systematic-debug/README.md +81 -0
  177. package/systematic-debug/SKILL.md +59 -0
  178. package/systematic-debug/agents/openai.yaml +4 -0
  179. package/text-to-short-video/.env.example +36 -0
  180. package/text-to-short-video/LICENSE +21 -0
  181. package/text-to-short-video/README.md +82 -0
  182. package/text-to-short-video/SKILL.md +221 -0
  183. package/text-to-short-video/agents/openai.yaml +4 -0
  184. package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +350 -0
  185. package/version-release/CHANGELOG.md +53 -0
  186. package/version-release/LICENSE +21 -0
  187. package/version-release/README.md +28 -0
  188. package/version-release/SKILL.md +94 -0
  189. package/version-release/agents/openai.yaml +4 -0
  190. package/version-release/references/branch-naming.md +15 -0
  191. package/version-release/references/changelog-writing.md +8 -0
  192. package/version-release/references/commit-messages.md +19 -0
  193. package/version-release/references/readme-writing.md +12 -0
  194. package/version-release/references/semantic-versioning.md +12 -0
  195. package/video-production/CHANGELOG.md +104 -0
  196. package/video-production/LICENSE +18 -0
  197. package/video-production/README.md +68 -0
  198. package/video-production/SKILL.md +213 -0
  199. package/video-production/agents/openai.yaml +4 -0
  200. package/video-production/references/plan-template.md +54 -0
  201. package/video-production/references/roles-json.md +41 -0
  202. package/weekly-financial-event-report/SKILL.md +195 -0
  203. package/weekly-financial-event-report/agents/openai.yaml +4 -0
  204. 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("<", "&lt;")
98
+ .replace(">", "&gt;")
99
+ .replace('"', "&quot;")
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 "&amp;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
+ };