@laitszkin/apollo-toolkit 3.13.2 → 3.14.1
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 +36 -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 +23 -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 +190 -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 +20 -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 +213 -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,177 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import importlib.util
|
|
6
|
-
import io
|
|
7
|
-
import json
|
|
8
|
-
import random
|
|
9
|
-
import string
|
|
10
|
-
import tempfile
|
|
11
|
-
import types
|
|
12
|
-
import unittest
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from unittest.mock import patch
|
|
15
|
-
|
|
16
|
-
PIL_AVAILABLE = importlib.util.find_spec("PIL") is not None
|
|
17
|
-
|
|
18
|
-
if PIL_AVAILABLE:
|
|
19
|
-
from PIL import Image
|
|
20
|
-
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "generate_storyboard_images.py"
|
|
21
|
-
SPEC = importlib.util.spec_from_file_location("generate_storyboard_images", SCRIPT_PATH)
|
|
22
|
-
MODULE = importlib.util.module_from_spec(SPEC)
|
|
23
|
-
SPEC.loader.exec_module(MODULE)
|
|
24
|
-
else:
|
|
25
|
-
Image = None
|
|
26
|
-
MODULE = None
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def png_bytes(width: int, height: int, color: tuple[int, int, int] = (64, 128, 255)) -> bytes:
|
|
30
|
-
buffer = io.BytesIO()
|
|
31
|
-
Image.new("RGB", (width, height), color).save(buffer, format="PNG")
|
|
32
|
-
return buffer.getvalue()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@unittest.skipUnless(PIL_AVAILABLE, "Pillow is required for storyboard image tests")
|
|
36
|
-
class GenerateStoryboardImagesTests(unittest.TestCase):
|
|
37
|
-
def test_sanitize_component_property_removes_invalid_path_characters(self) -> None:
|
|
38
|
-
generator = random.Random(20260418)
|
|
39
|
-
alphabet = string.ascii_letters + string.digits + string.punctuation + " \t中文"
|
|
40
|
-
|
|
41
|
-
for _ in range(200):
|
|
42
|
-
raw = "".join(generator.choice(alphabet) for _ in range(generator.randint(0, 30)))
|
|
43
|
-
sanitized = MODULE.sanitize_component(raw, "fallback")
|
|
44
|
-
with self.subTest(raw=raw, sanitized=sanitized):
|
|
45
|
-
self.assertTrue(sanitized)
|
|
46
|
-
self.assertNotRegex(sanitized, MODULE.INVALID_PATH_CHARS)
|
|
47
|
-
self.assertNotIn(" ", sanitized)
|
|
48
|
-
|
|
49
|
-
def test_parse_image_dimensions_supports_png_and_jpeg(self) -> None:
|
|
50
|
-
png = png_bytes(320, 180)
|
|
51
|
-
self.assertEqual(MODULE.parse_image_dimensions(png), (320, 180))
|
|
52
|
-
|
|
53
|
-
buffer = io.BytesIO()
|
|
54
|
-
Image.new("RGB", (160, 90), (0, 0, 0)).save(buffer, format="JPEG")
|
|
55
|
-
self.assertEqual(MODULE.parse_image_dimensions(buffer.getvalue()), (160, 90))
|
|
56
|
-
|
|
57
|
-
def test_parse_prompts_file_supports_structured_scene_mode(self) -> None:
|
|
58
|
-
payload = {
|
|
59
|
-
"characters": [
|
|
60
|
-
{
|
|
61
|
-
"id": "hero",
|
|
62
|
-
"name": "Kai",
|
|
63
|
-
"appearance": "short black hair",
|
|
64
|
-
"outfit": "blue jacket",
|
|
65
|
-
"description": "determined teenage inventor",
|
|
66
|
-
}
|
|
67
|
-
],
|
|
68
|
-
"scenes": [
|
|
69
|
-
{
|
|
70
|
-
"title": "Workshop",
|
|
71
|
-
"description": "Kai repairs a flickering lantern inside a cramped workshop.",
|
|
72
|
-
"character_ids": ["hero"],
|
|
73
|
-
"character_descriptions": {"hero": "focused and covered with grease"},
|
|
74
|
-
"camera": "medium shot",
|
|
75
|
-
}
|
|
76
|
-
],
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
80
|
-
prompts_file = Path(temp_dir) / "prompts.json"
|
|
81
|
-
prompts_file.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
|
82
|
-
items = MODULE.parse_prompts_file(prompts_file)
|
|
83
|
-
|
|
84
|
-
self.assertEqual(len(items), 1)
|
|
85
|
-
self.assertEqual(items[0]["title"], "Workshop")
|
|
86
|
-
prompt_payload = json.loads(items[0]["prompt"])
|
|
87
|
-
self.assertEqual(prompt_payload["characters"][0]["description"], "focused and covered with grease")
|
|
88
|
-
self.assertEqual(prompt_payload["camera"], "medium shot")
|
|
89
|
-
|
|
90
|
-
def test_build_scene_json_prompt_rejects_unknown_override_character(self) -> None:
|
|
91
|
-
scene = {
|
|
92
|
-
"title": "Workshop",
|
|
93
|
-
"description": "Lantern repair",
|
|
94
|
-
"character_ids": ["hero"],
|
|
95
|
-
"character_descriptions": {"ghost": "not listed"},
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
with self.assertRaises(SystemExit) as context:
|
|
99
|
-
MODULE.build_scene_json_prompt(
|
|
100
|
-
scene=scene,
|
|
101
|
-
scene_index=1,
|
|
102
|
-
prompts_file=Path("prompts.json"),
|
|
103
|
-
character_profiles={
|
|
104
|
-
"hero": {
|
|
105
|
-
"id": "hero",
|
|
106
|
-
"name": "Kai",
|
|
107
|
-
"appearance": "short black hair",
|
|
108
|
-
"outfit": "blue jacket",
|
|
109
|
-
"description": "determined teenage inventor",
|
|
110
|
-
}
|
|
111
|
-
},
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
self.assertIn("ids not listed in character_ids", str(context.exception))
|
|
115
|
-
|
|
116
|
-
def test_generate_image_supports_b64_payload(self) -> None:
|
|
117
|
-
raw = png_bytes(100, 60)
|
|
118
|
-
|
|
119
|
-
with patch.object(
|
|
120
|
-
MODULE,
|
|
121
|
-
"post_json",
|
|
122
|
-
return_value={"data": [{"b64_json": MODULE.base64.b64encode(raw).decode("ascii"), "revised_prompt": "ok"}]},
|
|
123
|
-
):
|
|
124
|
-
image_bytes, revised_prompt = MODULE.generate_image(
|
|
125
|
-
base_url="https://example.com",
|
|
126
|
-
api_key="token",
|
|
127
|
-
image_model="gpt-image-1",
|
|
128
|
-
prompt="scene",
|
|
129
|
-
aspect_ratio=None,
|
|
130
|
-
image_size=None,
|
|
131
|
-
quality=None,
|
|
132
|
-
style=None,
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
self.assertEqual(image_bytes, raw)
|
|
136
|
-
self.assertEqual(revised_prompt, "ok")
|
|
137
|
-
|
|
138
|
-
def test_main_generates_cropped_images_and_summary(self) -> None:
|
|
139
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
140
|
-
project_dir = Path(temp_dir)
|
|
141
|
-
args = types.SimpleNamespace(
|
|
142
|
-
content_name="Lantern / Story",
|
|
143
|
-
project_dir=str(project_dir),
|
|
144
|
-
env_file=str(project_dir / ".env"),
|
|
145
|
-
api_url="https://example.com",
|
|
146
|
-
api_key="token",
|
|
147
|
-
prompts_file=None,
|
|
148
|
-
prompt=["Lantern workshop at dusk"],
|
|
149
|
-
image_model="gpt-image-1",
|
|
150
|
-
aspect_ratio="1:1",
|
|
151
|
-
image_size=None,
|
|
152
|
-
quality=None,
|
|
153
|
-
style=None,
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
with patch.object(MODULE, "parse_args", return_value=args), patch.object(
|
|
157
|
-
MODULE, "load_dotenv_file", return_value=False
|
|
158
|
-
), patch.object(
|
|
159
|
-
MODULE,
|
|
160
|
-
"generate_image",
|
|
161
|
-
return_value=(png_bytes(200, 100), "Lantern workshop at dusk, improved"),
|
|
162
|
-
):
|
|
163
|
-
exit_code = MODULE.main()
|
|
164
|
-
|
|
165
|
-
self.assertEqual(exit_code, 0)
|
|
166
|
-
output_dir = project_dir / "pictures" / "Lantern_Story"
|
|
167
|
-
summary = json.loads((output_dir / "storyboard.json").read_text(encoding="utf-8"))
|
|
168
|
-
self.assertEqual(summary["aspect_ratio"], "1:1")
|
|
169
|
-
self.assertEqual(summary["images"][0]["width"], 100)
|
|
170
|
-
self.assertEqual(summary["images"][0]["height"], 100)
|
|
171
|
-
self.assertEqual(summary["images"][0]["source_width"], 200)
|
|
172
|
-
self.assertEqual(summary["images"][0]["source_height"], 100)
|
|
173
|
-
self.assertEqual(summary["images"][0]["revised_prompt"], "Lantern workshop at dusk, improved")
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if __name__ == "__main__":
|
|
177
|
-
unittest.main()
|
|
Binary file
|
|
Binary file
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
import argparse
|
|
3
|
-
import json
|
|
4
|
-
import subprocess
|
|
5
|
-
import sys
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
ISSUE_FIELDS = "number,title,state,updatedAt,url,labels,assignees"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def positive_int(raw: str) -> int:
|
|
12
|
-
value = int(raw)
|
|
13
|
-
if value <= 0:
|
|
14
|
-
raise argparse.ArgumentTypeError("limit must be a positive integer")
|
|
15
|
-
return value
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def parse_args() -> argparse.Namespace:
|
|
19
|
-
parser = argparse.ArgumentParser(
|
|
20
|
-
description="Find remote GitHub issues with gh CLI and display table or JSON output."
|
|
21
|
-
)
|
|
22
|
-
parser.add_argument("--repo", help="Target repository in owner/name format.")
|
|
23
|
-
parser.add_argument("--state", default="open", choices=["open", "closed", "all"])
|
|
24
|
-
parser.add_argument("--limit", type=positive_int, default=50)
|
|
25
|
-
parser.add_argument(
|
|
26
|
-
"--label",
|
|
27
|
-
action="append",
|
|
28
|
-
default=[],
|
|
29
|
-
help="Label filter (repeat for multiple labels).",
|
|
30
|
-
)
|
|
31
|
-
parser.add_argument("--search", help="Search query for issue list filtering.")
|
|
32
|
-
parser.add_argument(
|
|
33
|
-
"--output",
|
|
34
|
-
choices=["table", "json"],
|
|
35
|
-
default="table",
|
|
36
|
-
help="Output format.",
|
|
37
|
-
)
|
|
38
|
-
return parser.parse_args()
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def build_command(args: argparse.Namespace) -> list[str]:
|
|
42
|
-
cmd = [
|
|
43
|
-
"gh",
|
|
44
|
-
"issue",
|
|
45
|
-
"list",
|
|
46
|
-
"--state",
|
|
47
|
-
args.state,
|
|
48
|
-
"--limit",
|
|
49
|
-
str(args.limit),
|
|
50
|
-
"--json",
|
|
51
|
-
ISSUE_FIELDS,
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
if args.repo:
|
|
55
|
-
cmd.extend(["--repo", args.repo])
|
|
56
|
-
for label in args.label:
|
|
57
|
-
cmd.extend(["--label", label])
|
|
58
|
-
if args.search:
|
|
59
|
-
cmd.extend(["--search", args.search])
|
|
60
|
-
|
|
61
|
-
return cmd
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def truncate(text: str, width: int) -> str:
|
|
65
|
-
if len(text) <= width:
|
|
66
|
-
return text
|
|
67
|
-
if width <= 3:
|
|
68
|
-
return text[:width]
|
|
69
|
-
return text[: width - 3] + "..."
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def format_labels(issue: dict[str, Any]) -> str:
|
|
73
|
-
labels = issue.get("labels", [])
|
|
74
|
-
names = [item.get("name", "") for item in labels if item.get("name")]
|
|
75
|
-
return ",".join(names)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def format_assignees(issue: dict[str, Any]) -> str:
|
|
79
|
-
assignees = issue.get("assignees", [])
|
|
80
|
-
logins = [item.get("login", "") for item in assignees if item.get("login")]
|
|
81
|
-
return ",".join(logins) if logins else "-"
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def print_table(issues: list[dict[str, Any]]) -> None:
|
|
85
|
-
columns = {
|
|
86
|
-
"number": 7,
|
|
87
|
-
"title": 54,
|
|
88
|
-
"labels": 22,
|
|
89
|
-
"assignees": 18,
|
|
90
|
-
"updated": 20,
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
header = (
|
|
94
|
-
f"{'NUMBER':<{columns['number']}} "
|
|
95
|
-
f"{'TITLE':<{columns['title']}} "
|
|
96
|
-
f"{'LABELS':<{columns['labels']}} "
|
|
97
|
-
f"{'ASSIGNEES':<{columns['assignees']}} "
|
|
98
|
-
f"{'UPDATED':<{columns['updated']}}"
|
|
99
|
-
)
|
|
100
|
-
print(header)
|
|
101
|
-
print("-" * len(header))
|
|
102
|
-
|
|
103
|
-
for issue in issues:
|
|
104
|
-
number = f"#{issue.get('number', '')}"
|
|
105
|
-
title = truncate(str(issue.get("title", "")), columns["title"])
|
|
106
|
-
labels = truncate(format_labels(issue), columns["labels"])
|
|
107
|
-
assignees = truncate(format_assignees(issue), columns["assignees"])
|
|
108
|
-
updated = truncate(str(issue.get("updatedAt", "")), columns["updated"])
|
|
109
|
-
|
|
110
|
-
row = (
|
|
111
|
-
f"{number:<{columns['number']}} "
|
|
112
|
-
f"{title:<{columns['title']}} "
|
|
113
|
-
f"{labels:<{columns['labels']}} "
|
|
114
|
-
f"{assignees:<{columns['assignees']}} "
|
|
115
|
-
f"{updated:<{columns['updated']}}"
|
|
116
|
-
)
|
|
117
|
-
print(row)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def main() -> int:
|
|
121
|
-
args = parse_args()
|
|
122
|
-
cmd = build_command(args)
|
|
123
|
-
|
|
124
|
-
try:
|
|
125
|
-
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
126
|
-
except FileNotFoundError:
|
|
127
|
-
print("Error: gh CLI is not installed or not in PATH.", file=sys.stderr)
|
|
128
|
-
return 1
|
|
129
|
-
except subprocess.CalledProcessError as exc:
|
|
130
|
-
print(exc.stderr.strip() or "gh issue list failed.", file=sys.stderr)
|
|
131
|
-
return exc.returncode
|
|
132
|
-
|
|
133
|
-
try:
|
|
134
|
-
issues = json.loads(result.stdout)
|
|
135
|
-
except json.JSONDecodeError:
|
|
136
|
-
print("Error: unable to parse gh output as JSON.", file=sys.stderr)
|
|
137
|
-
return 1
|
|
138
|
-
|
|
139
|
-
if args.output == "json":
|
|
140
|
-
print(json.dumps(issues, indent=2))
|
|
141
|
-
return 0
|
|
142
|
-
|
|
143
|
-
print_table(issues)
|
|
144
|
-
return 0
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if __name__ == "__main__":
|
|
148
|
-
sys.exit(main())
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import argparse
|
|
5
|
-
import json
|
|
6
|
-
import subprocess
|
|
7
|
-
import sys
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
ISSUE_FIELDS = "number,title,body,state,author,labels,assignees,comments,createdAt,updatedAt,closedAt,url"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def parse_args() -> argparse.Namespace:
|
|
14
|
-
parser = argparse.ArgumentParser(
|
|
15
|
-
description="Read a remote GitHub issue with gh CLI and display summary or JSON output."
|
|
16
|
-
)
|
|
17
|
-
parser.add_argument("issue", help="Issue number or URL.")
|
|
18
|
-
parser.add_argument("--repo", help="Target repository in owner/name format.")
|
|
19
|
-
parser.add_argument(
|
|
20
|
-
"--comments",
|
|
21
|
-
action="store_true",
|
|
22
|
-
help="Keep issue comments in formatted output.",
|
|
23
|
-
)
|
|
24
|
-
parser.add_argument(
|
|
25
|
-
"--json",
|
|
26
|
-
action="store_true",
|
|
27
|
-
help="Print raw JSON output.",
|
|
28
|
-
)
|
|
29
|
-
return parser.parse_args()
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def build_command(args: argparse.Namespace) -> list[str]:
|
|
33
|
-
cmd = [
|
|
34
|
-
"gh",
|
|
35
|
-
"issue",
|
|
36
|
-
"view",
|
|
37
|
-
args.issue,
|
|
38
|
-
"--json",
|
|
39
|
-
ISSUE_FIELDS,
|
|
40
|
-
]
|
|
41
|
-
if args.repo:
|
|
42
|
-
cmd.extend(["--repo", args.repo])
|
|
43
|
-
return cmd
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def join_names(items: list[dict], key: str) -> str:
|
|
47
|
-
values = [item.get(key, "") for item in items if item.get(key)]
|
|
48
|
-
return ", ".join(values) if values else "-"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def print_summary(issue: dict, include_comments: bool) -> None:
|
|
52
|
-
print(f"Number: #{issue.get('number', '')}")
|
|
53
|
-
print(f"Title: {issue.get('title', '')}")
|
|
54
|
-
print(f"State: {issue.get('state', '')}")
|
|
55
|
-
print(f"URL: {issue.get('url', '')}")
|
|
56
|
-
print(f"Author: {(issue.get('author') or {}).get('login', '-')}")
|
|
57
|
-
print(f"Labels: {join_names(issue.get('labels', []), 'name')}")
|
|
58
|
-
print(f"Assignees: {join_names(issue.get('assignees', []), 'login')}")
|
|
59
|
-
print(f"Created: {issue.get('createdAt', '')}")
|
|
60
|
-
print(f"Updated: {issue.get('updatedAt', '')}")
|
|
61
|
-
print(f"Closed: {issue.get('closedAt', '') or '-'}")
|
|
62
|
-
print("")
|
|
63
|
-
print("Body:")
|
|
64
|
-
print(issue.get("body", "") or "-")
|
|
65
|
-
|
|
66
|
-
if include_comments:
|
|
67
|
-
comments = issue.get("comments", [])
|
|
68
|
-
print("")
|
|
69
|
-
print(f"Comments ({len(comments)}):")
|
|
70
|
-
if not comments:
|
|
71
|
-
print("-")
|
|
72
|
-
return
|
|
73
|
-
for comment in comments:
|
|
74
|
-
author = (comment.get("author") or {}).get("login", "-")
|
|
75
|
-
created = comment.get("createdAt", "")
|
|
76
|
-
body = comment.get("body", "") or "-"
|
|
77
|
-
print(f"- [{created}] {author}: {body}")
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def main() -> int:
|
|
81
|
-
args = parse_args()
|
|
82
|
-
cmd = build_command(args)
|
|
83
|
-
|
|
84
|
-
try:
|
|
85
|
-
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
86
|
-
except FileNotFoundError:
|
|
87
|
-
print("Error: gh CLI is not installed or not in PATH.", file=sys.stderr)
|
|
88
|
-
return 1
|
|
89
|
-
except subprocess.CalledProcessError as exc:
|
|
90
|
-
print(exc.stderr.strip() or "gh issue view failed.", file=sys.stderr)
|
|
91
|
-
return exc.returncode
|
|
92
|
-
|
|
93
|
-
try:
|
|
94
|
-
issue = json.loads(result.stdout)
|
|
95
|
-
except json.JSONDecodeError:
|
|
96
|
-
print("Error: unable to parse gh output as JSON.", file=sys.stderr)
|
|
97
|
-
return 1
|
|
98
|
-
|
|
99
|
-
if args.json:
|
|
100
|
-
print(json.dumps(issue, indent=2))
|
|
101
|
-
return 0
|
|
102
|
-
|
|
103
|
-
print_summary(issue, include_comments=args.comments)
|
|
104
|
-
return 0
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if __name__ == "__main__":
|
|
108
|
-
sys.exit(main())
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import argparse
|
|
6
|
-
import importlib.util
|
|
7
|
-
import io
|
|
8
|
-
import json
|
|
9
|
-
import unittest
|
|
10
|
-
from argparse import Namespace
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from unittest.mock import patch
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "find_issues.py"
|
|
16
|
-
SPEC = importlib.util.spec_from_file_location("find_issues", SCRIPT_PATH)
|
|
17
|
-
MODULE = importlib.util.module_from_spec(SPEC)
|
|
18
|
-
SPEC.loader.exec_module(MODULE)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class FindIssuesTests(unittest.TestCase):
|
|
22
|
-
def test_build_command_with_filters(self) -> None:
|
|
23
|
-
args = Namespace(
|
|
24
|
-
repo="owner/repo",
|
|
25
|
-
state="open",
|
|
26
|
-
limit=25,
|
|
27
|
-
label=["bug", "needs-triage"],
|
|
28
|
-
search="panic parser",
|
|
29
|
-
output="table",
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
cmd = MODULE.build_command(args)
|
|
33
|
-
|
|
34
|
-
self.assertEqual(
|
|
35
|
-
cmd,
|
|
36
|
-
[
|
|
37
|
-
"gh",
|
|
38
|
-
"issue",
|
|
39
|
-
"list",
|
|
40
|
-
"--state",
|
|
41
|
-
"open",
|
|
42
|
-
"--limit",
|
|
43
|
-
"25",
|
|
44
|
-
"--json",
|
|
45
|
-
MODULE.ISSUE_FIELDS,
|
|
46
|
-
"--repo",
|
|
47
|
-
"owner/repo",
|
|
48
|
-
"--label",
|
|
49
|
-
"bug",
|
|
50
|
-
"--label",
|
|
51
|
-
"needs-triage",
|
|
52
|
-
"--search",
|
|
53
|
-
"panic parser",
|
|
54
|
-
],
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
def test_positive_int_rejects_zero(self) -> None:
|
|
58
|
-
with self.assertRaises(argparse.ArgumentTypeError):
|
|
59
|
-
MODULE.positive_int("0")
|
|
60
|
-
|
|
61
|
-
def test_truncate_handles_small_width(self) -> None:
|
|
62
|
-
self.assertEqual(MODULE.truncate("abcdef", 2), "ab")
|
|
63
|
-
|
|
64
|
-
def test_print_table_handles_missing_fields(self) -> None:
|
|
65
|
-
with patch("sys.stdout", new_callable=io.StringIO) as stdout:
|
|
66
|
-
MODULE.print_table([
|
|
67
|
-
{
|
|
68
|
-
"number": 42,
|
|
69
|
-
"title": "Fix formatter fallback",
|
|
70
|
-
}
|
|
71
|
-
])
|
|
72
|
-
|
|
73
|
-
output = stdout.getvalue()
|
|
74
|
-
self.assertIn("NUMBER", output)
|
|
75
|
-
self.assertIn("#42", output)
|
|
76
|
-
|
|
77
|
-
def test_main_returns_error_when_gh_missing(self) -> None:
|
|
78
|
-
args = Namespace(
|
|
79
|
-
repo=None,
|
|
80
|
-
state="open",
|
|
81
|
-
limit=50,
|
|
82
|
-
label=[],
|
|
83
|
-
search=None,
|
|
84
|
-
output="table",
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
with patch.object(MODULE, "parse_args", return_value=args), patch(
|
|
88
|
-
"subprocess.run", side_effect=FileNotFoundError
|
|
89
|
-
), patch("sys.stderr", new_callable=io.StringIO) as stderr:
|
|
90
|
-
code = MODULE.main()
|
|
91
|
-
|
|
92
|
-
self.assertEqual(code, 1)
|
|
93
|
-
self.assertIn("not in PATH", stderr.getvalue())
|
|
94
|
-
|
|
95
|
-
def test_main_json_output(self) -> None:
|
|
96
|
-
args = Namespace(
|
|
97
|
-
repo=None,
|
|
98
|
-
state="open",
|
|
99
|
-
limit=50,
|
|
100
|
-
label=[],
|
|
101
|
-
search=None,
|
|
102
|
-
output="json",
|
|
103
|
-
)
|
|
104
|
-
payload = [
|
|
105
|
-
{
|
|
106
|
-
"number": 1,
|
|
107
|
-
"title": "Fix bug",
|
|
108
|
-
"labels": [],
|
|
109
|
-
"assignees": [],
|
|
110
|
-
"updatedAt": "2026-02-26T00:00:00Z",
|
|
111
|
-
}
|
|
112
|
-
]
|
|
113
|
-
|
|
114
|
-
class Result:
|
|
115
|
-
stdout = json.dumps(payload)
|
|
116
|
-
|
|
117
|
-
with patch.object(MODULE, "parse_args", return_value=args), patch(
|
|
118
|
-
"subprocess.run", return_value=Result()
|
|
119
|
-
), patch("sys.stdout", new_callable=io.StringIO) as stdout:
|
|
120
|
-
code = MODULE.main()
|
|
121
|
-
|
|
122
|
-
self.assertEqual(code, 0)
|
|
123
|
-
self.assertEqual(json.loads(stdout.getvalue()), payload)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if __name__ == "__main__":
|
|
127
|
-
unittest.main()
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import importlib.util
|
|
6
|
-
import io
|
|
7
|
-
import json
|
|
8
|
-
import unittest
|
|
9
|
-
from argparse import Namespace
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from unittest.mock import patch
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "read_issue.py"
|
|
15
|
-
SPEC = importlib.util.spec_from_file_location("read_issue", SCRIPT_PATH)
|
|
16
|
-
MODULE = importlib.util.module_from_spec(SPEC)
|
|
17
|
-
SPEC.loader.exec_module(MODULE)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class ReadIssueTests(unittest.TestCase):
|
|
21
|
-
def test_build_command_with_repo(self) -> None:
|
|
22
|
-
args = Namespace(issue="123", repo="owner/repo", comments=False, json=False)
|
|
23
|
-
|
|
24
|
-
self.assertEqual(
|
|
25
|
-
MODULE.build_command(args),
|
|
26
|
-
[
|
|
27
|
-
"gh",
|
|
28
|
-
"issue",
|
|
29
|
-
"view",
|
|
30
|
-
"123",
|
|
31
|
-
"--json",
|
|
32
|
-
MODULE.ISSUE_FIELDS,
|
|
33
|
-
"--repo",
|
|
34
|
-
"owner/repo",
|
|
35
|
-
],
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
def test_main_returns_error_when_gh_missing(self) -> None:
|
|
39
|
-
args = Namespace(issue="123", repo=None, comments=False, json=False)
|
|
40
|
-
|
|
41
|
-
with patch.object(MODULE, "parse_args", return_value=args), patch(
|
|
42
|
-
"subprocess.run", side_effect=FileNotFoundError
|
|
43
|
-
), patch("sys.stderr", new_callable=io.StringIO) as stderr:
|
|
44
|
-
code = MODULE.main()
|
|
45
|
-
|
|
46
|
-
self.assertEqual(code, 1)
|
|
47
|
-
self.assertIn("not in PATH", stderr.getvalue())
|
|
48
|
-
|
|
49
|
-
def test_main_json_output(self) -> None:
|
|
50
|
-
args = Namespace(issue="123", repo=None, comments=False, json=True)
|
|
51
|
-
payload = {
|
|
52
|
-
"number": 123,
|
|
53
|
-
"title": "Need better retries",
|
|
54
|
-
"body": "detail",
|
|
55
|
-
"state": "OPEN",
|
|
56
|
-
"author": {"login": "octocat"},
|
|
57
|
-
"labels": [],
|
|
58
|
-
"assignees": [],
|
|
59
|
-
"comments": [],
|
|
60
|
-
"createdAt": "2026-03-24T00:00:00Z",
|
|
61
|
-
"updatedAt": "2026-03-24T01:00:00Z",
|
|
62
|
-
"closedAt": None,
|
|
63
|
-
"url": "https://github.com/owner/repo/issues/123",
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
class Result:
|
|
67
|
-
stdout = json.dumps(payload)
|
|
68
|
-
|
|
69
|
-
with patch.object(MODULE, "parse_args", return_value=args), patch(
|
|
70
|
-
"subprocess.run", return_value=Result()
|
|
71
|
-
), patch("sys.stdout", new_callable=io.StringIO) as stdout:
|
|
72
|
-
code = MODULE.main()
|
|
73
|
-
|
|
74
|
-
self.assertEqual(code, 0)
|
|
75
|
-
self.assertEqual(json.loads(stdout.getvalue()), payload)
|
|
76
|
-
|
|
77
|
-
def test_print_summary_includes_comments(self) -> None:
|
|
78
|
-
issue = {
|
|
79
|
-
"number": 123,
|
|
80
|
-
"title": "Need better retries",
|
|
81
|
-
"body": "detail",
|
|
82
|
-
"state": "OPEN",
|
|
83
|
-
"author": {"login": "octocat"},
|
|
84
|
-
"labels": [{"name": "bug"}],
|
|
85
|
-
"assignees": [{"login": "alice"}],
|
|
86
|
-
"comments": [
|
|
87
|
-
{
|
|
88
|
-
"author": {"login": "bob"},
|
|
89
|
-
"createdAt": "2026-03-24T02:00:00Z",
|
|
90
|
-
"body": "extra context",
|
|
91
|
-
}
|
|
92
|
-
],
|
|
93
|
-
"createdAt": "2026-03-24T00:00:00Z",
|
|
94
|
-
"updatedAt": "2026-03-24T01:00:00Z",
|
|
95
|
-
"closedAt": None,
|
|
96
|
-
"url": "https://github.com/owner/repo/issues/123",
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
with patch("sys.stdout", new_callable=io.StringIO) as stdout:
|
|
100
|
-
MODULE.print_summary(issue, include_comments=True)
|
|
101
|
-
|
|
102
|
-
output = stdout.getvalue()
|
|
103
|
-
self.assertIn("#123", output)
|
|
104
|
-
self.assertIn("bug", output)
|
|
105
|
-
self.assertIn("extra context", output)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if __name__ == "__main__":
|
|
109
|
-
unittest.main()
|