From b351075561fdff999f38bea6925e6daf600405df Mon Sep 17 00:00:00 2001 From: zhi Date: Sat, 21 Mar 2026 21:38:08 +0000 Subject: [PATCH] chore: remove legacy Python CLI and update README - Remove cli.py (superseded by Go-based hf CLI) - Update README to point to HarborForge.Cli for CLI usage --- README.md | 24 +--- cli.py | 408 ------------------------------------------------------ 2 files changed, 2 insertions(+), 430 deletions(-) delete mode 100755 cli.py diff --git a/README.md b/README.md index e1ea154..175a709 100644 --- a/README.md +++ b/README.md @@ -98,29 +98,9 @@ Agent/人类协同任务管理平台 - FastAPI 后端 ## CLI -```bash -# 环境变量 -export HARBORFORGE_URL=http://localhost:8000 -export HARBORFORGE_TOKEN= +The legacy Python CLI (`cli.py`) has been retired. Use the Go-based `hf` CLI instead. -# 命令 -python3 cli.py login -python3 cli.py issues [-p project_id] [-t type] [-s status] -python3 cli.py create-issue "title" -p 1 -r 1 [-t resolution --summary "..." --positions "..." --pending "..."] -python3 cli.py search "keyword" -python3 cli.py transition -python3 cli.py stats [-p project_id] -python3 cli.py projects -python3 cli.py users -python3 cli.py milestones [-p project_id] -python3 cli.py milestone-progress -python3 cli.py notifications -u [--unread] -python3 cli.py overdue [-p project_id] -python3 cli.py log-time [-d "description"] -python3 cli.py worklogs -python3 cli.py health -python3 cli.py version -``` +See [HarborForge.Cli](../HarborForge.Cli/README.md) for installation and usage. ## 技术栈 diff --git a/cli.py b/cli.py deleted file mode 100755 index 8346916..0000000 --- a/cli.py +++ /dev/null @@ -1,408 +0,0 @@ -#!/usr/bin/env python3 -"""HarborForge CLI - 简易命令行工具""" - -import argparse -import json -import os -import sys -import urllib.error -import urllib.parse -import urllib.request - -BASE_URL = os.environ.get("HARBORFORGE_URL", "http://localhost:8000") -TOKEN = os.environ.get("HARBORFORGE_TOKEN", "") - - -STATUS_ICON = { - "open": "🟢", - "pending": "🟡", - "freeze": "🧊", - "undergoing": "🔵", - "completed": "✅", - "closed": "⚫", -} -TYPE_ICON = { - "resolution": "⚖️", - "story": "📖", - "test": "🧪", - "issue": "📌", - "maintenance": "🛠️", - "research": "🔬", - "review": "🧐", -} - - -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 is not None 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 - raw = resp.read() - return json.loads(raw) if raw else None - 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_tasks(args): - params = [] - if args.project: - params.append(f"project_id={args.project}") - if args.type: - params.append(f"task_type={args.type}") - if args.status: - params.append(f"task_status={args.status}") - qs = f"?{'&'.join(params)}" if params else "" - result = _request("GET", f"/tasks{qs}") - items = result.get("items", result if isinstance(result, list) else []) - for task in items: - status_icon = STATUS_ICON.get(task["status"], "❓") - type_icon = TYPE_ICON.get(task.get("task_type"), "📌") - print(f" {status_icon} {type_icon} #{task['id']} [{task['priority']}] {task['title']}") - - -def cmd_task_create(args): - data = { - "title": args.title, - "project_id": args.project, - "milestone_id": args.milestone, - "reporter_id": args.reporter, - "task_type": args.type, - "priority": args.priority or "medium", - } - if args.description: - data["description"] = args.description - if args.assignee: - data["assignee_id"] = args.assignee - if args.subtype: - data["task_subtype"] = args.subtype - - 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", "/tasks", data) - print(f"Created task #{result['id']}: {result['title']}") - - -def cmd_projects(args): - projects = _request("GET", "/projects") - for project in projects: - print(f" #{project['id']} {project['name']} - {project.get('description', '')}") - - -def cmd_users(args): - users = _request("GET", "/users") - for user in users: - role = "👑" if user["is_admin"] else "👤" - print(f" {role} #{user['id']} {user['username']} ({user.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={urllib.parse.quote(args.query)}"] - if args.project: - params.append(f"project_id={args.project}") - result = _request("GET", f"/search/tasks?{'&'.join(params)}") - items = result.get("items", result if isinstance(result, list) else []) - if not items: - print(" No results found.") - return - for task in items: - status_icon = STATUS_ICON.get(task["status"], "❓") - type_icon = TYPE_ICON.get(task.get("task_type"), "📌") - print(f" {status_icon} {type_icon} #{task['id']} [{task['priority']}] {task['title']}") - - -def cmd_transition(args): - body = {} - if args.comment: - body["comment"] = args.comment - result = _request("POST", f"/tasks/{args.task_id}/transition?new_status={args.status}", body or None) - print(f"Task #{result['id']} transitioned to: {result['status']}") - - -# ── Propose commands ────────────────────────────────────────────── - -def cmd_proposes(args): - if not args.project: - print("Error: --project is required for proposes", file=sys.stderr) - sys.exit(1) - result = _request("GET", f"/projects/{args.project}/proposes") - items = result if isinstance(result, list) else result.get("items", []) - if not items: - print(" No proposes found.") - return - for p in items: - status_icon = STATUS_ICON.get(p["status"], "❓") - feat = f" → task {p['feat_task_id']}" if p.get("feat_task_id") else "" - print(f" {status_icon} 💡 {p['propose_code']} {p['title']}{feat}") - - -def cmd_propose_create(args): - data = {"title": args.title} - if args.description: - data["description"] = args.description - result = _request("POST", f"/projects/{args.project}/proposes", data) - print(f"Created propose {result['propose_code']}: {result['title']}") - - -def cmd_propose_accept(args): - result = _request("POST", f"/projects/{args.project}/proposes/{args.propose_id}/accept?milestone_id={args.milestone}") - print(f"Propose #{args.propose_id} accepted → task {result.get('feat_task_id', '?')}") - - -def cmd_propose_reject(args): - data = {} - if args.reason: - data["reason"] = args.reason - result = _request("POST", f"/projects/{args.project}/proposes/{args.propose_id}/reject", data or None) - print(f"Propose #{args.propose_id} rejected") - - -def cmd_propose_reopen(args): - result = _request("POST", f"/projects/{args.project}/proposes/{args.propose_id}/reopen") - print(f"Propose #{args.propose_id} reopened") - - -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_tasks']}") - print("By status:") - for status_name, count in stats["by_status"].items(): - if count > 0: - print(f" {status_name}: {count}") - print("By type:") - for task_type, count in stats["by_type"].items(): - if count > 0: - print(f" {task_type}: {count}") - - -def cmd_milestones(args): - params = [] - if args.project: - params.append(f"project_id={args.project}") - if args.status: - params.append(f"status={args.status}") - qs = f"?{'&'.join(params)}" if params else "" - milestones = _request("GET", f"/milestones{qs}") - if not milestones: - print(" No milestones found.") - return - for milestone in milestones: - status_icon = STATUS_ICON.get(milestone["status"], "⚪") - due = f" (due: {milestone['due_date'][:10]})" if milestone.get("due_date") else "" - print(f" {status_icon} #{milestone['id']} {milestone['title']}{due}") - - -def cmd_milestone_progress(args): - result = _request("GET", f"/milestones/{args.milestone_id}/progress") - bar_len = 20 - filled = int(bar_len * result["progress_pct"] / 100) - bar = "█" * filled + "░" * (bar_len - filled) - print(f" {result['title']}") - print(f" [{bar}] {result['progress_pct']}% ({result['completed']}/{result['total_tasks']})") - - -def cmd_notifications(args): - params = [] - if args.unread: - params.append("unread_only=true") - qs = f"?{'&'.join(params)}" if params else "" - notifications = _request("GET", f"/notifications{qs}") - if not notifications: - print(" No notifications.") - return - for notification in notifications: - icon = "🔴" if not notification["is_read"] else "⚪" - print(f" {icon} [{notification['type']}] {notification.get('message') or notification['title']}") - - -def cmd_overdue(args): - print("Overdue tasks are not supported by the current milestone-based task schema.") - - -def cmd_log_time(args): - from datetime import datetime - - data = { - "task_id": args.task_id, - "user_id": args.user_id, - "hours": args.hours, - "logged_date": datetime.utcnow().isoformat(), - } - if args.desc: - data["description"] = args.desc - result = _request("POST", "/worklogs", data) - print(f"Logged {result['hours']}h on task #{result['task_id']} (log #{result['id']})") - - -def cmd_worklogs(args): - logs = _request("GET", f"/tasks/{args.task_id}/worklogs") - for log in logs: - desc = f" - {log['description']}" if log.get("description") else "" - print(f" [{log['id']}] {log['hours']}h by user#{log['user_id']} on {log['logged_date']}{desc}") - summary = _request("GET", f"/tasks/{args.task_id}/worklogs/summary") - print(f" Total: {summary['total_hours']}h ({summary['log_count']} logs)") - - -def main(): - parser = argparse.ArgumentParser(description="HarborForge CLI") - sub = parser.add_subparsers(dest="command") - - p_login = sub.add_parser("login", help="Login and get token") - p_login.add_argument("username") - p_login.add_argument("password") - - p_tasks = sub.add_parser("tasks", aliases=["issues"], help="List tasks") - p_tasks.add_argument("--project", "-p", type=int) - p_tasks.add_argument("--type", "-t", choices=["story", "test", "resolution", "issue", "maintenance", "research", "review"]) - p_tasks.add_argument("--status", "-s", choices=["open", "pending", "undergoing", "completed", "closed"]) - - p_create = sub.add_parser("create-task", aliases=["create-issue"], help="Create a task") - p_create.add_argument("title") - p_create.add_argument("--project", "-p", type=int, required=True) - p_create.add_argument("--milestone", "-m", type=int, required=True) - p_create.add_argument("--reporter", "-r", type=int, required=True) - p_create.add_argument("--type", "-t", default="issue", choices=["story", "test", "resolution", "issue", "maintenance", "research", "review"]) - p_create.add_argument("--subtype") - p_create.add_argument("--priority", choices=["low", "medium", "high", "critical"]) - p_create.add_argument("--description", "-d") - p_create.add_argument("--assignee", "-a", type=int) - p_create.add_argument("--summary") - p_create.add_argument("--positions") - p_create.add_argument("--pending") - - sub.add_parser("projects", help="List projects") - sub.add_parser("users", help="List users") - sub.add_parser("version", help="Show version") - sub.add_parser("health", help="Health check") - - p_search = sub.add_parser("search", help="Search tasks") - p_search.add_argument("query") - p_search.add_argument("--project", "-p", type=int) - - p_trans = sub.add_parser("transition", help="Transition task status") - p_trans.add_argument("task_id", type=int) - p_trans.add_argument("status", choices=["open", "pending", "undergoing", "completed", "closed"]) - p_trans.add_argument("--comment", "-c", help="Comment (required for undergoing→completed)") - - p_stats = sub.add_parser("stats", help="Dashboard stats") - p_stats.add_argument("--project", "-p", type=int) - - p_ms = sub.add_parser("milestones", help="List milestones") - p_ms.add_argument("--project", "-p", type=int) - p_ms.add_argument("--status", "-s", choices=["open", "freeze", "undergoing", "completed", "closed"]) - - p_msp = sub.add_parser("milestone-progress", help="Show milestone progress") - p_msp.add_argument("milestone_id", type=int) - - p_notif = sub.add_parser("notifications", help="List notifications for current token user") - p_notif.add_argument("--unread", action="store_true") - - p_overdue = sub.add_parser("overdue", help="Explain overdue-task support status") - p_overdue.add_argument("--project", "-p", type=int) - - p_logtime = sub.add_parser("log-time", help="Log time on a task") - p_logtime.add_argument("task_id", type=int) - p_logtime.add_argument("user_id", type=int) - p_logtime.add_argument("hours", type=float) - p_logtime.add_argument("--desc", "-d", type=str) - - p_worklogs = sub.add_parser("worklogs", help="List work logs for a task") - p_worklogs.add_argument("task_id", type=int) - - # ── Propose subcommands ── - p_proposes = sub.add_parser("proposes", help="List proposes for a project") - p_proposes.add_argument("--project", "-p", type=int, required=True) - - p_pc = sub.add_parser("propose-create", help="Create a propose") - p_pc.add_argument("title") - p_pc.add_argument("--project", "-p", type=int, required=True) - p_pc.add_argument("--description", "-d") - - p_pa = sub.add_parser("propose-accept", help="Accept a propose into a milestone") - p_pa.add_argument("propose_id", type=int) - p_pa.add_argument("--project", "-p", type=int, required=True) - p_pa.add_argument("--milestone", "-m", type=int, required=True) - - p_pr = sub.add_parser("propose-reject", help="Reject a propose") - p_pr.add_argument("propose_id", type=int) - p_pr.add_argument("--project", "-p", type=int, required=True) - p_pr.add_argument("--reason", "-r") - - p_pro = sub.add_parser("propose-reopen", help="Reopen a rejected propose") - p_pro.add_argument("propose_id", type=int) - p_pro.add_argument("--project", "-p", type=int, required=True) - - args = parser.parse_args() - if not args.command: - parser.print_help() - sys.exit(1) - - cmds = { - "login": cmd_login, - "tasks": cmd_tasks, - "issues": cmd_tasks, - "create-task": cmd_task_create, - "create-issue": cmd_task_create, - "projects": cmd_projects, - "users": cmd_users, - "version": cmd_version, - "health": cmd_health, - "search": cmd_search, - "transition": cmd_transition, - "stats": cmd_stats, - "milestones": cmd_milestones, - "milestone-progress": cmd_milestone_progress, - "notifications": cmd_notifications, - "overdue": cmd_overdue, - "log-time": cmd_log_time, - "worklogs": cmd_worklogs, - "proposes": cmd_proposes, - "propose-create": cmd_propose_create, - "propose-accept": cmd_propose_accept, - "propose-reject": cmd_propose_reject, - "propose-reopen": cmd_propose_reopen, - } - cmds[args.command](args) - - -if __name__ == "__main__": - main()