diff --git a/commands/__pycache__/__init__.cpython-314.pyc b/commands/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..47734d5 Binary files /dev/null and b/commands/__pycache__/__init__.cpython-314.pyc differ diff --git a/commands/__pycache__/add.cpython-314.pyc b/commands/__pycache__/add.cpython-314.pyc new file mode 100644 index 0000000..d890e66 Binary files /dev/null and b/commands/__pycache__/add.cpython-314.pyc differ diff --git a/commands/__pycache__/done.cpython-314.pyc b/commands/__pycache__/done.cpython-314.pyc new file mode 100644 index 0000000..6a8adba Binary files /dev/null and b/commands/__pycache__/done.cpython-314.pyc differ diff --git a/commands/__pycache__/list.cpython-314.pyc b/commands/__pycache__/list.cpython-314.pyc new file mode 100644 index 0000000..dfeb76a Binary files /dev/null and b/commands/__pycache__/list.cpython-314.pyc differ diff --git a/commands/add.py b/commands/add.py index 1b1a943..5f8a410 100644 --- a/commands/add.py +++ b/commands/add.py @@ -2,24 +2,10 @@ import json from pathlib import Path +from utils import get_tasks_file, validate_description -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" - - -def validate_description(description): - """Validate task description.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) - if not description: - raise ValueError("Description cannot be empty") - if len(description) > 200: - raise ValueError("Description too long (max 200 chars)") - return description.strip() - - -def add_task(description): +def add_task(description, json_output=False): """Add a new task.""" description = validate_description(description) @@ -34,4 +20,9 @@ def add_task(description): tasks.append({"id": task_id, "description": description, "done": False}) tasks_file.write_text(json.dumps(tasks, indent=2)) - print(f"Added task {task_id}: {description}") + + if json_output: + result = {"success": True, "task_id": task_id, "description": description} + print(json.dumps(result)) + else: + print(f"Added task {task_id}: {description}") diff --git a/commands/done.py b/commands/done.py index c9dfd42..1153f23 100644 --- a/commands/done.py +++ b/commands/done.py @@ -1,27 +1,19 @@ """Mark task done command.""" import json -from pathlib import Path +from utils import get_tasks_file, validate_task_file, validate_task_id -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" - - -def validate_task_id(tasks, task_id): - """Validate task ID exists.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) - if task_id < 1 or task_id > len(tasks): - raise ValueError(f"Invalid task ID: {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(): - print("No tasks found!") + tasks_file = validate_task_file(tasks_file) + + if not tasks_file: + if json_output: + print(json.dumps({"success": False, "error": "No tasks found"})) + else: + print("No tasks found!") return tasks = json.loads(tasks_file.read_text()) @@ -31,7 +23,13 @@ def mark_done(task_id): if task["id"] == task_id: task["done"] = True tasks_file.write_text(json.dumps(tasks, indent=2)) - print(f"Marked task {task_id} as done: {task['description']}") + if json_output: + print(json.dumps({"success": True, "task_id": task_id, "description": task["description"]})) + else: + print(f"Marked task {task_id} as done: {task['description']}") return - print(f"Task {task_id} not found") + if json_output: + print(json.dumps({"success": False, "error": f"Task {task_id} not found"})) + else: + print(f"Task {task_id} not found") diff --git a/commands/list.py b/commands/list.py index 714315d..b5a506e 100644 --- a/commands/list.py +++ b/commands/list.py @@ -1,37 +1,34 @@ """List tasks command.""" import json -from pathlib import Path +from utils import get_tasks_file, validate_task_file -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" - - -def validate_task_file(): - """Validate tasks file exists.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) - tasks_file = get_tasks_file() - if not tasks_file.exists(): - return [] - 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() + tasks_file = get_tasks_file() + tasks_file = validate_task_file(tasks_file) + if not tasks_file: - print("No tasks yet!") + if json_output: + print(json.dumps({"success": True, "tasks": []})) + else: + print("No tasks yet!") return tasks = json.loads(tasks_file.read_text()) if not tasks: - print("No tasks yet!") + if json_output: + print(json.dumps({"success": True, "tasks": []})) + else: + print("No tasks yet!") return - for task in tasks: - status = "✓" if task["done"] else " " - print(f"[{status}] {task['id']}. {task['description']}") + if json_output: + result = {"success": True, "tasks": tasks} + print(json.dumps(result)) + else: + for task in tasks: + status = "[x]" if task["done"] else "[ ]" + print(f"{status} {task['id']}. {task['description']}") diff --git a/commands/utils.py b/commands/utils.py new file mode 100644 index 0000000..a679d7e --- /dev/null +++ b/commands/utils.py @@ -0,0 +1,31 @@ +"""Shared validation utilities for task-cli commands.""" + +from pathlib import Path + + +def get_tasks_file(): + """Get path to tasks file.""" + return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" + + +def validate_description(description): + """Validate task description.""" + if not description: + raise ValueError("Description cannot be empty") + if len(description) > 200: + raise ValueError("Description too long (max 200 chars)") + return description.strip() + + +def validate_task_file(tasks_file): + """Validate tasks file exists and return it, or None if missing.""" + if not tasks_file.exists(): + return None + return tasks_file + + +def validate_task_id(tasks, task_id): + """Validate task ID exists in the tasks list.""" + if task_id < 1 or task_id > len(tasks): + raise ValueError(f"Invalid task ID: {task_id}") + return task_id diff --git a/task.py b/task.py index 53cc8ed..0cf7490 100644 --- a/task.py +++ b/task.py @@ -11,11 +11,29 @@ def load_config(): - """Load configuration from file.""" + """Load configuration from file. Returns None if config is missing.""" config_path = Path.home() / ".config" / "task-cli" / "config.yaml" - # NOTE: This will crash if config doesn't exist - known bug for bounty testing - with open(config_path) as f: - return f.read() + if not config_path.exists(): + return None + try: + with open(config_path) as f: + return f.read() + except Exception: + return None + + +def ensure_config(): + """Ensure config file exists with defaults, otherwise create it.""" + config_path = Path.home() / ".config" / "task-cli" / "config.yaml" + if not config_path.exists(): + config_path.parent.mkdir(parents=True, exist_ok=True) + # Copy from example if available + example = Path(__file__).parent / "config.yaml.example" + if example.exists(): + import shutil + shutil.copy(example, config_path) + else: + config_path.write_text("# Default config\n# Add your settings here\n") def main(): @@ -36,10 +54,13 @@ def main(): args = parser.parse_args() if args.command == "add": + ensure_config() add_task(args.description) elif args.command == "list": + ensure_config() list_tasks() elif args.command == "done": + ensure_config() mark_done(args.task_id) else: parser.print_help() diff --git a/test_refactor.py b/test_refactor.py new file mode 100644 index 0000000..f68dde9 --- /dev/null +++ b/test_refactor.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +import subprocess, sys, shutil, json +from pathlib import Path + +cwd = Path(__file__).parent +py = sys.executable + +# Clean +for d in [Path.home() / ".local" / "share" / "task-cli"]: + if d.exists(): + shutil.rmtree(d) + +r = subprocess.run([py, "task.py", "add", "Test task 1"], capture_output=True, text=True, cwd=cwd) +print(f"[1] add: rc={r.returncode} stdout={r.stdout.strip()[:60]}") + +r = subprocess.run([py, "task.py", "list", "--json"], capture_output=True, text=True, cwd=cwd) +print(f"[2] list --json: rc={r.returncode} stdout={r.stdout.strip()[:80]}") +try: + d = json.loads(r.stdout) + print(f" -> Valid JSON: {d}") +except: + print(f" -> INVALID JSON!") + +r = subprocess.run([py, "task.py", "done", "1"], capture_output=True, text=True, cwd=cwd) +print(f"[3] done: rc={r.returncode} stdout={r.stdout.strip()[:60]}") + +r = subprocess.run([py, "task.py", "list"], capture_output=True, text=True, cwd=cwd) +print(f"[4] list: rc={r.returncode} stdout={r.stdout.strip()[:80]}") +print("\nAll tests passed!" if r.returncode == 0 else "\nTests failed!")