From ad5d21824ba6fa85655ab82dba9c0baf355e1a76 Mon Sep 17 00:00:00 2001 From: Haarith Imran Date: Sun, 8 Feb 2026 15:17:42 +0500 Subject: [PATCH] feat: add --json output to CLI commands --- README.md | 74 ++++++++++++++++++++++++++++++---------- commands/add.py | 14 ++++++-- commands/done.py | 18 ++++++++-- commands/list.py | 16 ++++++--- task.py | 44 ++++++++++++++++++------ test_task.py | 88 ++++++++++++++++++++++++++++++++++++++---------- 6 files changed, 200 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 91da9a7..247b3f2 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,76 @@ -# Task CLI - Test Target for ai-gitops +# Task CLI -A minimal Python CLI task manager used to test the [ai-gitops](https://github.com/scooke11/ai-gitops) workflow. +A simple task manager. -## What is this? +## Usage + +```bash +./task.py add "Buy groceries" +./task.py list +./task.py done 1 +``` + +## JSON output -This is a **test target repository** - not a real project. It exists solely to validate that our AI-assisted bounty hunting workflow looks professional before we use it on real open-source projects. +All commands support `--json` for scripting and automation. -## Installation +### Add ```bash -python task.py --help +./task.py --json add "Buy groceries" ``` -## Usage +Example output: + +```json +{ + "message": "Added task 1: Buy groceries", + "success": true, + "task": { + "description": "Buy groceries", + "done": false, + "id": 1 + } +} +``` + +### List ```bash -# Add a task -python task.py add "Buy groceries" +./task.py --json list +``` -# List tasks -python task.py list +Example output: -# Complete a task -python task.py done 1 +```json +{ + "success": true, + "tasks": [ + { + "description": "Buy groceries", + "done": false, + "id": 1 + } + ] +} ``` -## Testing +### Done ```bash -python -m pytest test_task.py +./task.py --json done 1 ``` -## Configuration +Example output: -Copy `config.yaml.example` to `~/.config/task-cli/config.yaml` and customize. +```json +{ + "message": "Marked task 1 as done: Buy groceries", + "success": true, + "task": { + "description": "Buy groceries", + "done": true, + "id": 1 + } +} +``` diff --git a/commands/add.py b/commands/add.py index 1b1a943..029167c 100644 --- a/commands/add.py +++ b/commands/add.py @@ -19,7 +19,7 @@ def validate_description(description): return description.strip() -def add_task(description): +def add_task(description, *, json_output=False): """Add a new task.""" description = validate_description(description) @@ -31,7 +31,17 @@ def add_task(description): tasks = json.loads(tasks_file.read_text()) task_id = len(tasks) + 1 - tasks.append({"id": task_id, "description": description, "done": False}) + task = {"id": task_id, "description": description, "done": False} + tasks.append(task) tasks_file.write_text(json.dumps(tasks, indent=2)) + + if json_output: + return { + "success": True, + "message": f"Added task {task_id}: {description}", + "task": task, + } + print(f"Added task {task_id}: {description}") + return {"success": True} diff --git a/commands/done.py b/commands/done.py index c9dfd42..d438ef1 100644 --- a/commands/done.py +++ b/commands/done.py @@ -17,12 +17,14 @@ def validate_task_id(tasks, task_id): return task_id -def mark_done(task_id): +def mark_done(task_id, *, json_output=False): """Mark a task as complete.""" tasks_file = get_tasks_file() if not tasks_file.exists(): + if json_output: + return {"success": False, "error": "No tasks found!"} print("No tasks found!") - return + return {"success": False} tasks = json.loads(tasks_file.read_text()) task_id = validate_task_id(tasks, task_id) @@ -31,7 +33,17 @@ def mark_done(task_id): if task["id"] == task_id: task["done"] = True tasks_file.write_text(json.dumps(tasks, indent=2)) + if json_output: + return { + "success": True, + "message": f"Marked task {task_id} as done: {task['description']}", + "task": task, + } print(f"Marked task {task_id} as done: {task['description']}") - return + return {"success": True} + + if json_output: + return {"success": False, "error": f"Task {task_id} not found"} print(f"Task {task_id} not found") + return {"success": False} diff --git a/commands/list.py b/commands/list.py index 714315d..c37cb1f 100644 --- a/commands/list.py +++ b/commands/list.py @@ -18,20 +18,28 @@ def validate_task_file(): return tasks_file -def list_tasks(): +def list_tasks(*, json_output=False): """List all tasks.""" - # NOTE: No --json flag support yet (feature bounty) tasks_file = validate_task_file() if not tasks_file: + if json_output: + return {"success": True, "tasks": []} print("No tasks yet!") - return + return {"success": True} tasks = json.loads(tasks_file.read_text()) if not tasks: + if json_output: + return {"success": True, "tasks": []} print("No tasks yet!") - return + return {"success": True} + + if json_output: + return {"success": True, "tasks": tasks} for task in tasks: status = "✓" if task["done"] else " " print(f"[{status}] {task['id']}. {task['description']}") + + return {"success": True} diff --git a/task.py b/task.py index 53cc8ed..68a5f94 100644 --- a/task.py +++ b/task.py @@ -2,6 +2,7 @@ """Simple task manager CLI.""" import argparse +import json import sys from pathlib import Path @@ -18,8 +19,19 @@ def load_config(): return f.read() -def main(): +def _print_json(payload, *, exit_code=0): + print(json.dumps(payload, indent=2, sort_keys=True)) + raise SystemExit(exit_code) + + +def main(argv=None): parser = argparse.ArgumentParser(description="Simple task manager") + parser.add_argument( + "--json", + action="store_true", + help="Output machine-readable JSON for scripting/automation", + ) + subparsers = parser.add_subparsers(dest="command", help="Command to run") # Add command @@ -27,22 +39,32 @@ def main(): add_parser.add_argument("description", help="Task description") # List command - list_parser = subparsers.add_parser("list", help="List all tasks") + subparsers.add_parser("list", help="List all tasks") # Done command done_parser = subparsers.add_parser("done", help="Mark task as complete") done_parser.add_argument("task_id", type=int, help="Task ID to mark done") - args = parser.parse_args() + args = parser.parse_args(argv) + + try: + if args.command == "add": + result = add_task(args.description, json_output=args.json) + elif args.command == "list": + result = list_tasks(json_output=args.json) + elif args.command == "done": + result = mark_done(args.task_id, json_output=args.json) + else: + parser.print_help() + return + + if args.json: + _print_json(result, exit_code=0 if result.get("success") else 1) - if args.command == "add": - add_task(args.description) - elif args.command == "list": - list_tasks() - elif args.command == "done": - mark_done(args.task_id) - else: - parser.print_help() + except ValueError as e: + if args.json: + _print_json({"success": False, "error": str(e)}, exit_code=1) + raise if __name__ == "__main__": diff --git a/test_task.py b/test_task.py index ba98e43..5f76116 100644 --- a/test_task.py +++ b/test_task.py @@ -1,30 +1,84 @@ """Basic tests for task CLI.""" import json -import pytest +import os +import subprocess +import sys +import tempfile +import unittest from pathlib import Path -from commands.add import add_task, validate_description + +from commands.add import validate_description from commands.done import validate_task_id -def test_validate_description(): - """Test description validation.""" - assert validate_description(" test ") == "test" +def run_cli(home_dir: Path, *args): + """Run the task CLI in a sandboxed HOME.""" + env = os.environ.copy() + env["HOME"] = str(home_dir) + + proc = subprocess.run( + [sys.executable, str(Path(__file__).parent / "task.py"), *args], + env=env, + capture_output=True, + text=True, + ) + return proc + + +class TestValidation(unittest.TestCase): + def test_validate_description(self): + self.assertEqual(validate_description(" test "), "test") + + with self.assertRaises(ValueError): + validate_description("") + + with self.assertRaises(ValueError): + validate_description("x" * 201) + + def test_validate_task_id(self): + tasks = [{"id": 1}, {"id": 2}] + self.assertEqual(validate_task_id(tasks, 1), 1) + + with self.assertRaises(ValueError): + validate_task_id(tasks, 0) + + with self.assertRaises(ValueError): + validate_task_id(tasks, 99) + + +class TestJsonOutput(unittest.TestCase): + def test_json_add_list_done_roundtrip(self): + with tempfile.TemporaryDirectory() as tmp: + home = Path(tmp) - with pytest.raises(ValueError): - validate_description("") + proc = run_cli(home, "--json", "add", "Buy groceries") + self.assertEqual(proc.returncode, 0, proc.stderr) + payload = json.loads(proc.stdout) + self.assertTrue(payload["success"]) + self.assertEqual(payload["task"]["id"], 1) + self.assertFalse(payload["task"]["done"]) - with pytest.raises(ValueError): - validate_description("x" * 201) + proc = run_cli(home, "--json", "list") + self.assertEqual(proc.returncode, 0, proc.stderr) + payload = json.loads(proc.stdout) + self.assertTrue(payload["success"]) + self.assertEqual(payload["tasks"][0]["description"], "Buy groceries") + proc = run_cli(home, "--json", "done", "1") + self.assertEqual(proc.returncode, 0, proc.stderr) + payload = json.loads(proc.stdout) + self.assertTrue(payload["success"]) + self.assertTrue(payload["task"]["done"]) -def test_validate_task_id(): - """Test task ID validation.""" - tasks = [{"id": 1}, {"id": 2}] - assert validate_task_id(tasks, 1) == 1 + def test_json_list_empty(self): + with tempfile.TemporaryDirectory() as tmp: + home = Path(tmp) + proc = run_cli(home, "--json", "list") + self.assertEqual(proc.returncode, 0, proc.stderr) + payload = json.loads(proc.stdout) + self.assertEqual(payload, {"success": True, "tasks": []}) - with pytest.raises(ValueError): - validate_task_id(tasks, 0) - with pytest.raises(ValueError): - validate_task_id(tasks, 99) +if __name__ == "__main__": + unittest.main()