Skip to content

Commit 99742be

Browse files
tbitcsoz-agent
andcommitted
feat: multi-language detection, self-update command
- Importer detects primary + secondary languages (threshold: 5 files or 5%) - CLI import shows all languages: 'verilog + python, c, vhdl' - Architecture and AGENTS.md overlay include full language list - New 'specsmith self-update' command: auto-detects channel (stable/dev), supports --channel override and --version pinning - updater.run_self_update accepts target_version for pinned installs Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent 318a2d9 commit 99742be

13 files changed

Lines changed: 81 additions & 20 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111
- **Dynamic versioning**: `__version__` now reads from `importlib.metadata` at runtime instead of hardcoded strings. Docs use `{{ version }}` placeholders resolved by MkDocs hook. Tests are version-agnostic.
12+
- **Multi-language detection**: importer now detects and reports all significant languages (primary + secondary). Architecture, AGENTS.md, and CLI display show the full language mix.
13+
- **`specsmith self-update`** command: auto-detects channel (stable/dev), supports `--channel` override and `--version` pinning.
1214
- **Dev-release workflow for managed projects** (#35): `specsmith init` with gitflow + GitHub + Python now generates `.github/workflows/dev-release.yml`.
1315
- **No-hardcoded-versions rule** (H10): governance template and WARP rule enforce `pyproject.toml` as single version source of truth.
1416
- **Separate PyPI badges**: README shows both stable (blue) and dev (orange) version badges.
1517

18+
### Fixed
19+
- **Import with large AGENTS.md** (#46): extraction now uses broader keyword matching, unmatched sections go to rules.md, oversized AGENTS.md is backed up and replaced with a hub.
20+
1621
### Changed
1722
- RTD default version set to `stable`, default branch set to `develop` (`latest` now builds from develop).
1823
- Docs version references use dynamic `{{ version }}` instead of hardcoded strings.

src/specsmith/cli.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,10 @@ def import_project(project_dir: str, force: bool, guided: bool, dry_run: bool) -
478478
result = detect_project(root)
479479

480480
console.print(f" Files: {result.file_count}")
481-
console.print(f" Language: [cyan]{result.primary_language or 'unknown'}[/cyan]")
481+
lang_display = result.primary_language or "unknown"
482+
if result.secondary_languages:
483+
lang_display += f" + {', '.join(result.secondary_languages)}"
484+
console.print(f" Languages: [cyan]{lang_display}[/cyan]")
482485
console.print(f" Build system: {result.build_system or 'not detected'}")
483486
console.print(f" Test framework: {result.test_framework or 'not detected'}")
484487
console.print(f" CI: {result.existing_ci or 'not detected'}")
@@ -1205,6 +1208,50 @@ def update_cmd(check_only: bool, auto_yes: bool, project_dir: str) -> None:
12051208
console.print(f" [green]\u2713[/green] {a}")
12061209

12071210

1211+
@main.command(name="self-update")
1212+
@click.option(
1213+
"--channel",
1214+
type=click.Choice(["stable", "dev"]),
1215+
default=None,
1216+
help="Force channel (default: auto-detect from installed version).",
1217+
)
1218+
@click.option("--version", "target_version", default="", help="Install a specific version.")
1219+
def self_update_cmd(channel: str | None, target_version: str) -> None:
1220+
"""Update specsmith to the latest version.
1221+
1222+
Auto-detects channel: stable builds upgrade to latest stable,
1223+
dev builds upgrade to latest dev. Use --channel to override.
1224+
Use --version to pin a specific version.
1225+
"""
1226+
from specsmith.updater import check_latest_version, get_update_channel, run_self_update
1227+
1228+
current_channel = get_update_channel()
1229+
effective_channel = channel or current_channel
1230+
1231+
if target_version:
1232+
console.print(f"[bold]Installing specsmith {target_version}...[/bold]")
1233+
success, msg = run_self_update(target_version=target_version)
1234+
else:
1235+
current, latest, effective_channel = check_latest_version(channel=effective_channel)
1236+
if not latest:
1237+
console.print("[yellow]Could not reach PyPI.[/yellow]")
1238+
return
1239+
if current == latest:
1240+
console.print(
1241+
f"[green]\u2713[/green] specsmith {current} is up to date ({effective_channel})."
1242+
)
1243+
return
1244+
console.print(f" Current: {current} ({current_channel})")
1245+
console.print(f" Latest: {latest} ({effective_channel})")
1246+
success, msg = run_self_update(channel=effective_channel)
1247+
1248+
if success:
1249+
console.print("[green]\u2713[/green] Updated successfully.")
1250+
console.print(" Restart your shell to use the new version.")
1251+
else:
1252+
console.print(f"[red]\u2717[/red] Update failed: {msg}")
1253+
1254+
12081255
@main.command(name="migrate-project")
12091256
@click.option("--project-dir", type=click.Path(exists=True), default=".")
12101257
@click.option("--dry-run", is_flag=True, default=False, help="Show changes without writing.")

src/specsmith/importer.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class DetectionResult:
101101
root: Path
102102
languages: dict[str, int] = field(default_factory=dict)
103103
primary_language: str = ""
104+
secondary_languages: list[str] = field(default_factory=list)
104105
build_system: str = ""
105106
test_framework: str = ""
106107
vcs_platform: str = ""
@@ -183,6 +184,14 @@ def detect_project(root: Path) -> DetectionResult:
183184
result.languages = dict(lang_counter.most_common())
184185
if lang_counter:
185186
result.primary_language = lang_counter.most_common(1)[0][0]
187+
# Secondary languages: all others above a minimum threshold (5 files or 5%)
188+
total = sum(lang_counter.values())
189+
threshold = max(5, int(total * 0.05))
190+
result.secondary_languages = [
191+
lang
192+
for lang, count in lang_counter.most_common()
193+
if lang != result.primary_language and count >= threshold
194+
]
186195

187196
# Build system — check root AND first-level subdirectories
188197
for indicator, system in _BUILD_SYSTEMS.items():
@@ -847,6 +856,8 @@ def _write(rel_path: str, content: str) -> None:
847856

848857
name = result.root.name
849858
lang = result.primary_language or "unknown"
859+
all_langs = [lang] + (result.secondary_languages or [])
860+
lang_display = ", ".join(all_langs) if len(all_langs) > 1 else lang
850861
today = date.today().isoformat()
851862
ptype = result.inferred_type.value if result.inferred_type else "unknown"
852863

@@ -857,7 +868,7 @@ def _write(rel_path: str, content: str) -> None:
857868
"This project was imported by specsmith. The governance files contain "
858869
"detected structure. Review and enrich with your agent.\n\n"
859870
"## Project Summary\n"
860-
f"- **Language**: {lang}\n"
871+
f"- **Languages**: {lang_display}\n"
861872
f"- **Build system**: {result.build_system or 'not detected'}\n"
862873
f"- **Test framework**: {result.test_framework or 'not detected'}\n"
863874
f"- **Files detected**: {result.file_count}\n"
@@ -915,7 +926,7 @@ def _write(rel_path: str, content: str) -> None:
915926
f"# Architecture — {name}\n\n"
916927
"Architecture auto-generated from project detection.\n\n"
917928
"## Overview\n"
918-
f"- **Language**: {lang}\n"
929+
f"- **Languages**: {lang_display}\n"
919930
f"- **Build system**: {result.build_system or 'not detected'}\n"
920931
f"- **Test framework**: {result.test_framework or 'not detected'}\n\n"
921932
)

src/specsmith/updater.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,22 @@ def is_outdated() -> bool:
5959
return current != latest
6060

6161

62-
def run_self_update(*, channel: str = "") -> tuple[bool, str]:
62+
def run_self_update(
63+
*, channel: str = "", target_version: str = "",
64+
) -> tuple[bool, str]:
6365
"""Update specsmith via pip.
6466
65-
Uses --pre flag for dev channel.
67+
If target_version is set, installs that exact version.
68+
Otherwise uses --pre flag for dev channel, plain upgrade for stable.
6669
"""
67-
if not channel:
68-
channel = get_update_channel()
69-
70-
cmd = ["pip", "install", "--upgrade", "specsmith"]
71-
if channel == "dev":
72-
cmd.insert(2, "--pre")
70+
if target_version:
71+
cmd = ["pip", "install", f"specsmith=={target_version}"]
72+
else:
73+
if not channel:
74+
channel = get_update_channel()
75+
cmd = ["pip", "install", "--upgrade", "specsmith"]
76+
if channel == "dev":
77+
cmd.insert(2, "--pre")
7378

7479
try:
7580
result = subprocess.run(

tests/sandbox/test_sandbox_import.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
import yaml
1515
from click.testing import CliRunner
16-
1716
from specsmith.cli import main
1817

1918

tests/sandbox/test_sandbox_new.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
import yaml
1414
from click.testing import CliRunner
15-
1615
from specsmith.cli import main
1716

1817

tests/sandbox/test_sandbox_types.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import yaml
1010
from click.testing import CliRunner
11-
1211
from specsmith.cli import main
1312

1413

tests/test_auditor.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from pathlib import Path
88

99
import pytest
10-
1110
from specsmith.auditor import run_audit
1211

1312

tests/test_cli.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import yaml
1010
from click.testing import CliRunner
11-
1211
from specsmith.cli import main
1312
from specsmith.config import ProjectConfig, ProjectType
1413
from specsmith.scaffolder import scaffold_project

tests/test_integrations.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from pathlib import Path
88

99
import pytest
10-
1110
from specsmith.config import ProjectConfig, ProjectType
1211
from specsmith.integrations import get_adapter, list_adapters
1312
from specsmith.integrations.claude_code import ClaudeCodeAdapter

0 commit comments

Comments
 (0)