use std::io::{self, Write};
use std::path::Path;
use tracing::info;
use crate::config::{Config, DEFAULT_PROJECT_CONFIG, DEFAULT_PROMPTS};
use crate::error::{Error, Result};
use crate::project::{PROMPTS_FILE_DEFAULT, ProjectLayout};
use crate::store::Store;
pub fn run(path: &Path, force: bool) -> Result<()> {
let layout = ProjectLayout::new(path);
if path.exists() {
let confirmed = if force {
true
} else {
confirm_overwrite(path)?
};
if !confirmed {
return Err(Error::Store(format!(
"init aborted — `{}` left untouched",
path.display()
)));
}
if let Ok(cwd) = std::env::current_dir() {
if let Ok(abs_target) = std::fs::canonicalize(path) {
if cwd.starts_with(&abs_target) {
return Err(Error::Store(format!(
"refusing to wipe `{}` — your current directory lives inside it",
abs_target.display()
)));
}
}
}
std::fs::remove_dir_all(path).map_err(Error::Io)?;
}
layout.create_layout()?;
let config_path = layout.config_path();
std::fs::write(&config_path, DEFAULT_PROJECT_CONFIG)?;
info!(path = %config_path.display(), "wrote project config");
let prompts_path = layout.root.join(PROMPTS_FILE_DEFAULT);
std::fs::write(&prompts_path, DEFAULT_PROMPTS)?;
info!(path = %prompts_path.display(), "wrote prompt library");
let cfg = Config::load(&config_path)?;
let store = Store::open(layout.clone(), &cfg)?;
if cfg.ai.reseed_prompt_examples {
if let Err(e) = seed_prompt_examples(&cfg, &store) {
tracing::warn!(
target: "inkhaven::init",
"could not seed Prompts.book examples: {e}",
);
}
}
eprintln!("Initialized inkhaven project at {}", layout.root.display());
eprintln!(" config: {}", layout.config_path().display());
eprintln!(" prompts: {}", layout.root.join(PROMPTS_FILE_DEFAULT).display());
eprintln!(" store db: {}", layout.metadata_db_path().display());
eprintln!(" vecstore: {}", layout.vecstore_path().display());
eprintln!(" books: {}", layout.books_path().display());
Ok(())
}
pub(crate) fn seed_prompt_examples(cfg: &Config, store: &Store) -> Result<()> {
use crate::store::hierarchy::Hierarchy;
use crate::store::{
InsertPosition, NodeKind, SYSTEM_TAG_PROMPTS,
};
let lang = if cfg.language.trim().is_empty() {
"English".to_owned()
} else {
cfg.language.trim().to_owned()
};
let seeds: [(&str, String); 5] = [
(
"grammar-check.example",
format!(
"// F7 — grammar check the open paragraph.\n\
// Rename this paragraph to `grammar-check` (drop `.example`)\n\
// to take effect; until then inkhaven uses the built-in default.\n\n\
{}\n",
crate::tui::app::grammar_check_default_prompt(&lang),
),
),
(
"explain-diagnostic.example",
format!(
"// Ctrl+F12 — AI-explain the typst diagnostic at the cursor.\n\
// Rename to `explain-diagnostic` to take effect.\n\n\
{}\n",
crate::tui::app::explain_diagnostic_default_prompt(),
),
),
(
"critique-edit.example",
format!(
"// F12 (editor mode) — what's weak about the open paragraph.\n\
// Rename to `critique-edit` to take effect.\n\n\
{}\n",
crate::tui::app::critique_edit_default_prompt(),
),
),
(
"critique-changes.example",
format!(
"// F12 (split-edit mode) — evaluate the changes from the snapshot.\n\
// Rename to `critique-changes` to take effect.\n\n\
{}\n",
crate::tui::app::critique_changes_default_prompt(),
),
),
(
"timeline-health.example",
format!(
"// 1.2.6+ — Ctrl+V t · y/Y/Ctrl+Y · timeline\n\
// consistency audit. Rename to `timeline-health`\n\
// to take effect.\n\n\
{}\n",
crate::tui::app::timeline_health_default_prompt(),
),
),
];
let hierarchy = Hierarchy::load(store)?;
let prompts_book = hierarchy
.iter()
.find(|n| {
n.kind == NodeKind::Book
&& n.system_tag.as_deref() == Some(SYSTEM_TAG_PROMPTS)
})
.cloned()
.ok_or_else(|| {
Error::Store("Prompts system book missing after Store::open".into())
})?;
for (title, body) in &seeds {
let h = Hierarchy::load(store)?;
let already = h.iter().any(|n| {
n.kind == NodeKind::Paragraph
&& n.parent_id == Some(prompts_book.id)
&& n.title.eq_ignore_ascii_case(title)
});
if already {
continue;
}
let mut created = store.create_node(
cfg,
&h,
NodeKind::Paragraph,
title,
Some(&prompts_book),
None,
InsertPosition::End,
)?;
if let Some(rel) = &created.file {
let abs = store.project_root().join(rel);
std::fs::write(&abs, body.as_bytes()).map_err(Error::Io)?;
store.update_paragraph_content(&mut created, body.as_bytes())?;
}
}
Ok(())
}
fn confirm_overwrite(path: &Path) -> Result<bool> {
eprint!(
"Directory `{}` already exists. Remove it and re-initialise? [y/N] ",
path.display()
);
io::stderr().flush().ok();
let mut buf = String::new();
if io::stdin().read_line(&mut buf).map_err(Error::Io)? == 0 {
return Ok(false);
}
let answer = buf.trim().to_ascii_lowercase();
Ok(matches!(answer.as_str(), "y" | "yes"))
}