pub mod agents;
pub mod detect;
pub mod evidence;
pub mod fit;
pub mod probe;
pub mod report;
pub mod screen;
pub mod settings;
pub mod writer;
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
use crate::init::detect::detect;
use crate::init::evidence::scan;
use crate::init::fit::{measure, Churn};
use crate::init::probe::probe;
use crate::init::report::{
epilogue, notes, render_settings, summarize, summary_line, warnings, Report,
};
use crate::init::screen::{run, Outcome};
use crate::init::settings::{defaults, to_configuration, Confidence, SettingId, Settings, Value};
use crate::init::writer::render;
pub const IWE_MARKER: &str = ".iwe";
pub const CONFIG_FILE_NAME: &str = "config.toml";
pub const EXIT_OK: i32 = 0;
pub const EXIT_ERROR: i32 = 1;
pub const EXIT_ALREADY_INITIALIZED: i32 = 2;
#[derive(Debug, Default, Clone)]
pub struct Overrides {
pub library: Option<String>,
pub link_format: Option<String>,
pub refs_extension: Option<String>,
pub format: Option<String>,
pub date_format: Option<String>,
}
impl Overrides {
fn entries(&self) -> Vec<(SettingId, String)> {
let mut entries = Vec::new();
if let Some(value) = &self.library {
entries.push((SettingId::LibraryPath, value.clone()));
}
if let Some(value) = &self.link_format {
entries.push((SettingId::LinkFormat, value.clone()));
}
if let Some(value) = &self.refs_extension {
entries.push((SettingId::RefsExtension, value.clone()));
}
if let Some(value) = &self.format {
entries.push((SettingId::Format, value.clone()));
}
if let Some(value) = &self.date_format {
entries.push((SettingId::KeyDateFormat, value.clone()));
}
entries
}
fn is_empty(&self) -> bool {
self.entries().is_empty()
}
}
#[derive(Debug, Default, Clone)]
pub struct InitOptions {
pub auto: bool,
pub dry_run: bool,
pub use_defaults: bool,
pub json: bool,
pub overrides: Overrides,
}
pub fn init_library(root: &Path, options: &InitOptions) -> i32 {
let marker = root.join(IWE_MARKER);
if marker.exists() {
return already_initialized(root, &marker, options);
}
let probes = probe(root);
let (mut chosen, base_defaults, evidence) = if options.use_defaults {
let evidence = evidence::Evidence::default();
(defaults(), defaults(), evidence)
} else {
let evidence = scan(root);
let detected = detect(&evidence, &probes);
(detected, defaults(), evidence)
};
apply_overrides(&mut chosen, &options.overrides);
let interactive = !options.auto
&& !options.dry_run
&& !options.use_defaults
&& !options.json
&& std::io::stdin().is_terminal()
&& std::io::stdout().is_terminal();
let mut confirmed = false;
if interactive {
let detected = chosen.clone();
let stdin = std::io::stdin();
let mut input = stdin.lock();
let mut output = std::io::stdout();
match run(
root,
&evidence,
&detected,
&base_defaults,
&mut input,
&mut output,
) {
Outcome::Write(selected) => {
chosen = selected;
confirmed = true;
}
Outcome::Quit => {
println!("nothing written");
return EXIT_OK;
}
}
}
let config = to_configuration(&chosen);
let churn = measure(root, &config);
let default_churn = if options.use_defaults {
churn
} else {
measure(root, &to_configuration(&base_defaults))
};
let rendered = render(&chosen);
let warnings = warnings(&evidence, &chosen, &probes);
let notes = notes(&evidence);
if options.dry_run {
return finish_dry_run(
root,
options,
&chosen,
&evidence,
&rendered,
churn,
default_churn,
warnings,
notes,
);
}
if let Err(error) = std::fs::create_dir(&marker) {
eprintln!("failed to create {}: {}", marker.display(), error);
return EXIT_ERROR;
}
if let Err(error) = std::fs::write(marker.join(CONFIG_FILE_NAME), &rendered) {
eprintln!("failed to write the configuration: {}", error);
return EXIT_ERROR;
}
let mut artifacts = Vec::new();
if chosen.agents_enabled() {
artifacts = write_agent_artifacts(root, &chosen, !confirmed);
}
if options.json {
let mut notes = notes;
notes.extend(artifacts.iter().cloned());
let report = Report {
written: true,
config_path: config_path_display(root, &marker),
settings: chosen.values(),
confidence: chosen.confidences(),
evidence: summarize(&evidence),
normalize_churn: churn,
default_churn,
warnings,
notes,
};
print_json(&report);
return EXIT_OK;
}
if !options.use_defaults && !interactive {
println!("{}", summary_line(&evidence));
}
for warning in &warnings {
println!("warning: {}", warning);
}
for note in ¬es {
println!("note: {}", note);
}
println!("initialized {}", config_path_display(root, &marker));
for artifact in &artifacts {
println!("{}", artifact);
}
if !options.use_defaults && churn.total > 0 {
println!("iwe normalize would rewrite {}", churn.render());
}
for line in epilogue(&probes) {
println!("{}", line);
}
EXIT_OK
}
#[allow(clippy::too_many_arguments)]
fn finish_dry_run(
root: &Path,
options: &InitOptions,
chosen: &Settings,
evidence: &evidence::Evidence,
rendered: &str,
churn: Churn,
default_churn: Churn,
warnings: Vec<String>,
notes: Vec<String>,
) -> i32 {
let marker = root.join(IWE_MARKER);
if options.json {
let report = Report {
written: false,
config_path: config_path_display(root, &marker),
settings: chosen.values(),
confidence: chosen.confidences(),
evidence: summarize(evidence),
normalize_churn: churn,
default_churn,
warnings,
notes,
};
print_json(&report);
return EXIT_OK;
}
println!("{}", summary_line(evidence));
println!();
println!("{}", render_settings(chosen));
println!(
"normalize churn โ detected {} ยท defaults {}",
churn.render(),
default_churn.render()
);
for warning in &warnings {
println!("warning: {}", warning);
}
for note in ¬es {
println!("note: {}", note);
}
println!();
println!("{}", rendered);
println!("dry run โ nothing written");
EXIT_OK
}
fn already_initialized(root: &Path, marker: &Path, options: &InitOptions) -> i32 {
if options.json {
let payload = serde_json::json!({
"written": false,
"config_path": config_path_display(root, marker),
"error": "already initialized",
});
println!(
"{}",
serde_json::to_string_pretty(&payload).expect("report serializes")
);
return EXIT_ALREADY_INITIALIZED;
}
if marker.is_dir() {
eprintln!(
"already initialized โ inspect {}",
config_path_display(root, marker)
);
} else {
eprintln!("{} already exists and is not a directory", IWE_MARKER);
}
EXIT_ALREADY_INITIALIZED
}
fn apply_overrides(settings: &mut Settings, overrides: &Overrides) {
if overrides.is_empty() {
return;
}
for (id, value) in overrides.entries() {
settings.set(
id,
Value::text(value),
Confidence::Overridden,
"set on the command line",
);
}
}
fn write_agent_artifacts(root: &Path, settings: &Settings, print_only: bool) -> Vec<String> {
if print_only {
return vec![
"add this section to AGENTS.md:".to_string(),
agents::instructions(settings),
"register the MCP server in .mcp.json:".to_string(),
agents::mcp_snippet(),
];
}
let mut lines = Vec::new();
match agents::write_instructions(root, settings) {
Ok(message) => lines.push(message),
Err(error) => lines.push(format!("warning: {}", error)),
}
match agents::register_mcp(root) {
Ok(message) => lines.push(message),
Err(error) => lines.push(format!("warning: {}", error)),
}
lines
}
fn config_path_display(root: &Path, marker: &Path) -> String {
let full = marker.join(CONFIG_FILE_NAME);
full.strip_prefix(root)
.map(|relative| relative.display().to_string())
.unwrap_or_else(|_| full.display().to_string())
}
fn print_json(report: &Report) {
println!(
"{}",
serde_json::to_string_pretty(report).expect("report serializes")
);
}
pub fn current_root() -> PathBuf {
std::env::current_dir().expect("to get current dir")
}