Skip to content
Merged
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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.11.0] - 2026-04-17

### Added

- **JSON output for `tsk list`**: New `--json` flag emits tasks as a JSON array on
stdout, making the command scriptable and LLM-friendly without the truncation
imposed by the Rich table view.
- Each task object includes `id` (short numeric display ID, matching the table
view), `uuid` (stable underlying identifier), `title`, `status`, `priority`,
`repo`, `project`, `assignees`, `tags`, `links`, `due`, `created`, `modified`,
`depends`, `parent`, and `description`.
- All dates are ISO 8601.
- Composes with every existing filter (`--status`, `--priority`, `--repo`,
`--project`, `--assignee`, `--tag`, `--archived`).
- Empty result sets return `[]`.
- Merge-conflict warnings redirected to stderr in JSON mode so stdout stays
valid JSON.
- Example: `tsk list --json -s pending | jq '.[] | select(.priority=="H")'`

## [0.10.18] - 2026-02-02

### Documentation
Expand Down
2 changes: 1 addition & 1 deletion src/taskrepo/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.10.18"
__version__ = "0.11.0"
53 changes: 48 additions & 5 deletions src/taskrepo/cli/commands/list.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""List command for displaying tasks."""

import json
import sys
from pathlib import Path

import click
Expand All @@ -8,6 +10,34 @@
from taskrepo.core.repository import RepositoryManager
from taskrepo.tui.display import display_tasks_table
from taskrepo.utils.conflict_detection import display_conflict_warning, scan_all_repositories
from taskrepo.utils.id_mapping import get_display_id_from_uuid


def _task_to_dict(task) -> dict:
"""Serialize a Task to a JSON-compatible dict.

The ``id`` field is the short numeric display ID (same as the table view);
``uuid`` is the stable underlying identifier. All dates are ISO 8601.
"""
display_id = get_display_id_from_uuid(task.id)
return {
"id": display_id if display_id is not None else None,
Comment on lines +16 to +24
"uuid": task.id,
"title": task.title,
"status": task.status,
"priority": task.priority,
"repo": task.repo,
"project": task.project,
"assignees": list(task.assignees),
"tags": list(task.tags),
"links": list(task.links),
"due": task.due.isoformat() if task.due else None,
"created": task.created.isoformat() if task.created else None,
"modified": task.modified.isoformat() if task.modified else None,
"depends": list(task.depends),
"parent": task.parent,
"description": task.description,
}


@click.command(name="list")
Expand All @@ -18,20 +48,23 @@
@click.option("--assignee", "-a", help="Filter by assignee")
@click.option("--tag", "-t", help="Filter by tag")
@click.option("--archived", is_flag=True, help="Show archived tasks")
@click.option("--json", "json_output", is_flag=True, help="Output as JSON (machine-readable, no truncation)")
@click.pass_context
def list_tasks(ctx, repo, project, status, priority, assignee, tag, archived):
def list_tasks(ctx, repo, project, status, priority, assignee, tag, archived, json_output):
"""List tasks with optional filters.

By default, shows all non-archived tasks (including completed).
Use --archived to show archived tasks instead.
Use --json for machine-readable output suitable for scripting or LLMs.
"""
config = ctx.obj["config"]
manager = RepositoryManager(config.parent_dir)

# Check for unresolved merge conflicts and warn user
# Check for unresolved merge conflicts and warn user.
# In JSON mode, emit the warning to stderr so stdout stays valid JSON.
conflicts = scan_all_repositories(Path(config.parent_dir).expanduser())
if conflicts:
console = Console()
console = Console(stderr=True) if json_output else Console()
display_conflict_warning(conflicts, console)

# Get tasks (including or excluding archived based on flag)
Expand Down Expand Up @@ -80,7 +113,10 @@ def list_tasks(ctx, repo, project, status, priority, assignee, tag, archived):

# Display results
if not tasks:
click.echo("No tasks found.")
if json_output:
click.echo("[]")
else:
click.echo("No tasks found.")
return

# Sort tasks before display (always sort, regardless of filters)
Expand All @@ -89,9 +125,16 @@ def list_tasks(ctx, repo, project, status, priority, assignee, tag, archived):

sorted_tasks = sort_tasks(tasks, config, all_tasks=all_tasks)

# Only rebalance IDs for unfiltered views (like sync does)
# Only rebalance IDs for unfiltered views (like sync does).
# Do this before serializing so JSON short IDs match the table view.
if not has_filters:
save_id_cache(sorted_tasks, rebalance=True)

if json_output:
payload = [_task_to_dict(t) for t in sorted_tasks]
json.dump(payload, sys.stdout, indent=2, default=str)
sys.stdout.write("\n")
Comment on lines +133 to +136
return

# Display sorted tasks
display_tasks_table(sorted_tasks, config, save_cache=False)
Loading