doit-toolkit-cli 0.1.9__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.
Files changed (134) hide show
  1. doit_cli/__init__.py +1356 -0
  2. doit_cli/cli/__init__.py +26 -0
  3. doit_cli/cli/analytics_command.py +616 -0
  4. doit_cli/cli/context_command.py +213 -0
  5. doit_cli/cli/diagram_command.py +304 -0
  6. doit_cli/cli/fixit_command.py +641 -0
  7. doit_cli/cli/hooks_command.py +211 -0
  8. doit_cli/cli/init_command.py +613 -0
  9. doit_cli/cli/memory_command.py +293 -0
  10. doit_cli/cli/status_command.py +117 -0
  11. doit_cli/cli/sync_prompts_command.py +248 -0
  12. doit_cli/cli/validate_command.py +196 -0
  13. doit_cli/cli/verify_command.py +204 -0
  14. doit_cli/cli/workflow_mixin.py +224 -0
  15. doit_cli/cli/xref_command.py +555 -0
  16. doit_cli/formatters/__init__.py +8 -0
  17. doit_cli/formatters/base.py +38 -0
  18. doit_cli/formatters/json_formatter.py +126 -0
  19. doit_cli/formatters/markdown_formatter.py +97 -0
  20. doit_cli/formatters/rich_formatter.py +257 -0
  21. doit_cli/main.py +49 -0
  22. doit_cli/models/__init__.py +139 -0
  23. doit_cli/models/agent.py +74 -0
  24. doit_cli/models/analytics_models.py +384 -0
  25. doit_cli/models/context_config.py +464 -0
  26. doit_cli/models/crossref_models.py +182 -0
  27. doit_cli/models/diagram_models.py +363 -0
  28. doit_cli/models/fixit_models.py +355 -0
  29. doit_cli/models/hook_config.py +125 -0
  30. doit_cli/models/project.py +91 -0
  31. doit_cli/models/results.py +121 -0
  32. doit_cli/models/search_models.py +228 -0
  33. doit_cli/models/status_models.py +195 -0
  34. doit_cli/models/sync_models.py +146 -0
  35. doit_cli/models/template.py +77 -0
  36. doit_cli/models/validation_models.py +175 -0
  37. doit_cli/models/workflow_models.py +319 -0
  38. doit_cli/prompts/__init__.py +5 -0
  39. doit_cli/prompts/fixit_prompts.py +344 -0
  40. doit_cli/prompts/interactive.py +390 -0
  41. doit_cli/rules/__init__.py +5 -0
  42. doit_cli/rules/builtin_rules.py +160 -0
  43. doit_cli/services/__init__.py +79 -0
  44. doit_cli/services/agent_detector.py +168 -0
  45. doit_cli/services/analytics_service.py +218 -0
  46. doit_cli/services/architecture_generator.py +290 -0
  47. doit_cli/services/backup_service.py +204 -0
  48. doit_cli/services/config_loader.py +113 -0
  49. doit_cli/services/context_loader.py +1121 -0
  50. doit_cli/services/coverage_calculator.py +142 -0
  51. doit_cli/services/crossref_service.py +237 -0
  52. doit_cli/services/cycle_time_calculator.py +134 -0
  53. doit_cli/services/date_inferrer.py +349 -0
  54. doit_cli/services/diagram_service.py +337 -0
  55. doit_cli/services/drift_detector.py +109 -0
  56. doit_cli/services/entity_parser.py +301 -0
  57. doit_cli/services/er_diagram_generator.py +197 -0
  58. doit_cli/services/fixit_service.py +699 -0
  59. doit_cli/services/github_service.py +192 -0
  60. doit_cli/services/hook_manager.py +258 -0
  61. doit_cli/services/hook_validator.py +528 -0
  62. doit_cli/services/input_validator.py +322 -0
  63. doit_cli/services/memory_search.py +527 -0
  64. doit_cli/services/mermaid_validator.py +334 -0
  65. doit_cli/services/prompt_transformer.py +91 -0
  66. doit_cli/services/prompt_writer.py +133 -0
  67. doit_cli/services/query_interpreter.py +428 -0
  68. doit_cli/services/report_exporter.py +219 -0
  69. doit_cli/services/report_generator.py +256 -0
  70. doit_cli/services/requirement_parser.py +112 -0
  71. doit_cli/services/roadmap_summarizer.py +209 -0
  72. doit_cli/services/rule_engine.py +443 -0
  73. doit_cli/services/scaffolder.py +215 -0
  74. doit_cli/services/score_calculator.py +172 -0
  75. doit_cli/services/section_parser.py +204 -0
  76. doit_cli/services/spec_scanner.py +327 -0
  77. doit_cli/services/state_manager.py +355 -0
  78. doit_cli/services/status_reporter.py +143 -0
  79. doit_cli/services/task_parser.py +347 -0
  80. doit_cli/services/template_manager.py +710 -0
  81. doit_cli/services/template_reader.py +158 -0
  82. doit_cli/services/user_journey_generator.py +214 -0
  83. doit_cli/services/user_story_parser.py +232 -0
  84. doit_cli/services/validation_service.py +188 -0
  85. doit_cli/services/validator.py +232 -0
  86. doit_cli/services/velocity_tracker.py +173 -0
  87. doit_cli/services/workflow_engine.py +405 -0
  88. doit_cli/templates/agent-file-template.md +28 -0
  89. doit_cli/templates/checklist-template.md +39 -0
  90. doit_cli/templates/commands/doit.checkin.md +363 -0
  91. doit_cli/templates/commands/doit.constitution.md +187 -0
  92. doit_cli/templates/commands/doit.documentit.md +485 -0
  93. doit_cli/templates/commands/doit.fixit.md +181 -0
  94. doit_cli/templates/commands/doit.implementit.md +265 -0
  95. doit_cli/templates/commands/doit.planit.md +262 -0
  96. doit_cli/templates/commands/doit.reviewit.md +355 -0
  97. doit_cli/templates/commands/doit.roadmapit.md +368 -0
  98. doit_cli/templates/commands/doit.scaffoldit.md +458 -0
  99. doit_cli/templates/commands/doit.specit.md +521 -0
  100. doit_cli/templates/commands/doit.taskit.md +304 -0
  101. doit_cli/templates/commands/doit.testit.md +277 -0
  102. doit_cli/templates/config/context.yaml +134 -0
  103. doit_cli/templates/config/hooks.yaml +93 -0
  104. doit_cli/templates/config/validation-rules.yaml +64 -0
  105. doit_cli/templates/github-issue-templates/epic.yml +78 -0
  106. doit_cli/templates/github-issue-templates/feature.yml +116 -0
  107. doit_cli/templates/github-issue-templates/task.yml +129 -0
  108. doit_cli/templates/hooks/.gitkeep +0 -0
  109. doit_cli/templates/hooks/post-commit.sh +25 -0
  110. doit_cli/templates/hooks/post-merge.sh +75 -0
  111. doit_cli/templates/hooks/pre-commit.sh +17 -0
  112. doit_cli/templates/hooks/pre-push.sh +18 -0
  113. doit_cli/templates/memory/completed_roadmap.md +50 -0
  114. doit_cli/templates/memory/constitution.md +125 -0
  115. doit_cli/templates/memory/roadmap.md +61 -0
  116. doit_cli/templates/plan-template.md +146 -0
  117. doit_cli/templates/scripts/bash/check-prerequisites.sh +166 -0
  118. doit_cli/templates/scripts/bash/common.sh +156 -0
  119. doit_cli/templates/scripts/bash/create-new-feature.sh +297 -0
  120. doit_cli/templates/scripts/bash/setup-plan.sh +61 -0
  121. doit_cli/templates/scripts/bash/update-agent-context.sh +675 -0
  122. doit_cli/templates/scripts/powershell/check-prerequisites.ps1 +148 -0
  123. doit_cli/templates/scripts/powershell/common.ps1 +137 -0
  124. doit_cli/templates/scripts/powershell/create-new-feature.ps1 +283 -0
  125. doit_cli/templates/scripts/powershell/setup-plan.ps1 +61 -0
  126. doit_cli/templates/scripts/powershell/update-agent-context.ps1 +406 -0
  127. doit_cli/templates/spec-template.md +159 -0
  128. doit_cli/templates/tasks-template.md +313 -0
  129. doit_cli/templates/vscode-settings.json +14 -0
  130. doit_toolkit_cli-0.1.9.dist-info/METADATA +324 -0
  131. doit_toolkit_cli-0.1.9.dist-info/RECORD +134 -0
  132. doit_toolkit_cli-0.1.9.dist-info/WHEEL +4 -0
  133. doit_toolkit_cli-0.1.9.dist-info/entry_points.txt +2 -0
  134. doit_toolkit_cli-0.1.9.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()