use std::io::IsTerminal;
use std::time::{Duration, Instant};
use anyhow::Result;
use dialoguer::{
theme::{ColorfulTheme, SimpleTheme, Theme},
MultiSelect,
};
use gitcortex_store::branch;
use indicatif::{ProgressBar, ProgressStyle};
mod detect;
pub mod editors;
pub(crate) mod helpers;
pub(crate) mod universal;
use super::serve_lock;
use crate::style;
use detect::parse_editor_flag;
use editors::{install_for_editor, EditorKind};
use helpers::repo_root;
use universal::{
ensure_hooks_scope, initial_index, install_hooks, write_agent_guide, write_ci_workflow,
write_gitcortex_ignore,
};
pub fn run(
ci: bool,
editor: Option<&str>,
global_editor_config: bool,
shared_git_hooks: bool,
) -> Result<()> {
let repo_root = repo_root()?;
if serve_lock::is_active(&repo_root)? {
anyhow::bail!(
"cannot initialise while the repository graph is active; close editor MCP sessions and stop `gcx viz`, then retry"
);
}
ensure_hooks_scope(&repo_root, shared_git_hooks)?;
let repo_id = branch::storage_repo_id(&repo_root);
let start = Instant::now();
print_banner();
let already_prompted = branch::has_prompted_for_editor(&repo_id);
let (editors, should_mark_prompted): (Vec<EditorKind>, bool) = match editor {
Some(flag) => (parse_editor_flag(flag)?, false),
None if !already_prompted
&& std::io::stdin().is_terminal()
&& std::io::stdout().is_terminal() =>
{
(prompt_editor_choice().unwrap_or_default(), true)
}
None => (Vec::new(), false),
};
write_gitcortex_ignore(&repo_root)?;
let spinner = start_spinner("Indexing repository…");
let (nodes, edges) = initial_index(&repo_root)?;
finish_spinner(spinner, "Repository indexed");
if should_mark_prompted {
branch::mark_editor_prompted(&repo_id)?;
}
write_agent_guide(&repo_root)?;
for ed in &editors {
install_for_editor(ed, &repo_root, global_editor_config)?;
}
if ci {
write_ci_workflow(&repo_root)?;
}
let hooks = install_hooks(&repo_root, shared_git_hooks)?;
let editor_names: Vec<&str> = editors.iter().map(|e| e.display_name()).collect();
let ms = start.elapsed().as_millis();
println!();
println!(
"{} GitCortex initialised {}",
style::paint(style::success_style(), "✔"),
style::paint(style::hint_style(), &format!("({ms}ms)"))
);
print_field("Graph", &format!("{nodes} nodes | {edges} edges"));
print_field("Hooks", &format!("{hooks} git hooks installed"));
if editor_names.is_empty() {
print_field(
"Editors",
"none (use `gcx init --editor <name>` to configure one)",
);
} else {
print_field("Editors", &editor_names.join(", "));
}
if !global_editor_config
&& editors
.iter()
.any(|editor| matches!(editor, EditorKind::Windsurf))
{
print_field(
"Note",
"global MCP registration skipped; rerun with --global-editor-config to enable it",
);
}
print_field("Universal", ".gitcortex/AGENT_GUIDE.md, .gitcortex/ignore");
if ci {
print_field("CI", ".github/workflows/gcx-blast-radius.yml");
}
println!();
Ok(())
}
const GLYPH_G: [&str; 7] = [
" ### ", "# #", "# ", "# ##", "# #", "# #", " ### ",
];
const GLYPH_C: [&str; 7] = [
" ####", "# ", "# ", "# ", "# ", "# ", " ####",
];
const GLYPH_X: [&str; 7] = [
"# #", "# #", " # # ", " # ", " # # ", "# #", "# #",
];
const BLOCK: &str = "██";
const BLANK: &str = " ";
const LEFT_BRACKET: [&str; 7] = ["⎡", "⎢", "⎢", "⎢", "⎢", "⎢", "⎣"];
const RIGHT_BRACKET: [&str; 7] = ["⎤", "⎥", "⎥", "⎥", "⎥", "⎥", "⎦"];
fn print_banner() {
if !std::io::stdout().is_terminal() {
println!(
"{} {}",
style::paint(style::brand_style(), "gcx"),
style::paint(style::hint_style(), "GitCortex knowledge graph")
);
return;
}
let brand = style::brand_style();
println!();
for row in 0..7 {
let l = style::paint(brand, LEFT_BRACKET[row]);
let g = render_glyph_row(GLYPH_G[row], brand);
let c = render_glyph_row(GLYPH_C[row], brand);
let x = render_glyph_row(GLYPH_X[row], brand);
let r = style::paint(brand, RIGHT_BRACKET[row]);
println!(" {l} {g} {c} {x} {r}");
}
println!();
println!(
" {}",
style::paint(style::hint_style(), "GitCortex knowledge graph")
);
println!();
}
fn render_glyph_row(row: &str, glyph_style: anstyle::Style) -> String {
let painted: String = row
.chars()
.map(|c| if c == '#' { BLOCK } else { BLANK })
.collect();
style::paint(glyph_style, &painted)
}
fn print_field(label: &str, value: &str) {
println!(
" {}{}",
style::paint(style::label_style(), &format!("{label:<10} ")),
value
);
}
fn start_spinner(message: &str) -> Option<ProgressBar> {
if !std::io::stderr().is_terminal() {
return None;
}
let bar = ProgressBar::new_spinner();
bar.enable_steady_tick(Duration::from_millis(80));
let template = if style::enabled() {
"{spinner:.cyan} {msg}"
} else {
"{spinner} {msg}"
};
bar.set_style(
ProgressStyle::with_template(template).unwrap_or_else(|_| ProgressStyle::default_spinner()),
);
bar.set_message(message.to_owned());
Some(bar)
}
fn finish_spinner(bar: Option<ProgressBar>, message: &str) {
if let Some(bar) = bar {
bar.finish_and_clear();
println!("{} {message}", style::paint(style::success_style(), "✔"));
}
}
fn prompt_editor_choice() -> Result<Vec<EditorKind>> {
const CHOICES: &[(&str, &str)] = &[
("Claude Code", "claude"),
("Cursor", "cursor"),
("Windsurf", "windsurf"),
("GitHub Copilot", "copilot"),
("Antigravity", "antigravity"),
("Codex", "codex"),
];
let labels: Vec<&str> = CHOICES.iter().map(|(label, _)| *label).collect();
let colorful = ColorfulTheme::default();
let plain = SimpleTheme;
let theme: &dyn Theme = if style::enabled() { &colorful } else { &plain };
let selection = MultiSelect::with_theme(theme)
.with_prompt("Which AI assistant(s) do you use? (space to toggle, enter to confirm)")
.items(&labels)
.interact_opt();
let indices = match selection {
Ok(Some(indices)) => indices,
Ok(None) | Err(_) => return Ok(Vec::new()),
};
let mut editors = Vec::new();
for index in indices {
editors.extend(parse_editor_flag(CHOICES[index].1)?);
}
Ok(editors)
}