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,249 @@
|
|
|
1
|
+
"""Upgrade command implementation for Spec Kitty CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from specify_cli.cli.helpers import console, show_banner
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def upgrade(
|
|
17
|
+
dry_run: bool = typer.Option(
|
|
18
|
+
False, "--dry-run", help="Preview changes without applying"
|
|
19
|
+
),
|
|
20
|
+
force: bool = typer.Option(False, "--force", help="Skip confirmation prompts"),
|
|
21
|
+
target: Optional[str] = typer.Option(
|
|
22
|
+
None, "--target", help="Target version (defaults to current CLI version)"
|
|
23
|
+
),
|
|
24
|
+
json_output: bool = typer.Option(False, "--json", help="Output results as JSON"),
|
|
25
|
+
verbose: bool = typer.Option(
|
|
26
|
+
False, "--verbose", "-v", help="Show detailed migration information"
|
|
27
|
+
),
|
|
28
|
+
no_worktrees: bool = typer.Option(
|
|
29
|
+
False, "--no-worktrees", help="Skip upgrading worktrees"
|
|
30
|
+
),
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Upgrade a Spec Kitty project to the current version.
|
|
33
|
+
|
|
34
|
+
Detects the project's current version and applies all necessary migrations
|
|
35
|
+
to bring it up to date with the installed CLI version.
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
spec-kitty upgrade # Upgrade to current version
|
|
39
|
+
spec-kitty upgrade --dry-run # Preview changes
|
|
40
|
+
spec-kitty upgrade --target 0.6.5 # Upgrade to specific version
|
|
41
|
+
"""
|
|
42
|
+
if not json_output:
|
|
43
|
+
show_banner()
|
|
44
|
+
|
|
45
|
+
project_path = Path.cwd()
|
|
46
|
+
kittify_dir = project_path / ".kittify"
|
|
47
|
+
specify_dir = project_path / ".specify" # Old name
|
|
48
|
+
|
|
49
|
+
# Check if this is a Spec Kitty project
|
|
50
|
+
if not kittify_dir.exists() and not specify_dir.exists():
|
|
51
|
+
if json_output:
|
|
52
|
+
console.print(json.dumps({"error": "Not a Spec Kitty project"}))
|
|
53
|
+
else:
|
|
54
|
+
console.print("[red]Error:[/red] Not a Spec Kitty project.")
|
|
55
|
+
console.print(
|
|
56
|
+
"[dim]Run 'spec-kitty init' to initialize a project.[/dim]"
|
|
57
|
+
)
|
|
58
|
+
raise typer.Exit(1)
|
|
59
|
+
|
|
60
|
+
# Import upgrade system (lazy to avoid circular imports)
|
|
61
|
+
from specify_cli.upgrade.detector import VersionDetector
|
|
62
|
+
from specify_cli.upgrade.registry import MigrationRegistry
|
|
63
|
+
from specify_cli.upgrade.runner import MigrationRunner
|
|
64
|
+
|
|
65
|
+
# Import migrations to register them
|
|
66
|
+
from specify_cli.upgrade import migrations # noqa: F401
|
|
67
|
+
|
|
68
|
+
# Detect current version
|
|
69
|
+
detector = VersionDetector(project_path)
|
|
70
|
+
current_version = detector.detect_version()
|
|
71
|
+
|
|
72
|
+
# Determine target version
|
|
73
|
+
if target is None:
|
|
74
|
+
from specify_cli import __version__
|
|
75
|
+
|
|
76
|
+
target_version = __version__
|
|
77
|
+
else:
|
|
78
|
+
target_version = target
|
|
79
|
+
|
|
80
|
+
if not json_output:
|
|
81
|
+
console.print(f"[cyan]Current version:[/cyan] {current_version}")
|
|
82
|
+
console.print(f"[cyan]Target version:[/cyan] {target_version}")
|
|
83
|
+
console.print()
|
|
84
|
+
|
|
85
|
+
# Get needed migrations
|
|
86
|
+
# Handle "unknown" version by treating it as very old (0.0.0)
|
|
87
|
+
version_for_migration = "0.0.0" if current_version == "unknown" else current_version
|
|
88
|
+
migrations_needed = MigrationRegistry.get_applicable(version_for_migration, target_version, project_path=project_path)
|
|
89
|
+
|
|
90
|
+
if not migrations_needed:
|
|
91
|
+
if json_output:
|
|
92
|
+
console.print(
|
|
93
|
+
json.dumps(
|
|
94
|
+
{
|
|
95
|
+
"status": "up_to_date",
|
|
96
|
+
"current_version": current_version,
|
|
97
|
+
"target_version": target_version,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
console.print("[green]Project is already up to date![/green]")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
# Show migration plan
|
|
106
|
+
if not json_output:
|
|
107
|
+
table = Table(
|
|
108
|
+
title="Migration Plan", show_lines=False, header_style="bold cyan"
|
|
109
|
+
)
|
|
110
|
+
table.add_column("Migration", style="bright_white")
|
|
111
|
+
table.add_column("Description", style="dim")
|
|
112
|
+
table.add_column("Target", style="cyan")
|
|
113
|
+
|
|
114
|
+
for migration in migrations_needed:
|
|
115
|
+
table.add_row(
|
|
116
|
+
migration.migration_id,
|
|
117
|
+
migration.description,
|
|
118
|
+
migration.target_version,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
console.print(table)
|
|
122
|
+
console.print()
|
|
123
|
+
|
|
124
|
+
if verbose:
|
|
125
|
+
# Show detection results
|
|
126
|
+
console.print("[dim]Detection results:[/dim]")
|
|
127
|
+
for migration in migrations_needed:
|
|
128
|
+
detected = migration.detect(project_path)
|
|
129
|
+
can_apply, reason = migration.can_apply(project_path)
|
|
130
|
+
status = "[green]ready[/green]" if detected and can_apply else "[yellow]skipped[/yellow]"
|
|
131
|
+
console.print(f" {migration.migration_id}: {status}")
|
|
132
|
+
if not can_apply and reason:
|
|
133
|
+
console.print(f" [dim]{reason}[/dim]")
|
|
134
|
+
console.print()
|
|
135
|
+
|
|
136
|
+
# Confirm if not dry-run and not forced
|
|
137
|
+
if not dry_run and not force:
|
|
138
|
+
proceed = typer.confirm(
|
|
139
|
+
f"Apply {len(migrations_needed)} migration(s)?",
|
|
140
|
+
default=True,
|
|
141
|
+
)
|
|
142
|
+
if not proceed:
|
|
143
|
+
console.print("[yellow]Upgrade cancelled.[/yellow]")
|
|
144
|
+
raise typer.Exit(0)
|
|
145
|
+
|
|
146
|
+
# Run migrations
|
|
147
|
+
runner = MigrationRunner(project_path, console)
|
|
148
|
+
result = runner.upgrade(
|
|
149
|
+
target_version,
|
|
150
|
+
dry_run=dry_run,
|
|
151
|
+
force=force,
|
|
152
|
+
include_worktrees=not no_worktrees,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if json_output:
|
|
156
|
+
# Build detailed migrations array
|
|
157
|
+
migrations_detail = []
|
|
158
|
+
for migration in migrations_needed:
|
|
159
|
+
status = "applied" if migration.migration_id in result.migrations_applied else (
|
|
160
|
+
"skipped" if migration.migration_id in result.migrations_skipped else "pending"
|
|
161
|
+
)
|
|
162
|
+
migrations_detail.append({
|
|
163
|
+
"id": migration.migration_id,
|
|
164
|
+
"description": migration.description,
|
|
165
|
+
"target_version": migration.target_version,
|
|
166
|
+
"status": status,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
output = {
|
|
170
|
+
"status": "success" if result.success else "failed",
|
|
171
|
+
"current_version": result.from_version,
|
|
172
|
+
"target_version": result.to_version,
|
|
173
|
+
"dry_run": result.dry_run,
|
|
174
|
+
"migrations": migrations_detail,
|
|
175
|
+
"migrations_applied": result.migrations_applied,
|
|
176
|
+
"migrations_skipped": result.migrations_skipped,
|
|
177
|
+
"success": result.success,
|
|
178
|
+
"errors": result.errors,
|
|
179
|
+
"warnings": result.warnings,
|
|
180
|
+
}
|
|
181
|
+
console.print(json.dumps(output, indent=2))
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
# Display results
|
|
185
|
+
console.print()
|
|
186
|
+
|
|
187
|
+
if result.dry_run:
|
|
188
|
+
console.print(
|
|
189
|
+
Panel(
|
|
190
|
+
"[yellow]DRY RUN[/yellow] - No changes were made",
|
|
191
|
+
border_style="yellow",
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if result.migrations_applied:
|
|
196
|
+
console.print("[green]Migrations applied:[/green]")
|
|
197
|
+
for m in result.migrations_applied:
|
|
198
|
+
console.print(f" [green]✓[/green] {m}")
|
|
199
|
+
|
|
200
|
+
if result.migrations_skipped:
|
|
201
|
+
console.print("[dim]Migrations skipped (already applied or not needed):[/dim]")
|
|
202
|
+
for m in result.migrations_skipped:
|
|
203
|
+
console.print(f" [dim]○[/dim] {m}")
|
|
204
|
+
|
|
205
|
+
if result.warnings:
|
|
206
|
+
console.print("[yellow]Warnings:[/yellow]")
|
|
207
|
+
for w in result.warnings:
|
|
208
|
+
console.print(f" [yellow]![/yellow] {w}")
|
|
209
|
+
|
|
210
|
+
if result.errors:
|
|
211
|
+
console.print("[red]Errors:[/red]")
|
|
212
|
+
for e in result.errors:
|
|
213
|
+
console.print(f" [red]✗[/red] {e}")
|
|
214
|
+
|
|
215
|
+
console.print()
|
|
216
|
+
if result.success:
|
|
217
|
+
console.print(
|
|
218
|
+
f"[bold green]Upgrade complete![/bold green] {result.from_version} -> {result.to_version}"
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
console.print("[bold red]Upgrade failed.[/bold red]")
|
|
222
|
+
raise typer.Exit(1)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def list_legacy_features() -> None:
|
|
226
|
+
"""List legacy worktrees blocking 0.11.0 upgrade."""
|
|
227
|
+
from specify_cli.tasks_support import find_repo_root
|
|
228
|
+
from specify_cli.upgrade.migrations.m_0_11_0_workspace_per_wp import (
|
|
229
|
+
detect_legacy_worktrees,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
repo_root = find_repo_root()
|
|
233
|
+
legacy = detect_legacy_worktrees(repo_root)
|
|
234
|
+
|
|
235
|
+
if not legacy:
|
|
236
|
+
console.print("[green]✓[/green] No legacy worktrees found")
|
|
237
|
+
console.print("Project is ready for 0.11.0 upgrade")
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
console.print(f"[yellow]Legacy worktrees found:[/yellow] {len(legacy)}\n")
|
|
241
|
+
for worktree in legacy:
|
|
242
|
+
console.print(f" - {worktree.name}")
|
|
243
|
+
|
|
244
|
+
console.print("\n[cyan]Action required:[/cyan]")
|
|
245
|
+
console.print(" Complete: spec-kitty merge <feature>")
|
|
246
|
+
console.print(" OR Delete: git worktree remove .worktrees/<feature>")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
__all__ = ["upgrade", "list_legacy_features"]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Encoding validation command for Spec Kitty CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from specify_cli.acceptance import AcceptanceError, detect_feature_slug
|
|
13
|
+
from specify_cli.cli.helpers import check_version_compatibility, console, get_project_root_or_exit
|
|
14
|
+
from specify_cli.core.project_resolver import resolve_worktree_aware_feature_dir
|
|
15
|
+
from specify_cli.tasks_support import TaskCliError, find_repo_root
|
|
16
|
+
from specify_cli.text_sanitization import detect_problematic_characters, sanitize_directory, sanitize_file
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_encoding(
|
|
20
|
+
feature: Optional[str] = typer.Option(None, "--feature", help="Feature slug to validate (auto-detected when omitted)"),
|
|
21
|
+
fix: bool = typer.Option(False, "--fix", help="Automatically fix encoding errors by sanitizing files"),
|
|
22
|
+
check_all: bool = typer.Option(False, "--all", help="Check all features, not just one"),
|
|
23
|
+
backup: bool = typer.Option(True, "--backup/--no-backup", help="Create .bak files before fixing"),
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Validate and optionally fix file encoding in feature artifacts.
|
|
26
|
+
|
|
27
|
+
Scans markdown files for Windows-1252 smart quotes and other problematic
|
|
28
|
+
characters that cause UTF-8 encoding errors. Can automatically fix issues
|
|
29
|
+
by replacing problematic characters with safe alternatives.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
repo_root = find_repo_root()
|
|
33
|
+
except TaskCliError as exc:
|
|
34
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
35
|
+
raise typer.Exit(1)
|
|
36
|
+
|
|
37
|
+
project_root = get_project_root_or_exit(repo_root)
|
|
38
|
+
check_version_compatibility(project_root, "validate-encoding")
|
|
39
|
+
|
|
40
|
+
if check_all:
|
|
41
|
+
# Validate all features
|
|
42
|
+
kitty_specs = repo_root / "kitty-specs"
|
|
43
|
+
if not kitty_specs.exists():
|
|
44
|
+
console.print("[yellow]No kitty-specs directory found.[/yellow]")
|
|
45
|
+
raise typer.Exit(0)
|
|
46
|
+
|
|
47
|
+
feature_dirs = [d for d in kitty_specs.iterdir() if d.is_dir()]
|
|
48
|
+
if not feature_dirs:
|
|
49
|
+
console.print("[yellow]No feature directories found.[/yellow]")
|
|
50
|
+
raise typer.Exit(0)
|
|
51
|
+
|
|
52
|
+
console.print(f"[cyan]Checking encoding for {len(feature_dirs)} features...[/cyan]")
|
|
53
|
+
console.print()
|
|
54
|
+
|
|
55
|
+
total_issues = 0
|
|
56
|
+
total_fixed = 0
|
|
57
|
+
|
|
58
|
+
for feature_dir in sorted(feature_dirs):
|
|
59
|
+
issues, fixed = _validate_feature_dir(feature_dir, fix=fix, backup=backup)
|
|
60
|
+
total_issues += issues
|
|
61
|
+
total_fixed += fixed
|
|
62
|
+
|
|
63
|
+
console.print()
|
|
64
|
+
console.print(Panel(
|
|
65
|
+
f"[bold]Summary:[/bold]\n"
|
|
66
|
+
f"Total files with issues: [yellow]{total_issues}[/yellow]\n"
|
|
67
|
+
f"Total files fixed: [green]{total_fixed}[/green]",
|
|
68
|
+
title="Encoding Validation Complete",
|
|
69
|
+
border_style="cyan" if total_issues == 0 else "yellow",
|
|
70
|
+
))
|
|
71
|
+
|
|
72
|
+
raise typer.Exit(0 if total_issues == 0 or fix else 1)
|
|
73
|
+
|
|
74
|
+
# Validate single feature
|
|
75
|
+
try:
|
|
76
|
+
feature_slug = (feature or detect_feature_slug(repo_root, cwd=Path.cwd())).strip()
|
|
77
|
+
except AcceptanceError as exc:
|
|
78
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
|
|
81
|
+
feature_dir = resolve_worktree_aware_feature_dir(repo_root, feature_slug, Path.cwd(), console)
|
|
82
|
+
|
|
83
|
+
if not feature_dir.exists():
|
|
84
|
+
console.print(f"[red]Error:[/red] Feature directory not found: {feature_dir}")
|
|
85
|
+
raise typer.Exit(1)
|
|
86
|
+
|
|
87
|
+
console.print(f"[cyan]Validating encoding for feature:[/cyan] {feature_slug}")
|
|
88
|
+
console.print()
|
|
89
|
+
|
|
90
|
+
issues, fixed = _validate_feature_dir(feature_dir, fix=fix, backup=backup)
|
|
91
|
+
|
|
92
|
+
if issues == 0:
|
|
93
|
+
console.print("[green]✓ All files are properly UTF-8 encoded![/green]")
|
|
94
|
+
raise typer.Exit(0)
|
|
95
|
+
elif fix and fixed > 0:
|
|
96
|
+
console.print()
|
|
97
|
+
console.print(f"[green]✓ Fixed {fixed} file(s) with encoding issues.[/green]")
|
|
98
|
+
if backup:
|
|
99
|
+
console.print("[dim]Backup files (.bak) were created.[/dim]")
|
|
100
|
+
raise typer.Exit(0)
|
|
101
|
+
else:
|
|
102
|
+
console.print()
|
|
103
|
+
console.print(f"[yellow]Found {issues} file(s) with encoding issues.[/yellow]")
|
|
104
|
+
console.print("[dim]Run with --fix to automatically repair these files.[/dim]")
|
|
105
|
+
raise typer.Exit(1)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _validate_feature_dir(feature_dir: Path, *, fix: bool, backup: bool) -> tuple[int, int]:
|
|
109
|
+
"""Validate encoding for a single feature directory.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Tuple of (issues_found, files_fixed)
|
|
113
|
+
"""
|
|
114
|
+
console.print(f"[cyan]Checking:[/cyan] {feature_dir.name}")
|
|
115
|
+
|
|
116
|
+
# Scan all markdown files
|
|
117
|
+
results = sanitize_directory(feature_dir, pattern="**/*.md", backup=backup, dry_run=not fix)
|
|
118
|
+
|
|
119
|
+
files_with_issues = []
|
|
120
|
+
files_fixed = []
|
|
121
|
+
file_errors = []
|
|
122
|
+
|
|
123
|
+
for file_path_str, (was_modified, error) in results.items():
|
|
124
|
+
file_path = Path(file_path_str)
|
|
125
|
+
relative_path = file_path.relative_to(feature_dir) if file_path.is_relative_to(feature_dir) else file_path
|
|
126
|
+
|
|
127
|
+
if error:
|
|
128
|
+
file_errors.append((relative_path, error))
|
|
129
|
+
elif was_modified:
|
|
130
|
+
files_with_issues.append(relative_path)
|
|
131
|
+
if fix:
|
|
132
|
+
files_fixed.append(relative_path)
|
|
133
|
+
|
|
134
|
+
# Display results
|
|
135
|
+
if files_with_issues:
|
|
136
|
+
table = Table(title=f"Files with Encoding Issues: {feature_dir.name}", show_header=True)
|
|
137
|
+
table.add_column("File", style="cyan")
|
|
138
|
+
table.add_column("Status", style="yellow")
|
|
139
|
+
|
|
140
|
+
for file_path in files_with_issues:
|
|
141
|
+
status = "[green]Fixed[/green]" if fix else "[yellow]Needs Fix[/yellow]"
|
|
142
|
+
table.add_row(str(file_path), status)
|
|
143
|
+
|
|
144
|
+
console.print(table)
|
|
145
|
+
|
|
146
|
+
# Show detailed character issues for first file
|
|
147
|
+
if files_with_issues and not fix:
|
|
148
|
+
first_file = feature_dir / files_with_issues[0]
|
|
149
|
+
try:
|
|
150
|
+
# Read with fallback encoding
|
|
151
|
+
try:
|
|
152
|
+
content = first_file.read_text(encoding="utf-8-sig")
|
|
153
|
+
except UnicodeDecodeError:
|
|
154
|
+
content_bytes = first_file.read_bytes()
|
|
155
|
+
for encoding in ("cp1252", "latin-1"):
|
|
156
|
+
try:
|
|
157
|
+
content = content_bytes.decode(encoding)
|
|
158
|
+
break
|
|
159
|
+
except UnicodeDecodeError:
|
|
160
|
+
continue
|
|
161
|
+
else:
|
|
162
|
+
content = content_bytes.decode("utf-8", errors="replace")
|
|
163
|
+
|
|
164
|
+
issues = detect_problematic_characters(content)
|
|
165
|
+
if issues:
|
|
166
|
+
console.print()
|
|
167
|
+
console.print(f"[yellow]Example issues in {files_with_issues[0]}:[/yellow]")
|
|
168
|
+
for line_num, col, char, replacement in issues[:5]: # Show first 5
|
|
169
|
+
console.print(
|
|
170
|
+
f" Line {line_num}, col {col}: '{char}' (U+{ord(char):04X}) → '{replacement}'"
|
|
171
|
+
)
|
|
172
|
+
if len(issues) > 5:
|
|
173
|
+
console.print(f" ... and {len(issues) - 5} more")
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
if file_errors:
|
|
178
|
+
console.print()
|
|
179
|
+
console.print("[red]Errors encountered:[/red]")
|
|
180
|
+
for file_path, error in file_errors:
|
|
181
|
+
console.print(f" [red]✗[/red] {file_path}: {error}")
|
|
182
|
+
|
|
183
|
+
return len(files_with_issues), len(files_fixed)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
__all__ = ["validate_encoding"]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Task metadata validation command for Spec Kitty CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from specify_cli.acceptance import AcceptanceError, detect_feature_slug
|
|
14
|
+
from specify_cli.cli.helpers import check_version_compatibility, console, get_project_root_or_exit
|
|
15
|
+
from specify_cli.core.project_resolver import resolve_worktree_aware_feature_dir
|
|
16
|
+
from specify_cli.task_metadata_validation import (
|
|
17
|
+
detect_lane_mismatch,
|
|
18
|
+
repair_lane_mismatch,
|
|
19
|
+
scan_all_tasks_for_mismatches,
|
|
20
|
+
validate_task_metadata,
|
|
21
|
+
)
|
|
22
|
+
from specify_cli.tasks_support import TaskCliError, find_repo_root
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def validate_tasks(
|
|
26
|
+
feature: Optional[str] = typer.Option(
|
|
27
|
+
None, "--feature", help="Feature slug to validate (auto-detected when omitted)"
|
|
28
|
+
),
|
|
29
|
+
fix: bool = typer.Option(False, "--fix", help="Automatically repair metadata inconsistencies"),
|
|
30
|
+
check_all: bool = typer.Option(False, "--all", help="Check all features, not just one"),
|
|
31
|
+
agent: Optional[str] = typer.Option(None, "--agent", help="Agent name for activity log"),
|
|
32
|
+
shell_pid: Optional[str] = typer.Option(None, "--shell-pid", help="Shell PID for activity log"),
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Validate and optionally fix task metadata inconsistencies.
|
|
35
|
+
|
|
36
|
+
Detects when work package frontmatter doesn't match file location:
|
|
37
|
+
- File in tasks/for_review/ but lane: "planned" in frontmatter
|
|
38
|
+
- File in tasks/doing/ but lane: "done" in frontmatter
|
|
39
|
+
- etc.
|
|
40
|
+
|
|
41
|
+
Can automatically fix by updating frontmatter to match directory.
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
repo_root = find_repo_root()
|
|
45
|
+
except TaskCliError as exc:
|
|
46
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
47
|
+
raise typer.Exit(1)
|
|
48
|
+
|
|
49
|
+
project_root = get_project_root_or_exit(repo_root)
|
|
50
|
+
check_version_compatibility(project_root, "validate-tasks")
|
|
51
|
+
|
|
52
|
+
# Get agent and shell_pid from environment if not provided
|
|
53
|
+
if not agent:
|
|
54
|
+
agent = os.environ.get("SPEC_KITTY_AGENT", "system")
|
|
55
|
+
if not shell_pid:
|
|
56
|
+
shell_pid = str(os.getpid())
|
|
57
|
+
|
|
58
|
+
if check_all:
|
|
59
|
+
# Validate all features
|
|
60
|
+
kitty_specs = repo_root / "kitty-specs"
|
|
61
|
+
worktrees = repo_root / ".worktrees"
|
|
62
|
+
|
|
63
|
+
feature_dirs = []
|
|
64
|
+
if kitty_specs.exists():
|
|
65
|
+
feature_dirs.extend([d for d in kitty_specs.iterdir() if d.is_dir()])
|
|
66
|
+
if worktrees.exists():
|
|
67
|
+
for wt_dir in worktrees.iterdir():
|
|
68
|
+
if wt_dir.is_dir():
|
|
69
|
+
wt_specs = wt_dir / "kitty-specs"
|
|
70
|
+
if wt_specs.exists():
|
|
71
|
+
feature_dirs.extend([d for d in wt_specs.iterdir() if d.is_dir()])
|
|
72
|
+
|
|
73
|
+
if not feature_dirs:
|
|
74
|
+
console.print("[yellow]No feature directories found.[/yellow]")
|
|
75
|
+
raise typer.Exit(0)
|
|
76
|
+
|
|
77
|
+
console.print(f"[cyan]Checking task metadata for {len(feature_dirs)} features...[/cyan]")
|
|
78
|
+
console.print()
|
|
79
|
+
|
|
80
|
+
total_mismatches = 0
|
|
81
|
+
total_fixed = 0
|
|
82
|
+
|
|
83
|
+
for feature_dir in sorted(feature_dirs, key=lambda d: d.name):
|
|
84
|
+
mismatches, fixed = _validate_feature_tasks(
|
|
85
|
+
feature_dir, fix=fix, agent=agent, shell_pid=shell_pid
|
|
86
|
+
)
|
|
87
|
+
total_mismatches += mismatches
|
|
88
|
+
total_fixed += fixed
|
|
89
|
+
|
|
90
|
+
console.print()
|
|
91
|
+
console.print(
|
|
92
|
+
Panel(
|
|
93
|
+
f"[bold]Summary:[/bold]\n"
|
|
94
|
+
f"Total mismatches found: [yellow]{total_mismatches}[/yellow]\n"
|
|
95
|
+
f"Total mismatches fixed: [green]{total_fixed}[/green]",
|
|
96
|
+
title="Task Metadata Validation Complete",
|
|
97
|
+
border_style="cyan" if total_mismatches == 0 else "yellow",
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
raise typer.Exit(0 if total_mismatches == 0 or fix else 1)
|
|
102
|
+
|
|
103
|
+
# Validate single feature
|
|
104
|
+
try:
|
|
105
|
+
feature_slug = (feature or detect_feature_slug(repo_root, cwd=Path.cwd())).strip()
|
|
106
|
+
except AcceptanceError as exc:
|
|
107
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
108
|
+
raise typer.Exit(1)
|
|
109
|
+
|
|
110
|
+
feature_dir = resolve_worktree_aware_feature_dir(repo_root, feature_slug, Path.cwd(), console)
|
|
111
|
+
|
|
112
|
+
if not feature_dir.exists():
|
|
113
|
+
console.print(f"[red]Error:[/red] Feature directory not found: {feature_dir}")
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
|
|
116
|
+
console.print(f"[cyan]Validating task metadata for feature:[/cyan] {feature_slug}")
|
|
117
|
+
console.print()
|
|
118
|
+
|
|
119
|
+
mismatches, fixed = _validate_feature_tasks(
|
|
120
|
+
feature_dir, fix=fix, agent=agent, shell_pid=shell_pid
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if mismatches == 0:
|
|
124
|
+
console.print("[green]✓ All task metadata is consistent![/green]")
|
|
125
|
+
raise typer.Exit(0)
|
|
126
|
+
elif fix and fixed > 0:
|
|
127
|
+
console.print()
|
|
128
|
+
console.print(f"[green]✓ Fixed {fixed} metadata mismatch(es).[/green]")
|
|
129
|
+
raise typer.Exit(0)
|
|
130
|
+
else:
|
|
131
|
+
console.print()
|
|
132
|
+
console.print(f"[yellow]Found {mismatches} metadata mismatch(es).[/yellow]")
|
|
133
|
+
console.print("[dim]Run with --fix to automatically repair these mismatches.[/dim]")
|
|
134
|
+
raise typer.Exit(1)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _validate_feature_tasks(
|
|
138
|
+
feature_dir: Path, *, fix: bool, agent: str, shell_pid: str
|
|
139
|
+
) -> tuple[int, int]:
|
|
140
|
+
"""Validate task metadata for a single feature directory.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Tuple of (mismatches_found, mismatches_fixed)
|
|
144
|
+
"""
|
|
145
|
+
console.print(f"[cyan]Checking:[/cyan] {feature_dir.name}")
|
|
146
|
+
|
|
147
|
+
mismatches_dict = scan_all_tasks_for_mismatches(feature_dir)
|
|
148
|
+
|
|
149
|
+
if not mismatches_dict:
|
|
150
|
+
console.print(f" [green]✓[/green] No metadata mismatches")
|
|
151
|
+
return 0, 0
|
|
152
|
+
|
|
153
|
+
# Display mismatches in a table
|
|
154
|
+
table = Table(title=f"Task Metadata Mismatches: {feature_dir.name}", show_header=True)
|
|
155
|
+
table.add_column("File", style="cyan")
|
|
156
|
+
table.add_column("Expected Lane", style="green")
|
|
157
|
+
table.add_column("Actual Lane", style="yellow")
|
|
158
|
+
table.add_column("Status", style="white")
|
|
159
|
+
|
|
160
|
+
fixed_count = 0
|
|
161
|
+
for file_path, (_, expected, actual) in mismatches_dict.items():
|
|
162
|
+
full_path = feature_dir / file_path
|
|
163
|
+
|
|
164
|
+
status = "[yellow]Needs Fix[/yellow]"
|
|
165
|
+
if fix:
|
|
166
|
+
was_repaired, error = repair_lane_mismatch(
|
|
167
|
+
full_path, agent=agent, shell_pid=shell_pid, add_history=True, dry_run=False
|
|
168
|
+
)
|
|
169
|
+
if was_repaired:
|
|
170
|
+
status = "[green]Fixed[/green]"
|
|
171
|
+
fixed_count += 1
|
|
172
|
+
elif error:
|
|
173
|
+
status = f"[red]Error: {error}[/red]"
|
|
174
|
+
|
|
175
|
+
table.add_row(file_path, expected or "?", actual or "?", status)
|
|
176
|
+
|
|
177
|
+
console.print(table)
|
|
178
|
+
|
|
179
|
+
if fix:
|
|
180
|
+
console.print()
|
|
181
|
+
console.print(f" [green]Fixed {fixed_count} of {len(mismatches_dict)} mismatches[/green]")
|
|
182
|
+
|
|
183
|
+
return len(mismatches_dict), fixed_count
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
__all__ = ["validate_tasks"]
|