mirror of
https://github.com/openai/skills.git
synced 2026-09-19 23:46:30 +03:00
add skill for gitlab address comments (#61)
## Summary: - add experimental gitlab-address-comments skill docs and workflow steps - include a fetch_comments.py helper to pull MR discussions via glab api - add Apache 2.0 license for the new skill bundle ## Testing: - manually QA'd
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: gitlab-address-comments
|
||||
description: Help address review/issue comments on the open GitLab MR for the current branch using glab CLI. Use when the user wants help addressing review/issue comments on an open GitLab MR
|
||||
metadata:
|
||||
short-description: Address comments in a GitLab MR review
|
||||
---
|
||||
|
||||
# MR Comment Handler
|
||||
|
||||
Find the open MR for the current branch and address its review threads using `glab`. Run all `glab` commands with elevated network access.
|
||||
|
||||
## Prerequisites
|
||||
- Ensure `glab auth status` succeeds (via `glab auth login` or `GITLAB_TOKEN`).
|
||||
- Ensure `glab` is at least v1.80.4.
|
||||
- When sandboxing blocks network calls, rerun with `sandbox_permissions=require_escalated`.
|
||||
- Sanity check auth up front:
|
||||
```bash
|
||||
glab auth status
|
||||
```
|
||||
|
||||
## 1) Resolve the MR for the current branch
|
||||
Do a quick check so we know which MR we are about to operate on:
|
||||
```bash
|
||||
branch="$(git rev-parse --abbrev-ref HEAD)"
|
||||
glab mr view "$branch" --output json
|
||||
```
|
||||
If this fails, the fetch script below will still try to locate the MR by `source_branch`.
|
||||
|
||||
## 2) Fetch unresolved discussions to `/tmp`
|
||||
Use the local script to fetch MR discussions via `glab api`. This filters out bot/system-only threads and returns unresolved discussions when `--open-comments` is set.
|
||||
```bash
|
||||
skill_dir="<path-to-skill>"
|
||||
branch="$(git rev-parse --abbrev-ref HEAD)"
|
||||
safe_branch="${branch//\//_}"
|
||||
out="/tmp/${safe_branch}_mr_open_discussions.json"
|
||||
python "$skill_dir/scripts/fetch_comments.py" --open-comments --output "$out"
|
||||
```
|
||||
If you want the full payload (including resolved discussions), drop `--open-comments`.
|
||||
|
||||
## 3) Summarize, triage, and ask once
|
||||
- Load the JSON and number each unresolved discussion.
|
||||
- Start with a compact summary list instead of dumping full threads.
|
||||
- Sort by: unresolved first (already filtered), then most recently updated.
|
||||
- When possible, group or label by file path to reduce context switching.
|
||||
- In the summary list, show: number, discussion id, author, file:line (if present), and a one-line summary.
|
||||
- Ask for a batch selection in one shot. Accept: `1,3,5-7`, `all`, `none`, or `top N`.
|
||||
- If the user does not choose, suggest a small default set (for example `top 3`) with a short rationale.
|
||||
- Only after selection, show the full thread and code context for the selected numbers.
|
||||
- When showing code context, include 3 lines before and after and clearly mark the referenced line(s).
|
||||
|
||||
## 4) Implement fixes for the selected discussions
|
||||
- Apply focused fixes that address the selected threads.
|
||||
- Run the most relevant tests or checks you can in-repo.
|
||||
- Report back with: what changed, which discussion numbers were addressed, and any follow-ups.
|
||||
|
||||
Notes:
|
||||
- If `glab` hits auth or rate issues, prompt the user to run `glab auth login` or re-export `GITLAB_TOKEN`, then retry.
|
||||
- If no open MR is found for the branch, say so clearly and ask for the MR URL or IID.
|
||||
- Do not prompt “address or skip?” one comment at a time unless the user explicitly asks for that mode.
|
||||
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fetch GitLab merge request discussions (including inline threads) for the MR
|
||||
associated with the current git branch, by shelling out to:
|
||||
|
||||
glab api
|
||||
|
||||
Requires:
|
||||
- `glab auth status` succeeds (uses glab config/keyring or GITLAB_TOKEN)
|
||||
- current branch has an associated open MR
|
||||
|
||||
Usage:
|
||||
python scripts/fetch_comments.py > /tmp/mr_comments.json
|
||||
python scripts/fetch_comments.py --open-comments > /tmp/open_threads.json
|
||||
python scripts/fetch_comments.py --output /tmp/mr_comments.json
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any, Iterable
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
def _run(cmd: list[str]) -> str:
|
||||
p = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if p.returncode != 0:
|
||||
raise RuntimeError(f"Command failed: {' '.join(cmd)}\n{p.stderr}")
|
||||
return p.stdout
|
||||
|
||||
|
||||
def _run_json(cmd: list[str]) -> Any:
|
||||
out = _run(cmd)
|
||||
try:
|
||||
return json.loads(out)
|
||||
except json.JSONDecodeError as e:
|
||||
raise RuntimeError(f"Failed to parse JSON from command output: {e}\nRaw:\n{out}") from e
|
||||
|
||||
|
||||
def _ensure_glab_authenticated() -> None:
|
||||
try:
|
||||
_run(["glab", "auth", "status"])
|
||||
except RuntimeError as exc:
|
||||
raise RuntimeError(
|
||||
"glab auth status failed; run `glab auth login` or set GITLAB_TOKEN"
|
||||
) from exc
|
||||
|
||||
|
||||
def _git_current_branch() -> str:
|
||||
return _run(["git", "rev-parse", "--abbrev-ref", "HEAD"]).strip()
|
||||
|
||||
|
||||
def _git_origin_url() -> str:
|
||||
return _run(["git", "remote", "get-url", "origin"]).strip()
|
||||
|
||||
|
||||
def _strip_dot_git(path: str) -> str:
|
||||
return path[:-4] if path.endswith(".git") else path
|
||||
|
||||
|
||||
def _parse_project_path(remote_url: str) -> str:
|
||||
"""
|
||||
Convert a git remote URL into a GitLab project path (group/subgroup/project).
|
||||
Supports common SSH and HTTPS formats.
|
||||
"""
|
||||
# HTTPS: https://gitlab.example.com/group/project.git
|
||||
https_match = re.match(r"^https?://[^/]+/(.+)$", remote_url)
|
||||
if https_match:
|
||||
return _strip_dot_git(https_match.group(1))
|
||||
|
||||
# SSH scp-like: git@gitlab.example.com:group/project.git
|
||||
ssh_match = re.match(r"^(?:ssh://)?git@[^:/]+[:/](.+)$", remote_url)
|
||||
if ssh_match:
|
||||
return _strip_dot_git(ssh_match.group(1))
|
||||
|
||||
raise RuntimeError(f"Unable to parse GitLab project path from origin URL: {remote_url}")
|
||||
|
||||
|
||||
def _glab_api_get(endpoint: str, params: dict[str, Any] | None = None) -> Any:
|
||||
cmd = ["glab", "api", endpoint, "-X", "GET"]
|
||||
for key, value in (params or {}).items():
|
||||
if value is None:
|
||||
continue
|
||||
cmd += ["-F", f"{key}={value}"]
|
||||
return _run_json(cmd)
|
||||
|
||||
|
||||
def _paginate(endpoint: str, base_params: dict[str, Any], per_page: int = 100, max_pages: int = 20) -> list[Any]:
|
||||
results: list[Any] = []
|
||||
page = 1
|
||||
|
||||
while page <= max_pages:
|
||||
params = dict(base_params)
|
||||
params.update({"per_page": per_page, "page": page})
|
||||
chunk = _glab_api_get(endpoint, params=params)
|
||||
|
||||
if not isinstance(chunk, list) or not chunk:
|
||||
break
|
||||
|
||||
results.extend(chunk)
|
||||
|
||||
if len(chunk) < per_page:
|
||||
break
|
||||
page += 1
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _encode_project_path(project_path: str) -> str:
|
||||
# GitLab API accepts URL-encoded project paths in place of numeric IDs.
|
||||
return quote(project_path, safe="")
|
||||
|
||||
|
||||
def _find_open_mr_for_branch(project: str, branch: str) -> dict[str, Any]:
|
||||
endpoint = f"/projects/{project}/merge_requests"
|
||||
mrs = _paginate(
|
||||
endpoint,
|
||||
base_params={
|
||||
"state": "opened",
|
||||
"source_branch": branch,
|
||||
"order_by": "updated_at",
|
||||
"sort": "desc",
|
||||
},
|
||||
)
|
||||
if not mrs:
|
||||
raise RuntimeError(f"No open merge request found for source branch: {branch}")
|
||||
# Prefer the most recently updated MR for this branch.
|
||||
return mrs[0]
|
||||
|
||||
|
||||
def _get_mr(project: str, mr_iid: int) -> dict[str, Any]:
|
||||
endpoint = f"/projects/{project}/merge_requests/{mr_iid}"
|
||||
mr = _glab_api_get(endpoint)
|
||||
if not isinstance(mr, dict):
|
||||
raise RuntimeError("Unexpected response when fetching merge request details")
|
||||
return mr
|
||||
|
||||
|
||||
def _discussion_notes(discussion: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
notes = discussion.get("notes")
|
||||
return notes if isinstance(notes, list) else []
|
||||
|
||||
|
||||
def _is_bot_or_system_note(note: dict[str, Any]) -> bool:
|
||||
if note.get("system"):
|
||||
return True
|
||||
author = note.get("author") or {}
|
||||
if author.get("bot"):
|
||||
return True
|
||||
username = str(author.get("username") or "").lower()
|
||||
return username.endswith("[bot]") or username.endswith("-bot")
|
||||
|
||||
|
||||
def _filter_bot_notes(notes: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
return [n for n in notes if not _is_bot_or_system_note(n)]
|
||||
|
||||
|
||||
def _discussion_is_open(discussion: dict[str, Any]) -> bool:
|
||||
# Prefer the top-level resolved flag if present.
|
||||
resolved = discussion.get("resolved")
|
||||
if resolved is True:
|
||||
return False
|
||||
|
||||
notes = _discussion_notes(discussion)
|
||||
resolvable_notes = [n for n in notes if n.get("resolvable")]
|
||||
if resolvable_notes:
|
||||
# If any resolvable note is still unresolved, treat the discussion as open.
|
||||
return any(not bool(n.get("resolved")) for n in resolvable_notes)
|
||||
|
||||
# Fallback: treat unresolved/unknown as open.
|
||||
return not bool(resolved)
|
||||
|
||||
|
||||
def _discussion_has_non_bot_content(discussion: dict[str, Any]) -> bool:
|
||||
notes = _discussion_notes(discussion)
|
||||
return len(_filter_bot_notes(notes)) > 0
|
||||
|
||||
|
||||
def fetch_all(project_path: str, branch: str) -> dict[str, Any]:
|
||||
encoded_project = _encode_project_path(project_path)
|
||||
|
||||
mr_hint = _find_open_mr_for_branch(encoded_project, branch)
|
||||
mr_iid = int(mr_hint["iid"])
|
||||
mr = _get_mr(encoded_project, mr_iid)
|
||||
|
||||
discussions_endpoint = f"/projects/{encoded_project}/merge_requests/{mr_iid}/discussions"
|
||||
discussions = _paginate(discussions_endpoint, base_params={})
|
||||
|
||||
# Drop pure bot/system discussions.
|
||||
discussions = [d for d in discussions if _discussion_has_non_bot_content(d)]
|
||||
|
||||
open_discussions = [d for d in discussions if _discussion_is_open(d)]
|
||||
|
||||
mr_meta = {
|
||||
"iid": mr.get("iid"),
|
||||
"project_id": mr.get("project_id"),
|
||||
"web_url": mr.get("web_url"),
|
||||
"title": mr.get("title"),
|
||||
"state": mr.get("state"),
|
||||
"source_branch": mr.get("source_branch"),
|
||||
"target_branch": mr.get("target_branch"),
|
||||
"updated_at": mr.get("updated_at"),
|
||||
}
|
||||
|
||||
return {
|
||||
"merge_request": mr_meta,
|
||||
"project": {
|
||||
"path": project_path,
|
||||
"encoded_path": encoded_project,
|
||||
"branch": branch,
|
||||
},
|
||||
"discussions": discussions,
|
||||
"open_discussions": open_discussions,
|
||||
}
|
||||
|
||||
|
||||
def _build_arg_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="Fetch GitLab MR discussions for the current branch")
|
||||
parser.add_argument(
|
||||
"--open-comments",
|
||||
action="store_true",
|
||||
help="emit only unresolved/open discussions (still includes merge_request metadata)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
help="optional path to write JSON output; defaults to stdout",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = _build_arg_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
_ensure_glab_authenticated()
|
||||
|
||||
branch = _git_current_branch()
|
||||
origin = _git_origin_url()
|
||||
project_path = _parse_project_path(origin)
|
||||
|
||||
result = fetch_all(project_path=project_path, branch=branch)
|
||||
|
||||
if args.open_comments:
|
||||
payload: Any = {
|
||||
"merge_request": result["merge_request"],
|
||||
"project": result["project"],
|
||||
"open_discussions": result["open_discussions"],
|
||||
}
|
||||
else:
|
||||
payload = result
|
||||
|
||||
output = json.dumps(payload, indent=2)
|
||||
|
||||
if args.output:
|
||||
out_path = Path(args.output)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
f.write(output)
|
||||
f.write("\n")
|
||||
else:
|
||||
print(output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except RuntimeError as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
sys.exit(2)
|
||||
Reference in New Issue
Block a user