doit-toolkit-cli 0.1.10__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.
Potentially problematic release.
This version of doit-toolkit-cli might be problematic. Click here for more details.
- doit_cli/__init__.py +1356 -0
- doit_cli/cli/__init__.py +26 -0
- doit_cli/cli/analytics_command.py +616 -0
- doit_cli/cli/context_command.py +213 -0
- doit_cli/cli/diagram_command.py +304 -0
- doit_cli/cli/fixit_command.py +641 -0
- doit_cli/cli/hooks_command.py +211 -0
- doit_cli/cli/init_command.py +613 -0
- doit_cli/cli/memory_command.py +293 -0
- doit_cli/cli/roadmapit_command.py +10 -0
- doit_cli/cli/status_command.py +117 -0
- doit_cli/cli/sync_prompts_command.py +248 -0
- doit_cli/cli/validate_command.py +196 -0
- doit_cli/cli/verify_command.py +204 -0
- doit_cli/cli/workflow_mixin.py +224 -0
- doit_cli/cli/xref_command.py +555 -0
- doit_cli/formatters/__init__.py +8 -0
- doit_cli/formatters/base.py +38 -0
- doit_cli/formatters/json_formatter.py +126 -0
- doit_cli/formatters/markdown_formatter.py +97 -0
- doit_cli/formatters/rich_formatter.py +257 -0
- doit_cli/main.py +51 -0
- doit_cli/models/__init__.py +139 -0
- doit_cli/models/agent.py +74 -0
- doit_cli/models/analytics_models.py +384 -0
- doit_cli/models/context_config.py +464 -0
- doit_cli/models/crossref_models.py +182 -0
- doit_cli/models/diagram_models.py +363 -0
- doit_cli/models/fixit_models.py +355 -0
- doit_cli/models/hook_config.py +125 -0
- doit_cli/models/project.py +91 -0
- doit_cli/models/results.py +121 -0
- doit_cli/models/search_models.py +228 -0
- doit_cli/models/status_models.py +195 -0
- doit_cli/models/sync_models.py +146 -0
- doit_cli/models/template.py +77 -0
- doit_cli/models/validation_models.py +175 -0
- doit_cli/models/workflow_models.py +319 -0
- doit_cli/prompts/__init__.py +5 -0
- doit_cli/prompts/fixit_prompts.py +344 -0
- doit_cli/prompts/interactive.py +390 -0
- doit_cli/rules/__init__.py +5 -0
- doit_cli/rules/builtin_rules.py +160 -0
- doit_cli/services/__init__.py +79 -0
- doit_cli/services/agent_detector.py +168 -0
- doit_cli/services/analytics_service.py +218 -0
- doit_cli/services/architecture_generator.py +290 -0
- doit_cli/services/backup_service.py +204 -0
- doit_cli/services/config_loader.py +113 -0
- doit_cli/services/context_loader.py +1123 -0
- doit_cli/services/coverage_calculator.py +142 -0
- doit_cli/services/crossref_service.py +237 -0
- doit_cli/services/cycle_time_calculator.py +134 -0
- doit_cli/services/date_inferrer.py +349 -0
- doit_cli/services/diagram_service.py +337 -0
- doit_cli/services/drift_detector.py +109 -0
- doit_cli/services/entity_parser.py +301 -0
- doit_cli/services/er_diagram_generator.py +197 -0
- doit_cli/services/fixit_service.py +699 -0
- doit_cli/services/github_service.py +192 -0
- doit_cli/services/hook_manager.py +258 -0
- doit_cli/services/hook_validator.py +528 -0
- doit_cli/services/input_validator.py +322 -0
- doit_cli/services/memory_search.py +527 -0
- doit_cli/services/mermaid_validator.py +334 -0
- doit_cli/services/prompt_transformer.py +91 -0
- doit_cli/services/prompt_writer.py +133 -0
- doit_cli/services/query_interpreter.py +428 -0
- doit_cli/services/report_exporter.py +219 -0
- doit_cli/services/report_generator.py +256 -0
- doit_cli/services/requirement_parser.py +112 -0
- doit_cli/services/roadmap_summarizer.py +209 -0
- doit_cli/services/rule_engine.py +443 -0
- doit_cli/services/scaffolder.py +215 -0
- doit_cli/services/score_calculator.py +172 -0
- doit_cli/services/section_parser.py +204 -0
- doit_cli/services/spec_scanner.py +327 -0
- doit_cli/services/state_manager.py +355 -0
- doit_cli/services/status_reporter.py +143 -0
- doit_cli/services/task_parser.py +347 -0
- doit_cli/services/template_manager.py +710 -0
- doit_cli/services/template_reader.py +158 -0
- doit_cli/services/user_journey_generator.py +214 -0
- doit_cli/services/user_story_parser.py +232 -0
- doit_cli/services/validation_service.py +188 -0
- doit_cli/services/validator.py +232 -0
- doit_cli/services/velocity_tracker.py +173 -0
- doit_cli/services/workflow_engine.py +405 -0
- doit_cli/templates/agent-file-template.md +28 -0
- doit_cli/templates/checklist-template.md +39 -0
- doit_cli/templates/commands/doit.checkin.md +363 -0
- doit_cli/templates/commands/doit.constitution.md +187 -0
- doit_cli/templates/commands/doit.documentit.md +485 -0
- doit_cli/templates/commands/doit.fixit.md +181 -0
- doit_cli/templates/commands/doit.implementit.md +265 -0
- doit_cli/templates/commands/doit.planit.md +262 -0
- doit_cli/templates/commands/doit.reviewit.md +355 -0
- doit_cli/templates/commands/doit.roadmapit.md +389 -0
- doit_cli/templates/commands/doit.scaffoldit.md +458 -0
- doit_cli/templates/commands/doit.specit.md +521 -0
- doit_cli/templates/commands/doit.taskit.md +304 -0
- doit_cli/templates/commands/doit.testit.md +277 -0
- doit_cli/templates/config/context.yaml +134 -0
- doit_cli/templates/config/hooks.yaml +93 -0
- doit_cli/templates/config/validation-rules.yaml +64 -0
- doit_cli/templates/github-issue-templates/epic.yml +78 -0
- doit_cli/templates/github-issue-templates/feature.yml +116 -0
- doit_cli/templates/github-issue-templates/task.yml +129 -0
- doit_cli/templates/hooks/.gitkeep +0 -0
- doit_cli/templates/hooks/post-commit.sh +25 -0
- doit_cli/templates/hooks/post-merge.sh +75 -0
- doit_cli/templates/hooks/pre-commit.sh +17 -0
- doit_cli/templates/hooks/pre-push.sh +18 -0
- doit_cli/templates/memory/completed_roadmap.md +50 -0
- doit_cli/templates/memory/constitution.md +125 -0
- doit_cli/templates/memory/roadmap.md +61 -0
- doit_cli/templates/plan-template.md +146 -0
- doit_cli/templates/scripts/bash/check-prerequisites.sh +166 -0
- doit_cli/templates/scripts/bash/common.sh +156 -0
- doit_cli/templates/scripts/bash/create-new-feature.sh +297 -0
- doit_cli/templates/scripts/bash/setup-plan.sh +61 -0
- doit_cli/templates/scripts/bash/update-agent-context.sh +675 -0
- doit_cli/templates/scripts/powershell/check-prerequisites.ps1 +148 -0
- doit_cli/templates/scripts/powershell/common.ps1 +137 -0
- doit_cli/templates/scripts/powershell/create-new-feature.ps1 +283 -0
- doit_cli/templates/scripts/powershell/setup-plan.ps1 +61 -0
- doit_cli/templates/scripts/powershell/update-agent-context.ps1 +406 -0
- doit_cli/templates/spec-template.md +159 -0
- doit_cli/templates/tasks-template.md +313 -0
- doit_cli/templates/vscode-settings.json +14 -0
- doit_toolkit_cli-0.1.10.dist-info/METADATA +324 -0
- doit_toolkit_cli-0.1.10.dist-info/RECORD +135 -0
- doit_toolkit_cli-0.1.10.dist-info/WHEEL +4 -0
- doit_toolkit_cli-0.1.10.dist-info/entry_points.txt +2 -0
- doit_toolkit_cli-0.1.10.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Backup service for preserving files during updates."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import shutil
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BackupService:
|
|
10
|
+
"""Service for creating backups before update operations."""
|
|
11
|
+
|
|
12
|
+
BACKUP_DIR = "backups"
|
|
13
|
+
TIMESTAMP_FORMAT = "%Y%m%dT%H%M%S"
|
|
14
|
+
|
|
15
|
+
def __init__(self, project_path: Path):
|
|
16
|
+
"""Initialize backup service.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
project_path: Root path of the project
|
|
20
|
+
"""
|
|
21
|
+
self.project_path = project_path
|
|
22
|
+
self.doit_folder = project_path / ".doit"
|
|
23
|
+
self.backups_folder = self.doit_folder / self.BACKUP_DIR
|
|
24
|
+
|
|
25
|
+
def create_backup(
|
|
26
|
+
self,
|
|
27
|
+
files: list[Path],
|
|
28
|
+
backup_name: Optional[str] = None,
|
|
29
|
+
) -> Optional[Path]:
|
|
30
|
+
"""Create a timestamped backup of specified files.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
files: List of file paths to backup
|
|
34
|
+
backup_name: Optional custom backup name (defaults to timestamp)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Path to backup directory, or None if no files to backup
|
|
38
|
+
"""
|
|
39
|
+
if not files:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
# Generate timestamp-based backup directory name
|
|
43
|
+
timestamp = datetime.now().strftime(self.TIMESTAMP_FORMAT)
|
|
44
|
+
backup_dir_name = backup_name or timestamp
|
|
45
|
+
backup_dir = self.backups_folder / backup_dir_name
|
|
46
|
+
|
|
47
|
+
# Create backup directory
|
|
48
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
# Copy each file preserving relative structure
|
|
51
|
+
for file_path in files:
|
|
52
|
+
if not file_path.exists():
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
# Calculate relative path from project root
|
|
56
|
+
try:
|
|
57
|
+
rel_path = file_path.relative_to(self.project_path)
|
|
58
|
+
except ValueError:
|
|
59
|
+
# File is not under project path, use filename only
|
|
60
|
+
rel_path = Path(file_path.name)
|
|
61
|
+
|
|
62
|
+
# Create backup target path
|
|
63
|
+
backup_target = backup_dir / rel_path
|
|
64
|
+
backup_target.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
# Copy file
|
|
67
|
+
shutil.copy2(file_path, backup_target)
|
|
68
|
+
|
|
69
|
+
return backup_dir
|
|
70
|
+
|
|
71
|
+
def create_doit_backup(self, include_memory: bool = False) -> Optional[Path]:
|
|
72
|
+
"""Create a backup of doit-managed files.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
include_memory: Whether to include .doit/memory/ files
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Path to backup directory, or None if no files to backup
|
|
79
|
+
"""
|
|
80
|
+
files_to_backup = []
|
|
81
|
+
|
|
82
|
+
# Find all doit-prefixed files in command directories
|
|
83
|
+
command_dirs = [
|
|
84
|
+
self.project_path / ".claude" / "commands",
|
|
85
|
+
self.project_path / ".github" / "prompts",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
for cmd_dir in command_dirs:
|
|
89
|
+
if cmd_dir.exists():
|
|
90
|
+
# Backup doit-prefixed files
|
|
91
|
+
for pattern in ["doit.*.md", "doit-*.prompt.md"]:
|
|
92
|
+
files_to_backup.extend(cmd_dir.glob(pattern))
|
|
93
|
+
|
|
94
|
+
# Optionally include memory folder
|
|
95
|
+
if include_memory:
|
|
96
|
+
memory_dir = self.doit_folder / "memory"
|
|
97
|
+
if memory_dir.exists():
|
|
98
|
+
files_to_backup.extend(memory_dir.rglob("*"))
|
|
99
|
+
|
|
100
|
+
# Filter to only include actual files (not directories)
|
|
101
|
+
files_to_backup = [f for f in files_to_backup if f.is_file()]
|
|
102
|
+
|
|
103
|
+
return self.create_backup(files_to_backup)
|
|
104
|
+
|
|
105
|
+
def list_backups(self) -> list[Path]:
|
|
106
|
+
"""List all available backups.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
List of backup directory paths, sorted by name (newest first)
|
|
110
|
+
"""
|
|
111
|
+
if not self.backups_folder.exists():
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
backups = [
|
|
115
|
+
d for d in self.backups_folder.iterdir()
|
|
116
|
+
if d.is_dir() and not d.name.startswith(".")
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
# Sort by name (timestamp format ensures chronological order)
|
|
120
|
+
return sorted(backups, reverse=True)
|
|
121
|
+
|
|
122
|
+
def restore_backup(self, backup_path: Path) -> dict:
|
|
123
|
+
"""Restore files from a backup.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
backup_path: Path to backup directory
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Dict with 'restored' list of paths and 'errors' list
|
|
130
|
+
"""
|
|
131
|
+
result = {
|
|
132
|
+
"restored": [],
|
|
133
|
+
"errors": [],
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if not backup_path.exists() or not backup_path.is_dir():
|
|
137
|
+
result["errors"].append(f"Backup not found: {backup_path}")
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
# Walk through backup and restore files
|
|
141
|
+
for backup_file in backup_path.rglob("*"):
|
|
142
|
+
if not backup_file.is_file():
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# Calculate original location
|
|
146
|
+
rel_path = backup_file.relative_to(backup_path)
|
|
147
|
+
original_path = self.project_path / rel_path
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
# Ensure parent directory exists
|
|
151
|
+
original_path.parent.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
|
|
153
|
+
# Copy backup file to original location
|
|
154
|
+
shutil.copy2(backup_file, original_path)
|
|
155
|
+
result["restored"].append(original_path)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
result["errors"].append(f"Failed to restore {rel_path}: {e}")
|
|
158
|
+
|
|
159
|
+
return result
|
|
160
|
+
|
|
161
|
+
def cleanup_old_backups(self, keep_count: int = 5) -> list[Path]:
|
|
162
|
+
"""Remove old backups, keeping only the most recent ones.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
keep_count: Number of recent backups to keep
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
List of removed backup paths
|
|
169
|
+
"""
|
|
170
|
+
backups = self.list_backups()
|
|
171
|
+
removed = []
|
|
172
|
+
|
|
173
|
+
if len(backups) <= keep_count:
|
|
174
|
+
return removed
|
|
175
|
+
|
|
176
|
+
# Remove older backups
|
|
177
|
+
for old_backup in backups[keep_count:]:
|
|
178
|
+
try:
|
|
179
|
+
shutil.rmtree(old_backup)
|
|
180
|
+
removed.append(old_backup)
|
|
181
|
+
except Exception:
|
|
182
|
+
# Silently skip backups that can't be removed
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
return removed
|
|
186
|
+
|
|
187
|
+
def get_backup_size(self, backup_path: Path) -> int:
|
|
188
|
+
"""Calculate total size of a backup in bytes.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
backup_path: Path to backup directory
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Total size in bytes
|
|
195
|
+
"""
|
|
196
|
+
if not backup_path.exists():
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
total = 0
|
|
200
|
+
for file_path in backup_path.rglob("*"):
|
|
201
|
+
if file_path.is_file():
|
|
202
|
+
total += file_path.stat().st_size
|
|
203
|
+
|
|
204
|
+
return total
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Config loader for validation rules configuration."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from ..models.validation_models import (
|
|
9
|
+
CustomRule,
|
|
10
|
+
RuleOverride,
|
|
11
|
+
ValidationConfig,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfigLoader:
|
|
16
|
+
"""Loads validation configuration from YAML files."""
|
|
17
|
+
|
|
18
|
+
# Default configuration file path
|
|
19
|
+
DEFAULT_CONFIG_PATH = ".doit/validation-rules.yaml"
|
|
20
|
+
|
|
21
|
+
def __init__(self, project_root: Optional[Path] = None) -> None:
|
|
22
|
+
"""Initialize config loader.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
project_root: Project root directory. Defaults to cwd.
|
|
26
|
+
"""
|
|
27
|
+
self.project_root = project_root or Path.cwd()
|
|
28
|
+
|
|
29
|
+
def load(self, config_path: Optional[Path] = None) -> ValidationConfig:
|
|
30
|
+
"""Load validation configuration from YAML file.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
config_path: Path to config file. Uses default if None.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
ValidationConfig with loaded or default settings.
|
|
37
|
+
"""
|
|
38
|
+
if config_path is None:
|
|
39
|
+
config_path = self.project_root / self.DEFAULT_CONFIG_PATH
|
|
40
|
+
|
|
41
|
+
if not config_path.exists():
|
|
42
|
+
return ValidationConfig.default()
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
with open(config_path, encoding="utf-8") as f:
|
|
46
|
+
data = yaml.safe_load(f) or {}
|
|
47
|
+
except yaml.YAMLError:
|
|
48
|
+
# Return default config on parse error
|
|
49
|
+
return ValidationConfig.default()
|
|
50
|
+
|
|
51
|
+
return self._from_dict(data, str(config_path))
|
|
52
|
+
|
|
53
|
+
def _from_dict(self, data: dict, path: str = "") -> ValidationConfig:
|
|
54
|
+
"""Create ValidationConfig from dictionary.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
data: Parsed YAML data.
|
|
58
|
+
path: Path to config file for reference.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Populated ValidationConfig.
|
|
62
|
+
"""
|
|
63
|
+
# Parse disabled rules
|
|
64
|
+
disabled_rules = data.get("disabled_rules", [])
|
|
65
|
+
|
|
66
|
+
# Parse overrides
|
|
67
|
+
overrides = []
|
|
68
|
+
for override_data in data.get("overrides", []):
|
|
69
|
+
if "rule" in override_data and "severity" in override_data:
|
|
70
|
+
overrides.append(
|
|
71
|
+
RuleOverride(
|
|
72
|
+
rule=override_data["rule"],
|
|
73
|
+
severity=override_data["severity"],
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Parse custom rules
|
|
78
|
+
custom_rules = []
|
|
79
|
+
for rule_data in data.get("custom_rules", []):
|
|
80
|
+
if "name" in rule_data and "pattern" in rule_data:
|
|
81
|
+
custom_rules.append(
|
|
82
|
+
CustomRule(
|
|
83
|
+
name=rule_data["name"],
|
|
84
|
+
description=rule_data.get("description", ""),
|
|
85
|
+
pattern=rule_data["pattern"],
|
|
86
|
+
severity=rule_data.get("severity", "warning"),
|
|
87
|
+
category=rule_data.get("category", "custom"),
|
|
88
|
+
check=rule_data.get("check", "present"),
|
|
89
|
+
max=rule_data.get("max"),
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return ValidationConfig(
|
|
94
|
+
path=path,
|
|
95
|
+
version=data.get("version", "1.0"),
|
|
96
|
+
enabled=data.get("enabled", True),
|
|
97
|
+
disabled_rules=disabled_rules,
|
|
98
|
+
overrides=overrides,
|
|
99
|
+
custom_rules=custom_rules,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def load_validation_config(project_root: Optional[Path] = None) -> ValidationConfig:
|
|
104
|
+
"""Convenience function to load validation config.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
project_root: Project root directory.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
ValidationConfig from file or defaults.
|
|
111
|
+
"""
|
|
112
|
+
loader = ConfigLoader(project_root)
|
|
113
|
+
return loader.load()
|