Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class RepoFeatures:
has_hooks: bool = False
has_agents: bool = False
has_memory: bool = False
is_stale: bool = False # True if no commits in 3+ months
is_new: bool = False # True if created within last 7 days
mcp_servers: list[str] = field(default_factory=list)
custom_commands: list[str] = field(default_factory=list)
claude_action_names: list[str] = field(default_factory=list)
Expand All @@ -40,6 +42,8 @@ class OrgStats:
hooks_count: int = 0
agents_count: int = 0
memory_count: int = 0
stale_count: int = 0
new_count: int = 0

# Detailed breakdowns
mcp_server_counter: Counter = field(default_factory=Counter)
Expand Down Expand Up @@ -68,6 +72,10 @@ def aggregate(org_name: str, repos: list[RepoFeatures]) -> OrgStats:
stats.agents_count += 1
if repo.has_memory:
stats.memory_count += 1
if repo.is_stale:
stats.stale_count += 1
if repo.is_new:
stats.new_count += 1

for server in repo.mcp_servers:
stats.mcp_server_counter[server] += 1
Expand Down
21 changes: 12 additions & 9 deletions src/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ def _render_adoption(stats: OrgStats, config: Config, show_bar: bool = True) ->
("Has Agents", stats.agents_count),
("Has Hooks", stats.hooks_count),
("Has GitHub Actions", stats.claude_actions_count),
("New (<7 days)", stats.new_count),
("Stale (3+ months)", stats.stale_count),
]
# Only show items with count > 0
items = [(label, count) for label, count in items if count > 0]
Expand All @@ -67,16 +69,9 @@ def _render_details(stats: OrgStats, _config: Config) -> list[str]:
if not stats.repos:
return []

# Only include repos with at least one feature
active_repos = [r for r in stats.repos if any([
r.has_claude_md, r.has_claude_dir, r.has_mcp_servers,
r.has_custom_commands, r.has_claude_actions, r.has_hooks,
r.has_agents, r.has_memory,
])]
if not active_repos:
return []
active_repos = stats.repos

headers = ["Repo", "CLAUDE.md", ".claude/", "MCP", "Skills", "Actions", "Hooks", "Agents", "Memory"]
headers = ["Repo", "CLAUDE.md", ".claude/", "MCP", "Skills", "Actions", "Hooks", "Agents", "Memory", "New", "Stale"]
lines = [
"\n<details>",
"<summary>Per-repo breakdown</summary>",
Expand All @@ -88,6 +83,12 @@ def _render_details(stats: OrgStats, _config: Config) -> list[str]:
def check(val: bool) -> str:
return "✅" if val else ""

def new(val: bool) -> str:
return "🆕" if val else ""

def stale(val: bool) -> str:
return "⚠️" if val else ""

for repo in sorted(active_repos, key=lambda r: r.name.lower()):
row = [
repo.name,
Expand All @@ -99,6 +100,8 @@ def check(val: bool) -> str:
check(repo.has_hooks),
check(repo.has_agents),
check(repo.has_memory),
new(repo.is_new),
stale(repo.is_stale),
]
lines.append("| " + " | ".join(row) + " |")

Expand Down
10 changes: 10 additions & 0 deletions src/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ def scan_repo(gh: Github, repo) -> RepoFeatures:

_check_rate_limit(gh)

# Check if repo is stale (no commits in 3+ months)
if repo.pushed_at:
days_since_push = (datetime.datetime.now(datetime.timezone.utc) - repo.pushed_at).days
features.is_stale = days_since_push > 90

# Check if repo is new (created within last 7 days)
if repo.created_at:
days_since_creation = (datetime.datetime.now(datetime.timezone.utc) - repo.created_at).days
features.is_new = days_since_creation < 7

# Get full tree in one API call
try:
default_branch = repo.default_branch
Expand Down
30 changes: 30 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ def test_aggregate_empty_repos(self):
assert stats.hooks_count == 0
assert stats.agents_count == 0
assert stats.memory_count == 0
assert stats.stale_count == 0
assert stats.new_count == 0
assert stats.mcp_server_counter == Counter()
assert stats.custom_command_counter == Counter()
assert stats.claude_action_counter == Counter()
Expand Down Expand Up @@ -121,6 +123,30 @@ def test_aggregate_counts_hooks(self):
assert stats.hook_type_counter["PreToolUse"] == 2
assert stats.hook_type_counter["PostToolUse"] == 1

def test_aggregate_counts_stale_repos(self):
repos = [
RepoFeatures(name="repo1", is_stale=True),
RepoFeatures(name="repo2", is_stale=False),
RepoFeatures(name="repo3", is_stale=True),
RepoFeatures(name="repo4", is_stale=False),
]
stats = OrgStats.aggregate("test-org", repos)

assert stats.total_repos == 4
assert stats.stale_count == 2

def test_aggregate_counts_new_repos(self):
repos = [
RepoFeatures(name="repo1", is_new=True),
RepoFeatures(name="repo2", is_new=False),
RepoFeatures(name="repo3", is_new=True),
RepoFeatures(name="repo4", is_new=False),
]
stats = OrgStats.aggregate("test-org", repos)

assert stats.total_repos == 4
assert stats.new_count == 2

def test_aggregate_preserves_repos_list(self):
repos = [
RepoFeatures(name="repo1", has_claude_md=True),
Expand All @@ -145,6 +171,8 @@ def test_aggregate_all_features_at_once(self):
has_hooks=True,
has_agents=True,
has_memory=True,
is_stale=True,
is_new=False,
mcp_servers=["filesystem", "github"],
custom_commands=["review"],
claude_action_names=["claude-code-action"],
Expand All @@ -162,6 +190,8 @@ def test_aggregate_all_features_at_once(self):
assert stats.hooks_count == 1
assert stats.agents_count == 1
assert stats.memory_count == 1
assert stats.stale_count == 1
assert stats.new_count == 0
assert len(stats.mcp_server_counter) == 2
assert len(stats.custom_command_counter) == 1
assert len(stats.claude_action_counter) == 1
Expand Down
37 changes: 29 additions & 8 deletions tests/test_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def test_details_section(self):
result = render_stats(stats, config)
assert "Per-repo breakdown" in result
assert "repo-a" in result
assert "repo-d" not in result # no features
assert "repo-d" in result # All repos shown (including those without features)
assert ".claude/" in result # .claude/ column present

def test_custom_bar_length(self):
Expand Down Expand Up @@ -166,11 +166,11 @@ def test_detail_table_has_correct_columns(self):
config = _make_config(show_sections=["details"])
result = render_stats(stats, config)

# Check header row
assert "| Repo | CLAUDE.md | .claude/ | MCP | Skills | Actions | Hooks | Agents | Memory |" in result
# Check header row includes new "New" and "Stale" columns
assert "| Repo | CLAUDE.md | .claude/ | MCP | Skills | Actions | Hooks | Agents | Memory | New | Stale |" in result

def test_detail_table_only_shows_repos_with_features(self):
"""Repos with no features should not appear in detail table."""
def test_detail_table_shows_all_repos(self):
"""All repos should appear in detail table, including those without features."""
repos = [
RepoFeatures(name="active-repo", has_claude_md=True),
RepoFeatures(name="empty-repo"),
Expand All @@ -182,7 +182,7 @@ def test_detail_table_only_shows_repos_with_features(self):

assert "active-repo" in result
assert "another-active" in result
assert "empty-repo" not in result
assert "empty-repo" in result # All repos shown now

def test_detail_table_checkmarks(self):
"""Verify checkmarks appear for enabled features."""
Expand All @@ -209,6 +209,25 @@ def test_detail_table_checkmarks(self):
# Should have 8 checkmarks (one for each feature column)
assert result.count("✅") == 8

def test_detail_table_new_and_stale_indicators(self):
"""Verify new and stale indicators appear in detail table."""
repos = [
RepoFeatures(name="new-repo", is_new=True, has_claude_md=True),
RepoFeatures(name="stale-repo", is_stale=True, has_claude_dir=True),
RepoFeatures(name="normal-repo", has_agents=True),
]
stats = OrgStats.aggregate("test-org", repos)
config = _make_config(show_sections=["details"])
result = render_stats(stats, config)

# Should have 1 new indicator (🆕) and 1 stale indicator (⚠️)
assert result.count("🆕") == 1
assert result.count("⚠️") == 1
# Verify they appear in correct rows
assert "new-repo" in result
assert "stale-repo" in result
assert "normal-repo" in result

def test_adoption_only_shows_features_with_nonzero_count(self):
"""Adoption section should only show features that exist in at least one repo."""
repos = [
Expand Down Expand Up @@ -332,8 +351,10 @@ def test_details_section_with_only_inactive_repos(self):
config = _make_config(show_sections=["details"])
result = render_stats(stats, config)

# Should be empty (no active repos to show)
assert result == ""
# Should still show all repos, even those without features
assert "Per-repo breakdown" in result
assert "empty1" in result
assert "empty2" in result

def test_format_row_with_zero_total(self):
"""Test _format_row handles total=0 without division error."""
Expand Down
Loading