use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
const SKILL_CONTENT: &str = r#"---
name: km-analyze
description: Analyze a code repository using km (code metrics) tool
---
# km — Code Metrics Analysis Skill
You have access to the `km` CLI tool for comprehensive code analysis. Use it to analyze repositories across multiple dimensions.
## Available Commands
Run these via the Bash tool. Always use `--format json` for machine-readable output.
### Lines of Code
```bash
km loc [PATH] --format json
```
Language breakdown: files, blank lines, comment lines, code lines.
### Code Health Score
```bash
km score [PATH] --format json
km score [PATH] --format json --model legacy
```
Overall grade (A++ to F--). Default model (cogcom): 5 dimensions — cognitive complexity, duplication, indentation, Halstead effort, file size. Legacy model (--model legacy): 6 dimensions — MI, cyclomatic complexity, duplication, indentation, Halstead effort, file size.
### Score Diff (requires git)
```bash
km score diff [PATH] --format json --git-ref HEAD~1
```
Compare current code health score against a git ref. Shows per-dimension deltas.
### Cognitive Complexity
```bash
km cogcom [PATH] --format json --top 20
```
SonarSource method (2017). Measures how difficult code is to understand, penalizing deep nesting.
### Cyclomatic Complexity
```bash
km cycom [PATH] --format json --top 20
```
Per-file and per-function complexity. High values indicate hard-to-test code.
### Maintainability Index
```bash
km miv [PATH] --format json --top 20
```
Verifysoft variant (with comment weight). Values below 65 are hard to maintain.
### Halstead Complexity
```bash
km hal [PATH] --format json --top 20 --sort-by effort
```
Effort, volume, and estimated bugs per file.
### Indentation Complexity
```bash
km indent [PATH] --format json
```
Indentation depth stddev — high values suggest deeply nested code.
### Duplicate Code
```bash
km dups [PATH] --format json --report
```
Duplicate blocks across the project.
### Hotspots (requires git)
```bash
km hotspots [PATH] --format json --top 20
```
Files that change frequently AND have high complexity — top refactoring targets.
### Code Ownership (requires git)
```bash
km knowledge [PATH] --format json --top 20
```
Bus factor risk per file via git blame analysis.
### Temporal Coupling (requires git)
```bash
km tc [PATH] --format json --top 20
```
Files that change together in commits — hidden dependencies.
### Blast Radius of a Change (requires git)
```bash
km impact [PATH] --format json --since-ref origin/main
km impact [PATH] --format json --pr 123
```
How far a change reaches before it is merged. `--since-ref` measures the current branch against a ref; `--pr` measures a GitHub pull request and needs the `gh` CLI. Read it in this order:
- `structural.direct`: source files that use what changed. `exposure` is `calls` (it calls a function that changed), `refers` (it uses the module without a call that tells) or `elsewhere` (it only calls functions the change leaves alone). `protection` is `direct` (a test refers to it or mirrors its path), `named` (a test carries its name), `users` (only what uses it is tested) or `none`.
- `structural.unprotected`: files that call what changed and that no test reaches. Each one is a place the change can break unnoticed: recommend an integration test there.
- `structural.untested_entry_points`: command-line tasks and scripts that call what changed and have no test. Mention them after the unprotected files; they are run by hand, so a missing test there weighs less.
- `structural.radius`: files that call what changed, and those that use them, over the source files measured. Past the first step it is by file, so read it as an estimate from above; the direct callers are the firm part. `structural.upper_bound` is the same when every use of a changed file counts whatever the function; quote the radius, not the bound.
- `structural.functions`: the public functions the change affects, or `null` when it touches code outside functions and cannot be narrowed.
- `projects.reached` and `projects.affected`: projects of a monorepo that depend on the changed ones; `affected` is the list whose tests should run.
- `logical_radius.missing`: files that usually change together with the changed ones and are not in the change.
## Analysis Workflow
1. Start with `km score` for the overall health grade
2. Run `km loc` for project size and language breakdown
3. Use `km cogcom`, `km cycom` and `km miv` to find the most complex/unmaintainable files
4. Run `km hotspots` to find high-risk change-prone files
5. Check `km dups` for code duplication opportunities
6. Optionally run `km knowledge` and `km tc` for team/architecture insights
7. Use `km score diff --git-ref HEAD~N` to track score changes over time
8. To review a branch or a pull request, run `km impact` and report the unprotected files first
## Output Format
Produce a structured report with:
- **Overview**: Project size, languages, overall grade
- **Code Health**: Score breakdown by dimension
- **Complexity Hotspots**: Worst files by complexity
- **Maintainability**: Files hardest to maintain
- **Key Findings**: Notable patterns and risks
- **Recommendations**: Prioritized, actionable suggestions
Reference specific file names and metric values. Be concise but thorough.
"#;
pub fn install(provider: &str, with_permissions: bool) -> Result<(), Box<dyn std::error::Error>> {
if provider != "claude" {
return Err(format!("Unsupported provider: {provider}. Supported: claude").into());
}
println!("Where do you want to install the km skill?");
println!(" 1) Project-level (.claude/skills/km-analyze/)");
println!(" 2) User-level (~/.claude/skills/km-analyze/)");
print!("Choose [1/2]: ");
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
let choice = choice.trim();
let (skill_dir, project_root) = match choice {
"1" => {
let repo = git2::Repository::discover(".")
.map_err(|_| "Could not find a git repository. Run from a project directory or use option 2 (user-level).")?;
let workdir = repo
.workdir()
.ok_or("Could not determine repository working directory")?;
(
workdir.join(".claude/skills/km-analyze"),
Some(workdir.to_path_buf()),
)
}
"2" => {
let home = std::env::var("HOME").map_err(|_| "Could not determine home directory")?;
(PathBuf::from(home).join(".claude/skills/km-analyze"), None)
}
_ => return Err("Invalid choice. Please enter 1 or 2.".into()),
};
fs::create_dir_all(&skill_dir)?;
let skill_path = skill_dir.join("SKILL.md");
fs::write(&skill_path, SKILL_CONTENT)?;
println!("Skill installed at: {}", skill_path.display());
println!("Claude Code will now be able to use km for code analysis.");
if with_permissions {
if let Some(root) = project_root {
super::permissions::install(&root)?;
} else {
eprintln!(
"warning: --with-permissions only applies to project-level installs, skipping"
);
}
}
Ok(())
}