spec-kitty-cli 0.12.1__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.
- spec_kitty_cli-0.12.1.dist-info/METADATA +1767 -0
- spec_kitty_cli-0.12.1.dist-info/RECORD +242 -0
- spec_kitty_cli-0.12.1.dist-info/WHEEL +4 -0
- spec_kitty_cli-0.12.1.dist-info/entry_points.txt +2 -0
- spec_kitty_cli-0.12.1.dist-info/licenses/LICENSE +21 -0
- specify_cli/__init__.py +171 -0
- specify_cli/acceptance.py +627 -0
- specify_cli/agent_utils/README.md +157 -0
- specify_cli/agent_utils/__init__.py +9 -0
- specify_cli/agent_utils/status.py +356 -0
- specify_cli/cli/__init__.py +6 -0
- specify_cli/cli/commands/__init__.py +46 -0
- specify_cli/cli/commands/accept.py +189 -0
- specify_cli/cli/commands/agent/__init__.py +22 -0
- specify_cli/cli/commands/agent/config.py +382 -0
- specify_cli/cli/commands/agent/context.py +191 -0
- specify_cli/cli/commands/agent/feature.py +1057 -0
- specify_cli/cli/commands/agent/release.py +11 -0
- specify_cli/cli/commands/agent/tasks.py +1253 -0
- specify_cli/cli/commands/agent/workflow.py +801 -0
- specify_cli/cli/commands/context.py +246 -0
- specify_cli/cli/commands/dashboard.py +85 -0
- specify_cli/cli/commands/implement.py +973 -0
- specify_cli/cli/commands/init.py +827 -0
- specify_cli/cli/commands/init_help.py +62 -0
- specify_cli/cli/commands/merge.py +755 -0
- specify_cli/cli/commands/mission.py +240 -0
- specify_cli/cli/commands/ops.py +265 -0
- specify_cli/cli/commands/orchestrate.py +640 -0
- specify_cli/cli/commands/repair.py +175 -0
- specify_cli/cli/commands/research.py +165 -0
- specify_cli/cli/commands/sync.py +364 -0
- specify_cli/cli/commands/upgrade.py +249 -0
- specify_cli/cli/commands/validate_encoding.py +186 -0
- specify_cli/cli/commands/validate_tasks.py +186 -0
- specify_cli/cli/commands/verify.py +310 -0
- specify_cli/cli/helpers.py +123 -0
- specify_cli/cli/step_tracker.py +91 -0
- specify_cli/cli/ui.py +192 -0
- specify_cli/core/__init__.py +53 -0
- specify_cli/core/agent_context.py +311 -0
- specify_cli/core/config.py +96 -0
- specify_cli/core/context_validation.py +362 -0
- specify_cli/core/dependency_graph.py +351 -0
- specify_cli/core/git_ops.py +129 -0
- specify_cli/core/multi_parent_merge.py +323 -0
- specify_cli/core/paths.py +260 -0
- specify_cli/core/project_resolver.py +110 -0
- specify_cli/core/stale_detection.py +263 -0
- specify_cli/core/tool_checker.py +79 -0
- specify_cli/core/utils.py +43 -0
- specify_cli/core/vcs/__init__.py +114 -0
- specify_cli/core/vcs/detection.py +341 -0
- specify_cli/core/vcs/exceptions.py +85 -0
- specify_cli/core/vcs/git.py +1304 -0
- specify_cli/core/vcs/jujutsu.py +1208 -0
- specify_cli/core/vcs/protocol.py +285 -0
- specify_cli/core/vcs/types.py +249 -0
- specify_cli/core/version_checker.py +261 -0
- specify_cli/core/worktree.py +506 -0
- specify_cli/dashboard/__init__.py +28 -0
- specify_cli/dashboard/diagnostics.py +204 -0
- specify_cli/dashboard/handlers/__init__.py +17 -0
- specify_cli/dashboard/handlers/api.py +143 -0
- specify_cli/dashboard/handlers/base.py +65 -0
- specify_cli/dashboard/handlers/features.py +390 -0
- specify_cli/dashboard/handlers/router.py +81 -0
- specify_cli/dashboard/handlers/static.py +50 -0
- specify_cli/dashboard/lifecycle.py +541 -0
- specify_cli/dashboard/scanner.py +437 -0
- specify_cli/dashboard/server.py +123 -0
- specify_cli/dashboard/static/dashboard/dashboard.css +722 -0
- specify_cli/dashboard/static/dashboard/dashboard.js +1424 -0
- specify_cli/dashboard/static/spec-kitty.png +0 -0
- specify_cli/dashboard/templates/__init__.py +36 -0
- specify_cli/dashboard/templates/index.html +258 -0
- specify_cli/doc_generators.py +621 -0
- specify_cli/doc_state.py +408 -0
- specify_cli/frontmatter.py +384 -0
- specify_cli/gap_analysis.py +915 -0
- specify_cli/gitignore_manager.py +300 -0
- specify_cli/guards.py +145 -0
- specify_cli/legacy_detector.py +83 -0
- specify_cli/manifest.py +286 -0
- specify_cli/merge/__init__.py +63 -0
- specify_cli/merge/executor.py +653 -0
- specify_cli/merge/forecast.py +215 -0
- specify_cli/merge/ordering.py +126 -0
- specify_cli/merge/preflight.py +230 -0
- specify_cli/merge/state.py +185 -0
- specify_cli/merge/status_resolver.py +354 -0
- specify_cli/mission.py +654 -0
- specify_cli/missions/documentation/command-templates/implement.md +309 -0
- specify_cli/missions/documentation/command-templates/plan.md +275 -0
- specify_cli/missions/documentation/command-templates/review.md +344 -0
- specify_cli/missions/documentation/command-templates/specify.md +206 -0
- specify_cli/missions/documentation/command-templates/tasks.md +189 -0
- specify_cli/missions/documentation/mission.yaml +113 -0
- specify_cli/missions/documentation/templates/divio/explanation-template.md +192 -0
- specify_cli/missions/documentation/templates/divio/howto-template.md +168 -0
- specify_cli/missions/documentation/templates/divio/reference-template.md +179 -0
- specify_cli/missions/documentation/templates/divio/tutorial-template.md +146 -0
- specify_cli/missions/documentation/templates/generators/jsdoc.json.template +18 -0
- specify_cli/missions/documentation/templates/generators/sphinx-conf.py.template +36 -0
- specify_cli/missions/documentation/templates/plan-template.md +269 -0
- specify_cli/missions/documentation/templates/release-template.md +222 -0
- specify_cli/missions/documentation/templates/spec-template.md +172 -0
- specify_cli/missions/documentation/templates/task-prompt-template.md +140 -0
- specify_cli/missions/documentation/templates/tasks-template.md +159 -0
- specify_cli/missions/research/command-templates/merge.md +388 -0
- specify_cli/missions/research/command-templates/plan.md +125 -0
- specify_cli/missions/research/command-templates/review.md +144 -0
- specify_cli/missions/research/command-templates/tasks.md +225 -0
- specify_cli/missions/research/mission.yaml +115 -0
- specify_cli/missions/research/templates/data-model-template.md +33 -0
- specify_cli/missions/research/templates/plan-template.md +161 -0
- specify_cli/missions/research/templates/research/evidence-log.csv +18 -0
- specify_cli/missions/research/templates/research/source-register.csv +18 -0
- specify_cli/missions/research/templates/research-template.md +35 -0
- specify_cli/missions/research/templates/spec-template.md +64 -0
- specify_cli/missions/research/templates/task-prompt-template.md +148 -0
- specify_cli/missions/research/templates/tasks-template.md +114 -0
- specify_cli/missions/software-dev/command-templates/accept.md +75 -0
- specify_cli/missions/software-dev/command-templates/analyze.md +183 -0
- specify_cli/missions/software-dev/command-templates/checklist.md +286 -0
- specify_cli/missions/software-dev/command-templates/clarify.md +157 -0
- specify_cli/missions/software-dev/command-templates/constitution.md +432 -0
- specify_cli/missions/software-dev/command-templates/dashboard.md +101 -0
- specify_cli/missions/software-dev/command-templates/implement.md +41 -0
- specify_cli/missions/software-dev/command-templates/merge.md +383 -0
- specify_cli/missions/software-dev/command-templates/plan.md +171 -0
- specify_cli/missions/software-dev/command-templates/review.md +32 -0
- specify_cli/missions/software-dev/command-templates/specify.md +321 -0
- specify_cli/missions/software-dev/command-templates/tasks.md +566 -0
- specify_cli/missions/software-dev/mission.yaml +100 -0
- specify_cli/missions/software-dev/templates/plan-template.md +132 -0
- specify_cli/missions/software-dev/templates/spec-template.md +116 -0
- specify_cli/missions/software-dev/templates/task-prompt-template.md +140 -0
- specify_cli/missions/software-dev/templates/tasks-template.md +159 -0
- specify_cli/orchestrator/__init__.py +75 -0
- specify_cli/orchestrator/agent_config.py +224 -0
- specify_cli/orchestrator/agents/__init__.py +170 -0
- specify_cli/orchestrator/agents/augment.py +112 -0
- specify_cli/orchestrator/agents/base.py +243 -0
- specify_cli/orchestrator/agents/claude.py +112 -0
- specify_cli/orchestrator/agents/codex.py +106 -0
- specify_cli/orchestrator/agents/copilot.py +137 -0
- specify_cli/orchestrator/agents/cursor.py +139 -0
- specify_cli/orchestrator/agents/gemini.py +115 -0
- specify_cli/orchestrator/agents/kilocode.py +94 -0
- specify_cli/orchestrator/agents/opencode.py +132 -0
- specify_cli/orchestrator/agents/qwen.py +96 -0
- specify_cli/orchestrator/config.py +455 -0
- specify_cli/orchestrator/executor.py +642 -0
- specify_cli/orchestrator/integration.py +1230 -0
- specify_cli/orchestrator/monitor.py +898 -0
- specify_cli/orchestrator/scheduler.py +832 -0
- specify_cli/orchestrator/state.py +508 -0
- specify_cli/orchestrator/testing/__init__.py +122 -0
- specify_cli/orchestrator/testing/availability.py +346 -0
- specify_cli/orchestrator/testing/fixtures.py +684 -0
- specify_cli/orchestrator/testing/paths.py +218 -0
- specify_cli/plan_validation.py +107 -0
- specify_cli/scripts/debug-dashboard-scan.py +61 -0
- specify_cli/scripts/tasks/acceptance_support.py +695 -0
- specify_cli/scripts/tasks/task_helpers.py +506 -0
- specify_cli/scripts/tasks/tasks_cli.py +848 -0
- specify_cli/scripts/validate_encoding.py +180 -0
- specify_cli/task_metadata_validation.py +274 -0
- specify_cli/tasks_support.py +447 -0
- specify_cli/template/__init__.py +47 -0
- specify_cli/template/asset_generator.py +206 -0
- specify_cli/template/github_client.py +334 -0
- specify_cli/template/manager.py +193 -0
- specify_cli/template/renderer.py +99 -0
- specify_cli/templates/AGENTS.md +190 -0
- specify_cli/templates/POWERSHELL_SYNTAX.md +229 -0
- specify_cli/templates/agent-file-template.md +35 -0
- specify_cli/templates/checklist-template.md +42 -0
- specify_cli/templates/claudeignore-template +58 -0
- specify_cli/templates/command-templates/accept.md +141 -0
- specify_cli/templates/command-templates/analyze.md +253 -0
- specify_cli/templates/command-templates/checklist.md +352 -0
- specify_cli/templates/command-templates/clarify.md +224 -0
- specify_cli/templates/command-templates/constitution.md +432 -0
- specify_cli/templates/command-templates/dashboard.md +175 -0
- specify_cli/templates/command-templates/implement.md +190 -0
- specify_cli/templates/command-templates/merge.md +374 -0
- specify_cli/templates/command-templates/plan.md +171 -0
- specify_cli/templates/command-templates/research.md +88 -0
- specify_cli/templates/command-templates/review.md +510 -0
- specify_cli/templates/command-templates/specify.md +321 -0
- specify_cli/templates/command-templates/status.md +92 -0
- specify_cli/templates/command-templates/tasks.md +199 -0
- specify_cli/templates/git-hooks/pre-commit +22 -0
- specify_cli/templates/git-hooks/pre-commit-agent-check +37 -0
- specify_cli/templates/git-hooks/pre-commit-encoding-check +142 -0
- specify_cli/templates/plan-template.md +108 -0
- specify_cli/templates/spec-template.md +118 -0
- specify_cli/templates/task-prompt-template.md +165 -0
- specify_cli/templates/tasks-template.md +161 -0
- specify_cli/templates/vscode-settings.json +13 -0
- specify_cli/text_sanitization.py +225 -0
- specify_cli/upgrade/__init__.py +18 -0
- specify_cli/upgrade/detector.py +239 -0
- specify_cli/upgrade/metadata.py +182 -0
- specify_cli/upgrade/migrations/__init__.py +65 -0
- specify_cli/upgrade/migrations/base.py +80 -0
- specify_cli/upgrade/migrations/m_0_10_0_python_only.py +359 -0
- specify_cli/upgrade/migrations/m_0_10_12_constitution_cleanup.py +99 -0
- specify_cli/upgrade/migrations/m_0_10_14_update_implement_slash_command.py +176 -0
- specify_cli/upgrade/migrations/m_0_10_1_populate_slash_commands.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_2_update_slash_commands.py +172 -0
- specify_cli/upgrade/migrations/m_0_10_6_workflow_simplification.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_8_fix_memory_structure.py +252 -0
- specify_cli/upgrade/migrations/m_0_10_9_repair_templates.py +168 -0
- specify_cli/upgrade/migrations/m_0_11_0_workspace_per_wp.py +182 -0
- specify_cli/upgrade/migrations/m_0_11_1_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_1_update_implement_slash_command.py +160 -0
- specify_cli/upgrade/migrations/m_0_11_2_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_3_workflow_agent_flag.py +114 -0
- specify_cli/upgrade/migrations/m_0_12_0_documentation_mission.py +155 -0
- specify_cli/upgrade/migrations/m_0_12_1_remove_kitty_specs_from_gitignore.py +183 -0
- specify_cli/upgrade/migrations/m_0_2_0_specify_to_kittify.py +80 -0
- specify_cli/upgrade/migrations/m_0_4_8_gitignore_agents.py +118 -0
- specify_cli/upgrade/migrations/m_0_5_0_encoding_hooks.py +141 -0
- specify_cli/upgrade/migrations/m_0_6_5_commands_rename.py +169 -0
- specify_cli/upgrade/migrations/m_0_6_7_ensure_missions.py +228 -0
- specify_cli/upgrade/migrations/m_0_7_2_worktree_commands_dedup.py +89 -0
- specify_cli/upgrade/migrations/m_0_7_3_update_scripts.py +114 -0
- specify_cli/upgrade/migrations/m_0_8_0_remove_active_mission.py +82 -0
- specify_cli/upgrade/migrations/m_0_8_0_worktree_agents_symlink.py +148 -0
- specify_cli/upgrade/migrations/m_0_9_0_frontmatter_only_lanes.py +346 -0
- specify_cli/upgrade/migrations/m_0_9_1_complete_lane_migration.py +656 -0
- specify_cli/upgrade/migrations/m_0_9_2_research_mission_templates.py +221 -0
- specify_cli/upgrade/registry.py +121 -0
- specify_cli/upgrade/runner.py +284 -0
- specify_cli/validators/__init__.py +14 -0
- specify_cli/validators/paths.py +154 -0
- specify_cli/validators/research.py +428 -0
- specify_cli/verify_enhanced.py +270 -0
- specify_cli/workspace_context.py +224 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"chat.promptFilesRecommendations": {
|
|
3
|
+
"spec-kitty.constitution": true,
|
|
4
|
+
"spec-kitty.specify": true,
|
|
5
|
+
"spec-kitty.plan": true,
|
|
6
|
+
"spec-kitty.tasks": true,
|
|
7
|
+
"spec-kitty.implement": true
|
|
8
|
+
},
|
|
9
|
+
"chat.tools.terminal.autoApprove": {
|
|
10
|
+
".kittify/scripts/bash/": true,
|
|
11
|
+
".kittify/scripts/ps/": true
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Text sanitization utilities for preventing encoding errors.
|
|
2
|
+
|
|
3
|
+
This module provides utilities to normalize Windows-1252 smart quotes and other
|
|
4
|
+
problematic characters that can cause UTF-8 encoding errors in markdown files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"sanitize_markdown_text",
|
|
15
|
+
"sanitize_file",
|
|
16
|
+
"detect_problematic_characters",
|
|
17
|
+
"PROBLEMATIC_CHARS",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# Map of Windows-1252 / problematic characters to safe UTF-8 replacements
|
|
21
|
+
PROBLEMATIC_CHARS = {
|
|
22
|
+
# Smart quotes (Windows-1252 bytes 0x91-0x94)
|
|
23
|
+
"\u2018": "'", # LEFT SINGLE QUOTATION MARK → apostrophe
|
|
24
|
+
"\u2019": "'", # RIGHT SINGLE QUOTATION MARK → apostrophe
|
|
25
|
+
"\u201c": '"', # LEFT DOUBLE QUOTATION MARK → straight quote
|
|
26
|
+
"\u201d": '"', # RIGHT DOUBLE QUOTATION MARK → straight quote
|
|
27
|
+
# Em/en dashes
|
|
28
|
+
"\u2013": "--", # EN DASH → double hyphen
|
|
29
|
+
"\u2014": "---", # EM DASH → triple hyphen
|
|
30
|
+
# Mathematical operators that may come from cp1252
|
|
31
|
+
"\u00b1": "+/-", # PLUS-MINUS SIGN → +/-
|
|
32
|
+
"\u00d7": "x", # MULTIPLICATION SIGN → x
|
|
33
|
+
"\u00f7": "/", # DIVISION SIGN → /
|
|
34
|
+
# Ellipsis
|
|
35
|
+
"\u2026": "...", # HORIZONTAL ELLIPSIS → three periods
|
|
36
|
+
# Bullets
|
|
37
|
+
"\u2022": "*", # BULLET → asterisk
|
|
38
|
+
"\u2023": ">", # TRIANGULAR BULLET → greater than
|
|
39
|
+
# Degree symbol (often problematic)
|
|
40
|
+
"\u00b0": " degrees", # DEGREE SIGN → " degrees"
|
|
41
|
+
# Non-breaking space (invisible but causes issues)
|
|
42
|
+
"\u00a0": " ", # NO-BREAK SPACE → regular space
|
|
43
|
+
# Trademark/copyright symbols
|
|
44
|
+
"\u2122": "(TM)", # TRADE MARK SIGN
|
|
45
|
+
"\u00a9": "(C)", # COPYRIGHT SIGN
|
|
46
|
+
"\u00ae": "(R)", # REGISTERED SIGN
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Compile regex for detecting any problematic character
|
|
50
|
+
_PROBLEMATIC_PATTERN = re.compile(
|
|
51
|
+
"[" + "".join(re.escape(char) for char in PROBLEMATIC_CHARS.keys()) + "]"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def sanitize_markdown_text(text: str, *, preserve_utf8: bool = False) -> str:
|
|
56
|
+
"""Sanitize markdown text by replacing problematic characters.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
text: The markdown text to sanitize
|
|
60
|
+
preserve_utf8: If True, only replace characters that cause encoding issues.
|
|
61
|
+
If False (default), replace all problematic characters for
|
|
62
|
+
maximum compatibility.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Sanitized text with problematic characters replaced
|
|
66
|
+
|
|
67
|
+
Examples:
|
|
68
|
+
>>> sanitize_markdown_text("User's "favorite" feature")
|
|
69
|
+
'User\\'s "favorite" feature'
|
|
70
|
+
|
|
71
|
+
>>> sanitize_markdown_text("Price: $100 ± $10")
|
|
72
|
+
'Price: $100 +/- $10'
|
|
73
|
+
|
|
74
|
+
>>> sanitize_markdown_text("Temperature: 72° outside")
|
|
75
|
+
'Temperature: 72 degrees outside'
|
|
76
|
+
"""
|
|
77
|
+
if not text:
|
|
78
|
+
return text
|
|
79
|
+
|
|
80
|
+
# Replace each problematic character with its safe equivalent
|
|
81
|
+
result = text
|
|
82
|
+
for problematic, replacement in PROBLEMATIC_CHARS.items():
|
|
83
|
+
if problematic in result:
|
|
84
|
+
result = result.replace(problematic, replacement)
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def detect_problematic_characters(
|
|
90
|
+
text: str,
|
|
91
|
+
) -> list[tuple[int, int, str, str]]:
|
|
92
|
+
"""Detect problematic characters in text and return their locations.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
text: The text to check
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
List of tuples: (line_number, column, character, suggested_replacement)
|
|
99
|
+
Line numbers are 1-indexed, columns are 0-indexed.
|
|
100
|
+
|
|
101
|
+
Examples:
|
|
102
|
+
>>> text = "Line 1\\nUser's "test"\\nLine 3"
|
|
103
|
+
>>> issues = detect_problematic_characters(text)
|
|
104
|
+
>>> len(issues)
|
|
105
|
+
3
|
|
106
|
+
>>> issues[0]
|
|
107
|
+
(2, 4, '\u2019', "'")
|
|
108
|
+
"""
|
|
109
|
+
issues: list[tuple[int, int, str, str]] = []
|
|
110
|
+
|
|
111
|
+
lines = text.splitlines(keepends=True)
|
|
112
|
+
for line_num, line in enumerate(lines, start=1):
|
|
113
|
+
for match in _PROBLEMATIC_PATTERN.finditer(line):
|
|
114
|
+
char = match.group(0)
|
|
115
|
+
replacement = PROBLEMATIC_CHARS.get(char, "?")
|
|
116
|
+
issues.append((line_num, match.start(), char, replacement))
|
|
117
|
+
|
|
118
|
+
return issues
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def sanitize_file(
|
|
122
|
+
file_path: Path,
|
|
123
|
+
*,
|
|
124
|
+
backup: bool = True,
|
|
125
|
+
dry_run: bool = False,
|
|
126
|
+
) -> tuple[bool, Optional[str]]:
|
|
127
|
+
"""Sanitize a markdown file in place.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
file_path: Path to the markdown file to sanitize
|
|
131
|
+
backup: If True, create a .bak file before modifying
|
|
132
|
+
dry_run: If True, only check and report, don't modify
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Tuple of (was_modified, error_message)
|
|
136
|
+
- was_modified: True if the file had problematic characters
|
|
137
|
+
- error_message: None if successful, error message if failed
|
|
138
|
+
|
|
139
|
+
Examples:
|
|
140
|
+
>>> from pathlib import Path
|
|
141
|
+
>>> from tempfile import NamedTemporaryFile
|
|
142
|
+
>>> with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
|
143
|
+
... f.write('User's "test"')
|
|
144
|
+
... tmp_path = Path(f.name)
|
|
145
|
+
>>> modified, error = sanitize_file(tmp_path, backup=False)
|
|
146
|
+
>>> modified
|
|
147
|
+
True
|
|
148
|
+
>>> tmp_path.read_text()
|
|
149
|
+
'User\\'s "test"'
|
|
150
|
+
>>> tmp_path.unlink() # cleanup
|
|
151
|
+
"""
|
|
152
|
+
if not file_path.exists():
|
|
153
|
+
return False, f"File not found: {file_path}"
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
# Try reading as UTF-8 first
|
|
157
|
+
try:
|
|
158
|
+
original_text = file_path.read_text(encoding="utf-8-sig")
|
|
159
|
+
encoding_issue = False
|
|
160
|
+
except UnicodeDecodeError:
|
|
161
|
+
# Fall back to cp1252 or latin-1
|
|
162
|
+
encoding_issue = True
|
|
163
|
+
original_bytes = file_path.read_bytes()
|
|
164
|
+
for encoding in ("cp1252", "latin-1"):
|
|
165
|
+
try:
|
|
166
|
+
original_text = original_bytes.decode(encoding)
|
|
167
|
+
break
|
|
168
|
+
except UnicodeDecodeError:
|
|
169
|
+
continue
|
|
170
|
+
else:
|
|
171
|
+
# Last resort: replace invalid characters
|
|
172
|
+
original_text = original_bytes.decode("utf-8", errors="replace")
|
|
173
|
+
|
|
174
|
+
# Strip UTF-8 BOM if present in the text
|
|
175
|
+
original_text = original_text.lstrip('\ufeff')
|
|
176
|
+
|
|
177
|
+
# Sanitize the text
|
|
178
|
+
sanitized_text = sanitize_markdown_text(original_text)
|
|
179
|
+
|
|
180
|
+
# Check if any changes were made
|
|
181
|
+
if sanitized_text == original_text and not encoding_issue:
|
|
182
|
+
return False, None # No changes needed
|
|
183
|
+
|
|
184
|
+
if dry_run:
|
|
185
|
+
return True, None # Would modify but dry run
|
|
186
|
+
|
|
187
|
+
# Create backup if requested
|
|
188
|
+
if backup:
|
|
189
|
+
backup_path = file_path.with_suffix(file_path.suffix + ".bak")
|
|
190
|
+
backup_path.write_bytes(file_path.read_bytes())
|
|
191
|
+
|
|
192
|
+
# Write sanitized content
|
|
193
|
+
file_path.write_text(sanitized_text, encoding="utf-8")
|
|
194
|
+
return True, None
|
|
195
|
+
|
|
196
|
+
except Exception as exc:
|
|
197
|
+
return False, f"Error sanitizing {file_path}: {exc}"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def sanitize_directory(
|
|
201
|
+
directory: Path,
|
|
202
|
+
*,
|
|
203
|
+
pattern: str = "**/*.md",
|
|
204
|
+
backup: bool = False,
|
|
205
|
+
dry_run: bool = False,
|
|
206
|
+
) -> dict[str, tuple[bool, Optional[str]]]:
|
|
207
|
+
"""Sanitize all markdown files in a directory.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
directory: Directory to scan
|
|
211
|
+
pattern: Glob pattern for files to sanitize (default: **/*.md)
|
|
212
|
+
backup: If True, create .bak files before modifying
|
|
213
|
+
dry_run: If True, only check and report, don't modify
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Dictionary mapping file paths to (was_modified, error_message) tuples
|
|
217
|
+
"""
|
|
218
|
+
results: dict[str, tuple[bool, Optional[str]]] = {}
|
|
219
|
+
|
|
220
|
+
for file_path in directory.glob(pattern):
|
|
221
|
+
if file_path.is_file():
|
|
222
|
+
result = sanitize_file(file_path, backup=backup, dry_run=dry_run)
|
|
223
|
+
results[str(file_path)] = result
|
|
224
|
+
|
|
225
|
+
return results
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Spec Kitty upgrade system for migrating projects between versions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .metadata import MigrationRecord, ProjectMetadata
|
|
6
|
+
from .detector import VersionDetector
|
|
7
|
+
from .registry import MigrationRegistry, register
|
|
8
|
+
from .runner import MigrationRunner, UpgradeResult
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"MigrationRecord",
|
|
12
|
+
"ProjectMetadata",
|
|
13
|
+
"VersionDetector",
|
|
14
|
+
"MigrationRegistry",
|
|
15
|
+
"MigrationRunner",
|
|
16
|
+
"UpgradeResult",
|
|
17
|
+
"register",
|
|
18
|
+
]
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Version detection for Spec Kitty projects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
from .metadata import ProjectMetadata
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VersionDetector:
|
|
12
|
+
"""Detects project version through heuristics when metadata is missing."""
|
|
13
|
+
|
|
14
|
+
# Agent directories that should be in .gitignore (v0.4.8+)
|
|
15
|
+
EXPECTED_AGENTS = [
|
|
16
|
+
".claude/",
|
|
17
|
+
".codex/",
|
|
18
|
+
".opencode/",
|
|
19
|
+
".windsurf/",
|
|
20
|
+
".gemini/",
|
|
21
|
+
".cursor/",
|
|
22
|
+
".qwen/",
|
|
23
|
+
".kilocode/",
|
|
24
|
+
".augment/",
|
|
25
|
+
".roo/",
|
|
26
|
+
".amazonq/",
|
|
27
|
+
".github/copilot/",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
def __init__(self, project_path: Path):
|
|
31
|
+
"""Initialize the detector.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
project_path: Root of the project
|
|
35
|
+
"""
|
|
36
|
+
self.project_path = project_path
|
|
37
|
+
self.kittify_dir = project_path / ".kittify"
|
|
38
|
+
self.specify_dir = project_path / ".specify" # Old name
|
|
39
|
+
|
|
40
|
+
def detect_version(self) -> str:
|
|
41
|
+
"""Detect the approximate version of a project.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
A version string like "0.1.0" (oldest detectable)
|
|
45
|
+
or "0.6.7" (current).
|
|
46
|
+
"""
|
|
47
|
+
# First try to load from metadata
|
|
48
|
+
if self.kittify_dir.exists():
|
|
49
|
+
metadata = ProjectMetadata.load(self.kittify_dir)
|
|
50
|
+
if metadata:
|
|
51
|
+
return metadata.version
|
|
52
|
+
|
|
53
|
+
# Heuristic detection based on directory structure
|
|
54
|
+
return self._detect_from_structure()
|
|
55
|
+
|
|
56
|
+
def _detect_from_structure(self) -> str:
|
|
57
|
+
"""Detect version from project structure."""
|
|
58
|
+
# v0.1.x: Uses .specify/ directory and /specs/
|
|
59
|
+
if self.specify_dir.exists():
|
|
60
|
+
return "0.1.0"
|
|
61
|
+
|
|
62
|
+
# No .kittify at all - not initialized or very old
|
|
63
|
+
if not self.kittify_dir.exists():
|
|
64
|
+
return "0.0.0"
|
|
65
|
+
|
|
66
|
+
# Check for command-templates vs commands directory
|
|
67
|
+
# v0.6.5+ uses command-templates/
|
|
68
|
+
templates_dir = self.kittify_dir / "templates"
|
|
69
|
+
missions_dir = self.kittify_dir / "missions"
|
|
70
|
+
|
|
71
|
+
# Check templates location
|
|
72
|
+
has_command_templates = (templates_dir / "command-templates").exists()
|
|
73
|
+
has_old_commands = (templates_dir / "commands").exists()
|
|
74
|
+
|
|
75
|
+
# Check missions for command-templates
|
|
76
|
+
has_mission_command_templates = False
|
|
77
|
+
has_mission_commands = False
|
|
78
|
+
if missions_dir.exists():
|
|
79
|
+
for mission in missions_dir.iterdir():
|
|
80
|
+
if mission.is_dir():
|
|
81
|
+
if (mission / "command-templates").exists():
|
|
82
|
+
has_mission_command_templates = True
|
|
83
|
+
if (mission / "commands").exists():
|
|
84
|
+
has_mission_commands = True
|
|
85
|
+
|
|
86
|
+
# v0.6.5+: Has command-templates (not commands)
|
|
87
|
+
if has_command_templates or has_mission_command_templates:
|
|
88
|
+
if not has_old_commands and not has_mission_commands:
|
|
89
|
+
return "0.6.5"
|
|
90
|
+
|
|
91
|
+
# v0.6.4 and earlier: Has old commands/ directory
|
|
92
|
+
if has_old_commands or has_mission_commands:
|
|
93
|
+
return "0.6.4"
|
|
94
|
+
|
|
95
|
+
# Check for git hooks (v0.5.0+)
|
|
96
|
+
git_hooks = self.project_path / ".git" / "hooks"
|
|
97
|
+
if git_hooks.exists() and (git_hooks / "pre-commit").exists():
|
|
98
|
+
try:
|
|
99
|
+
hook_content = (git_hooks / "pre-commit").read_text(
|
|
100
|
+
encoding="utf-8", errors="ignore"
|
|
101
|
+
)
|
|
102
|
+
if "spec-kitty" in hook_content.lower() or "encoding" in hook_content.lower():
|
|
103
|
+
return "0.5.0"
|
|
104
|
+
except OSError:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
# Check .gitignore for agent directories (v0.4.8+)
|
|
108
|
+
gitignore = self.project_path / ".gitignore"
|
|
109
|
+
if gitignore.exists():
|
|
110
|
+
try:
|
|
111
|
+
content = gitignore.read_text(encoding="utf-8-sig", errors="ignore")
|
|
112
|
+
agent_dirs = [".claude/", ".codex/", ".gemini/", ".cursor/"]
|
|
113
|
+
agent_count = sum(1 for d in agent_dirs if d in content)
|
|
114
|
+
if agent_count >= 4:
|
|
115
|
+
return "0.4.8"
|
|
116
|
+
except OSError:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
# Check for missions directory (v0.2.0+)
|
|
120
|
+
if missions_dir.exists():
|
|
121
|
+
return "0.2.0"
|
|
122
|
+
|
|
123
|
+
# Default to oldest .kittify-based version
|
|
124
|
+
return "0.2.0"
|
|
125
|
+
|
|
126
|
+
def get_needed_migrations(self, target_version: str) -> List[str]:
|
|
127
|
+
"""Get list of migration IDs needed to reach target version.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
target_version: Version to upgrade to
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
List of migration IDs in order
|
|
134
|
+
"""
|
|
135
|
+
from .registry import MigrationRegistry
|
|
136
|
+
|
|
137
|
+
current = self.detect_version()
|
|
138
|
+
migrations = MigrationRegistry.get_applicable(current, target_version)
|
|
139
|
+
return [m.migration_id for m in migrations]
|
|
140
|
+
|
|
141
|
+
def has_old_commands_structure(self) -> bool:
|
|
142
|
+
"""Check if the project uses old commands/ directories.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
True if old commands/ directories exist
|
|
146
|
+
"""
|
|
147
|
+
templates_commands = self.kittify_dir / "templates" / "commands"
|
|
148
|
+
if templates_commands.exists():
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
missions_dir = self.kittify_dir / "missions"
|
|
152
|
+
if missions_dir.exists():
|
|
153
|
+
for mission in missions_dir.iterdir():
|
|
154
|
+
if mission.is_dir() and (mission / "commands").exists():
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
def has_old_specify_structure(self) -> bool:
|
|
160
|
+
"""Check if the project uses old .specify/ structure.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
True if .specify/ directory exists
|
|
164
|
+
"""
|
|
165
|
+
return self.specify_dir.exists()
|
|
166
|
+
|
|
167
|
+
def count_missing_agent_gitignore_entries(self) -> int:
|
|
168
|
+
"""Count how many agent directories are missing from .gitignore.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Number of missing entries
|
|
172
|
+
"""
|
|
173
|
+
gitignore = self.project_path / ".gitignore"
|
|
174
|
+
if not gitignore.exists():
|
|
175
|
+
return len(self.EXPECTED_AGENTS)
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
content = gitignore.read_text(encoding="utf-8-sig", errors="ignore")
|
|
179
|
+
except OSError:
|
|
180
|
+
return len(self.EXPECTED_AGENTS)
|
|
181
|
+
|
|
182
|
+
missing = [d for d in self.EXPECTED_AGENTS if d not in content]
|
|
183
|
+
return len(missing)
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def detect_broken_mission_system(cls, project_path: Path) -> bool:
|
|
187
|
+
"""Detect if the mission system has corrupted files.
|
|
188
|
+
|
|
189
|
+
Checks for:
|
|
190
|
+
1. Missing mission.yaml files in mission directories
|
|
191
|
+
2. Invalid YAML syntax in mission.yaml files
|
|
192
|
+
3. Missing required fields (name)
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
project_path: Path to the project root
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
True if mission system is broken/corrupted, False if healthy
|
|
199
|
+
"""
|
|
200
|
+
import yaml
|
|
201
|
+
|
|
202
|
+
missions_dir = project_path / ".kittify" / "missions"
|
|
203
|
+
|
|
204
|
+
# No missions directory at all is broken
|
|
205
|
+
if not missions_dir.exists():
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
# Check each mission directory
|
|
209
|
+
has_any_mission = False
|
|
210
|
+
for mission_dir in missions_dir.iterdir():
|
|
211
|
+
if not mission_dir.is_dir():
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
has_any_mission = True
|
|
215
|
+
mission_yaml = mission_dir / "mission.yaml"
|
|
216
|
+
|
|
217
|
+
# Check if mission.yaml exists
|
|
218
|
+
if not mission_yaml.exists():
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
# Check if mission.yaml is valid YAML with required fields
|
|
222
|
+
try:
|
|
223
|
+
with open(mission_yaml, encoding="utf-8") as f:
|
|
224
|
+
data = yaml.safe_load(f)
|
|
225
|
+
|
|
226
|
+
# Check required fields
|
|
227
|
+
if not data or "name" not in data:
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
except yaml.YAMLError:
|
|
231
|
+
return True
|
|
232
|
+
except OSError:
|
|
233
|
+
return True
|
|
234
|
+
|
|
235
|
+
# If no mission directories found, that's broken
|
|
236
|
+
if not has_any_mission:
|
|
237
|
+
return True
|
|
238
|
+
|
|
239
|
+
return False
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Project metadata management for Spec Kitty upgrade system."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class MigrationRecord:
|
|
15
|
+
"""Record of a single migration application."""
|
|
16
|
+
|
|
17
|
+
id: str
|
|
18
|
+
applied_at: datetime
|
|
19
|
+
result: str # "success", "skipped", "failed"
|
|
20
|
+
notes: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ProjectMetadata:
|
|
25
|
+
"""Metadata for a Spec Kitty project stored in .kittify/metadata.yaml."""
|
|
26
|
+
|
|
27
|
+
version: str
|
|
28
|
+
initialized_at: datetime
|
|
29
|
+
last_upgraded_at: Optional[datetime] = None
|
|
30
|
+
python_version: str = ""
|
|
31
|
+
platform: str = ""
|
|
32
|
+
platform_version: str = ""
|
|
33
|
+
applied_migrations: List[MigrationRecord] = field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def load(cls, kittify_dir: Path) -> Optional["ProjectMetadata"]:
|
|
37
|
+
"""Load metadata from .kittify/metadata.yaml.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
kittify_dir: Path to the .kittify directory
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
ProjectMetadata if file exists, None otherwise
|
|
44
|
+
"""
|
|
45
|
+
metadata_path = kittify_dir / "metadata.yaml"
|
|
46
|
+
if not metadata_path.exists():
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
with open(metadata_path, "r", encoding="utf-8-sig") as f:
|
|
51
|
+
data = yaml.safe_load(f)
|
|
52
|
+
except (OSError, yaml.YAMLError):
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
if not data:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
spec_kitty = data.get("spec_kitty", {})
|
|
59
|
+
env = data.get("environment", {})
|
|
60
|
+
migrations_data = data.get("migrations", {}).get("applied", [])
|
|
61
|
+
|
|
62
|
+
applied = []
|
|
63
|
+
for m in migrations_data:
|
|
64
|
+
try:
|
|
65
|
+
applied.append(
|
|
66
|
+
MigrationRecord(
|
|
67
|
+
id=m["id"],
|
|
68
|
+
applied_at=datetime.fromisoformat(m["applied_at"]),
|
|
69
|
+
result=m["result"],
|
|
70
|
+
notes=m.get("notes"),
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
except (KeyError, ValueError):
|
|
74
|
+
# Skip malformed migration records
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
initialized_at_str = spec_kitty.get("initialized_at")
|
|
78
|
+
try:
|
|
79
|
+
initialized_at = (
|
|
80
|
+
datetime.fromisoformat(initialized_at_str)
|
|
81
|
+
if initialized_at_str
|
|
82
|
+
else datetime.now()
|
|
83
|
+
)
|
|
84
|
+
except ValueError:
|
|
85
|
+
initialized_at = datetime.now()
|
|
86
|
+
|
|
87
|
+
last_upgraded_str = spec_kitty.get("last_upgraded_at")
|
|
88
|
+
try:
|
|
89
|
+
last_upgraded_at = (
|
|
90
|
+
datetime.fromisoformat(last_upgraded_str) if last_upgraded_str else None
|
|
91
|
+
)
|
|
92
|
+
except ValueError:
|
|
93
|
+
last_upgraded_at = None
|
|
94
|
+
|
|
95
|
+
return cls(
|
|
96
|
+
version=spec_kitty.get("version", "unknown"),
|
|
97
|
+
initialized_at=initialized_at,
|
|
98
|
+
last_upgraded_at=last_upgraded_at,
|
|
99
|
+
python_version=env.get("python_version", ""),
|
|
100
|
+
platform=env.get("platform", ""),
|
|
101
|
+
platform_version=env.get("platform_version", ""),
|
|
102
|
+
applied_migrations=applied,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def save(self, kittify_dir: Path) -> None:
|
|
106
|
+
"""Save metadata to .kittify/metadata.yaml.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
kittify_dir: Path to the .kittify directory
|
|
110
|
+
"""
|
|
111
|
+
metadata_path = kittify_dir / "metadata.yaml"
|
|
112
|
+
kittify_dir.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
|
|
114
|
+
data = {
|
|
115
|
+
"spec_kitty": {
|
|
116
|
+
"version": self.version,
|
|
117
|
+
"initialized_at": self.initialized_at.isoformat(),
|
|
118
|
+
"last_upgraded_at": (
|
|
119
|
+
self.last_upgraded_at.isoformat() if self.last_upgraded_at else None
|
|
120
|
+
),
|
|
121
|
+
},
|
|
122
|
+
"environment": {
|
|
123
|
+
"python_version": self.python_version,
|
|
124
|
+
"platform": self.platform,
|
|
125
|
+
"platform_version": self.platform_version,
|
|
126
|
+
},
|
|
127
|
+
"migrations": {
|
|
128
|
+
"applied": [
|
|
129
|
+
{
|
|
130
|
+
"id": m.id,
|
|
131
|
+
"applied_at": m.applied_at.isoformat(),
|
|
132
|
+
"result": m.result,
|
|
133
|
+
"notes": m.notes,
|
|
134
|
+
}
|
|
135
|
+
for m in self.applied_migrations
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Add header comment
|
|
141
|
+
header = (
|
|
142
|
+
"# Spec Kitty Project Metadata\n"
|
|
143
|
+
"# Auto-generated by spec-kitty init/upgrade\n"
|
|
144
|
+
"# DO NOT EDIT MANUALLY\n\n"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
with open(metadata_path, "w", encoding="utf-8") as f:
|
|
148
|
+
f.write(header)
|
|
149
|
+
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
|
150
|
+
|
|
151
|
+
def has_migration(self, migration_id: str) -> bool:
|
|
152
|
+
"""Check if a migration has been successfully applied.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
migration_id: The ID of the migration to check
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
True if migration was applied successfully
|
|
159
|
+
"""
|
|
160
|
+
return any(
|
|
161
|
+
m.id == migration_id and m.result == "success"
|
|
162
|
+
for m in self.applied_migrations
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def record_migration(
|
|
166
|
+
self, migration_id: str, result: str, notes: Optional[str] = None
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Record a migration application.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
migration_id: The ID of the migration
|
|
172
|
+
result: The result ("success", "skipped", "failed")
|
|
173
|
+
notes: Optional notes about the migration
|
|
174
|
+
"""
|
|
175
|
+
self.applied_migrations.append(
|
|
176
|
+
MigrationRecord(
|
|
177
|
+
id=migration_id,
|
|
178
|
+
applied_at=datetime.now(),
|
|
179
|
+
result=result,
|
|
180
|
+
notes=notes,
|
|
181
|
+
)
|
|
182
|
+
)
|