#!/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 cmd_search(args): params = [f"q={args.query}"] if args.project: params.append(f"project_id={args.project}") qs = "&".join(params) issues = _request("GET", f"/search/issues?{qs}") if not issues: print(" No results found.") return for i in issues: status_icon = {"open": "\U0001f7e2", "in_progress": "\U0001f535", "resolved": "\u2705", "closed": "\u26ab", "blocked": "\U0001f534"}.get(i["status"], "\u2753") type_icon = {"resolution": "\u2696\ufe0f", "task": "\U0001f4cb", "story": "\U0001f4d6", "test": "\U0001f9ea"}.get(i["issue_type"], "\U0001f4cc") print(f" {status_icon} {type_icon} #{i['id']} [{i['priority']}] {i['title']}") def cmd_transition(args): result = _request("POST", f"/issues/{args.issue_id}/transition?new_status={args.status}") print(f"Issue #{result['id']} transitioned to: {result['status']}") def cmd_stats(args): params = f"?project_id={args.project}" if args.project else "" stats = _request("GET", f"/dashboard/stats{params}") print(f"Total: {stats['total']}") print("By status:") for s, c in stats["by_status"].items(): if c > 0: print(f" {s}: {c}") print("By type:") for t, c in stats["by_type"].items(): if c > 0: print(f" {t}: {c}") 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") # search p_search = sub.add_parser("search", help="Search issues") p_search.add_argument("query") p_search.add_argument("--project", "-p", type=int) # transition p_trans = sub.add_parser("transition", help="Transition issue status") p_trans.add_argument("issue_id", type=int) p_trans.add_argument("status", choices=["open", "in_progress", "resolved", "closed", "blocked"]) # stats p_stats = sub.add_parser("stats", help="Dashboard stats") p_stats.add_argument("--project", "-p", type=int) 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, "search": cmd_search, "transition": cmd_transition, "stats": cmd_stats, } cmds[args.command](args) if __name__ == "__main__": import urllib.parse main()