@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
|
@@ -1,705 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import argparse
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
import re
|
|
8
|
-
import shutil
|
|
9
|
-
import subprocess
|
|
10
|
-
import sys
|
|
11
|
-
import tempfile
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from urllib import error, request
|
|
14
|
-
|
|
15
|
-
GITHUB_API_BASE = "https://api.github.com"
|
|
16
|
-
README_ACCEPT = "application/vnd.github.raw+json"
|
|
17
|
-
JSON_ACCEPT = "application/vnd.github+json"
|
|
18
|
-
DEFAULT_REPRO_ZH = "尚未穩定重現;需補充更多執行期資料。"
|
|
19
|
-
DEFAULT_REPRO_EN = "Not yet reliably reproducible; more runtime evidence is required."
|
|
20
|
-
ISSUE_TYPE_PROBLEM = "problem"
|
|
21
|
-
ISSUE_TYPE_FEATURE = "feature"
|
|
22
|
-
ISSUE_TYPE_PERFORMANCE = "performance"
|
|
23
|
-
ISSUE_TYPE_SECURITY = "security"
|
|
24
|
-
ISSUE_TYPE_DOCS = "docs"
|
|
25
|
-
ISSUE_TYPE_OBSERVABILITY = "observability"
|
|
26
|
-
ISSUE_TYPES = [
|
|
27
|
-
ISSUE_TYPE_PROBLEM,
|
|
28
|
-
ISSUE_TYPE_FEATURE,
|
|
29
|
-
ISSUE_TYPE_PERFORMANCE,
|
|
30
|
-
ISSUE_TYPE_SECURITY,
|
|
31
|
-
ISSUE_TYPE_DOCS,
|
|
32
|
-
ISSUE_TYPE_OBSERVABILITY,
|
|
33
|
-
]
|
|
34
|
-
PROBLEM_BDD_MARKER_GROUPS = (
|
|
35
|
-
(
|
|
36
|
-
r"Expected Behavior\s*\(BDD\)",
|
|
37
|
-
r"Current Behavior\s*\(BDD\)",
|
|
38
|
-
r"Behavior Gap",
|
|
39
|
-
),
|
|
40
|
-
(
|
|
41
|
-
r"預期行為\s*[((]BDD[))]",
|
|
42
|
-
r"(?:目前|當前)行為\s*[((]BDD[))]",
|
|
43
|
-
r"行為(?:落差|差異)",
|
|
44
|
-
),
|
|
45
|
-
)
|
|
46
|
-
TEXT_FIELDS = (
|
|
47
|
-
"title",
|
|
48
|
-
"problem_description",
|
|
49
|
-
"suspected_cause",
|
|
50
|
-
"reproduction",
|
|
51
|
-
"proposal",
|
|
52
|
-
"reason",
|
|
53
|
-
"suggested_architecture",
|
|
54
|
-
"impact",
|
|
55
|
-
"evidence",
|
|
56
|
-
"suggested_action",
|
|
57
|
-
"affected_scope",
|
|
58
|
-
)
|
|
59
|
-
PAYLOAD_FIELDS = frozenset(
|
|
60
|
-
(
|
|
61
|
-
"title",
|
|
62
|
-
"issue_type",
|
|
63
|
-
"problem_description",
|
|
64
|
-
"suspected_cause",
|
|
65
|
-
"reproduction",
|
|
66
|
-
"proposal",
|
|
67
|
-
"reason",
|
|
68
|
-
"suggested_architecture",
|
|
69
|
-
"impact",
|
|
70
|
-
"evidence",
|
|
71
|
-
"suggested_action",
|
|
72
|
-
"severity",
|
|
73
|
-
"affected_scope",
|
|
74
|
-
"repo",
|
|
75
|
-
"dry_run",
|
|
76
|
-
)
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def parse_args() -> argparse.Namespace:
|
|
81
|
-
parser = argparse.ArgumentParser(
|
|
82
|
-
description=(
|
|
83
|
-
"Publish a structured GitHub issue or feature proposal. "
|
|
84
|
-
"Auth order: gh CLI login -> GitHub token -> draft only."
|
|
85
|
-
)
|
|
86
|
-
)
|
|
87
|
-
parser.add_argument(
|
|
88
|
-
"--payload-file",
|
|
89
|
-
help=(
|
|
90
|
-
"Path to a JSON payload file. Use '-' to read JSON from stdin. "
|
|
91
|
-
"CLI flags override values loaded from the payload."
|
|
92
|
-
),
|
|
93
|
-
)
|
|
94
|
-
parser.add_argument("--title", help="Issue title")
|
|
95
|
-
parser.add_argument(
|
|
96
|
-
"--issue-type",
|
|
97
|
-
choices=ISSUE_TYPES,
|
|
98
|
-
default=None,
|
|
99
|
-
help="Structured issue type to publish.",
|
|
100
|
-
)
|
|
101
|
-
parser.add_argument(
|
|
102
|
-
"--problem-description",
|
|
103
|
-
help="Issue section content: problem description",
|
|
104
|
-
)
|
|
105
|
-
parser.add_argument(
|
|
106
|
-
"--suspected-cause",
|
|
107
|
-
help="Issue section content: suspected cause",
|
|
108
|
-
)
|
|
109
|
-
parser.add_argument(
|
|
110
|
-
"--reproduction",
|
|
111
|
-
help="Issue section content: reproduction conditions (optional)",
|
|
112
|
-
)
|
|
113
|
-
parser.add_argument(
|
|
114
|
-
"--proposal",
|
|
115
|
-
help="Issue section content: feature proposal summary (optional; defaults to title)",
|
|
116
|
-
)
|
|
117
|
-
parser.add_argument(
|
|
118
|
-
"--reason",
|
|
119
|
-
help="Issue section content: why the feature is needed",
|
|
120
|
-
)
|
|
121
|
-
parser.add_argument(
|
|
122
|
-
"--suggested-architecture",
|
|
123
|
-
help="Issue section content: suggested architecture for the feature",
|
|
124
|
-
)
|
|
125
|
-
parser.add_argument(
|
|
126
|
-
"--impact",
|
|
127
|
-
help="Issue section content: concrete user, system, or business impact",
|
|
128
|
-
)
|
|
129
|
-
parser.add_argument(
|
|
130
|
-
"--evidence",
|
|
131
|
-
help="Issue section content: concrete evidence, metrics, logs, or repository facts",
|
|
132
|
-
)
|
|
133
|
-
parser.add_argument(
|
|
134
|
-
"--suggested-action",
|
|
135
|
-
help="Issue section content: suggested remediation, mitigation, or next action",
|
|
136
|
-
)
|
|
137
|
-
parser.add_argument(
|
|
138
|
-
"--severity",
|
|
139
|
-
choices=["critical", "high", "medium", "low"],
|
|
140
|
-
help="Issue section content: severity level for security-oriented issues",
|
|
141
|
-
)
|
|
142
|
-
parser.add_argument(
|
|
143
|
-
"--affected-scope",
|
|
144
|
-
help="Issue section content: affected endpoint, module, user cohort, or system surface",
|
|
145
|
-
)
|
|
146
|
-
parser.add_argument(
|
|
147
|
-
"--repo",
|
|
148
|
-
help="Target repository in owner/repo format. Defaults to origin remote.",
|
|
149
|
-
)
|
|
150
|
-
parser.add_argument(
|
|
151
|
-
"--dry-run",
|
|
152
|
-
action="store_true",
|
|
153
|
-
help="Build and print payload only, without creating an issue.",
|
|
154
|
-
)
|
|
155
|
-
return parser.parse_args()
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def normalize_payload_key(key: str) -> str:
|
|
159
|
-
return key.replace("-", "_")
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def read_payload_file(raw_path: str) -> dict[str, object]:
|
|
163
|
-
if raw_path == "-":
|
|
164
|
-
raw_content = sys.stdin.read()
|
|
165
|
-
context = "stdin"
|
|
166
|
-
else:
|
|
167
|
-
path = Path(raw_path).expanduser()
|
|
168
|
-
raw_content = path.read_text(encoding="utf-8")
|
|
169
|
-
context = str(path)
|
|
170
|
-
|
|
171
|
-
try:
|
|
172
|
-
payload = json.loads(raw_content)
|
|
173
|
-
except json.JSONDecodeError as exc:
|
|
174
|
-
raise SystemExit(f"Invalid JSON payload in {context}: {exc}") from exc
|
|
175
|
-
|
|
176
|
-
if not isinstance(payload, dict):
|
|
177
|
-
raise SystemExit(f"Invalid JSON payload in {context}: top-level value must be an object.")
|
|
178
|
-
|
|
179
|
-
normalized: dict[str, object] = {}
|
|
180
|
-
for raw_key, value in payload.items():
|
|
181
|
-
key = normalize_payload_key(str(raw_key))
|
|
182
|
-
if key not in PAYLOAD_FIELDS:
|
|
183
|
-
raise SystemExit(f"Unsupported payload key: {raw_key}")
|
|
184
|
-
normalized[key] = value
|
|
185
|
-
return normalized
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def payload_value_to_string(field_name: str, value: object) -> str | None:
|
|
189
|
-
if value is None:
|
|
190
|
-
return None
|
|
191
|
-
if isinstance(value, str):
|
|
192
|
-
return value
|
|
193
|
-
raise SystemExit(f"Payload field '{field_name}' must be a string or null.")
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def read_at_file_value(field_name: str, value: str | None) -> str | None:
|
|
197
|
-
if value is None:
|
|
198
|
-
return None
|
|
199
|
-
if value.startswith("@@"):
|
|
200
|
-
return value[1:]
|
|
201
|
-
if value == "@-":
|
|
202
|
-
return sys.stdin.read()
|
|
203
|
-
if value.startswith("@") and len(value) > 1:
|
|
204
|
-
path = Path(value[1:]).expanduser()
|
|
205
|
-
try:
|
|
206
|
-
return path.read_text(encoding="utf-8")
|
|
207
|
-
except OSError as exc:
|
|
208
|
-
raise SystemExit(f"Unable to read @{field_name} file {path}: {exc}") from exc
|
|
209
|
-
return value
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def hydrate_args(args: argparse.Namespace) -> argparse.Namespace:
|
|
213
|
-
payload_file = getattr(args, "payload_file", None)
|
|
214
|
-
if payload_file:
|
|
215
|
-
payload = read_payload_file(payload_file)
|
|
216
|
-
for field_name, value in payload.items():
|
|
217
|
-
current_value = getattr(args, field_name, None)
|
|
218
|
-
if field_name == "dry_run":
|
|
219
|
-
if not isinstance(value, bool):
|
|
220
|
-
raise SystemExit("Payload field 'dry_run' must be a boolean.")
|
|
221
|
-
if not current_value:
|
|
222
|
-
setattr(args, field_name, value)
|
|
223
|
-
continue
|
|
224
|
-
|
|
225
|
-
if field_name in TEXT_FIELDS:
|
|
226
|
-
value = payload_value_to_string(field_name, value)
|
|
227
|
-
elif not isinstance(value, str):
|
|
228
|
-
raise SystemExit(f"Payload field '{field_name}' must be a string.")
|
|
229
|
-
|
|
230
|
-
if current_value is None or current_value == "":
|
|
231
|
-
setattr(args, field_name, value)
|
|
232
|
-
|
|
233
|
-
if args.issue_type is None:
|
|
234
|
-
args.issue_type = ISSUE_TYPE_PROBLEM
|
|
235
|
-
if args.issue_type not in ISSUE_TYPES:
|
|
236
|
-
raise SystemExit(f"Invalid issue_type: {args.issue_type}")
|
|
237
|
-
|
|
238
|
-
for field_name in TEXT_FIELDS:
|
|
239
|
-
setattr(args, field_name, read_at_file_value(field_name, getattr(args, field_name, None)))
|
|
240
|
-
|
|
241
|
-
if not (args.title or "").strip():
|
|
242
|
-
raise SystemExit("Issue title is required. Pass --title or include title in --payload-file.")
|
|
243
|
-
return args
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def validate_issue_content_args(args: argparse.Namespace) -> None:
|
|
247
|
-
if args.issue_type == ISSUE_TYPE_FEATURE:
|
|
248
|
-
if not (args.reason or "").strip():
|
|
249
|
-
raise SystemExit("Feature issues require --reason.")
|
|
250
|
-
if not (args.suggested_architecture or "").strip():
|
|
251
|
-
raise SystemExit("Feature issues require --suggested-architecture.")
|
|
252
|
-
return
|
|
253
|
-
|
|
254
|
-
if args.issue_type == ISSUE_TYPE_PERFORMANCE:
|
|
255
|
-
require_non_empty(args.problem_description, "Performance issues require --problem-description.")
|
|
256
|
-
require_non_empty(args.impact, "Performance issues require --impact.")
|
|
257
|
-
require_non_empty(args.evidence, "Performance issues require --evidence.")
|
|
258
|
-
require_non_empty(args.suggested_action, "Performance issues require --suggested-action.")
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
if args.issue_type == ISSUE_TYPE_SECURITY:
|
|
262
|
-
require_non_empty(args.problem_description, "Security issues require --problem-description.")
|
|
263
|
-
require_non_empty(args.affected_scope, "Security issues require --affected-scope.")
|
|
264
|
-
require_non_empty(args.impact, "Security issues require --impact.")
|
|
265
|
-
require_non_empty(args.evidence, "Security issues require --evidence.")
|
|
266
|
-
require_non_empty(args.suggested_action, "Security issues require --suggested-action.")
|
|
267
|
-
require_non_empty(args.severity, "Security issues require --severity.")
|
|
268
|
-
return
|
|
269
|
-
|
|
270
|
-
if args.issue_type == ISSUE_TYPE_DOCS:
|
|
271
|
-
require_non_empty(args.problem_description, "Docs issues require --problem-description.")
|
|
272
|
-
require_non_empty(args.evidence, "Docs issues require --evidence.")
|
|
273
|
-
require_non_empty(args.suggested_action, "Docs issues require --suggested-action.")
|
|
274
|
-
return
|
|
275
|
-
|
|
276
|
-
if args.issue_type == ISSUE_TYPE_OBSERVABILITY:
|
|
277
|
-
require_non_empty(args.problem_description, "Observability issues require --problem-description.")
|
|
278
|
-
require_non_empty(args.impact, "Observability issues require --impact.")
|
|
279
|
-
require_non_empty(args.evidence, "Observability issues require --evidence.")
|
|
280
|
-
require_non_empty(args.suggested_action, "Observability issues require --suggested-action.")
|
|
281
|
-
return
|
|
282
|
-
|
|
283
|
-
if not (args.problem_description or "").strip():
|
|
284
|
-
raise SystemExit("Problem issues require --problem-description.")
|
|
285
|
-
if not (args.suspected_cause or "").strip():
|
|
286
|
-
raise SystemExit("Problem issues require --suspected-cause.")
|
|
287
|
-
if not has_required_problem_bdd_sections(args.problem_description or ""):
|
|
288
|
-
raise SystemExit(
|
|
289
|
-
"Problem issues require --problem-description to include "
|
|
290
|
-
"Expected Behavior (BDD), Current Behavior (BDD), and Behavior Gap sections."
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
def require_non_empty(value: str | None, message: str) -> None:
|
|
295
|
-
if not (value or "").strip():
|
|
296
|
-
raise SystemExit(message)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
def has_required_problem_bdd_sections(problem_description: str) -> bool:
|
|
300
|
-
normalized = problem_description.strip()
|
|
301
|
-
return any(
|
|
302
|
-
all(re.search(pattern, normalized, flags=re.IGNORECASE) for pattern in marker_group)
|
|
303
|
-
for marker_group in PROBLEM_BDD_MARKER_GROUPS
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
def run_command(args: list[str]) -> subprocess.CompletedProcess[str]:
|
|
308
|
-
return subprocess.run(args, check=False, capture_output=True, text=True)
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
def has_gh_auth() -> bool:
|
|
312
|
-
if shutil.which("gh") is None:
|
|
313
|
-
return False
|
|
314
|
-
result = run_command(["gh", "auth", "status"])
|
|
315
|
-
return result.returncode == 0
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
def get_token() -> str | None:
|
|
319
|
-
for key in ("GITHUB_TOKEN", "GH_TOKEN"):
|
|
320
|
-
value = os.getenv(key, "").strip()
|
|
321
|
-
if value:
|
|
322
|
-
return value
|
|
323
|
-
return None
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
def resolve_repo(explicit_repo: str | None) -> str:
|
|
327
|
-
if explicit_repo:
|
|
328
|
-
return validate_repo(explicit_repo)
|
|
329
|
-
|
|
330
|
-
remote_result = run_command(["git", "remote", "get-url", "origin"])
|
|
331
|
-
if remote_result.returncode != 0:
|
|
332
|
-
raise SystemExit("Unable to resolve origin remote. Pass --repo owner/repo.")
|
|
333
|
-
|
|
334
|
-
remote = remote_result.stdout.strip()
|
|
335
|
-
match = re.search(
|
|
336
|
-
r"github\.com[:/](?P<owner>[A-Za-z0-9_.-]+)/(?P<repo>[A-Za-z0-9_.-]+?)(?:\.git)?$",
|
|
337
|
-
remote,
|
|
338
|
-
)
|
|
339
|
-
if not match:
|
|
340
|
-
raise SystemExit("Origin remote is not a GitHub repository. Pass --repo owner/repo.")
|
|
341
|
-
|
|
342
|
-
return f"{match.group('owner')}/{match.group('repo')}"
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
def validate_repo(repo: str) -> str:
|
|
346
|
-
candidate = repo.strip()
|
|
347
|
-
if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", candidate):
|
|
348
|
-
raise SystemExit("Invalid repo format. Use owner/repo.")
|
|
349
|
-
return candidate
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
def github_request(
|
|
353
|
-
method: str,
|
|
354
|
-
path: str,
|
|
355
|
-
*,
|
|
356
|
-
token: str | None,
|
|
357
|
-
accept: str,
|
|
358
|
-
payload: dict | None = None,
|
|
359
|
-
) -> str:
|
|
360
|
-
headers = {
|
|
361
|
-
"Accept": accept,
|
|
362
|
-
"User-Agent": "open-github-issue-skill",
|
|
363
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
364
|
-
}
|
|
365
|
-
data = None
|
|
366
|
-
|
|
367
|
-
if token:
|
|
368
|
-
headers["Authorization"] = f"Bearer {token}"
|
|
369
|
-
|
|
370
|
-
if payload is not None:
|
|
371
|
-
data = json.dumps(payload).encode("utf-8")
|
|
372
|
-
headers["Content-Type"] = "application/json"
|
|
373
|
-
|
|
374
|
-
req = request.Request(
|
|
375
|
-
url=f"{GITHUB_API_BASE}{path}",
|
|
376
|
-
data=data,
|
|
377
|
-
headers=headers,
|
|
378
|
-
method=method,
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
try:
|
|
382
|
-
with request.urlopen(req, timeout=30) as response:
|
|
383
|
-
return response.read().decode("utf-8")
|
|
384
|
-
except error.HTTPError as exc:
|
|
385
|
-
detail = exc.read().decode("utf-8", errors="replace")
|
|
386
|
-
raise RuntimeError(f"GitHub API {exc.code} {path}: {detail}") from exc
|
|
387
|
-
except error.URLError as exc:
|
|
388
|
-
raise RuntimeError(f"GitHub API request failed for {path}: {exc.reason}") from exc
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
def fetch_remote_readme(repo: str, gh_authenticated: bool, token: str | None) -> str:
|
|
392
|
-
if gh_authenticated:
|
|
393
|
-
result = run_command(
|
|
394
|
-
["gh", "api", "-H", f"Accept: {README_ACCEPT}", f"repos/{repo}/readme"]
|
|
395
|
-
)
|
|
396
|
-
if result.returncode == 0:
|
|
397
|
-
return result.stdout
|
|
398
|
-
|
|
399
|
-
try:
|
|
400
|
-
return github_request(
|
|
401
|
-
"GET",
|
|
402
|
-
f"/repos/{repo}/readme",
|
|
403
|
-
token=token,
|
|
404
|
-
accept=README_ACCEPT,
|
|
405
|
-
)
|
|
406
|
-
except RuntimeError:
|
|
407
|
-
return ""
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
def detect_issue_language(readme_content: str) -> str:
|
|
411
|
-
if not readme_content.strip():
|
|
412
|
-
return "en"
|
|
413
|
-
|
|
414
|
-
chinese_chars = len(re.findall(r"[\u4e00-\u9fff]", readme_content))
|
|
415
|
-
language_chars = len(re.findall(r"[A-Za-z\u4e00-\u9fff]", readme_content))
|
|
416
|
-
|
|
417
|
-
if chinese_chars >= 20 and chinese_chars / max(language_chars, 1) >= 0.08:
|
|
418
|
-
return "zh"
|
|
419
|
-
return "en"
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
def build_issue_body(
|
|
423
|
-
*,
|
|
424
|
-
issue_type: str,
|
|
425
|
-
language: str,
|
|
426
|
-
title: str,
|
|
427
|
-
problem_description: str | None,
|
|
428
|
-
suspected_cause: str | None,
|
|
429
|
-
reproduction: str | None,
|
|
430
|
-
proposal: str | None,
|
|
431
|
-
reason: str | None,
|
|
432
|
-
suggested_architecture: str | None,
|
|
433
|
-
impact: str | None = None,
|
|
434
|
-
evidence: str | None = None,
|
|
435
|
-
suggested_action: str | None = None,
|
|
436
|
-
severity: str | None = None,
|
|
437
|
-
affected_scope: str | None = None,
|
|
438
|
-
) -> str:
|
|
439
|
-
if issue_type == ISSUE_TYPE_FEATURE:
|
|
440
|
-
proposal_text = (proposal or title).strip()
|
|
441
|
-
reason_text = (reason or "").strip()
|
|
442
|
-
architecture_text = (suggested_architecture or "").strip()
|
|
443
|
-
|
|
444
|
-
if language == "zh":
|
|
445
|
-
return (
|
|
446
|
-
"### 功能提案\n"
|
|
447
|
-
f"{proposal_text}\n\n"
|
|
448
|
-
"### 原因\n"
|
|
449
|
-
f"{reason_text}\n\n"
|
|
450
|
-
"### 建議架構\n"
|
|
451
|
-
f"{architecture_text}\n"
|
|
452
|
-
)
|
|
453
|
-
|
|
454
|
-
return (
|
|
455
|
-
"### Feature Proposal\n"
|
|
456
|
-
f"{proposal_text}\n\n"
|
|
457
|
-
"### Why This Is Needed\n"
|
|
458
|
-
f"{reason_text}\n\n"
|
|
459
|
-
"### Suggested Architecture\n"
|
|
460
|
-
f"{architecture_text}\n"
|
|
461
|
-
)
|
|
462
|
-
|
|
463
|
-
if issue_type == ISSUE_TYPE_PERFORMANCE:
|
|
464
|
-
if language == "zh":
|
|
465
|
-
return (
|
|
466
|
-
"### 效能問題\n"
|
|
467
|
-
f"{(problem_description or '').strip()}\n\n"
|
|
468
|
-
"### 影響\n"
|
|
469
|
-
f"{(impact or '').strip()}\n\n"
|
|
470
|
-
"### 證據\n"
|
|
471
|
-
f"{(evidence or '').strip()}\n\n"
|
|
472
|
-
"### 建議行動\n"
|
|
473
|
-
f"{(suggested_action or '').strip()}\n"
|
|
474
|
-
)
|
|
475
|
-
return (
|
|
476
|
-
"### Performance Problem\n"
|
|
477
|
-
f"{(problem_description or '').strip()}\n\n"
|
|
478
|
-
"### Impact\n"
|
|
479
|
-
f"{(impact or '').strip()}\n\n"
|
|
480
|
-
"### Evidence\n"
|
|
481
|
-
f"{(evidence or '').strip()}\n\n"
|
|
482
|
-
"### Suggested Action\n"
|
|
483
|
-
f"{(suggested_action or '').strip()}\n"
|
|
484
|
-
)
|
|
485
|
-
|
|
486
|
-
if issue_type == ISSUE_TYPE_SECURITY:
|
|
487
|
-
if language == "zh":
|
|
488
|
-
return (
|
|
489
|
-
"### 安全風險\n"
|
|
490
|
-
f"{(problem_description or '').strip()}\n\n"
|
|
491
|
-
"### 嚴重程度\n"
|
|
492
|
-
f"{(severity or '').strip()}\n\n"
|
|
493
|
-
"### 受影響範圍\n"
|
|
494
|
-
f"{(affected_scope or '').strip()}\n\n"
|
|
495
|
-
"### 影響\n"
|
|
496
|
-
f"{(impact or '').strip()}\n\n"
|
|
497
|
-
"### 證據\n"
|
|
498
|
-
f"{(evidence or '').strip()}\n\n"
|
|
499
|
-
"### 建議緩解\n"
|
|
500
|
-
f"{(suggested_action or '').strip()}\n"
|
|
501
|
-
)
|
|
502
|
-
return (
|
|
503
|
-
"### Security Risk\n"
|
|
504
|
-
f"{(problem_description or '').strip()}\n\n"
|
|
505
|
-
"### Severity\n"
|
|
506
|
-
f"{(severity or '').strip()}\n\n"
|
|
507
|
-
"### Affected Scope\n"
|
|
508
|
-
f"{(affected_scope or '').strip()}\n\n"
|
|
509
|
-
"### Impact\n"
|
|
510
|
-
f"{(impact or '').strip()}\n\n"
|
|
511
|
-
"### Evidence\n"
|
|
512
|
-
f"{(evidence or '').strip()}\n\n"
|
|
513
|
-
"### Suggested Mitigation\n"
|
|
514
|
-
f"{(suggested_action or '').strip()}\n"
|
|
515
|
-
)
|
|
516
|
-
|
|
517
|
-
if issue_type == ISSUE_TYPE_DOCS:
|
|
518
|
-
if language == "zh":
|
|
519
|
-
return (
|
|
520
|
-
"### 文件缺口\n"
|
|
521
|
-
f"{(problem_description or '').strip()}\n\n"
|
|
522
|
-
"### 證據\n"
|
|
523
|
-
f"{(evidence or '').strip()}\n\n"
|
|
524
|
-
"### 建議更新\n"
|
|
525
|
-
f"{(suggested_action or '').strip()}\n"
|
|
526
|
-
)
|
|
527
|
-
return (
|
|
528
|
-
"### Documentation Gap\n"
|
|
529
|
-
f"{(problem_description or '').strip()}\n\n"
|
|
530
|
-
"### Evidence\n"
|
|
531
|
-
f"{(evidence or '').strip()}\n\n"
|
|
532
|
-
"### Suggested Update\n"
|
|
533
|
-
f"{(suggested_action or '').strip()}\n"
|
|
534
|
-
)
|
|
535
|
-
|
|
536
|
-
if issue_type == ISSUE_TYPE_OBSERVABILITY:
|
|
537
|
-
if language == "zh":
|
|
538
|
-
return (
|
|
539
|
-
"### 可觀測性缺口\n"
|
|
540
|
-
f"{(problem_description or '').strip()}\n\n"
|
|
541
|
-
"### 影響\n"
|
|
542
|
-
f"{(impact or '').strip()}\n\n"
|
|
543
|
-
"### 證據\n"
|
|
544
|
-
f"{(evidence or '').strip()}\n\n"
|
|
545
|
-
"### 建議儀表化\n"
|
|
546
|
-
f"{(suggested_action or '').strip()}\n"
|
|
547
|
-
)
|
|
548
|
-
return (
|
|
549
|
-
"### Observability Gap\n"
|
|
550
|
-
f"{(problem_description or '').strip()}\n\n"
|
|
551
|
-
"### Impact\n"
|
|
552
|
-
f"{(impact or '').strip()}\n\n"
|
|
553
|
-
"### Evidence\n"
|
|
554
|
-
f"{(evidence or '').strip()}\n\n"
|
|
555
|
-
"### Suggested Instrumentation\n"
|
|
556
|
-
f"{(suggested_action or '').strip()}\n"
|
|
557
|
-
)
|
|
558
|
-
|
|
559
|
-
if language == "zh":
|
|
560
|
-
repro_text = (reproduction or DEFAULT_REPRO_ZH).strip()
|
|
561
|
-
return (
|
|
562
|
-
"### 問題描述\n"
|
|
563
|
-
f"{(problem_description or '').strip()}\n\n"
|
|
564
|
-
"### 推測原因\n"
|
|
565
|
-
f"{(suspected_cause or '').strip()}\n\n"
|
|
566
|
-
"### 重現條件(如有)\n"
|
|
567
|
-
f"{repro_text}\n"
|
|
568
|
-
)
|
|
569
|
-
|
|
570
|
-
repro_text = (reproduction or DEFAULT_REPRO_EN).strip()
|
|
571
|
-
return (
|
|
572
|
-
"### Problem Description\n"
|
|
573
|
-
f"{(problem_description or '').strip()}\n\n"
|
|
574
|
-
"### Suspected Cause\n"
|
|
575
|
-
f"{(suspected_cause or '').strip()}\n\n"
|
|
576
|
-
"### Reproduction Conditions (if available)\n"
|
|
577
|
-
f"{repro_text}\n"
|
|
578
|
-
)
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
def create_issue_with_gh(repo: str, title: str, body: str) -> str:
|
|
582
|
-
tmp_file: Path | None = None
|
|
583
|
-
try:
|
|
584
|
-
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".md", delete=False) as handle:
|
|
585
|
-
handle.write(body)
|
|
586
|
-
tmp_file = Path(handle.name)
|
|
587
|
-
|
|
588
|
-
result = run_command(
|
|
589
|
-
[
|
|
590
|
-
"gh",
|
|
591
|
-
"issue",
|
|
592
|
-
"create",
|
|
593
|
-
"--repo",
|
|
594
|
-
repo,
|
|
595
|
-
"--title",
|
|
596
|
-
title,
|
|
597
|
-
"--body-file",
|
|
598
|
-
str(tmp_file),
|
|
599
|
-
]
|
|
600
|
-
)
|
|
601
|
-
if result.returncode != 0:
|
|
602
|
-
raise RuntimeError(result.stderr.strip() or "gh issue create failed")
|
|
603
|
-
|
|
604
|
-
url_match = re.search(r"https://github\.com/[^\s]+/issues/\d+", result.stdout)
|
|
605
|
-
return url_match.group(0) if url_match else result.stdout.strip()
|
|
606
|
-
finally:
|
|
607
|
-
if tmp_file and tmp_file.exists():
|
|
608
|
-
tmp_file.unlink()
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
def create_issue_with_token(repo: str, title: str, body: str, token: str) -> str:
|
|
612
|
-
response = github_request(
|
|
613
|
-
"POST",
|
|
614
|
-
f"/repos/{repo}/issues",
|
|
615
|
-
token=token,
|
|
616
|
-
accept=JSON_ACCEPT,
|
|
617
|
-
payload={"title": title, "body": body},
|
|
618
|
-
)
|
|
619
|
-
parsed = json.loads(response)
|
|
620
|
-
issue_url = parsed.get("html_url", "")
|
|
621
|
-
if not issue_url:
|
|
622
|
-
raise RuntimeError("Issue created but response did not include html_url")
|
|
623
|
-
return issue_url
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
def main() -> int:
|
|
627
|
-
args = hydrate_args(parse_args())
|
|
628
|
-
validate_issue_content_args(args)
|
|
629
|
-
|
|
630
|
-
gh_authenticated = has_gh_auth()
|
|
631
|
-
token = get_token()
|
|
632
|
-
repo = resolve_repo(args.repo)
|
|
633
|
-
|
|
634
|
-
readme_content = fetch_remote_readme(repo, gh_authenticated, token)
|
|
635
|
-
language = detect_issue_language(readme_content)
|
|
636
|
-
|
|
637
|
-
issue_body = build_issue_body(
|
|
638
|
-
issue_type=args.issue_type,
|
|
639
|
-
language=language,
|
|
640
|
-
title=args.title,
|
|
641
|
-
problem_description=args.problem_description,
|
|
642
|
-
suspected_cause=args.suspected_cause,
|
|
643
|
-
reproduction=args.reproduction,
|
|
644
|
-
proposal=args.proposal,
|
|
645
|
-
reason=args.reason,
|
|
646
|
-
suggested_architecture=args.suggested_architecture,
|
|
647
|
-
impact=args.impact,
|
|
648
|
-
evidence=args.evidence,
|
|
649
|
-
suggested_action=args.suggested_action,
|
|
650
|
-
severity=args.severity,
|
|
651
|
-
affected_scope=args.affected_scope,
|
|
652
|
-
)
|
|
653
|
-
|
|
654
|
-
mode = "draft-only"
|
|
655
|
-
issue_url = ""
|
|
656
|
-
publish_error = ""
|
|
657
|
-
|
|
658
|
-
if args.dry_run:
|
|
659
|
-
mode = "dry-run"
|
|
660
|
-
elif gh_authenticated:
|
|
661
|
-
try:
|
|
662
|
-
issue_url = create_issue_with_gh(repo, args.title, issue_body)
|
|
663
|
-
mode = "gh-cli"
|
|
664
|
-
except RuntimeError as exc:
|
|
665
|
-
if token:
|
|
666
|
-
try:
|
|
667
|
-
issue_url = create_issue_with_token(repo, args.title, issue_body, token)
|
|
668
|
-
mode = "github-token"
|
|
669
|
-
except RuntimeError as token_exc:
|
|
670
|
-
publish_error = str(token_exc)
|
|
671
|
-
else:
|
|
672
|
-
publish_error = str(exc)
|
|
673
|
-
elif token:
|
|
674
|
-
try:
|
|
675
|
-
issue_url = create_issue_with_token(repo, args.title, issue_body, token)
|
|
676
|
-
mode = "github-token"
|
|
677
|
-
except RuntimeError as exc:
|
|
678
|
-
publish_error = str(exc)
|
|
679
|
-
|
|
680
|
-
output = {
|
|
681
|
-
"repo": repo,
|
|
682
|
-
"issue_type": args.issue_type,
|
|
683
|
-
"language": "zh" if language == "zh" else "en",
|
|
684
|
-
"mode": mode,
|
|
685
|
-
"issue_url": issue_url,
|
|
686
|
-
"issue_title": args.title,
|
|
687
|
-
"issue_body": issue_body,
|
|
688
|
-
"publish_error": publish_error,
|
|
689
|
-
}
|
|
690
|
-
print(json.dumps(output, ensure_ascii=False))
|
|
691
|
-
|
|
692
|
-
if mode == "draft-only":
|
|
693
|
-
if publish_error:
|
|
694
|
-
print(f"Issue publish failed. Return draft only: {publish_error}", file=sys.stderr)
|
|
695
|
-
else:
|
|
696
|
-
print(
|
|
697
|
-
"No authenticated gh CLI session and no GitHub token found. "
|
|
698
|
-
"Return draft issue body only.",
|
|
699
|
-
file=sys.stderr,
|
|
700
|
-
)
|
|
701
|
-
return 0
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
if __name__ == "__main__":
|
|
705
|
-
raise SystemExit(main())
|