Skip to content

Commit a05c92f

Browse files
committed
feat: add metaspec sync command for updating commands
Major new feature: metaspec sync Features: 1. Version Tracking - Generated speckits record MetaSpec version in pyproject.toml - Stored in [tool.metaspec] section 2. metaspec sync Command - Updates .metaspec/commands/ to latest version - Automatic backup with timestamps - Version detection and comparison - Safe and reversible operations Usage: cd my-speckit metaspec sync # Update to latest metaspec sync --check-only # Check version only metaspec sync --force # Force update Benefits: - Easy workflow updates (e.g., v0.5.8 workflow fixes) - No need to regenerate entire speckit - Git-friendly (can review diffs) - Safe with automatic backups Implementation: - src/metaspec/cli/sync.py (new) - src/metaspec/cli/main.py (register command) - src/metaspec/templates/base/pyproject.toml.j2 (add version tracking) - CHANGELOG.md (document feature) This solves the pain point of keeping MetaSpec commands up to date in existing speckits without regenerating everything.
1 parent f0e70e3 commit a05c92f

File tree

4 files changed

+226
-0
lines changed

4 files changed

+226
-0
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### ✨ Features
11+
12+
**metaspec sync Command**
13+
- Added `metaspec sync` command to update MetaSpec commands in generated speckits
14+
- Automatically backs up existing commands before updating
15+
- Detects version differences and shows what changed
16+
- Safe and reversible (backups created with timestamps)
17+
18+
**Version Tracking**
19+
- Generated speckits now record the MetaSpec version used to create them
20+
- Stored in `pyproject.toml` under `[tool.metaspec]`
21+
- Enables version detection and intelligent sync
22+
23+
**Usage**:
24+
```bash
25+
cd my-speckit
26+
metaspec sync # Update to latest
27+
metaspec sync --check-only # Check version only
28+
```
29+
1030
---
1131

1232
## [0.5.8] - 2025-11-14

src/metaspec/cli/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from metaspec.cli.info import info_command, list_command
1212
from metaspec.cli.init import init_command
1313
from metaspec.cli.search import install_command, search_command
14+
from metaspec.cli.sync import sync_command
1415

1516
app = typer.Typer(
1617
name="metaspec",
@@ -27,6 +28,7 @@
2728
app.command(name="contribute")(contribute_command)
2829
app.command(name="list")(list_command)
2930
app.command(name="info")(info_command)
31+
app.command(name="sync")(sync_command)
3032

3133

3234
@app.command("version")

src/metaspec/cli/sync.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
"""
2+
Sync command for updating MetaSpec commands in generated speckits.
3+
"""
4+
5+
import shutil
6+
import tomllib
7+
from datetime import datetime
8+
from pathlib import Path
9+
10+
import typer
11+
from rich.console import Console
12+
from rich.panel import Panel
13+
from rich.table import Table
14+
15+
from metaspec import __version__ as current_version
16+
17+
console = Console()
18+
19+
20+
def sync_command(
21+
check_only: bool = typer.Option(
22+
False,
23+
"--check-only",
24+
"-c",
25+
help="Check version without updating"
26+
),
27+
force: bool = typer.Option(
28+
False,
29+
"--force",
30+
"-f",
31+
help="Force update even if versions match"
32+
),
33+
) -> None:
34+
"""
35+
Sync MetaSpec commands to the latest version.
36+
37+
Updates .metaspec/commands/ with the latest command documents
38+
from the installed MetaSpec version. Automatically creates backups.
39+
40+
Example:
41+
$ cd my-speckit
42+
$ metaspec sync
43+
"""
44+
# Step 1: Verify we're in a speckit directory
45+
if not Path("pyproject.toml").exists():
46+
console.print(
47+
"[red]Error:[/red] Not in a speckit directory (pyproject.toml not found)",
48+
style="red"
49+
)
50+
console.print("\n💡 Run this command from your speckit root directory")
51+
raise typer.Exit(1)
52+
53+
# Step 2: Read generated_by version
54+
generated_version = _read_generated_version()
55+
56+
if generated_version is None:
57+
console.print(
58+
"[yellow]Warning:[/yellow] Could not detect MetaSpec version",
59+
style="yellow"
60+
)
61+
console.print("This speckit may have been generated with an older MetaSpec")
62+
if not typer.confirm("\nContinue anyway?", default=False):
63+
raise typer.Exit(0)
64+
generated_version = "unknown"
65+
66+
# Step 3: Compare versions
67+
console.print(Panel(
68+
f"[cyan]MetaSpec installed:[/cyan] {current_version}\n"
69+
f"[cyan]Speckit generated with:[/cyan] {generated_version}",
70+
title="🔍 Version Check",
71+
border_style="cyan"
72+
))
73+
74+
if generated_version == current_version and not force:
75+
console.print("\n✅ Already up to date!")
76+
return
77+
78+
if generated_version == "unknown":
79+
console.print("\n⚠️ [yellow]Cannot determine if update is needed[/yellow]")
80+
elif generated_version > current_version:
81+
console.print(
82+
f"\n⚠️ [yellow]Speckit was generated with newer MetaSpec ({generated_version})[/yellow]"
83+
)
84+
console.print("Consider upgrading MetaSpec: [cyan]pip install --upgrade meta-spec[/cyan]")
85+
86+
if check_only:
87+
if generated_version < current_version:
88+
console.print(
89+
f"\n💡 Run [cyan]metaspec sync[/cyan] to update to v{current_version}"
90+
)
91+
raise typer.Exit(0)
92+
93+
# Step 4: Confirm update
94+
if not typer.confirm(f"\nUpdate commands to v{current_version}?", default=True):
95+
console.print("Cancelled")
96+
raise typer.Exit(0)
97+
98+
# Step 5: Backup existing commands
99+
metaspec_dir = Path(".metaspec")
100+
commands_dir = metaspec_dir / "commands"
101+
102+
if not commands_dir.exists():
103+
console.print(
104+
f"[red]Error:[/red] {commands_dir} not found",
105+
style="red"
106+
)
107+
raise typer.Exit(1)
108+
109+
backup_dir = metaspec_dir / f"commands.backup-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
110+
111+
console.print(f"\n📦 Backing up to {backup_dir.name}...")
112+
shutil.copytree(commands_dir, backup_dir)
113+
console.print(" ✅ Backup complete")
114+
115+
# Step 6: Get new commands from installed MetaSpec
116+
try:
117+
from metaspec.generator import Generator
118+
gen = Generator()
119+
template_dir = Path(gen.env.loader.searchpath[0]) / "meta"
120+
except Exception as e:
121+
console.print(
122+
"[red]Error:[/red] Could not locate MetaSpec templates: " + str(e),
123+
style="red"
124+
)
125+
raise typer.Exit(1) from e
126+
127+
# Step 7: Copy new commands
128+
console.print("\n🔄 Updating commands...")
129+
130+
updated_files = []
131+
for command_group in ["sds", "sdd", "evolution"]:
132+
source_dir = template_dir / command_group / "commands"
133+
if not source_dir.exists():
134+
continue
135+
136+
for source_file in source_dir.glob("*.md.j2"):
137+
# Remove .j2 extension for destination
138+
dest_file = commands_dir / f"metaspec.{command_group}.{source_file.stem}"
139+
140+
# Read and render template (basic rendering, no complex logic)
141+
content = source_file.read_text()
142+
# Simple variable substitution for static content
143+
content = content.replace("{{ metaspec_version }}", current_version)
144+
145+
dest_file.write_text(content)
146+
updated_files.append(dest_file.name)
147+
148+
# Step 8: Update version in pyproject.toml
149+
_update_generated_version(current_version)
150+
151+
# Step 9: Show results
152+
console.print(f" ✅ Updated {len(updated_files)} command files")
153+
154+
# Create summary table
155+
table = Table(title="\n📊 Sync Summary", show_header=True, header_style="bold cyan")
156+
table.add_column("Item", style="cyan")
157+
table.add_column("Details", style="white")
158+
159+
table.add_row("Previous version", generated_version)
160+
table.add_row("Current version", current_version)
161+
table.add_row("Files updated", str(len(updated_files)))
162+
table.add_row("Backup location", backup_dir.name)
163+
164+
console.print(table)
165+
166+
console.print("\n✅ [green]Sync complete![/green]")
167+
console.print("\n💡 Next steps:")
168+
console.print(" • Review changes: [cyan]git diff .metaspec/[/cyan]")
169+
console.print(" • View changelog: [cyan]https://github.com/ACNet-AI/MetaSpec/blob/main/CHANGELOG.md[/cyan]")
170+
console.print(f" • Rollback if needed: [cyan]mv {backup_dir} {commands_dir}[/cyan]")
171+
172+
173+
def _read_generated_version() -> str | None:
174+
"""Read the MetaSpec version from pyproject.toml."""
175+
try:
176+
with open("pyproject.toml", "rb") as f:
177+
data = tomllib.load(f)
178+
return data.get("tool", {}).get("metaspec", {}).get("generated_by")
179+
except Exception:
180+
return None
181+
182+
183+
def _update_generated_version(version: str) -> None:
184+
"""Update the generated_by version in pyproject.toml."""
185+
try:
186+
pyproject_path = Path("pyproject.toml")
187+
content = pyproject_path.read_text()
188+
189+
# Simple regex replacement
190+
import re
191+
content = re.sub(
192+
r'(generated_by\s*=\s*")[^"]*(")',
193+
rf'\g<1>{version}\g<2>',
194+
content
195+
)
196+
197+
pyproject_path.write_text(content)
198+
except Exception as e:
199+
console.print(f"[yellow]Warning:[/yellow] Could not update version in pyproject.toml: {e}")
200+

src/metaspec/templates/base/pyproject.toml.j2

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ dev = [
4343
requires = ["setuptools>=61.0", "wheel"]
4444
build-backend = "setuptools.build_meta"
4545

46+
[tool.metaspec]
47+
# MetaSpec version used to generate this speckit
48+
generated_by = "{{ metaspec_version }}"
49+
4650
[tool.setuptools]
4751
package-dir = {"" = "src"}
4852

0 commit comments

Comments
 (0)