taskrepo 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- taskrepo/__init__.py +5 -0
- taskrepo/__version__.py +1 -0
- taskrepo/cli/__init__.py +1 -0
- taskrepo/cli/commands/__init__.py +1 -0
- taskrepo/cli/commands/add.py +166 -0
- taskrepo/cli/commands/config.py +161 -0
- taskrepo/cli/commands/delete.py +62 -0
- taskrepo/cli/commands/done.py +53 -0
- taskrepo/cli/commands/edit.py +85 -0
- taskrepo/cli/commands/list.py +235 -0
- taskrepo/cli/commands/sync.py +91 -0
- taskrepo/cli/main.py +120 -0
- taskrepo/core/__init__.py +1 -0
- taskrepo/core/config.py +199 -0
- taskrepo/core/repository.py +432 -0
- taskrepo/core/task.py +198 -0
- taskrepo/tui/__init__.py +1 -0
- taskrepo/tui/prompts.py +262 -0
- taskrepo/utils/__init__.py +1 -0
- taskrepo/utils/helpers.py +34 -0
- taskrepo/utils/id_mapping.py +94 -0
- taskrepo-0.1.0.dist-info/METADATA +0 -0
- taskrepo-0.1.0.dist-info/RECORD +26 -0
- taskrepo-0.1.0.dist-info/WHEEL +4 -0
- taskrepo-0.1.0.dist-info/entry_points.txt +3 -0
- taskrepo-0.1.0.dist-info/licenses/LICENSE +21 -0
taskrepo/__init__.py
ADDED
taskrepo/__version__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
taskrepo/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI module for TaskRepo."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI commands for TaskRepo."""
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Add command for creating new tasks."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from taskrepo.core.repository import RepositoryManager
|
|
6
|
+
from taskrepo.core.task import Task
|
|
7
|
+
from taskrepo.tui import prompts
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command()
|
|
11
|
+
@click.option("--repo", "-r", help="Repository name (will prompt if not specified)")
|
|
12
|
+
@click.option("--title", "-t", help="Task title (will prompt if not specified)")
|
|
13
|
+
@click.option("--project", "-p", help="Project name")
|
|
14
|
+
@click.option("--priority", type=click.Choice(["H", "M", "L"], case_sensitive=False), help="Task priority")
|
|
15
|
+
@click.option("--assignees", "-a", help="Comma-separated list of assignees (e.g., @user1,@user2)")
|
|
16
|
+
@click.option("--tags", help="Comma-separated list of tags")
|
|
17
|
+
@click.option("--due", help="Due date (e.g., 2025-12-31)")
|
|
18
|
+
@click.option("--description", "-d", help="Task description")
|
|
19
|
+
@click.option("--interactive/--no-interactive", "-i/-I", default=True, help="Use interactive mode")
|
|
20
|
+
@click.pass_context
|
|
21
|
+
def add(ctx, repo, title, project, priority, assignees, tags, due, description, interactive):
|
|
22
|
+
"""Add a new task."""
|
|
23
|
+
config = ctx.obj["config"]
|
|
24
|
+
manager = RepositoryManager(config.parent_dir)
|
|
25
|
+
|
|
26
|
+
repositories = manager.discover_repositories()
|
|
27
|
+
|
|
28
|
+
# Interactive mode
|
|
29
|
+
if interactive:
|
|
30
|
+
click.echo("Creating a new task...\n")
|
|
31
|
+
|
|
32
|
+
# Select repository
|
|
33
|
+
if not repo:
|
|
34
|
+
selected_repo = prompts.prompt_repository(repositories)
|
|
35
|
+
if not selected_repo:
|
|
36
|
+
click.echo("Cancelled.")
|
|
37
|
+
ctx.exit(0)
|
|
38
|
+
else:
|
|
39
|
+
selected_repo = manager.get_repository(repo)
|
|
40
|
+
if not selected_repo:
|
|
41
|
+
click.secho(f"Error: Repository '{repo}' not found", fg="red", err=True)
|
|
42
|
+
ctx.exit(1)
|
|
43
|
+
|
|
44
|
+
# Get task title
|
|
45
|
+
if not title:
|
|
46
|
+
title = prompts.prompt_title()
|
|
47
|
+
if not title:
|
|
48
|
+
click.echo("Cancelled.")
|
|
49
|
+
ctx.exit(0)
|
|
50
|
+
|
|
51
|
+
# Get project
|
|
52
|
+
if project is None:
|
|
53
|
+
existing_projects = selected_repo.get_projects()
|
|
54
|
+
project = prompts.prompt_project(existing_projects)
|
|
55
|
+
|
|
56
|
+
# Get priority
|
|
57
|
+
if priority is None:
|
|
58
|
+
priority = prompts.prompt_priority(config.default_priority)
|
|
59
|
+
|
|
60
|
+
# Get assignees
|
|
61
|
+
if assignees is None:
|
|
62
|
+
existing_assignees = selected_repo.get_assignees()
|
|
63
|
+
# Add default assignee to existing list if configured
|
|
64
|
+
if config.default_assignee and config.default_assignee not in existing_assignees:
|
|
65
|
+
existing_assignees = [config.default_assignee] + existing_assignees
|
|
66
|
+
assignees_list = prompts.prompt_assignees(existing_assignees)
|
|
67
|
+
# If no assignees entered and default_assignee is set, use it
|
|
68
|
+
if not assignees_list and config.default_assignee:
|
|
69
|
+
assignees_list = [config.default_assignee]
|
|
70
|
+
else:
|
|
71
|
+
assignees_list = [a.strip() for a in assignees.split(",")]
|
|
72
|
+
# Ensure @ prefix
|
|
73
|
+
assignees_list = [a if a.startswith("@") else f"@{a}" for a in assignees_list]
|
|
74
|
+
|
|
75
|
+
# Get tags
|
|
76
|
+
if tags is None:
|
|
77
|
+
existing_tags = selected_repo.get_tags()
|
|
78
|
+
tags_list = prompts.prompt_tags(existing_tags)
|
|
79
|
+
else:
|
|
80
|
+
tags_list = [t.strip() for t in tags.split(",")]
|
|
81
|
+
|
|
82
|
+
# Get due date
|
|
83
|
+
if due is None:
|
|
84
|
+
due_date = prompts.prompt_due_date()
|
|
85
|
+
else:
|
|
86
|
+
import dateparser
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
due_date = dateparser.parse(due, settings={'PREFER_DATES_FROM': 'future'})
|
|
90
|
+
if due_date is None:
|
|
91
|
+
raise ValueError("Could not parse date")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
click.secho(f"Error: Invalid due date: {e}", fg="red", err=True)
|
|
94
|
+
ctx.exit(1)
|
|
95
|
+
|
|
96
|
+
# Get description
|
|
97
|
+
if description is None:
|
|
98
|
+
description = prompts.prompt_description()
|
|
99
|
+
|
|
100
|
+
else:
|
|
101
|
+
# Non-interactive mode - validate required fields
|
|
102
|
+
if not repo or not title:
|
|
103
|
+
click.secho("Error: --repo and --title are required in non-interactive mode", fg="red", err=True)
|
|
104
|
+
ctx.exit(1)
|
|
105
|
+
|
|
106
|
+
selected_repo = manager.get_repository(repo)
|
|
107
|
+
if not selected_repo:
|
|
108
|
+
click.secho(f"Error: Repository '{repo}' not found", fg="red", err=True)
|
|
109
|
+
ctx.exit(1)
|
|
110
|
+
|
|
111
|
+
# Parse assignees
|
|
112
|
+
assignees_list = []
|
|
113
|
+
if assignees:
|
|
114
|
+
assignees_list = [a.strip() for a in assignees.split(",")]
|
|
115
|
+
assignees_list = [a if a.startswith("@") else f"@{a}" for a in assignees_list]
|
|
116
|
+
elif config.default_assignee:
|
|
117
|
+
# Use default assignee if none specified
|
|
118
|
+
assignees_list = [config.default_assignee]
|
|
119
|
+
|
|
120
|
+
# Parse tags
|
|
121
|
+
tags_list = []
|
|
122
|
+
if tags:
|
|
123
|
+
tags_list = [t.strip() for t in tags.split(",")]
|
|
124
|
+
|
|
125
|
+
# Parse due date
|
|
126
|
+
due_date = None
|
|
127
|
+
if due:
|
|
128
|
+
import dateparser
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
due_date = dateparser.parse(due, settings={'PREFER_DATES_FROM': 'future'})
|
|
132
|
+
if due_date is None:
|
|
133
|
+
raise ValueError("Could not parse date")
|
|
134
|
+
except Exception as e:
|
|
135
|
+
click.secho(f"Error: Invalid due date: {e}", fg="red", err=True)
|
|
136
|
+
ctx.exit(1)
|
|
137
|
+
|
|
138
|
+
if not priority:
|
|
139
|
+
priority = config.default_priority
|
|
140
|
+
|
|
141
|
+
if not description:
|
|
142
|
+
description = ""
|
|
143
|
+
|
|
144
|
+
# Generate task ID
|
|
145
|
+
task_id = selected_repo.next_task_id()
|
|
146
|
+
|
|
147
|
+
# Create task
|
|
148
|
+
task = Task(
|
|
149
|
+
id=task_id,
|
|
150
|
+
title=title,
|
|
151
|
+
status=config.default_status,
|
|
152
|
+
priority=priority.upper(),
|
|
153
|
+
project=project,
|
|
154
|
+
assignees=assignees_list,
|
|
155
|
+
tags=tags_list,
|
|
156
|
+
due=due_date,
|
|
157
|
+
description=description,
|
|
158
|
+
repo=selected_repo.name,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Save task
|
|
162
|
+
task_file = selected_repo.save_task(task)
|
|
163
|
+
|
|
164
|
+
click.echo()
|
|
165
|
+
click.secho(f"✓ Task created: {task}", fg="green")
|
|
166
|
+
click.echo(f" File: {task_file}")
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Config command for interactive configuration management."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from prompt_toolkit import prompt
|
|
7
|
+
from prompt_toolkit.completion import WordCompleter
|
|
8
|
+
|
|
9
|
+
from taskrepo.core.config import Config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command(name="config")
|
|
13
|
+
@click.pass_context
|
|
14
|
+
def config_cmd(ctx):
|
|
15
|
+
"""Interactive configuration management."""
|
|
16
|
+
config = ctx.obj["config"]
|
|
17
|
+
|
|
18
|
+
while True:
|
|
19
|
+
click.echo("\n" + "=" * 50)
|
|
20
|
+
click.echo("TaskRepo Configuration")
|
|
21
|
+
click.echo("=" * 50)
|
|
22
|
+
click.echo("\nWhat would you like to configure?\n")
|
|
23
|
+
click.echo(" 1. View current settings")
|
|
24
|
+
click.echo(" 2. Change parent directory")
|
|
25
|
+
click.echo(" 3. Set default priority")
|
|
26
|
+
click.echo(" 4. Set default status")
|
|
27
|
+
click.echo(" 5. Set default assignee")
|
|
28
|
+
click.echo(" 6. Configure task sorting")
|
|
29
|
+
click.echo(" 7. Reset to defaults")
|
|
30
|
+
click.echo(" 8. Exit")
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
choice = prompt("\nEnter choice (1-8): ", completer=WordCompleter(["1", "2", "3", "4", "5", "6", "7", "8"]))
|
|
34
|
+
except (KeyboardInterrupt, EOFError):
|
|
35
|
+
click.echo("\nExiting configuration.")
|
|
36
|
+
break
|
|
37
|
+
|
|
38
|
+
choice = choice.strip()
|
|
39
|
+
|
|
40
|
+
if choice == "1":
|
|
41
|
+
# View current settings
|
|
42
|
+
click.echo("\n" + "-" * 50)
|
|
43
|
+
click.echo("Current Configuration:")
|
|
44
|
+
click.echo("-" * 50)
|
|
45
|
+
click.echo(f" Config file: {config.config_path}")
|
|
46
|
+
click.echo(f" Parent directory: {config.parent_dir}")
|
|
47
|
+
click.echo(f" Default priority: {config.default_priority}")
|
|
48
|
+
click.echo(f" Default status: {config.default_status}")
|
|
49
|
+
default_assignee = config.default_assignee if config.default_assignee else "(none)"
|
|
50
|
+
click.echo(f" Default assignee: {default_assignee}")
|
|
51
|
+
sort_by = ", ".join(config.sort_by)
|
|
52
|
+
click.echo(f" Sort by: {sort_by}")
|
|
53
|
+
click.echo("-" * 50)
|
|
54
|
+
|
|
55
|
+
elif choice == "2":
|
|
56
|
+
# Change parent directory
|
|
57
|
+
click.echo(f"\nCurrent parent directory: {config.parent_dir}")
|
|
58
|
+
try:
|
|
59
|
+
new_dir = prompt("Enter new parent directory (or press Enter to cancel): ")
|
|
60
|
+
if new_dir.strip():
|
|
61
|
+
config.parent_dir = Path(new_dir.strip()).expanduser()
|
|
62
|
+
click.secho(f"✓ Parent directory updated to: {config.parent_dir}", fg="green")
|
|
63
|
+
else:
|
|
64
|
+
click.echo("Cancelled.")
|
|
65
|
+
except (KeyboardInterrupt, EOFError):
|
|
66
|
+
click.echo("\nCancelled.")
|
|
67
|
+
|
|
68
|
+
elif choice == "3":
|
|
69
|
+
# Set default priority
|
|
70
|
+
click.echo(f"\nCurrent default priority: {config.default_priority}")
|
|
71
|
+
try:
|
|
72
|
+
new_priority = prompt(
|
|
73
|
+
"Enter default priority (H/M/L): ",
|
|
74
|
+
completer=WordCompleter(["H", "M", "L"], ignore_case=True),
|
|
75
|
+
)
|
|
76
|
+
new_priority = new_priority.strip().upper()
|
|
77
|
+
if new_priority in {"H", "M", "L"}:
|
|
78
|
+
config.default_priority = new_priority
|
|
79
|
+
click.secho(f"✓ Default priority updated to: {new_priority}", fg="green")
|
|
80
|
+
elif new_priority:
|
|
81
|
+
click.secho("✗ Invalid priority. Must be H, M, or L.", fg="red")
|
|
82
|
+
except (KeyboardInterrupt, EOFError):
|
|
83
|
+
click.echo("\nCancelled.")
|
|
84
|
+
|
|
85
|
+
elif choice == "4":
|
|
86
|
+
# Set default status
|
|
87
|
+
click.echo(f"\nCurrent default status: {config.default_status}")
|
|
88
|
+
statuses = ["pending", "in_progress", "completed", "cancelled"]
|
|
89
|
+
try:
|
|
90
|
+
new_status = prompt(
|
|
91
|
+
"Enter default status: ",
|
|
92
|
+
completer=WordCompleter(statuses, ignore_case=True),
|
|
93
|
+
)
|
|
94
|
+
new_status = new_status.strip().lower()
|
|
95
|
+
if new_status in statuses:
|
|
96
|
+
config.default_status = new_status
|
|
97
|
+
click.secho(f"✓ Default status updated to: {new_status}", fg="green")
|
|
98
|
+
elif new_status:
|
|
99
|
+
click.secho(f"✗ Invalid status. Must be one of: {', '.join(statuses)}", fg="red")
|
|
100
|
+
except (KeyboardInterrupt, EOFError):
|
|
101
|
+
click.echo("\nCancelled.")
|
|
102
|
+
|
|
103
|
+
elif choice == "5":
|
|
104
|
+
# Set default assignee
|
|
105
|
+
current_assignee = config.default_assignee if config.default_assignee else "(none)"
|
|
106
|
+
click.echo(f"\nCurrent default assignee: {current_assignee}")
|
|
107
|
+
try:
|
|
108
|
+
new_assignee = prompt("Enter default assignee (GitHub handle, or leave empty for none): ")
|
|
109
|
+
new_assignee = new_assignee.strip()
|
|
110
|
+
if new_assignee:
|
|
111
|
+
if not new_assignee.startswith("@"):
|
|
112
|
+
new_assignee = f"@{new_assignee}"
|
|
113
|
+
config.default_assignee = new_assignee
|
|
114
|
+
click.secho(f"✓ Default assignee updated to: {new_assignee}", fg="green")
|
|
115
|
+
else:
|
|
116
|
+
config.default_assignee = None
|
|
117
|
+
click.secho("✓ Default assignee cleared", fg="green")
|
|
118
|
+
except (KeyboardInterrupt, EOFError):
|
|
119
|
+
click.echo("\nCancelled.")
|
|
120
|
+
|
|
121
|
+
elif choice == "6":
|
|
122
|
+
# Configure task sorting
|
|
123
|
+
click.echo(f"\nCurrent sort order: {', '.join(config.sort_by)}")
|
|
124
|
+
click.echo("\nAvailable sort fields:")
|
|
125
|
+
click.echo(" priority, due, created, modified, status, title, project")
|
|
126
|
+
click.echo(" (prefix with '-' for descending order, e.g., '-created')")
|
|
127
|
+
try:
|
|
128
|
+
new_sort = prompt("Enter sort fields (comma-separated): ")
|
|
129
|
+
if new_sort.strip():
|
|
130
|
+
sort_fields = [f.strip() for f in new_sort.split(",") if f.strip()]
|
|
131
|
+
try:
|
|
132
|
+
config.sort_by = sort_fields
|
|
133
|
+
click.secho(f"✓ Sort order updated to: {', '.join(sort_fields)}", fg="green")
|
|
134
|
+
except ValueError as e:
|
|
135
|
+
click.secho(f"✗ Error: {e}", fg="red")
|
|
136
|
+
else:
|
|
137
|
+
click.echo("Cancelled.")
|
|
138
|
+
except (KeyboardInterrupt, EOFError):
|
|
139
|
+
click.echo("\nCancelled.")
|
|
140
|
+
|
|
141
|
+
elif choice == "7":
|
|
142
|
+
# Reset to defaults
|
|
143
|
+
click.echo("\n⚠️ This will reset ALL configuration to defaults.")
|
|
144
|
+
try:
|
|
145
|
+
confirm = prompt("Are you sure? (yes/no): ")
|
|
146
|
+
if confirm.strip().lower() in {"yes", "y"}:
|
|
147
|
+
config._data = Config.DEFAULT_CONFIG.copy()
|
|
148
|
+
config.save()
|
|
149
|
+
click.secho("✓ Configuration reset to defaults", fg="green")
|
|
150
|
+
else:
|
|
151
|
+
click.echo("Cancelled.")
|
|
152
|
+
except (KeyboardInterrupt, EOFError):
|
|
153
|
+
click.echo("\nCancelled.")
|
|
154
|
+
|
|
155
|
+
elif choice == "8":
|
|
156
|
+
# Exit
|
|
157
|
+
click.echo("\nExiting configuration.")
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
else:
|
|
161
|
+
click.secho("✗ Invalid choice. Please enter a number from 1-8.", fg="red")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Delete command for removing tasks."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from taskrepo.core.repository import RepositoryManager
|
|
6
|
+
from taskrepo.utils.helpers import normalize_task_id
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command(name="delete")
|
|
10
|
+
@click.argument("task_id")
|
|
11
|
+
@click.option("--repo", "-r", help="Repository name (will search all repos if not specified)")
|
|
12
|
+
@click.option("--force", "-f", is_flag=True, help="Skip confirmation prompt")
|
|
13
|
+
@click.pass_context
|
|
14
|
+
def delete(ctx, task_id, repo, force):
|
|
15
|
+
"""Delete a task permanently.
|
|
16
|
+
|
|
17
|
+
TASK_ID: Task ID to delete
|
|
18
|
+
"""
|
|
19
|
+
config = ctx.obj["config"]
|
|
20
|
+
manager = RepositoryManager(config.parent_dir)
|
|
21
|
+
|
|
22
|
+
# Normalize task ID (convert "1" to "001", etc.)
|
|
23
|
+
task_id = normalize_task_id(task_id)
|
|
24
|
+
|
|
25
|
+
# Find the task
|
|
26
|
+
if repo:
|
|
27
|
+
repository = manager.get_repository(repo)
|
|
28
|
+
if not repository:
|
|
29
|
+
click.secho(f"Error: Repository '{repo}' not found", fg="red", err=True)
|
|
30
|
+
ctx.exit(1)
|
|
31
|
+
task = repository.get_task(task_id)
|
|
32
|
+
if not task:
|
|
33
|
+
click.secho(f"Error: Task '{task_id}' not found in repository '{repo}'", fg="red", err=True)
|
|
34
|
+
ctx.exit(1)
|
|
35
|
+
else:
|
|
36
|
+
# Search all repositories
|
|
37
|
+
task = None
|
|
38
|
+
repository = None
|
|
39
|
+
for r in manager.discover_repositories():
|
|
40
|
+
t = r.get_task(task_id)
|
|
41
|
+
if t:
|
|
42
|
+
task = t
|
|
43
|
+
repository = r
|
|
44
|
+
break
|
|
45
|
+
|
|
46
|
+
if not task:
|
|
47
|
+
click.secho(f"Error: Task '{task_id}' not found", fg="red", err=True)
|
|
48
|
+
ctx.exit(1)
|
|
49
|
+
|
|
50
|
+
# Confirmation prompt (unless --force flag is used)
|
|
51
|
+
if not force:
|
|
52
|
+
click.echo(f"\nTask to delete: {task}")
|
|
53
|
+
if not click.confirm("Are you sure you want to delete this task? This cannot be undone.", default=False):
|
|
54
|
+
click.echo("Deletion cancelled.")
|
|
55
|
+
ctx.exit(0)
|
|
56
|
+
|
|
57
|
+
# Delete the task
|
|
58
|
+
if repository.delete_task(task_id):
|
|
59
|
+
click.secho(f"✓ Task deleted: {task}", fg="green")
|
|
60
|
+
else:
|
|
61
|
+
click.secho(f"Error: Failed to delete task '{task_id}'", fg="red", err=True)
|
|
62
|
+
ctx.exit(1)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Done command for marking tasks as completed."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from taskrepo.core.repository import RepositoryManager
|
|
6
|
+
from taskrepo.utils.helpers import normalize_task_id
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command()
|
|
10
|
+
@click.argument("task_id")
|
|
11
|
+
@click.option("--repo", "-r", help="Repository name (will search all repos if not specified)")
|
|
12
|
+
@click.pass_context
|
|
13
|
+
def done(ctx, task_id, repo):
|
|
14
|
+
"""Mark a task as completed.
|
|
15
|
+
|
|
16
|
+
TASK_ID: Task ID to mark as done
|
|
17
|
+
"""
|
|
18
|
+
config = ctx.obj["config"]
|
|
19
|
+
manager = RepositoryManager(config.parent_dir)
|
|
20
|
+
|
|
21
|
+
# Normalize task ID (convert "1" to "001", etc.)
|
|
22
|
+
task_id = normalize_task_id(task_id)
|
|
23
|
+
|
|
24
|
+
# Find the task
|
|
25
|
+
if repo:
|
|
26
|
+
repository = manager.get_repository(repo)
|
|
27
|
+
if not repository:
|
|
28
|
+
click.secho(f"Error: Repository '{repo}' not found", fg="red", err=True)
|
|
29
|
+
ctx.exit(1)
|
|
30
|
+
task = repository.get_task(task_id)
|
|
31
|
+
if not task:
|
|
32
|
+
click.secho(f"Error: Task '{task_id}' not found in repository '{repo}'", fg="red", err=True)
|
|
33
|
+
ctx.exit(1)
|
|
34
|
+
else:
|
|
35
|
+
# Search all repositories
|
|
36
|
+
task = None
|
|
37
|
+
repository = None
|
|
38
|
+
for r in manager.discover_repositories():
|
|
39
|
+
t = r.get_task(task_id)
|
|
40
|
+
if t:
|
|
41
|
+
task = t
|
|
42
|
+
repository = r
|
|
43
|
+
break
|
|
44
|
+
|
|
45
|
+
if not task:
|
|
46
|
+
click.secho(f"Error: Task '{task_id}' not found", fg="red", err=True)
|
|
47
|
+
ctx.exit(1)
|
|
48
|
+
|
|
49
|
+
# Mark as completed
|
|
50
|
+
task.status = "completed"
|
|
51
|
+
repository.save_task(task)
|
|
52
|
+
|
|
53
|
+
click.secho(f"✓ Task marked as completed: {task}", fg="green")
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Edit command for modifying existing tasks."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from taskrepo.core.repository import RepositoryManager
|
|
10
|
+
from taskrepo.core.task import Task
|
|
11
|
+
from taskrepo.utils.helpers import normalize_task_id
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command()
|
|
15
|
+
@click.argument("task_id")
|
|
16
|
+
@click.option("--repo", "-r", help="Repository name (will search all repos if not specified)")
|
|
17
|
+
@click.option("--editor", "-e", envvar="EDITOR", default="vim", help="Editor to use (defaults to $EDITOR or vim)")
|
|
18
|
+
@click.pass_context
|
|
19
|
+
def edit(ctx, task_id, repo, editor):
|
|
20
|
+
"""Edit an existing task.
|
|
21
|
+
|
|
22
|
+
TASK_ID: Task ID to edit
|
|
23
|
+
"""
|
|
24
|
+
config = ctx.obj["config"]
|
|
25
|
+
manager = RepositoryManager(config.parent_dir)
|
|
26
|
+
|
|
27
|
+
# Normalize task ID (convert "1" to "001", etc.)
|
|
28
|
+
task_id = normalize_task_id(task_id)
|
|
29
|
+
|
|
30
|
+
# Find the task
|
|
31
|
+
if repo:
|
|
32
|
+
repository = manager.get_repository(repo)
|
|
33
|
+
if not repository:
|
|
34
|
+
click.secho(f"Error: Repository '{repo}' not found", fg="red", err=True)
|
|
35
|
+
ctx.exit(1)
|
|
36
|
+
task = repository.get_task(task_id)
|
|
37
|
+
if not task:
|
|
38
|
+
click.secho(f"Error: Task '{task_id}' not found in repository '{repo}'", fg="red", err=True)
|
|
39
|
+
ctx.exit(1)
|
|
40
|
+
else:
|
|
41
|
+
# Search all repositories
|
|
42
|
+
task = None
|
|
43
|
+
repository = None
|
|
44
|
+
for r in manager.discover_repositories():
|
|
45
|
+
t = r.get_task(task_id)
|
|
46
|
+
if t:
|
|
47
|
+
task = t
|
|
48
|
+
repository = r
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
if not task:
|
|
52
|
+
click.secho(f"Error: Task '{task_id}' not found", fg="red", err=True)
|
|
53
|
+
ctx.exit(1)
|
|
54
|
+
|
|
55
|
+
# Create temporary file with task content
|
|
56
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
|
|
57
|
+
temp_file = Path(f.name)
|
|
58
|
+
f.write(task.to_markdown())
|
|
59
|
+
|
|
60
|
+
# Open editor
|
|
61
|
+
try:
|
|
62
|
+
subprocess.run([editor, str(temp_file)], check=True)
|
|
63
|
+
except subprocess.CalledProcessError:
|
|
64
|
+
click.secho(f"Error: Editor '{editor}' failed", fg="red", err=True)
|
|
65
|
+
temp_file.unlink()
|
|
66
|
+
ctx.exit(1)
|
|
67
|
+
except FileNotFoundError:
|
|
68
|
+
click.secho(f"Error: Editor '{editor}' not found", fg="red", err=True)
|
|
69
|
+
temp_file.unlink()
|
|
70
|
+
ctx.exit(1)
|
|
71
|
+
|
|
72
|
+
# Read modified content
|
|
73
|
+
try:
|
|
74
|
+
content = temp_file.read_text()
|
|
75
|
+
modified_task = Task.from_markdown(content, task_id, repository.name)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
click.secho(f"Error: Failed to parse edited task: {e}", fg="red", err=True)
|
|
78
|
+
temp_file.unlink()
|
|
79
|
+
ctx.exit(1)
|
|
80
|
+
finally:
|
|
81
|
+
temp_file.unlink()
|
|
82
|
+
|
|
83
|
+
# Save modified task
|
|
84
|
+
repository.save_task(modified_task)
|
|
85
|
+
click.secho(f"✓ Task updated: {modified_task}", fg="green")
|