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.
- taskrepo-0.1.0/.gitignore +59 -0
- taskrepo-0.1.0/LICENSE +21 -0
- taskrepo-0.1.0/PKG-INFO +0 -0
- taskrepo-0.1.0/README.md +0 -0
- taskrepo-0.1.0/pyproject.toml +134 -0
- taskrepo-0.1.0/src/taskrepo/__init__.py +5 -0
- taskrepo-0.1.0/src/taskrepo/__version__.py +1 -0
- taskrepo-0.1.0/src/taskrepo/cli/__init__.py +1 -0
- taskrepo-0.1.0/src/taskrepo/cli/commands/__init__.py +1 -0
- taskrepo-0.1.0/src/taskrepo/cli/commands/add.py +166 -0
- taskrepo-0.1.0/src/taskrepo/cli/commands/config.py +161 -0
- taskrepo-0.1.0/src/taskrepo/cli/commands/delete.py +62 -0
- taskrepo-0.1.0/src/taskrepo/cli/commands/done.py +53 -0
- taskrepo-0.1.0/src/taskrepo/cli/commands/edit.py +85 -0
- taskrepo-0.1.0/src/taskrepo/cli/commands/list.py +235 -0
- taskrepo-0.1.0/src/taskrepo/cli/commands/sync.py +91 -0
- taskrepo-0.1.0/src/taskrepo/cli/main.py +120 -0
- taskrepo-0.1.0/src/taskrepo/core/__init__.py +1 -0
- taskrepo-0.1.0/src/taskrepo/core/config.py +199 -0
- taskrepo-0.1.0/src/taskrepo/core/repository.py +432 -0
- taskrepo-0.1.0/src/taskrepo/core/task.py +198 -0
- taskrepo-0.1.0/src/taskrepo/tui/__init__.py +1 -0
- taskrepo-0.1.0/src/taskrepo/tui/prompts.py +262 -0
- taskrepo-0.1.0/src/taskrepo/utils/__init__.py +1 -0
- taskrepo-0.1.0/src/taskrepo/utils/helpers.py +34 -0
- taskrepo-0.1.0/src/taskrepo/utils/id_mapping.py +94 -0
- taskrepo-0.1.0/tests/__init__.py +1 -0
- taskrepo-0.1.0/tests/integration/__init__.py +1 -0
- taskrepo-0.1.0/tests/unit/__init__.py +1 -0
- taskrepo-0.1.0/tests/unit/test_repository.py +274 -0
- 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.
|
taskrepo-0.1.0/PKG-INFO
ADDED
|
Binary file
|
taskrepo-0.1.0/README.md
ADDED
|
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 @@
|
|
|
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")
|