taskrepo 0.1.0__tar.gz

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.
Files changed (31) hide show
  1. taskrepo-0.1.0/.gitignore +59 -0
  2. taskrepo-0.1.0/LICENSE +21 -0
  3. taskrepo-0.1.0/PKG-INFO +0 -0
  4. taskrepo-0.1.0/README.md +0 -0
  5. taskrepo-0.1.0/pyproject.toml +134 -0
  6. taskrepo-0.1.0/src/taskrepo/__init__.py +5 -0
  7. taskrepo-0.1.0/src/taskrepo/__version__.py +1 -0
  8. taskrepo-0.1.0/src/taskrepo/cli/__init__.py +1 -0
  9. taskrepo-0.1.0/src/taskrepo/cli/commands/__init__.py +1 -0
  10. taskrepo-0.1.0/src/taskrepo/cli/commands/add.py +166 -0
  11. taskrepo-0.1.0/src/taskrepo/cli/commands/config.py +161 -0
  12. taskrepo-0.1.0/src/taskrepo/cli/commands/delete.py +62 -0
  13. taskrepo-0.1.0/src/taskrepo/cli/commands/done.py +53 -0
  14. taskrepo-0.1.0/src/taskrepo/cli/commands/edit.py +85 -0
  15. taskrepo-0.1.0/src/taskrepo/cli/commands/list.py +235 -0
  16. taskrepo-0.1.0/src/taskrepo/cli/commands/sync.py +91 -0
  17. taskrepo-0.1.0/src/taskrepo/cli/main.py +120 -0
  18. taskrepo-0.1.0/src/taskrepo/core/__init__.py +1 -0
  19. taskrepo-0.1.0/src/taskrepo/core/config.py +199 -0
  20. taskrepo-0.1.0/src/taskrepo/core/repository.py +432 -0
  21. taskrepo-0.1.0/src/taskrepo/core/task.py +198 -0
  22. taskrepo-0.1.0/src/taskrepo/tui/__init__.py +1 -0
  23. taskrepo-0.1.0/src/taskrepo/tui/prompts.py +262 -0
  24. taskrepo-0.1.0/src/taskrepo/utils/__init__.py +1 -0
  25. taskrepo-0.1.0/src/taskrepo/utils/helpers.py +34 -0
  26. taskrepo-0.1.0/src/taskrepo/utils/id_mapping.py +94 -0
  27. taskrepo-0.1.0/tests/__init__.py +1 -0
  28. taskrepo-0.1.0/tests/integration/__init__.py +1 -0
  29. taskrepo-0.1.0/tests/unit/__init__.py +1 -0
  30. taskrepo-0.1.0/tests/unit/test_repository.py +274 -0
  31. taskrepo-0.1.0/tests/unit/test_task.py +141 -0
@@ -0,0 +1,59 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ .venv/
25
+ venv/
26
+ ENV/
27
+ env/
28
+
29
+ # UV
30
+ uv.lock
31
+
32
+ # Testing
33
+ .pytest_cache/
34
+ .coverage
35
+ htmlcov/
36
+ .tox/
37
+ .nox/
38
+
39
+ # IDEs
40
+ .vscode/
41
+ .idea/
42
+ *.swp
43
+ *.swo
44
+ *~
45
+
46
+ # OS
47
+ .DS_Store
48
+ Thumbs.db
49
+
50
+ # MyPy
51
+ .mypy_cache/
52
+ .dmypy.json
53
+ dmypy.json
54
+
55
+ # Ruff
56
+ .ruff_cache/
57
+
58
+ # Local config (user-specific)
59
+ .taskreporc
taskrepo-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ricardo Henriques
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
Binary file
Binary file
@@ -0,0 +1,134 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "taskrepo"
7
+ dynamic = ["version"]
8
+ authors = [
9
+ { name = "Ricardo Henriques", email = "paxcalpt@gmail.com" }
10
+ ]
11
+ keywords = ["task-management", "git", "markdown", "cli", "taskwarrior", "productivity"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Software Development",
22
+ "Topic :: Office/Business :: Scheduling",
23
+ ]
24
+ description = "TaskWarrior-inspired CLI for managing tasks as markdown files in git repositories"
25
+ readme = "README.md"
26
+ requires-python = ">=3.10"
27
+ dependencies = [
28
+ "click>=8.0.0",
29
+ "prompt_toolkit>=3.0.0",
30
+ "GitPython>=3.1.0",
31
+ "PyYAML>=6.0.0",
32
+ "rich>=13.0.0",
33
+ "python-dateutil>=2.8.0",
34
+ "dateparser>=1.0.0",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/henriqueslab/TaskRepo"
39
+ Repository = "https://github.com/henriqueslab/TaskRepo"
40
+ Issues = "https://github.com/henriqueslab/TaskRepo/issues"
41
+
42
+ [project.optional-dependencies]
43
+ dev = [
44
+ "pytest>=7.4,<9.0",
45
+ "pytest-cov>=4.0",
46
+ "ruff>=0.12.2",
47
+ "mypy>=1.0",
48
+ "pre-commit>=4.2.0",
49
+ "types-PyYAML>=6.0.0",
50
+ "types-python-dateutil>=2.8.0",
51
+ ]
52
+
53
+ # Ruff configuration
54
+ [tool.ruff]
55
+ line-length = 120
56
+ target-version = "py311"
57
+
58
+ [tool.ruff.lint]
59
+ select = [
60
+ "E", # pycodestyle errors
61
+ "W", # pycodestyle warnings
62
+ "F", # Pyflakes
63
+ "I", # isort
64
+ "B", # flake8-bugbear
65
+ "C4", # flake8-comprehensions
66
+ "D", # pydocstyle
67
+ ]
68
+
69
+ ignore = [
70
+ "E203", # whitespace before ':'
71
+ "E501", # line too long
72
+ "D100", # Missing docstring in public module
73
+ "D101", # Missing docstring in public class
74
+ "D102", # Missing docstring in public method
75
+ "D103", # Missing docstring in public function
76
+ "D104", # Missing docstring in public package
77
+ "D105", # Missing docstring in magic method
78
+ "D107", # Missing docstring in __init__
79
+ ]
80
+
81
+ [tool.ruff.format]
82
+ quote-style = "double"
83
+ indent-style = "space"
84
+
85
+ [tool.ruff.lint.isort]
86
+ known-first-party = ["taskrepo"]
87
+
88
+ [tool.ruff.lint.pydocstyle]
89
+ convention = "google"
90
+
91
+ [tool.ruff.lint.per-file-ignores]
92
+ "tests/*" = ["D"]
93
+
94
+ [tool.mypy]
95
+ python_version = "3.11"
96
+ warn_return_any = false
97
+ warn_unused_configs = false
98
+ disallow_untyped_defs = false
99
+ check_untyped_defs = true
100
+ explicit_package_bases = true
101
+ ignore_missing_imports = true
102
+
103
+ [tool.pytest.ini_options]
104
+ testpaths = ["tests"]
105
+ python_files = ["test_*.py"]
106
+ python_classes = ["Test*"]
107
+ python_functions = ["test_*"]
108
+ addopts = [
109
+ "-v",
110
+ "--tb=short",
111
+ "--strict-markers",
112
+ "--strict-config",
113
+ ]
114
+
115
+ # Hatch configuration
116
+ [tool.hatch.version]
117
+ path = "src/taskrepo/__version__.py"
118
+
119
+ [tool.hatch.build.targets.wheel]
120
+ packages = ["src/taskrepo"]
121
+
122
+ [tool.hatch.build.targets.sdist]
123
+ include = [
124
+ "/src",
125
+ "/tests",
126
+ "/README.md",
127
+ "/LICENSE",
128
+ "/pyproject.toml"
129
+ ]
130
+
131
+ # Console scripts entry point
132
+ [project.scripts]
133
+ taskrepo = "taskrepo.cli.main:cli"
134
+ tsk = "taskrepo.cli.main:cli"
@@ -0,0 +1,5 @@
1
+ """TaskRepo - TaskWarrior-inspired CLI for managing tasks as markdown files in git repositories."""
2
+
3
+ from taskrepo.__version__ import __version__
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -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")