Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 claude-code/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,11 @@ Refer to `claude-config/smartem-decisions/REPO-GUIDELINES.md` for:

- Follow FandanGO plugin conventions
- Reference peer plugins in repos/FragmentScreen/ for patterns

---

## Git Commit Guidelines

- **No Claude attribution**: Do NOT add `Co-Authored-By: Claude` or any similar attribution lines to commit messages. Commits should appear as normal developer commits.
- Write clear, concise commit messages following conventional commits format when appropriate (feat, fix, docs, refactor, etc.)
- Focus on the "why" not the "what" in commit body
14 changes: 7 additions & 7 deletions core/repos.json
Original file line number Diff line number Diff line change
Expand Up @@ -361,13 +361,13 @@
],
"claudeConfig": {
"skills": [
{ "name": "database-admin", "path": "shared/skills/database-admin" },
{ "name": "devops", "path": "shared/skills/devops" },
{ "name": "technical-writer", "path": "shared/skills/technical-writer" },
{ "name": "git", "path": "shared/skills/git" },
{ "name": "github", "path": "shared/skills/github" },
{ "name": "ascii-art", "path": "shared/skills/ascii-art" },
{ "name": "playwright-skill", "path": "smartem-frontend/skills/playwright-skill" }
{ "name": "database-admin", "path": "claude-code/shared/skills/database-admin" },
{ "name": "devops", "path": "claude-code/shared/skills/devops" },
{ "name": "technical-writer", "path": "claude-code/shared/skills/technical-writer" },
{ "name": "git", "path": "claude-code/shared/skills/git" },
{ "name": "github", "path": "claude-code/shared/skills/github" },
{ "name": "ascii-art", "path": "claude-code/shared/skills/ascii-art" },
{ "name": "playwright-skill", "path": "claude-code/smartem-frontend/skills/playwright-skill" }
],
"defaultPermissions": {
"allow": [
Expand Down
48 changes: 43 additions & 5 deletions packages/smartem-workspace/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,44 @@ smartem-workspace init --preset full --no-interactive
| `aria-reference` | ARIA ecosystem repos for reference |
| `minimal` | Just smartem-devtools (workspace setup only) |

### Other commands
### Verify workspace setup

```bash
# Sync existing repos (git pull)
# Check all configuration
smartem-workspace check

# Check specific scope
smartem-workspace check --scope claude
smartem-workspace check --scope repos
smartem-workspace check --scope serena

# Auto-repair fixable issues (broken symlinks, missing dirs)
smartem-workspace check --fix
```

### Sync repositories

```bash
# Pull latest from all cloned repos
smartem-workspace sync

# Show workspace status
# Preview what would be pulled
smartem-workspace sync --dry-run
```

Sync skips repos with uncommitted changes or not on main/master branch.

### Other commands

```bash
# Show workspace status (alias for check)
smartem-workspace status

# Add a single repo
# Add a single repo (not yet implemented)
smartem-workspace add DiamondLightSource/smartem-frontend
```

### Options
### Init options

```
--path PATH Target directory (default: current directory)
Expand All @@ -68,6 +92,20 @@ smartem-workspace add DiamondLightSource/smartem-frontend
--skip-serena Skip Serena MCP setup
```

### Check options

```
--scope SCOPE Check scope: claude, repos, serena, or all (default: all)
--fix Attempt to fix issues (recreate symlinks, dirs)
--offline Use bundled config instead of fetching from GitHub
```

### Sync options

```
--dry-run, -n Show what would be done without making changes
```

## What it sets up

1. **Repository clones** - Organized by organization (DiamondLightSource, FragmentScreen, GitlabAriaPHP)
Expand Down
2 changes: 1 addition & 1 deletion packages/smartem-workspace/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "smartem-workspace"
version = "0.1.0"
version = "0.2.0"
description = "CLI tool to automate SmartEM multi-repo workspace setup"
readme = "README.md"
license = "Apache-2.0"
Expand Down
145 changes: 137 additions & 8 deletions packages/smartem-workspace/smartem_workspace/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@
import typer
from rich.console import Console

from smartem_workspace.commands.check import (
CheckScope,
apply_fixes,
print_report,
run_checks,
)
from smartem_workspace.commands.sync import print_sync_results, sync_all_repos
from smartem_workspace.config.loader import load_config
from smartem_workspace.setup.bootstrap import bootstrap_workspace
from smartem_workspace.utils.paths import find_workspace_root

app = typer.Typer(
name="smartem-workspace",
Expand Down Expand Up @@ -67,17 +75,138 @@ def init(


@app.command()
def sync() -> None:
"""Sync existing repos (git pull)."""
console.print("[yellow]Not implemented yet[/yellow]")
raise typer.Exit(1)
def check(
scope: Annotated[
str | None,
typer.Option("--scope", "-s", help="Check scope: claude, repos, serena, or all"),
] = None,
fix: Annotated[
bool,
typer.Option("--fix", help="Attempt to fix issues"),
] = False,
path: Annotated[
Path | None,
typer.Option("--path", "-p", help="Workspace path (auto-detected if not specified)"),
] = None,
offline: Annotated[
bool,
typer.Option("--offline", help="Use bundled config instead of fetching from GitHub"),
] = False,
) -> None:
"""Verify workspace setup and optionally repair issues."""
workspace_path = path or find_workspace_root()
if workspace_path is None:
console.print("[red]Could not find workspace root. Run from within a workspace or specify --path.[/red]")
raise typer.Exit(1)

config = load_config(offline=offline)
if config is None:
console.print("[red]Failed to load configuration[/red]")
raise typer.Exit(1)

check_scope = CheckScope.ALL
if scope:
try:
check_scope = CheckScope(scope.lower())
except ValueError:
console.print(f"[red]Invalid scope: {scope}. Use: claude, repos, serena, or all[/red]")
raise typer.Exit(1) from None

console.print(f"[bold]Checking workspace at {workspace_path}...[/bold]")
reports = run_checks(workspace_path, config, check_scope)

for report in reports:
print_report(report)

total_errors = sum(r.has_errors for r in reports)
total_warnings = sum(r.has_warnings for r in reports)
total_fixable = sum(r.fixable_count for r in reports)

console.print()
if total_errors or total_warnings:
parts = []
if total_errors:
parts.append(f"[red]{total_errors} error(s)[/red]")
if total_warnings:
parts.append(f"[yellow]{total_warnings} warning(s)[/yellow]")
console.print(f"Summary: {', '.join(parts)}")

if fix and total_fixable:
console.print("\n[bold]Applying fixes...[/bold]")
fixed, failed = apply_fixes(workspace_path, reports)
console.print(f"\nFixed {fixed} issue(s), {failed} failed")
if failed:
raise typer.Exit(1)
elif total_fixable and not fix:
console.print(f"\n[dim]{total_fixable} issue(s) can be fixed with --fix[/dim]")
raise typer.Exit(1)
else:
raise typer.Exit(1)
else:
console.print("[green]All checks passed![/green]")


@app.command()
def status() -> None:
"""Show workspace status."""
console.print("[yellow]Not implemented yet[/yellow]")
raise typer.Exit(1)
def sync(
dry_run: Annotated[
bool,
typer.Option("--dry-run", "-n", help="Show what would be done without making changes"),
] = False,
path: Annotated[
Path | None,
typer.Option("--path", "-p", help="Workspace path (auto-detected if not specified)"),
] = None,
) -> None:
"""Pull latest changes from all cloned repositories."""
workspace_path = path or find_workspace_root()
if workspace_path is None:
console.print("[red]Could not find workspace root. Run from within a workspace or specify --path.[/red]")
raise typer.Exit(1)

config = load_config()
if config is None:
console.print("[red]Failed to load configuration[/red]")
raise typer.Exit(1)

console.print("[bold blue]SmartEM Workspace Sync[/bold blue]")
console.print(f"Workspace: {workspace_path}")

results = sync_all_repos(workspace_path, config, dry_run=dry_run)
print_sync_results(results)

errors = sum(1 for r in results if r.status == "error")
if errors:
raise typer.Exit(1)

if dry_run:
would_update = sum(1 for r in results if r.status == "dry-run")
if would_update:
console.print("\n[dim]Run without --dry-run to apply changes[/dim]")


@app.command()
def status(
path: Annotated[
Path | None,
typer.Option("--path", "-p", help="Workspace path"),
] = None,
) -> None:
"""Show workspace status (alias for check --scope all)."""
workspace_path = path or find_workspace_root()
if workspace_path is None:
console.print("[red]Could not find workspace root.[/red]")
raise typer.Exit(1)

config = load_config()
if config is None:
console.print("[red]Failed to load configuration[/red]")
raise typer.Exit(1)

console.print(f"[bold]Workspace Status: {workspace_path}[/bold]")
reports = run_checks(workspace_path, config, CheckScope.ALL)

for report in reports:
print_report(report)


@app.command()
Expand Down
13 changes: 13 additions & 0 deletions packages/smartem-workspace/smartem_workspace/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Command implementations for smartem-workspace CLI."""

from smartem_workspace.commands.check import CheckReport, CheckResult, CheckScope, run_checks
from smartem_workspace.commands.sync import SyncResult, sync_all_repos

__all__ = [
"CheckReport",
"CheckResult",
"CheckScope",
"SyncResult",
"run_checks",
"sync_all_repos",
]
Loading