From d9b0345aa6e5dae2ec2bad39d0cbd3aaee29edb2 Mon Sep 17 00:00:00 2001 From: Zhi Date: Sun, 22 Feb 2026 02:44:47 +0000 Subject: [PATCH] feat: add CLI tool (login/issues/projects/users/health/version) --- cli.py | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100755 cli.py diff --git a/cli.py b/cli.py new file mode 100755 index 0000000..4613ca8 --- /dev/null +++ b/cli.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""HarborForge CLI - 简易命令行工具""" + +import argparse +import json +import os +import sys +import urllib.request +import urllib.error + +BASE_URL = os.environ.get("HARBORFORGE_URL", "http://localhost:8000") +TOKEN = os.environ.get("HARBORFORGE_TOKEN", "") + + +def _request(method, path, data=None): + url = f"{BASE_URL}{path}" + headers = {"Content-Type": "application/json"} + if TOKEN: + headers["Authorization"] = f"Bearer {TOKEN}" + + body = json.dumps(data).encode() if data else None + req = urllib.request.Request(url, data=body, headers=headers, method=method) + + try: + with urllib.request.urlopen(req) as resp: + if resp.status == 204: + return None + return json.loads(resp.read()) + except urllib.error.HTTPError as e: + print(f"Error {e.code}: {e.read().decode()}", file=sys.stderr) + sys.exit(1) + + +def cmd_login(args): + data = urllib.parse.urlencode({"username": args.username, "password": args.password}).encode() + req = urllib.request.Request(f"{BASE_URL}/auth/token", data=data, method="POST") + req.add_header("Content-Type", "application/x-www-form-urlencoded") + try: + with urllib.request.urlopen(req) as resp: + result = json.loads(resp.read()) + print(f"Token: {result['access_token']}") + print(f"\nExport it:\nexport HARBORFORGE_TOKEN={result['access_token']}") + except urllib.error.HTTPError as e: + print(f"Login failed: {e.read().decode()}", file=sys.stderr) + sys.exit(1) + + +def cmd_issues(args): + params = [] + if args.project: + params.append(f"project_id={args.project}") + if args.type: + params.append(f"issue_type={args.type}") + if args.status: + params.append(f"issue_status={args.status}") + qs = f"?{'&'.join(params)}" if params else "" + issues = _request("GET", f"/issues{qs}") + for i in issues: + status_icon = {"open": "🟢", "in_progress": "🔵", "resolved": "✅", "closed": "⚫", "blocked": "🔴"}.get(i["status"], "❓") + type_icon = {"resolution": "⚖️", "task": "📋", "story": "📖", "test": "🧪"}.get(i["issue_type"], "📌") + print(f" {status_icon} {type_icon} #{i['id']} [{i['priority']}] {i['title']}") + + +def cmd_issue_create(args): + data = { + "title": args.title, + "project_id": args.project, + "reporter_id": args.reporter, + "issue_type": args.type, + "priority": args.priority or "medium", + } + if args.description: + data["description"] = args.description + if args.assignee: + data["assignee_id"] = args.assignee + + # Resolution specific + if args.type == "resolution": + if args.summary: + data["resolution_summary"] = args.summary + if args.positions: + data["positions"] = args.positions + if args.pending: + data["pending_matters"] = args.pending + + result = _request("POST", "/issues", data) + print(f"Created issue #{result['id']}: {result['title']}") + + +def cmd_projects(args): + projects = _request("GET", "/projects") + for p in projects: + print(f" #{p['id']} {p['name']} - {p.get('description', '')}") + + +def cmd_users(args): + users = _request("GET", "/users") + for u in users: + role = "👑" if u["is_admin"] else "👤" + print(f" {role} #{u['id']} {u['username']} ({u.get('full_name', '')})") + + +def cmd_version(args): + result = _request("GET", "/version") + print(f"{result['name']} v{result['version']}") + + +def cmd_health(args): + result = _request("GET", "/health") + print(f"Status: {result['status']}") + + +def main(): + parser = argparse.ArgumentParser(description="HarborForge CLI") + sub = parser.add_subparsers(dest="command") + + # login + p_login = sub.add_parser("login", help="Login and get token") + p_login.add_argument("username") + p_login.add_argument("password") + + # issues + p_issues = sub.add_parser("issues", help="List issues") + p_issues.add_argument("--project", "-p", type=int) + p_issues.add_argument("--type", "-t", choices=["task", "story", "test", "resolution"]) + p_issues.add_argument("--status", "-s") + + # issue create + p_create = sub.add_parser("create-issue", help="Create an issue") + p_create.add_argument("title") + p_create.add_argument("--project", "-p", type=int, required=True) + p_create.add_argument("--reporter", "-r", type=int, required=True) + p_create.add_argument("--type", "-t", default="task", choices=["task", "story", "test", "resolution"]) + p_create.add_argument("--priority", choices=["low", "medium", "high", "critical"]) + p_create.add_argument("--description", "-d") + p_create.add_argument("--assignee", "-a", type=int) + # Resolution fields + p_create.add_argument("--summary") + p_create.add_argument("--positions") + p_create.add_argument("--pending") + + # projects + sub.add_parser("projects", help="List projects") + + # users + sub.add_parser("users", help="List users") + + # version + sub.add_parser("version", help="Show version") + + # health + sub.add_parser("health", help="Health check") + + args = parser.parse_args() + if not args.command: + parser.print_help() + sys.exit(1) + + cmds = { + "login": cmd_login, + "issues": cmd_issues, + "create-issue": cmd_issue_create, + "projects": cmd_projects, + "users": cmd_users, + "version": cmd_version, + "health": cmd_health, + } + cmds[args.command](args) + + +if __name__ == "__main__": + import urllib.parse + main()