use crate::{config_path, load_config, open_store, scc_dir};
fn engine_err(e: scc_engine::EngineError) -> crate::CliError {
let msg = e.to_string();
if msg.contains("database is locked") || msg.contains("database table is locked") {
return crate::CliError::Other(format!(
"{msg} (another scc process holds the index — retry, or stop `scc watch`/`scc serve` first)"
));
}
crate::CliError::Other(msg)
}
use std::io::Write;
use std::path::{Path, PathBuf};
pub fn cmd_init(root: &Path) -> crate::Result<()> {
let dir = scc_dir(root);
std::fs::create_dir_all(&dir)?;
let cfg_path = config_path(root);
if !cfg_path.exists() {
std::fs::write(&cfg_path, scc_indexer::Config::default_yaml())?;
println!("created {}", cfg_path.display());
} else {
println!("config exists: {}", cfg_path.display());
}
let store = open_store(root)?;
crate::ensure_scc_ignored(root);
println!(
"initialized SCC workspace for repository '{}' at {}",
store.repo_name,
dir.display()
);
println!("next: scc index");
Ok(())
}
pub fn cmd_index(root: &Path, quiet: bool) -> crate::Result<()> {
let config = load_config(root)?;
let report = scc_engine::index::full(root, &config).map_err(engine_err)?;
if !quiet {
println!(
"indexed {} file(s) ({} changed, {} added, {} removed, {} failed) in {:.2}s",
report.indexed,
report.changed,
report.added,
report.removed,
report.failed,
report.duration_ms as f64 / 1000.0
);
println!("analysis_quality: {}", report.analysis_quality.compact_line());
let s = &report.scan_stats;
println!(
"files: discovered={} indexed={} ignored={} unsupported={} oversized={} unreadable={}",
s.discovered, s.indexed, s.ignored, s.unsupported, s.oversized, s.unreadable
);
}
Ok(())
}
pub fn cmd_index_paths(root: &Path, paths: &[String], quiet: bool) -> crate::Result<()> {
let config = load_config(root)?;
let report = scc_engine::index::refresh_paths(root, &config, paths).map_err(engine_err)?;
if !quiet && report.indexed > 0 {
println!("refreshed {} file(s)", report.indexed);
}
Ok(())
}
pub fn cmd_status(root: &Path) -> crate::Result<()> {
let store = open_store(root)?;
let s = scc_engine::status::status(&store).map_err(engine_err)?;
println!("Repository: {} ({})", s.repository, s.repository_id);
if let Some(url) = &s.remote {
println!("Remote: {url}");
}
if s.indexed {
println!("Revision: {}", s.revision);
if let Some(b) = &s.branch {
println!("Branch: {b}");
}
println!("Indexed at: {}", s.indexed_at.as_deref().unwrap_or(""));
let mut keys: Vec<&String> = s.stats.keys().collect();
keys.sort();
for k in keys {
println!("{k}: {}", s.stats[k]);
}
if s.freshness == "CURRENT" {
println!("freshness: CURRENT — model matches working tree");
} else {
println!(
"freshness: STALE — {} file(s) changed since index (run `scc index`)",
s.stale_count
);
for f in &s.stale_files {
println!(" {f}");
}
}
if let Some(raw) = &s.analysis_quality {
match serde_json::from_str::<scc_core::AnalysisQuality>(raw) {
Ok(q) => println!("analysis_quality: {}", q.compact_line()),
Err(_) => println!("analysis_quality: {raw}"),
}
}
if let Some(v) = &s.scan_stats {
let n = |k: &str| v.get(k).and_then(|x| x.as_u64()).unwrap_or(0);
println!(
"files: discovered={} indexed={} ignored={} unsupported={} oversized={} unreadable={}",
n("discovered"),
n("indexed"),
n("ignored"),
n("unsupported"),
n("oversized"),
n("unreadable")
);
}
} else {
println!("not indexed yet — run `scc index`");
}
Ok(())
}
pub fn cmd_scan(root: &Path, path: Option<&str>, json: bool) -> crate::Result<()> {
let config = load_config(root)?;
let v = scc_engine::status::scan(root, &config, path).map_err(engine_err)?;
let indexed = v["indexed"].as_array().cloned().unwrap_or_default();
let skipped = v["skipped"].as_array().cloned().unwrap_or_default();
let strf = |o: &serde_json::Value, k: &str| o.get(k).and_then(|x| x.as_str()).unwrap_or("").to_string();
if json {
println!("{v}");
return Ok(());
}
if let Some(q) = path {
let q = q.trim_start_matches("./");
if let Some(f) = indexed.iter().find(|f| f.get("path").and_then(|p| p.as_str()) == Some(q)) {
println!("{}: indexed ({} {})", strf(f, "path"), strf(f, "language"), strf(f, "kind"));
return Ok(());
}
if let Some(sk) = skipped.iter().find(|f| f.get("path").and_then(|p| p.as_str()) == Some(q)) {
match sk.get("rule").and_then(|r| r.as_str()) {
Some(rule) => println!("{q}: {} ({rule})", sk.get("reason").and_then(|r| r.as_str()).unwrap_or("")),
None => println!("{q}: {}", sk.get("reason").and_then(|r| r.as_str()).unwrap_or("")),
}
std::process::exit(1);
}
println!("{q}: not found (no such file in working tree)");
std::process::exit(2);
}
let st = &v["stats"];
let n = |k: &str| st.get(k).and_then(|x| x.as_u64()).unwrap_or(0);
println!(
"scan: discovered={} indexed={} ignored={} unsupported={} oversized={} unreadable={} (live walk, not index-time counts)",
n("discovered"), n("indexed"), n("ignored"), n("unsupported"), n("oversized"), n("unreadable")
);
if !skipped.is_empty() {
use std::collections::BTreeMap;
let mut by_rule: BTreeMap<String, usize> = BTreeMap::new();
for sk in &skipped {
let key = format!("{}: {}", sk.get("reason").and_then(|r| r.as_str()).unwrap_or("?"), sk.get("rule").and_then(|r| r.as_str()).unwrap_or("?"));
*by_rule.entry(key).or_default() += 1;
}
println!("\nskips by rule:");
for (rule, n) in &by_rule {
println!(" {n:>6} {rule}");
}
}
if v.get("skipped_detail_capped").and_then(|x| x.as_u64()).unwrap_or(0) > 0 {
println!(
"\n(detail capped: showing {} of {} skipped paths; counts above are exact — rerun with a path (`scc scan <path>`) or `--json` for one verdict)",
skipped.len(),
skipped.len() + v.get("skipped_detail_capped").and_then(|x| x.as_u64()).unwrap_or(0) as usize
);
}
{
use std::collections::BTreeMap;
let mut by_dir: BTreeMap<String, usize> = BTreeMap::new();
for f in &indexed {
let p = f.get("path").and_then(|x| x.as_str()).unwrap_or("");
let top = p.split('/').next().unwrap_or(".").to_string();
let key = if p.contains('/') { top } else { "(root)".to_string() };
*by_dir.entry(key).or_default() += 1;
}
println!("\nindexed by top dir:");
let mut rows: Vec<_> = by_dir.into_iter().collect();
rows.sort_by_key(|r| std::cmp::Reverse(r.1));
for (dir, n) in rows.iter().take(15) {
println!(" {n:>6} {dir}/");
}
}
println!("\nrerun with a path (`scc scan <path>`) for one verdict, or `--json` for the full lists.");
Ok(())
}
pub fn cmd_languages() -> crate::Result<()> {
print!("{}", scc_core::support_matrix_markdown());
Ok(())
}
pub fn cmd_overview(root: &Path, json: bool) -> crate::Result<()> {
let store = open_store(root)?;
let config = load_config(root)?;
let stale = crate::stale_paths(&store)?;
let engine = scc_engine::workspace::open_engine(&store, &config, stale).map_err(engine_err)?;
let pack = engine.context().overview().map_err(engine_err)?;
if json {
println!("{}", serde_json::to_string_pretty(&pack)?);
} else {
print!("{}", pack.content);
}
Ok(())
}
pub fn cmd_atlas(
root: &Path,
budget: Option<usize>,
json: bool,
full: bool,
unbounded: bool,
) -> crate::Result<()> {
let store = open_store(root)?;
let config = load_config(root)?;
let stale = crate::stale_paths(&store)?;
let engine = scc_engine::workspace::open_engine(&store, &config, stale).map_err(engine_err)?;
let pack = engine.context().atlas(budget, full, unbounded).map_err(engine_err)?;
if json {
println!("{}", serde_json::to_string(&pack)?);
} else {
print!("{}", pack.content);
}
Ok(())
}
pub fn cmd_context_startup(root: &Path, budget_tokens: Option<usize>, full_size: bool) -> crate::Result<()> {
let store = open_store(root)?;
let config = load_config(root)?;
let stale = crate::stale_paths(&store)?;
let engine = scc_engine::workspace::open_engine(&store, &config, stale).map_err(engine_err)?;
let (startup, text) = engine.context().startup(&scc_api::StartupRequest { budget: budget_tokens }).map_err(engine_err)?;
if full_size {
let delivered: usize = scc_core::estimate_tokens(&text);
println!("startup full-size accounting");
println!(" delivered_tokens: {delivered}");
println!(" surface_tokens: {}", startup.surface_render.token_count);
println!(" atlas_budget_used: {}", startup.atlas_budget_used);
println!(" coverage_notes: {}", startup.coverage.len());
if startup.omissions.iter().any(|o| o != "none") {
println!("dropped/truncated sections:");
for o in &startup.omissions {
if o == "none" {
continue;
}
println!(" - {o}");
}
println!("recover with: scc surface --explain | scc context task <goal> | scc context structural --files <paths>");
} else {
println!("dropped/truncated sections: none (pack is complete)");
}
return Ok(());
}
print!("{text}");
Ok(())
}
pub fn cmd_surface(
root: &Path,
task: Option<&str>,
budget: Option<usize>,
explain: bool,
) -> crate::Result<()> {
let store = open_store(root)?;
let config = load_config(root)?;
let stale = crate::stale_paths(&store)?;
let engine = scc_engine::workspace::open_engine(&store, &config, stale).map_err(engine_err)?;
let tokens = budget.unwrap_or(scc_core::ContextBudget::default().surface);
let scorer = match task {
Some(goal) => {
let (scorer, _reranker) = scc_engine::inference::rankers(&store, &config, goal);
scorer
}
None => None,
};
let semantic: Option<&dyn scc_context::rank::SemanticScorer> =
scorer.as_ref().map(|s| s as &dyn scc_context::rank::SemanticScorer);
let req = scc_api::SurfaceRequest { task: task.map(|s| s.to_string()), budget: Some(tokens), explain, stages: None };
let (_result, text) = engine.context().surface(&req, semantic).map_err(engine_err)?;
print!("{text}");
Ok(())
}
pub fn cmd_important(
root: &Path,
limit: usize,
component: Option<&str>,
task: Option<&str>,
json: bool,
) -> crate::Result<()> {
let store = open_store(root)?;
let config = load_config(root)?;
let stale = crate::stale_paths(&store)?;
let engine = scc_engine::workspace::open_engine(&store, &config, stale).map_err(engine_err)?;
let (entries, tasked) = engine.context().important(limit, component, task).map_err(engine_err)?;
if json {
println!("{}", serde_json::to_string_pretty(&entries)?);
return Ok(());
}
print!("{}", scc_context::surface::render_important(&entries, tasked));
Ok(())
}
pub fn cmd_context_structural(
root: &Path,
files: &[String],
task: Option<&str>,
budget: Option<usize>,
) -> crate::Result<String> {
let out = scc_engine::invoke(
root,
"context.structural",
serde_json::json!({"files": files, "task": task, "budget": budget}),
)
.map_err(engine_err)?;
Ok(out.get("text").and_then(|t| t.as_str()).unwrap_or("").to_string())
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TaskContextArtifact {
pub pack: scc_context::ContextPack,
pub delta: String,
#[serde(default)]
pub delta_ids: Vec<String>,
#[serde(default)]
pub token_count: usize,
}
pub fn build_task_context(
root: &Path,
goal: &str,
files: &[String],
symbols: &[String],
budget: Option<usize>,
hook: bool,
) -> crate::Result<TaskContextArtifact> {
build_task_context_engine(root, goal, files, symbols, budget, hook).map_err(engine_err).map(from_engine_artifact)
}
fn from_engine_artifact(a: scc_engine::TaskContextArtifact) -> TaskContextArtifact {
TaskContextArtifact { pack: a.pack, delta: a.delta, delta_ids: a.delta_ids, token_count: a.token_count }
}
fn build_task_context_engine(
root: &Path,
goal: &str,
files: &[String],
symbols: &[String],
budget: Option<usize>,
hook: bool,
) -> scc_engine::Result<scc_engine::TaskContextArtifact> {
let out = scc_engine::invoke(
root,
"context.task",
serde_json::json!({"goal": goal, "files": files, "symbols": symbols, "budget": budget, "hook": hook}),
)?;
serde_json::from_value(out).map_err(|e| scc_engine::EngineError::Other(e.to_string()))
}
pub fn build_enriched_task_pack(
root: &Path,
goal: &str,
files: &[String],
symbols: &[String],
budget: Option<usize>,
hook: bool,
) -> crate::Result<scc_context::ContextPack> {
let out = scc_engine::invoke(
root,
"context.task_pack",
serde_json::json!({"goal": goal, "files": files, "symbols": symbols, "budget": budget, "hook": hook}),
)
.map_err(engine_err)?;
serde_json::from_value(out).map_err(|e: serde_json::Error| crate::CliError::Other(e.to_string()))
}
pub fn cmd_context_task(
root: &Path,
goal: &str,
files: &[String],
symbols: &[String],
budget: Option<usize>,
json: bool,
hook: bool,
) -> crate::Result<()> {
if hook {
let config = load_config(root)?;
if !config.context.inject_task_focus {
return Ok(());
}
}
let artifact = build_task_context(root, goal, files, symbols, budget, hook)?;
if json {
println!("{}", serde_json::to_string_pretty(&artifact)?);
} else {
print!("{}", artifact.pack.content);
print!("\n{}", artifact.delta);
}
Ok(())
}
pub fn build_task_pack(
root: &Path,
goal: &str,
files: &[String],
symbols: &[String],
budget: Option<usize>,
) -> crate::Result<scc_context::ContextPack> {
build_enriched_task_pack(root, goal, files, symbols, budget, false)
}
pub fn cmd_context_task_json(
root: &Path,
goal: &str,
files: &[String],
symbols: &[String],
budget: Option<usize>,
) -> crate::Result<String> {
Ok(serde_json::to_string_pretty(&build_task_context(
root, goal, files, symbols, budget, false,
)?)?)
}
pub fn cmd_context_docs(root: &Path, dependency: &str) -> crate::Result<()> {
print!("{}", scc_engine::invoke(root, "context.external_docs", serde_json::json!({"dependency": dependency})).map_err(engine_err)?);
Ok(())
}
pub fn cmd_context_subagent(
root: &Path,
goal: &str,
files: &[String],
symbols: &[String],
budget: Option<usize>,
json: bool,
) -> crate::Result<()> {
let store = open_store(root)?;
let config = load_config(root)?;
let stale = crate::stale_paths(&store)?;
let engine = scc_engine::workspace::open_engine(&store, &config, stale).map_err(engine_err)?;
let pack = engine.context().subagent(goal, files, symbols, budget).map_err(engine_err)?;
if json {
println!("{}", serde_json::to_string_pretty(&pack)?);
} else {
print!("{}", pack.content);
}
Ok(())
}
pub fn cmd_context_component(root: &Path, id: &str, json: bool, unbounded: bool) -> crate::Result<()> {
let store = open_store(root)?;
let config = load_config(root)?;
let stale = crate::stale_paths(&store)?;
let engine = scc_engine::workspace::open_engine(&store, &config, stale).map_err(engine_err)?;
let pack = engine.context().component(&scc_api::DetailRequest { id: id.into(), unbounded }).map_err(engine_err)?;
if json {
println!("{}", serde_json::to_string_pretty(&pack)?);
} else {
print!("{}", pack.content);
}
Ok(())
}
pub fn cmd_context_flow(root: &Path, id: &str, json: bool, unbounded: bool) -> crate::Result<()> {
let store = open_store(root)?;
let config = load_config(root)?;
let stale = crate::stale_paths(&store)?;
let engine = scc_engine::workspace::open_engine(&store, &config, stale).map_err(engine_err)?;
let pack = engine.context().flow(&scc_api::DetailRequest { id: id.into(), unbounded }).map_err(engine_err)?;
if json {
println!("{}", serde_json::to_string_pretty(&pack)?);
} else {
print!("{}", pack.content);
}
Ok(())
}
pub fn cmd_impact(
root: &Path,
files: &[String],
symbols: &[String],
diff: Option<&str>,
json: bool,
unbounded: bool,
) -> crate::Result<()> {
let store = open_store(root)?;
let config = load_config(root)?;
let stale = crate::stale_paths(&store)?;
let engine = scc_engine::workspace::open_engine(&store, &config, stale).map_err(engine_err)?;
let pack = engine.context().impact(&scc_api::ImpactRequest { files: files.to_vec(), symbols: symbols.to_vec(), diff: diff.map(|s| s.to_string()), unbounded }).map_err(engine_err)?;
if json {
println!("{}", serde_json::to_string_pretty(&pack)?);
} else {
print!("{}", pack.content);
}
Ok(())
}
pub fn cmd_verify(
root: &Path,
warnings_only: bool,
json: bool,
unbounded: bool,
) -> crate::Result<()> {
let store = open_store(root)?;
let config = load_config(root)?;
let stale = crate::stale_paths(&store)?;
let engine = scc_engine::workspace::open_engine(&store, &config, stale).map_err(engine_err)?;
let pack = engine.context().verify(unbounded).map_err(engine_err)?;
if warnings_only {
for w in &pack.warnings {
println!("âš {w}");
}
return Ok(());
}
if json {
println!("{}", serde_json::to_string_pretty(&pack)?);
return Ok(());
}
print!("{}", pack.content);
if !pack.content.ends_with('\n') {
println!();
}
Ok(())
}
pub fn cmd_drift(root: &Path, json: bool) -> crate::Result<()> {
let store = open_store(root)?;
let findings = scc_engine::misc::drift(&store).map_err(engine_err)?;
if json {
println!("{}", serde_json::to_string_pretty(&findings)?);
} else {
if findings.is_empty() {
println!("no drift findings");
}
for f in &findings {
println!("[{0}] {1} (#{2}, {3}): {4}", f.severity, f.kind, f.id, f.created_at, f.message);
}
}
Ok(())
}
pub fn cmd_system(_root: &Path, members: &[std::path::PathBuf], json: bool) -> crate::Result<()> {
let (members, all) = scc_engine::systems::stitch(members).map_err(engine_err)?;
if json {
println!("{}", serde_json::to_string_pretty(&all)?);
return Ok(());
}
println!(
"system: {} members, {} stitches",
members.len(),
all.len()
);
for m in &members {
println!("member {} ({})", m.repo_id, m.root.display());
}
for s in &all {
println!(
"[{:?}/{:?}] {} ({} ends)",
s.kind,
s.match_kind,
s.key,
s.ends.len()
);
for e in &s.ends {
match e.role {
Some(role) => println!(" {} {} ({role:?})", e.repo_id, e.entity_id),
None => println!(" {} {}", e.repo_id, e.entity_id),
}
}
}
Ok(())
}
pub fn cmd_history(root: &Path, json: bool) -> crate::Result<()> {
let store = open_store(root)?;
let revs = scc_engine::history::revisions(&store).map_err(engine_err)?;
if json {
println!("{}", serde_json::to_string_pretty(&revs)?);
return Ok(());
}
if revs.is_empty() {
println!("no graph revisions recorded");
return Ok(());
}
for r in &revs {
println!(
"rev {} (base {}): {} entities, {} rels, {} files | src={} ext={} | {}",
r.rev,
r.base_rev,
r.entity_count,
r.rel_count,
r.file_count,
&r.source_hash[..r.source_hash.len().min(12)],
r.extractor_version,
r.created_at,
);
}
Ok(())
}
pub fn cmd_diff(root: &Path, from: i64, to: i64, json: bool) -> crate::Result<()> {
let store = open_store(root)?;
let d = scc_engine::history::diff(&store, from, to).map_err(engine_err)?;
if json {
println!("{}", serde_json::to_string_pretty(&d)?);
return Ok(());
}
println!("diff rev {from}..{to}:");
for (label, ids) in [
("added entities", &d.added_entities),
("removed entities", &d.removed_entities),
("modified entities", &d.modified_entities),
("added relationships", &d.added_relationships),
("removed relationships", &d.removed_relationships),
("modified relationships", &d.modified_relationships),
] {
println!(" {label}: {}", ids.len());
for id in ids.iter().take(20) {
println!(" {id}");
}
if ids.len() > 20 {
println!(" … ({} more)", ids.len() - 20);
}
}
if !d.modified_kinds.is_empty() {
let kinds: Vec<String> = d
.modified_kinds
.iter()
.map(|(k, v)| format!("{k}:{v}"))
.collect();
println!(" modified kinds: {}", kinds.join(", "));
}
Ok(())
}
pub fn cmd_snapshot_save(
root: &Path,
task: &str,
budget: Option<usize>,
json: bool,
) -> crate::Result<()> {
let snap = scc_engine::misc::snapshot_save(root, task, budget).map_err(engine_err)?;
if json {
println!("{}", serde_json::to_string_pretty(&snap)?);
} else {
println!("snapshot {} ({} visible ids)", snap.id, snap.entity_ids.len());
}
Ok(())
}
pub fn cmd_snapshot_show(root: &Path, id: &str) -> crate::Result<()> {
let store = open_store(root)?;
match scc_engine::snapshots::get(&store, id).map_err(engine_err)? {
Some(s) => print!("{}", s.artifact),
None => println!("no snapshot {id}"),
}
Ok(())
}
pub fn cmd_snapshot_diff(root: &Path, id: &str, json: bool) -> crate::Result<()> {
let store = open_store(root)?;
match scc_engine::snapshots::diff(&store, id).map_err(engine_err)? {
Some(d) => {
if json {
println!("{}", serde_json::to_string_pretty(&d)?);
} else {
println!(
"snapshot rev {} vs current rev {}: {} still valid, {} invalidated, {} modified{}",
d.snapshot_revision,
d.current_revision,
d.still_valid.len(),
d.invalidated.len(),
d.modified_entities.len(),
if d.artifact_changed { " (artifact would re-render differently)" } else { "" },
);
for (label, ids) in [
("invalidated", &d.invalidated),
("modified entities", &d.modified_entities),
("changed relationships", &d.changed_relationships),
("changed contracts", &d.changed_contracts),
("changed state", &d.changed_state),
("changed flows", &d.changed_flows),
] {
if ids.is_empty() {
continue;
}
println!(" {label} ({}):", ids.len());
for f in ids.iter().take(20) {
println!(" - {f}");
}
if ids.len() > 20 {
println!(" … ({} more)", ids.len() - 20);
}
}
}
}
None => println!("no snapshot {id}"),
}
Ok(())
}
pub fn cmd_export(root: &Path, format: &str) -> crate::Result<()> {
let store = open_store(root)?;
match format {
"system-ir.json" => println!("{}", serde_json::to_string_pretty(&scc_engine::exports::system_ir(&store).map_err(engine_err)?)?),
"system-ir.jsonl" => {
let ir = scc_engine::exports::system_ir(&store).map_err(engine_err)?;
for line in scc_engine::exports::jsonl(&ir).map_err(engine_err)? {
println!("{line}");
}
}
"ccg" => println!("{}", serde_json::to_string_pretty(&scc_engine::exports::ccg(&scc_engine::exports::system_ir(&store).map_err(engine_err)?).map_err(engine_err)?)?),
"flow-graphs.json" => {
println!(
"{}",
serde_json::to_string_pretty(&store.flow_graphs()?)?
)
}
"capsule.md" => print!("{}", scc_engine::exports::capsule(root).map_err(engine_err)?),
other => {
return Err(crate::CliError::Other(format!(
"unknown export format '{other}' (use system-ir.json, system-ir.jsonl, ccg, or capsule.md)"
)))
}
}
Ok(())
}
pub fn cmd_query(root: &Path, query: &str, limit: usize) -> crate::Result<()> {
let store = open_store(root)?;
let hit = scc_engine::graph::query(&store, &scc_api::QueryRequest { query: query.into(), limit }).map_err(engine_err)?;
if !hit.symbols.is_empty() {
println!("— where —");
for (name, _sig, _kind, file, line) in &hit.symbols {
if *line > 0 {
println!("where: {name} {file}:{line}");
} else {
println!("where: {name} {file}");
}
}
}
println!("— entities —");
for e in &hit.entities {
println!("{} [{}]", e.name, e.kind);
}
println!("— symbols —");
for (name, sig, kind, file, line) in &hit.symbols {
if *line > 0 {
println!("{name} ({kind}) {file}:{line} {sig}");
} else {
println!("{name} ({kind}) {file} {sig}");
}
}
Ok(())
}
pub fn cmd_traverse(
root: &Path,
kind: Option<&str>,
name: Option<&str>,
from: &[String],
steps: &[String],
limit: usize,
) -> crate::Result<()> {
let mut parsed = Vec::new();
for spec in steps {
let mut parts = spec.splitn(3, ':');
let dir = parts.next().unwrap_or("").to_string();
if !["out", "in", "both"].contains(&dir.as_str()) {
return Err(crate::CliError::Other(format!(
"bad --step '{spec}' (dir must be out|in|both, e.g. out:calls:symbol)"
)));
}
parsed.push(scc_api::TraverseStep {
dir,
predicate: parts.next().filter(|s| !s.is_empty()).map(str::to_string),
where_kind: parts.next().filter(|s| !s.is_empty()).map(str::to_string),
limit: 0,
});
}
let store = open_store(root)?;
let config = load_config(root)?;
let stale = crate::stale_paths(&store)?;
let engine = scc_engine::workspace::open_engine(&store, &config, stale).map_err(engine_err)?;
let cc = engine.ctx();
let (entities, rels) = scc_engine::graph::traverse(
&cc,
&scc_api::TraverseRequest {
kind: kind.map(str::to_string),
name: name.map(str::to_string),
from_ids: from.to_vec(),
steps: parsed,
limit,
trusted_only: true,
},
)
.map_err(engine_err)?;
println!("— entities ({}) —", entities.len());
for e in &entities {
println!("{} [{}] {}", e.name, e.kind, e.id);
}
println!("— relationships ({}) —", rels.len());
for r in &rels {
println!("{} -{}-> {}", r.subject, r.predicate, r.object);
}
Ok(())
}
pub fn cmd_list_components(root: &Path) -> crate::Result<()> {
let store = open_store(root)?;
for c in scc_engine::graph::components(&store).map_err(engine_err)? {
println!("{}", c.name);
}
Ok(())
}
pub fn cmd_list_flows(root: &Path) -> crate::Result<()> {
let store = open_store(root)?;
for f in scc_engine::graph::flows(&store).map_err(engine_err)? {
println!("{} [{}] {}", f.name, crate::flow_kind_str(&f.kind), f.trigger.unwrap_or_default());
}
Ok(())
}
pub fn cmd_cochange(root: &Path, min_commits: u32) -> crate::Result<()> {
let (pairs, n) = scc_engine::misc::cochange(root, min_commits).map_err(engine_err)?;
if pairs.is_empty() {
println!("no co-change pairs with >= {min_commits} shared commits");
} else {
println!("co-change pairs (>= {min_commits} shared commits):");
for p in &pairs {
println!(" {} <-> {} ×{}", p.a, p.b, p.commits);
}
}
if n > 0 {
println!("enriched {n} components with co-change signal");
}
Ok(())
}
pub fn cmd_check_invariants(root: &Path) -> crate::Result<bool> {
let store = open_store(root)?;
let violations = scc_engine::misc::check_invariants(&store).map_err(engine_err)?;
for v in &violations {
println!("{}", v.message);
}
Ok(violations.is_empty())
}
pub fn cmd_ci_check(root: &Path, max_severity: &str) -> crate::Result<bool> {
let store = open_store(root)?;
let violations = scc_engine::misc::check_invariants(&store).map_err(engine_err)?;
for v in &violations {
println!("{}", v.message);
}
let (ok, lines) = scc_engine::misc::ci_check(&store, &violations, max_severity).map_err(engine_err)?;
for l in &lines {
if l.starts_with("[ci:") || *l == "ci check passed" {
println!("{l}");
}
}
Ok(ok)
}
pub fn cmd_checkpoint_save(root: &Path, json: bool) -> crate::Result<()> {
let data = scc_engine::checkpoint::capture(root).map_err(engine_err)?;
if json {
println!("{}", serde_json::to_string(&data)?);
} else {
println!("checkpoint saved to {}", crate::checkpoint_path(root).display());
}
Ok(())
}
pub fn cmd_checkpoint_load(root: &Path, inject: bool) -> crate::Result<()> {
if let Some(content) = scc_engine::checkpoint::load(root).map_err(engine_err)? {
print!("{content}");
} else if !inject {
println!("no checkpoint found");
}
Ok(())
}
pub fn cmd_watch(root: &Path) -> crate::Result<()> {
crate::httpd::watch_loop(root)
}
pub fn cmd_serve(root: &Path) -> crate::Result<()> {
crate::httpd::serve(root)
}
pub fn cmd_mcp(root: &Path) -> crate::Result<()> {
crate::mcp::serve_stdio(root)
}
pub fn cmd_setup_claude(root: &Path) -> crate::Result<()> {
crate::plugin::install(root)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetupHarness {
Claude,
Codex,
Opencode,
Omp,
Pi,
}
pub fn detect_harnesses(home: &Path, path_dirs: &[PathBuf], root: &Path) -> Vec<SetupHarness> {
let on_path = |bin: &str| {
path_dirs
.iter()
.any(|d| d.join(bin).is_file() || d.join(format!("{bin}.exe")).is_file())
};
let mut out = Vec::new();
if on_path("claude") || home.join(".claude").is_dir() {
out.push(SetupHarness::Claude);
}
if on_path("codex") || home.join(".codex").is_dir() {
out.push(SetupHarness::Codex);
}
if on_path("opencode") || home.join(".config").join("opencode").is_dir() {
out.push(SetupHarness::Opencode);
}
if on_path("omp") || root.join(".omp").is_dir() {
out.push(SetupHarness::Omp);
}
if on_path("pi") || root.join(".pi").is_dir() || home.join(".pi").is_dir() {
out.push(SetupHarness::Pi);
}
out
}
fn install_harness(root: &Path, h: SetupHarness) -> crate::Result<()> {
match h {
SetupHarness::Claude => cmd_setup_claude(root),
SetupHarness::Codex => crate::compress::cmd_setup_codex(root),
SetupHarness::Opencode => crate::compress::cmd_setup_opencode(root),
SetupHarness::Omp => crate::plugin_omp::cmd_setup_omp(root),
SetupHarness::Pi => crate::plugin_omp::cmd_setup_pi(root),
}
}
pub fn cmd_setup_detected(root: &Path, all: bool) -> crate::Result<()> {
let targets: Vec<SetupHarness> = if all {
vec![
SetupHarness::Claude,
SetupHarness::Codex,
SetupHarness::Opencode,
SetupHarness::Omp,
SetupHarness::Pi,
]
} else {
let home = std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("/"));
let path_dirs: Vec<PathBuf> = std::env::var_os("PATH")
.map(|p| std::env::split_paths(&p).collect())
.unwrap_or_default();
detect_harnesses(&home, &path_dirs, root)
};
if targets.is_empty() {
println!("No supported harness detected (looked for claude/codex/opencode/omp/pi");
println!("binaries, ~/.claude, ~/.codex, ~/.config/opencode, ~/.pi, and .omp//.pi/ dirs).");
println!("Run `scc setup all` to install for every harness, or `scc setup <harness>`.");
return Ok(());
}
for h in &targets {
println!("=== {:?} ===", h);
install_harness(root, *h)?;
println!();
}
println!("Installed for: {}", targets.iter().map(|h| format!("{h:?}")).collect::<Vec<_>>().join(", "));
if targets.contains(&SetupHarness::Codex) {
println!("Remaining manual step: add the printed entry to ~/.codex/hooks.json (user scope).");
}
Ok(())
}
pub fn cmd_ingest_runtime(root: &Path, body: &str) -> crate::Result<()> {
scc_engine::state::ingest_runtime(root, body).map_err(engine_err)?;
println!("accepted");
Ok(())
}
fn adapter_scope(name: &str) -> &'static str {
match name {
"context7" => "network+subprocess(npx)",
"serena" | "beads" | "cbm" | "hindsight" | "scip" | "gitnexus" | "narsil" => "filesystem",
_ => "filesystem",
}
}
fn configured_adapters(root: &Path) -> crate::Result<Vec<(String, &'static str)>> {
use scc_indexer::adapters::IntegrationCategory;
let config = load_config(root)?;
let enabled = |key: Option<&str>| -> bool {
match key {
None => true,
Some("serena") => config.integrations.serena,
Some("beads") => config.integrations.beads,
Some("hindsight") => config.integrations.hindsight,
Some("gitnexus") => config.integrations.gitnexus,
Some("context7_command") => !config.integrations.context7_command.is_empty(),
Some(_) => false,
}
};
Ok(scc_indexer::adapters::integration_registry()
.into_iter()
.filter(|d| {
d.category != IntegrationCategory::CompatibilityOnly
&& d.category != IntegrationCategory::InternalPass
&& d.category != IntegrationCategory::AgentIntegration
&& enabled(d.config_key)
})
.map(|d| (d.id.to_string(), adapter_scope(d.id)))
.collect())
}
pub fn cmd_adapters(root: &Path, json: bool) -> crate::Result<()> {
let manifests = scc_indexer::adapters::adapter_manifests();
if json {
println!("{}", serde_json::to_string_pretty(&manifests)?);
return Ok(());
}
for (name, scope) in configured_adapters(root)? {
println!("adapter: {name} scope: {scope}");
}
Ok(())
}
pub fn cmd_update(version: Option<&str>, dir: Option<&std::path::Path>, dry_run: bool) -> crate::Result<()> {
let exe = std::env::current_exe().map_err(|e| crate::CliError::Other(format!("cannot locate running binary: {e}")))?;
let default_dir = exe.parent().map(|p| p.to_path_buf()).unwrap_or_else(|| std::path::PathBuf::from("."));
let target_dir = dir.map(|p| p.to_path_buf()).unwrap_or(default_dir);
let resolved: String = match version {
Some(v) => v.to_string(),
None => latest_release_tag()?,
};
let base = std::env::var("SCC_DOWNLOAD_BASE").unwrap_or_else(|_| {
"https://github.com/carterlasalle/scc/releases/download".to_string()
});
let trimmed = base.trim_end_matches('/');
let release_dir = trimmed.strip_suffix("/releases/download").map(|prefix| {
format!("{prefix}/releases/download/v{resolved}")
});
let installer_url = match release_dir {
Some(dir) => format!("{dir}/install.sh"),
None => format!("{trimmed}/install.sh"),
};
let mut cmd = std::process::Command::new("sh");
cmd.arg("-c").arg(format!(
"curl -fsSL {url:?} | sh -s -- --version {ver} --dir {dir} {dry}",
url = installer_url,
ver = resolved,
dir = target_dir.display(),
dry = if dry_run { "--dry-run" } else { "" },
));
let status = cmd.status().map_err(|e| crate::CliError::Other(format!("update failed to launch installer: {e}")))?;
if !status.success() {
return Err(crate::CliError::Other(format!("installer exited with status {status}")));
}
Ok(())
}
fn latest_release_tag() -> crate::Result<String> {
let api_base = std::env::var("SCC_API_BASE")
.unwrap_or_else(|_| "https://api.github.com/repos/carterlasalle/scc".to_string());
let url = format!("{}/releases/latest", api_base.trim_end_matches('/'));
let out = std::process::Command::new("curl")
.args(["-fsSL", "--max-time", "15", &url])
.output()
.map_err(|e| crate::CliError::Other(format!("latest-release lookup failed: {e}")))?;
if !out.status.success() {
return Err(crate::CliError::Other(format!("latest-release lookup failed: {url}")));
}
let v: serde_json::Value = serde_json::from_slice(&out.stdout)
.map_err(|e| crate::CliError::Other(format!("latest-release response not JSON: {e}")))?;
let tag = v.get("tag_name").and_then(|t| t.as_str()).unwrap_or("").trim_start_matches('v');
if tag.is_empty() {
return Err(crate::CliError::Other("latest release has no tag_name".to_string()));
}
Ok(tag.to_string())
}
pub fn cmd_timing(root: &Path, all: bool, last: usize, json: bool) -> crate::Result<()> {
let home = std::env::var("HOME").map_err(|e| crate::CliError::Other(format!("cannot locate HOME: {e}")))?;
let log = std::path::PathBuf::from(home).join(".cache").join("scc").join("extension.log");
let raw = std::fs::read_to_string(&log).unwrap_or_default();
let mut rows: Vec<(String, String, f64, bool)> = Vec::new();
for line in raw.lines() {
let v: serde_json::Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(_) => continue,
};
let event = v.get("event").and_then(|e| e.as_str()).unwrap_or("");
if event == "inject" {
continue;
}
if event != "spawn" {
continue;
}
let ms = match v.get("ms").and_then(|m| m.as_f64()) {
Some(m) => m,
None => continue,
};
let cmd = v.get("cmd").and_then(|c| c.as_str()).unwrap_or("?").to_string();
let repo = v.get("repo").and_then(|r| r.as_str()).unwrap_or("?").to_string();
let ok = v.get("ok").and_then(|o| o.as_bool()).unwrap_or(true);
rows.push((cmd, repo, ms, ok));
}
let root_str = root.to_string_lossy().to_string();
if last > 0 && rows.len() > last {
rows = rows.split_off(rows.len() - last);
}
if !all {
rows.retain(|(_, repo, _, _)| repo == &root_str || root_str.starts_with(&format!("{repo}/")));
}
let mut groups: std::collections::BTreeMap<String, Vec<f64>> = std::collections::BTreeMap::new();
let mut fails: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
for (cmd, _, ms, ok) in &rows {
let short = cmd.strip_prefix("scc ").unwrap_or(cmd).split_whitespace().next().unwrap_or(cmd);
let key = format!("scc {short}");
groups.entry(key.clone()).or_default().push(*ms);
if !ok {
*fails.entry(key).or_default() += 1;
}
}
let mut out: Vec<serde_json::Value> = Vec::new();
for (cmd, mut ms) in groups {
ms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = ms.len();
let total: f64 = ms.iter().sum();
let pct = |p: f64| ms[((p * n as f64).ceil() as usize).saturating_sub(1).min(n - 1)];
out.push(serde_json::json!({
"cmd": cmd, "n": n, "total_ms": (total * 10.0).round() / 10.0,
"p50_ms": (pct(0.5) * 10.0).round() / 10.0,
"p95_ms": (pct(0.95) * 10.0).round() / 10.0,
"max_ms": ms[n - 1],
"failed": fails.get(&cmd).cloned().unwrap_or(0),
}));
}
out.sort_by(|a, b| {
b.get("total_ms").and_then(|t| t.as_f64()).unwrap_or(0.0)
.partial_cmp(&a.get("total_ms").and_then(|t| t.as_f64()).unwrap_or(0.0))
.unwrap_or(std::cmp::Ordering::Equal)
});
if json {
println!("{}", serde_json::to_string_pretty(&serde_json::json!({
"log": log.display().to_string(), "repo": if all { None } else { Some(root_str) }, "commands": out,
}))?);
return Ok(());
}
if out.is_empty() {
println!("no timing records ({}; use --all for every repository)", log.display());
return Ok(());
}
println!("{:<28} {:>5} {:>10} {:>8} {:>8} {:>8} {:>6}", "command", "n", "total_ms", "p50", "p95", "max", "fail");
for c in &out {
println!("{:<28} {:>5} {:>10} {:>8} {:>8} {:>8} {:>6}",
c.get("cmd").and_then(|v| v.as_str()).unwrap_or("?"),
c.get("n").and_then(|v| v.as_u64()).unwrap_or(0),
c.get("total_ms").and_then(|v| v.as_f64()).unwrap_or(0.0),
c.get("p50_ms").and_then(|v| v.as_f64()).unwrap_or(0.0),
c.get("p95_ms").and_then(|v| v.as_f64()).unwrap_or(0.0),
c.get("max_ms").and_then(|v| v.as_f64()).unwrap_or(0.0),
c.get("failed").and_then(|v| v.as_u64()).unwrap_or(0));
}
let raw2 = std::fs::read_to_string(&log).unwrap_or_default();
let mut injections = 0usize;
let mut startup_tok = 0u64;
let mut task_tok = 0u64;
let all_rows: Vec<serde_json::Value> = raw2.lines().filter_map(|l| serde_json::from_str(l).ok()).collect();
let scoped: Vec<&serde_json::Value> = if last > 0 && all_rows.len() > last {
all_rows[all_rows.len() - last..].iter().collect()
} else {
all_rows.iter().collect()
};
for v in scoped {
if v.get("event").and_then(|e| e.as_str()) != Some("inject") {
continue;
}
let repo = v.get("repo").and_then(|r| r.as_str()).unwrap_or("?");
if !all && !(repo == root_str.as_str() || root_str.starts_with(&format!("{repo}/"))) {
continue;
}
injections += 1;
startup_tok += v.get("startup_tokens").and_then(|t| t.as_u64()).unwrap_or(0);
task_tok += v.get("task_tokens").and_then(|t| t.as_u64()).unwrap_or(0);
}
println!();
if injections == 0 {
println!("tokens injected: no hook firings recorded (update the extension, then prompt once)");
} else {
println!("tokens injected: {injections} hook firing(s), startup {startup_tok}, task {task_tok}, total {}",
startup_tok + task_tok);
}
Ok(())
}
pub fn cmd_clean(root: &Path, force: bool) -> crate::Result<()> {
let db = root.join(".scc").join("scc.db");
let sidecars = [".scc/scc.db-wal", ".scc/scc.db-shm"];
let mut targets: Vec<std::path::PathBuf> = Vec::new();
if db.exists() {
targets.push(db.clone());
}
for s in sidecars {
let p = root.join(s);
if p.exists() {
targets.push(p);
}
}
if targets.is_empty() {
println!("nothing to clean (no .scc/scc.db at {})", root.display());
return Ok(());
}
let bytes: u64 = targets.iter().map(|p| std::fs::metadata(p).map(|m| m.len()).unwrap_or(0)).sum();
if !force {
println!("will remove {} file(s), freeing {} bytes:", targets.len(), bytes);
for t in &targets {
println!(" {}", t.display());
}
print!("proceed? [y/N] ");
use std::io::Write;
std::io::stdout().flush().map_err(|e| crate::CliError::Other(e.to_string()))?;
let mut answer = String::new();
std::io::stdin().read_line(&mut answer).map_err(|e| crate::CliError::Other(e.to_string()))?;
if !matches!(answer.trim().to_lowercase().as_str(), "y" | "yes") {
println!("aborted");
return Ok(());
}
}
for t in &targets {
std::fs::remove_file(t).map_err(|e| crate::CliError::Other(format!("cannot remove {}: {e}", t.display())))?;
}
println!("removed {} file(s), freed {} bytes; next `scc index` is a cold build", targets.len(), bytes);
Ok(())
}
pub fn cmd_doctor(root: &Path, json: bool, deep: bool, network: bool, strict: bool) -> crate::Result<bool> {
let store = open_store(root)?;
let config = load_config(root)?;
let rep = scc_engine::integrations::doctor_report(&store, &config, root, deep, network).map_err(engine_err)?;
let rows = rep.integrations;
let agent_rows = rep.agents;
let mut warnings = rep.warnings;
let (scc_version, rev, stale_len, dangling, stats) =
(rep.scc_version, rep.revision, rep.stale_files, rep.dangling_edges, rep.stats);
if json {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"scc": scc_version,
"revision": rev,
"stale_files": stale_len,
"dangling_edges": dangling,
"stats": stats,
"integrations": rows,
"agents": agent_rows.iter().map(|a| serde_json::json!({"id": a.id, "present": a.present, "detail": a.detail})).collect::<Vec<_>>(),
"warnings": warnings,
}))?
);
} else {
println!("SCC Doctor");
println!("==========");
println!();
println!("CORE");
println!(" scc {scc_version}");
println!(" revision {rev}");
println!(" stale {} file(s)", stale_len);
println!(" dangling {dangling} edge(s) (see `scc check-invariants`)");
println!(" entities {}", stats.get("entities").copied().unwrap_or(0));
println!();
println!("INTEGRATIONS");
for r in &rows {
let mark = if r.contributing {
"✓"
} else if r.configured {
"!"
} else {
"-"
};
let reach = match r.reachable {
Some(true) => "reachable; ",
Some(false) => "UNREACHABLE; ",
None => "",
};
println!(" {mark} {:<12} [{:<18}] {}{}", r.id, r.category, reach, r.detail);
}
println!();
println!("AGENTS");
for a in &agent_rows {
println!(" {} {:<12} {}", if a.present { "✓" } else { "-" }, a.id, a.detail);
}
println!();
if warnings == 0 {
println!("SUMMARY PASS ({} integrations contributing evidence)", rows.iter().filter(|r| r.contributing).count());
} else {
println!("SUMMARY {warnings} warning(s); run `scc doctor --deep` / `--network` for handshake detail");
}
}
if dangling > 0 {
warnings += 1;
}
Ok(!(strict && warnings > 0))
}
pub fn cmd_lessons_add(root: &Path, text: &str) -> crate::Result<()> {
let dir = scc_dir(root);
std::fs::create_dir_all(&dir)?;
let path = dir.join("lessons.jsonl");
let n = std::fs::read_to_string(&path)
.map(|s| s.lines().filter(|l| !l.trim().is_empty()).count())
.unwrap_or(0);
let id = format!("lesson-{}", n + 1);
let record = serde_json::json!({
"id": id,
"text": text,
"created_at": scc_core::now_rfc3339(),
});
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|e| crate::CliError::Other(format!("lessons: {e}")))?;
writeln!(f, "{record}").map_err(|e| crate::CliError::Other(format!("lessons: {e}")))?;
println!(
"appended {id} to {} (ingest with `scc import hindsight .scc/lessons.jsonl`)",
path.display()
);
Ok(())
}
pub fn cmd_lessons_list(root: &Path, limit: usize) -> crate::Result<()> {
let lessons = scc_engine::state::lessons_list(root, limit).map_err(engine_err)?;
if lessons.is_empty() {
println!("no lessons in the store — run `scc lessons add \"...\"` then `scc import hindsight .scc/lessons.jsonl`");
return Ok(());
}
for (content, tags) in lessons {
let tag_str = if tags.is_empty() {
String::new()
} else {
format!(" [{}]", tags.join(", "))
};
println!("- {content}{tag_str}");
}
Ok(())
}
pub fn cmd_beads(root: &Path) -> crate::Result<()> {
let active = scc_engine::state::beads(root, 20).map_err(engine_err)?;
if active.is_empty() {
println!("no active beads tasks (checked .beads/issues.jsonl)");
return Ok(());
}
println!("active beads tasks:");
for t in active {
println!("- {t}");
}
Ok(())
}
pub fn cmd_import(root: &Path, format: &str, file: &str) -> crate::Result<()> {
let report = scc_engine::state::import_evidence(root, format, file).map_err(engine_err)?;
println!(
"imported {} symbols, {} calls, {} imports ({} errors); evidence epoch bumped, derived layer recompiled",
report.symbols, report.calls, report.imports, report.errors
);
Ok(())
}
pub fn cmd_runtime_status(root: &Path, json: bool) -> crate::Result<()> {
let edges = scc_engine::state::runtime_edges(root).map_err(engine_err)?;
if json {
println!("{}", serde_json::to_string_pretty(&edges)?);
return Ok(());
}
if edges.is_empty() {
println!("no runtime observations ingested");
}
let total: u64 = edges.iter().map(|e| e.count).sum();
let errs: u64 = edges.iter().map(|e| e.errors).sum();
println!("{} observed edge(s), {} observations, {} error(s)", edges.len(), total, errs);
for e in &edges {
println!(
"- {} → {} ×{} (avg {:.1} ms, {} err, last {})",
e.source, e.target, e.count, e.latency_ms, e.errors, e.last_observed
);
}
Ok(())
}
pub fn cmd_runtime_reconcile(root: &Path, json: bool) -> crate::Result<()> {
let rec = scc_engine::state::reconcile(root).map_err(engine_err)?;
if json {
println!("{}", serde_json::to_string_pretty(&rec)?);
return Ok(());
}
println!("static-vs-observed reconciliation");
println!(" matched: {}", rec.matched.len());
println!(" observed not static: {}", rec.observed_not_static.len());
println!(" static not observed: {}", rec.static_not_observed.len());
const MAX_RECONCILE_DETAIL: usize = 20;
let cap = |label: &str, items: &[String]| {
for e in items.iter().take(MAX_RECONCILE_DETAIL) {
println!(" [{label}] {e}");
}
if items.len() > MAX_RECONCILE_DETAIL {
println!(
" …and {} more [{label}] (counts exact; --json for the full list)",
items.len() - MAX_RECONCILE_DETAIL
);
}
};
cap("matched", &rec.matched);
cap("runtime-only", &rec.observed_not_static);
cap("static-only", &rec.static_not_observed);
Ok(())
}
pub fn cmd_diagram(root: &Path, format: &str, out: Option<&str>) -> crate::Result<()> {
let store = open_store(root)?;
if store.snapshot_status()?.is_none() {
return Err(crate::CliError::Other("not indexed yet — run `scc index`".into()));
}
let v = scc_engine::exports::diagram(&store, format).map_err(engine_err)?;
let text = v.get("text").and_then(|t| t.as_str()).unwrap_or("");
if let Some(path) = out {
std::fs::write(path, text)?;
println!(
"diagram: {} nodes, {} edges, {} flows -> {path}",
v.get("nodes").and_then(|n| n.as_u64()).unwrap_or(0),
v.get("edges").and_then(|n| n.as_u64()).unwrap_or(0),
v.get("flows").and_then(|n| n.as_u64()).unwrap_or(0),
);
} else {
print!("{text}");
}
Ok(())
}
pub fn cmd_snap(
root: &Path,
out: Option<&str>,
max_chars: usize,
png: Option<&str>,
color: bool,
) -> crate::Result<()> {
let store = open_store(root)?;
if store.snapshot_status()?.is_none() {
return Err(crate::CliError::Other("not indexed yet — run `scc index`".into()));
}
let config = load_config(root)?;
let map = crate::viewer::map_text(&store, max_chars)?;
let rows = map.lines().count();
let (text_tokens, image_tokens) = crate::viewer::token_estimate(map.len(), rows);
let render = png.is_some() || config.context.snap_enabled;
if let Some(path) = out {
std::fs::write(path, &map)?;
} else {
print!("{map}");
if !map.ends_with('\n') {
println!();
}
}
if color {
eprintln!("note: --color is accepted for recipe parity; the pinned recipe renders monochrome unless edited");
}
println!("snap: {rows} rows, {} chars — text ~{text_tokens} tokens vs PNG ~{image_tokens} image tokens", map.len());
if render {
let dest = png.unwrap_or("scc-snap.png");
render_snap_png(&map, dest)?;
println!("snap png -> {dest}");
} else {
println!("render OFF by default (set context.snap_enabled=true or pass --png <file>); recipe:");
println!("{}", crate::viewer::snap_recipe());
}
Ok(())
}
fn render_snap_png(map: &str, dest: &str) -> crate::Result<()> {
let probe = std::process::Command::new("python3")
.arg("-c")
.arg("import PIL")
.output()
.map_err(|e| crate::CliError::Other(format!("snap: cannot run python3 ({e})")))?;
if !probe.status.success() {
return Err(crate::CliError::Other(
"snap: python3 has no Pillow (pip install pillow); map text written, recipe printed above".into(),
));
}
let dir = tempfile::TempDir::new()?;
let map_path = dir.path().join("map.txt");
let recipe_path = dir.path().join("snap.py");
std::fs::write(&map_path, map)?;
std::fs::write(&recipe_path, crate::viewer::snap_recipe())?;
let out = std::process::Command::new("python3")
.arg(&recipe_path)
.arg(&map_path)
.arg(dest)
.output()
.map_err(|e| crate::CliError::Other(format!("snap: cannot run python3 ({e}); recipe printed above")))?;
if !out.status.success() {
return Err(crate::CliError::Other(format!(
"snap: Pillow render failed: {}",
String::from_utf8_lossy(&out.stderr).trim()
)));
}
Ok(())
}
pub fn cmd_view(root: &Path, port: Option<u16>, no_open: bool) -> crate::Result<()> {
let store = open_store(root)?;
if store.snapshot_status()?.is_none() {
return Err(crate::CliError::Other("not indexed yet — run `scc index`".into()));
}
let addr = match port {
Some(p) => format!("127.0.0.1:{p}"),
None => "127.0.0.1:0".to_string(),
};
let server = tiny_http::Server::http(&addr)
.map_err(|e| crate::CliError::Other(format!("cannot bind {addr}: {e}")))?;
let bound = server.server_addr();
let url = format!("http://{bound}/");
println!("scc view: {url} (root {})", root.display());
if !no_open {
#[cfg(target_os = "macos")]
let _ = std::process::Command::new("open").arg(&url).spawn();
#[cfg(target_os = "linux")]
let _ = std::process::Command::new("xdg-open").arg(&url).spawn();
}
for request in server.incoming_requests() {
let url_path = request.url().to_string();
let method = request.method().clone();
if method == tiny_http::Method::Get && crate::viewer::is_viewer_path(&url_path) {
let store = match open_store(root) {
Ok(s) => s,
Err(e) => {
let _ = request.respond(
tiny_http::Response::from_string(format!("store error: {e}"))
.with_status_code(500),
);
continue;
}
};
let (status, body) = crate::viewer::serve_viewer(&store, &url_path);
let response = tiny_http::Response::from_string(body)
.with_status_code(status)
.with_header(
tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..])
.unwrap(),
);
let _ = request.respond(response);
continue;
}
let _ = request.respond(
tiny_http::Response::from_string("no such viewer route (scc serve owns /v1/*)")
.with_status_code(404),
);
}
Ok(())
}
pub fn cmd_rpc(root: &Path, _stdio: bool) -> crate::Result<()> {
scc_engine::rpc::serve_stdio(root).map_err(engine_err)
}
pub fn cmd_operations(describe: Option<&str>) -> crate::Result<()> {
if let Some(id) = describe {
match scc_engine::ops::describe(id) {
Some(d) => {
println!("{}", serde_json::to_string_pretty(&d)?);
match scc_engine::ops::input_schema(id) {
Some(s) => println!("{}", serde_json::to_string_pretty(&s)?),
None => println!("(no typed input schema: free-form object)"),
}
}
None => println!("unknown operation '{id}' (see `scc operations`)"),
}
return Ok(());
}
println!("Operation Mutation Stream Stability Description");
println!("--------------------------------------------------------------------------------");
for d in scc_engine::ops::OPERATIONS {
println!("{:<26} {:<14} {:<7} {:<13} {}", d.id, format!("{:?}", d.mutation), d.streaming, format!("{:?}", d.stability), d.description);
}
Ok(())
}
pub fn cmd_plugin_list(root: &Path) -> crate::Result<()> {
let out = scc_engine::invoke(root, "plugins.list", serde_json::json!({})).map_err(engine_err)?;
println!("{}", serde_json::to_string_pretty(&out)?);
Ok(())
}
pub fn cmd_plugin_describe(root: &Path, id: &str) -> crate::Result<()> {
let out = scc_engine::invoke(root, "plugins.describe", serde_json::json!({"id": id})).map_err(engine_err)?;
println!("{}", serde_json::to_string_pretty(&out)?);
Ok(())
}
pub fn cmd_plugin_doctor(root: &Path) -> crate::Result<()> {
let out = scc_engine::invoke(root, "plugins.doctor", serde_json::json!({})).map_err(engine_err)?;
println!("{}", serde_json::to_string_pretty(&out)?);
Ok(())
}
pub fn cmd_plugin_graph(root: &Path) -> crate::Result<()> {
let out = scc_engine::invoke(root, "plugins.graph", serde_json::json!({})).map_err(engine_err)?;
let groups = out.get("groups").and_then(|g| g.as_object());
match groups {
Some(g) if !g.is_empty() => {
for (ty, items) in g {
println!("{ty}:");
for it in items.as_array().cloned().unwrap_or_default() {
println!(" {} (priority {})", it.get("key").and_then(|k| k.as_str()).unwrap_or("?"), it.get("priority").and_then(|p| p.as_i64()).unwrap_or(0));
}
}
}
_ => println!("(no extensions registered)"),
}
Ok(())
}
pub fn cmd_plugin_lock(root: &Path) -> crate::Result<()> {
let out = scc_engine::invoke(root, "plugins.lock", serde_json::json!({})).map_err(engine_err)?;
println!("{}", serde_json::to_string_pretty(&out)?);
Ok(())
}
pub fn cmd_plugin_check(root: &Path) -> crate::Result<()> {
let out = scc_engine::invoke(root, "plugins.check", serde_json::json!({})).map_err(engine_err)?;
println!("{}", serde_json::to_string_pretty(&out)?);
if out.get("ok").and_then(|v| v.as_bool()) != Some(true) {
return Err(crate::CliError::Other(format!(
"plugin lock drift: {}",
out.get("drift").map(|v| v.to_string()).unwrap_or_default()
)));
}
Ok(())
}
pub fn cmd_plugin_enable(root: &Path, id: &str) -> crate::Result<()> {
let out = scc_engine::invoke(root, "plugins.enable", serde_json::json!({"id": id})).map_err(engine_err)?;
println!("{}", serde_json::to_string_pretty(&out)?);
Ok(())
}
pub fn cmd_plugin_disable(root: &Path, id: &str) -> crate::Result<()> {
let out = scc_engine::invoke(root, "plugins.disable", serde_json::json!({"id": id})).map_err(engine_err)?;
println!("{}", serde_json::to_string_pretty(&out)?);
Ok(())
}
pub fn cmd_plugin_invoke(root: &Path, operation: &str, input: &str) -> crate::Result<()> {
let input: serde_json::Value = serde_json::from_str(input).unwrap_or(serde_json::json!({}));
let out = scc_engine::invoke(root, operation, input).map_err(engine_err)?;
println!("{}", serde_json::to_string_pretty(&out)?);
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
fn ensure_scc_ignored_keeps_intent_committable() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path();
crate::ensure_scc_ignored(root);
let gi = std::fs::read_to_string(root.join(".gitignore")).unwrap();
assert!(gi.contains(".scc/*"), "{gi}");
assert!(gi.contains("!.scc/intent.yaml"), "{gi}");
assert!(gi.contains("!.scc/plugins.toml"), "{gi}");
assert!(gi.contains("!.scc/plugins.lock"), "{gi}");
crate::ensure_scc_ignored(root);
let gi2 = std::fs::read_to_string(root.join(".gitignore")).unwrap();
assert_eq!(gi, gi2, "idempotent");
std::fs::write(root.join(".gitignore"), "target/\n.scc/\n").unwrap();
crate::ensure_scc_ignored(root);
let gi3 = std::fs::read_to_string(root.join(".gitignore")).unwrap();
assert!(gi3.contains("target/"), "{gi3}");
assert!(!gi3.lines().any(|l| l.trim() == ".scc/"), "{gi3}");
assert!(gi3.contains("!.scc/intent.yaml"), "{gi3}");
}
#[test]
fn detect_harnesses_finds_bins_dirs_and_project_dirs() {
let home = tempfile::TempDir::new().unwrap();
let bindir = home.path().join("bin");
std::fs::create_dir_all(&bindir).unwrap();
std::fs::write(bindir.join("codex"), "#!/bin/sh\n").unwrap();
std::fs::create_dir_all(home.path().join(".config").join("opencode")).unwrap();
let root = tempfile::TempDir::new().unwrap();
std::fs::create_dir_all(root.path().join(".omp")).unwrap();
let found = detect_harnesses(
home.path(),
&[bindir],
root.path(),
);
assert!(found.contains(&SetupHarness::Codex), "codex via PATH: {found:?}");
assert!(found.contains(&SetupHarness::Opencode), "opencode via config dir: {found:?}");
assert!(found.contains(&SetupHarness::Omp), "omp via project dir: {found:?}");
assert!(!found.contains(&SetupHarness::Claude), "no claude present: {found:?}");
assert!(!found.contains(&SetupHarness::Pi), "no pi present: {found:?}");
}
#[test]
fn detect_harnesses_empty_when_nothing_present() {
let home = tempfile::TempDir::new().unwrap();
let root = tempfile::TempDir::new().unwrap();
let found = detect_harnesses(home.path(), &[], root.path());
assert!(found.is_empty(), "{found:?}");
}
use super::*;
#[test]
fn lessons_add_appends_jsonl_lines() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("repo");
std::fs::create_dir_all(&root).unwrap();
cmd_lessons_add(&root, "first lesson").unwrap();
cmd_lessons_add(&root, "second lesson").unwrap();
let path = root.join(".scc/lessons.jsonl");
let text = std::fs::read_to_string(&path).unwrap();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines.len(), 2, "a second add must append, not overwrite");
let first: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
assert_eq!(first["id"], "lesson-1");
assert_eq!(first["text"], "first lesson");
assert!(first["created_at"].is_string());
let second: serde_json::Value = serde_json::from_str(lines[1]).unwrap();
assert_eq!(second["id"], "lesson-2");
assert_eq!(second["text"], "second lesson");
let lesson: scc_indexer::adapters::hindsight::Lesson =
serde_json::from_str(lines[0]).unwrap();
assert_eq!(lesson.body(), "first lesson");
}
#[test]
fn adapters_lists_configured_scope() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("repo");
std::fs::create_dir_all(root.join(".scc")).unwrap();
std::fs::write(
root.join(".scc/config.yaml"),
"schema: 1\nintegrations:\n beads: true\n context7_command: \"npx -y @upstash/context7-mcp\"\n",
)
.unwrap();
let listing = configured_adapters(&root).unwrap();
assert!(
listing.contains(&("beads".to_string(), "filesystem")),
"{listing:?}"
);
assert!(
listing.contains(&("context7".to_string(), "network+subprocess(npx)")),
"{listing:?}"
);
assert!(
!listing.iter().any(|(n, _)| n == "hindsight"),
"disabled integrations must not be listed: {listing:?}"
);
assert!(listing.iter().any(|(n, _)| n == "scip"), "{listing:?}");
assert!(listing.iter().any(|(n, _)| n == "cbm"), "{listing:?}");
for (n, _) in &listing {
assert!(
scc_indexer::adapters::resolve_integration(n).is_some(),
"listed adapter {n} must resolve in the registry: {listing:?}"
);
}
assert!(listing.iter().any(|(n, _)| n == "tracelayer"), "{listing:?}");
assert!(!listing.iter().any(|(n, _)| n == "narsil"), "{listing:?}");
assert!(listing.iter().any(|(n, _)| n == "ccg"), "{listing:?}");
cmd_adapters(&root, false).unwrap();
assert!(cmd_doctor(&root, false, false, false, false).unwrap(), "doctor must pass clean");
assert!(cmd_doctor(&root, true, false, false, false).unwrap(), "doctor --json must pass clean");
}
}