use std::path::Path;
use ctx::error::{CtxError, Result};
use ctx::exit::Outcome;
use ctx::harness::{
self,
doctor::{is_healthy, run_doctor_checks, Severity},
FileAction, Mode,
};
use crate::cli::{HarnessCommand, HarnessMode, HarnessTarget};
pub fn run_harness(cmd: HarnessCommand, json: bool) -> Result<Outcome> {
match cmd {
HarnessCommand::Init {
target,
mode,
force,
} => {
let root = std::env::current_dir()?;
let HarnessTarget::Claude = target;
let mode = match mode {
HarnessMode::Local => Mode::Local,
HarnessMode::Plugin => Mode::Plugin,
};
run_init(&root, mode, force, json)
}
HarnessCommand::Compat { require } => run_compat(&require),
HarnessCommand::Doctor => {
let root = std::env::current_dir()?;
run_doctor(&root, json)
}
}
}
fn run_init(root: &Path, mode: Mode, force: bool, json_mode: bool) -> Result<Outcome> {
let plan = match mode {
Mode::Local => harness::plan_local(root),
Mode::Plugin => harness::plan_plugin(root),
};
let actions = harness::write_plan(root, &plan, force)?;
for (rel, action) in &actions {
match action {
FileAction::Created => eprintln!(" created {rel}"),
FileAction::Regenerated => eprintln!(" regenerated {rel}"),
FileAction::Overwritten => eprintln!(" overwrote {rel} (--force)"),
FileAction::SkippedModified => eprintln!(
" warning: skipped {rel}: modified since generation (use --force to overwrite)"
),
FileAction::SkippedForeign => eprintln!(
" warning: skipped {rel}: exists but was not generated by ctx (use --force to overwrite)"
),
FileAction::SkippedPolicy => eprintln!(
" note: skipped {rel}: never overwritten by ctx (it encodes your policy), not even with --force"
),
}
}
let snippet = harness::render_settings_snippet();
let guidance = harness::render_claude_md_block(root);
let settings_action = if mode == Mode::Local {
Some(harness::wire_local_settings(root)?)
} else {
None
};
if json_mode {
let files: Vec<serde_json::Value> = actions
.iter()
.map(|(rel, action)| serde_json::json!({ "path": rel, "action": action.as_str() }))
.collect();
let mut data = serde_json::json!({
"mode": match mode { Mode::Local => "local", Mode::Plugin => "plugin" },
"target": "claude",
"force": force,
"files": files,
});
if mode == Mode::Local {
data["settings_snippet"] = serde_json::Value::String(snippet);
data["claude_md_block"] = serde_json::Value::String(guidance);
if let Some(action) = &settings_action {
data["settings_action"] =
serde_json::Value::String(settings_action_str(action).to_string());
}
}
ctx::json::emit("harness.init", data)?;
return Ok(Outcome::Clean);
}
match mode {
Mode::Local => {
eprintln!();
match settings_action.expect("local mode always wires settings") {
harness::SettingsWireAction::Created => {
eprintln!("wired ctx hooks into .claude/settings.json");
}
harness::SettingsWireAction::Merged => {
eprintln!("merged ctx hooks into existing .claude/settings.json");
}
harness::SettingsWireAction::AlreadyWired => {
eprintln!(".claude/settings.json already wired for ctx");
}
harness::SettingsWireAction::SkippedInvalid => {
eprintln!(
"warning: .claude/settings.json is not valid JSON; not modified. \
Merge this snippet manually:"
);
eprintln!();
println!("{snippet}");
}
}
eprintln!();
eprintln!("Add this block to your CLAUDE.md so the model knows about ctx:");
eprintln!();
println!("{guidance}");
eprintln!("Then verify the integration with 'ctx harness doctor'.");
}
Mode::Plugin => {
eprintln!();
eprintln!("Plugin scaffold ready. Install it locally with:");
eprintln!(" claude");
eprintln!(" /plugin marketplace add ./");
eprintln!(" /plugin install ctx@ctx-local");
eprintln!("See the generated README.md for the full walkthrough.");
if !cfg!(feature = "mcp") {
eprintln!();
eprintln!(
"note: .mcp.json was not generated because this ctx build lacks the mcp \
feature. Install one with 'cargo install agentis-ctx --features mcp' and \
rerun 'ctx harness init --mode plugin' to wire the MCP server."
);
}
}
}
Ok(Outcome::Clean)
}
fn settings_action_str(action: &harness::SettingsWireAction) -> &'static str {
match action {
harness::SettingsWireAction::Created => "created",
harness::SettingsWireAction::Merged => "merged",
harness::SettingsWireAction::AlreadyWired => "already_wired",
harness::SettingsWireAction::SkippedInvalid => "skipped_invalid",
}
}
fn run_compat(require: &str) -> Result<Outcome> {
let current = semver::Version::parse(env!("CARGO_PKG_VERSION"))
.map_err(|e| CtxError::Other(format!("cannot parse own version: {e}")))?;
if ctx::harness::compat::satisfies(¤t, require)? {
Ok(Outcome::Clean)
} else {
eprintln!(
"ctx v{current} does not satisfy the required version '{require}'; update ctx and rerun 'ctx harness init'"
);
Ok(Outcome::CompatMismatch)
}
}
fn run_doctor(root: &Path, json_mode: bool) -> Result<Outcome> {
let findings = run_doctor_checks(root);
let healthy = is_healthy(&findings);
let errors = findings
.iter()
.filter(|f| f.severity == Severity::Error)
.count();
let warnings = findings
.iter()
.filter(|f| f.severity == Severity::Warning)
.count();
let info = findings.len() - errors - warnings;
if json_mode {
ctx::json::emit(
"harness.doctor",
serde_json::json!({
"healthy": healthy,
"binary_version": env!("CARGO_PKG_VERSION"),
"mcp_compiled": cfg!(feature = "mcp"),
"templates_version": env!("CARGO_PKG_VERSION"),
"summary": { "errors": errors, "warnings": warnings, "info": info },
"checks": findings,
}),
)?;
} else {
for finding in &findings {
let tag = match finding.severity {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Info => "info",
};
println!("[{tag}] {}: {}", finding.code, finding.message);
if let Some(hint) = &finding.hint {
println!(" hint: {hint}");
}
}
println!();
if healthy {
println!("Harness healthy ({info} info).");
} else {
println!(
"Problems found: {errors} error{}, {warnings} warning{}.",
if errors == 1 { "" } else { "s" },
if warnings == 1 { "" } else { "s" }
);
}
}
Ok(if healthy {
Outcome::Clean
} else {
Outcome::Findings
})
}