Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 57 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
}
}
```
14 changes: 12 additions & 2 deletions commands/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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}
18 changes: 15 additions & 3 deletions commands/done.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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}
16 changes: 12 additions & 4 deletions commands/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
44 changes: 33 additions & 11 deletions task.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""Simple task manager CLI."""

import argparse
import json
import sys
from pathlib import Path

Expand All @@ -18,31 +19,52 @@ 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
add_parser = subparsers.add_parser("add", help="Add a new task")
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__":
Expand Down
88 changes: 71 additions & 17 deletions test_task.py
Original file line number Diff line number Diff line change
@@ -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()