1use std::fmt::{self, Display};
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use anyhow::Result;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone)]
9pub struct InitOptions {
10 pub root: PathBuf,
11 pub db_path: PathBuf,
12 pub git_hooks: bool,
13 pub mcp_configs: bool,
14 pub force: bool,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct InitResult {
19 pub root: String,
20 pub created: Vec<String>,
21 pub skipped: Vec<String>,
22 pub next_steps: Vec<String>,
23}
24
25impl Display for InitResult {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 writeln!(f, "Initialized Tessera defaults under {}", self.root)?;
28 if !self.created.is_empty() {
29 writeln!(f, "Created:")?;
30 for path in &self.created {
31 writeln!(f, " {path}")?;
32 }
33 }
34 if !self.skipped.is_empty() {
35 writeln!(f, "Skipped existing files:")?;
36 for path in &self.skipped {
37 writeln!(f, " {path}")?;
38 }
39 }
40 writeln!(f, "Next:")?;
41 for step in &self.next_steps {
42 writeln!(f, " {step}")?;
43 }
44 Ok(())
45 }
46}
47
48pub fn run(options: InitOptions) -> Result<InitResult> {
49 let root = options
50 .root
51 .canonicalize()
52 .unwrap_or_else(|_| options.root.clone());
53 fs::create_dir_all(root.join(".tessera"))?;
54
55 let mut created = Vec::new();
56 let mut skipped = Vec::new();
57
58 write_file(
59 &root,
60 ".tessera/config.toml",
61 &config_toml(&options.db_path),
62 options.force,
63 &mut created,
64 &mut skipped,
65 )?;
66
67 if options.mcp_configs {
68 write_file(
69 &root,
70 ".tessera/mcp/codex.toml",
71 &codex_mcp(&options.db_path),
72 options.force,
73 &mut created,
74 &mut skipped,
75 )?;
76 write_file(
77 &root,
78 ".tessera/mcp/claude.json",
79 &claude_mcp(&options.db_path),
80 options.force,
81 &mut created,
82 &mut skipped,
83 )?;
84 write_file(
85 &root,
86 ".tessera/mcp/cursor.json",
87 &cursor_mcp(&options.db_path),
88 options.force,
89 &mut created,
90 &mut skipped,
91 )?;
92 }
93
94 if options.git_hooks {
95 write_file(
96 &root,
97 ".git/hooks/post-merge",
98 "#!/bin/sh\ntessera index .\n",
99 options.force,
100 &mut created,
101 &mut skipped,
102 )?;
103 write_file(
104 &root,
105 ".git/hooks/post-checkout",
106 "#!/bin/sh\ntessera index .\n",
107 options.force,
108 &mut created,
109 &mut skipped,
110 )?;
111 }
112
113 Ok(InitResult {
114 root: root.display().to_string(),
115 created,
116 skipped,
117 next_steps: vec![
118 format!("tessera index . --db {}", options.db_path.display()),
119 format!("tessera doctor --db {}", options.db_path.display()),
120 "tessera impact <symbol>".to_string(),
121 ],
122 })
123}
124
125fn write_file(
126 root: &Path,
127 rel_path: &str,
128 content: &str,
129 force: bool,
130 created: &mut Vec<String>,
131 skipped: &mut Vec<String>,
132) -> Result<()> {
133 let path = root.join(rel_path);
134 if path.exists() && !force {
135 skipped.push(rel_path.to_string());
136 return Ok(());
137 }
138 if let Some(parent) = path.parent() {
139 fs::create_dir_all(parent)?;
140 }
141 fs::write(&path, content)?;
142 created.push(rel_path.to_string());
143 Ok(())
144}
145
146fn config_toml(db_path: &Path) -> String {
147 format!(
148 "# Tessera project defaults\n[index]\ndb = \"{}\"\nwatch_poll_ms = 500\nwatch_debounce_ms = 250\n\n[include]\n# Empty means all supported source files under the root.\npaths = []\n\n[exclude]\n# Relative path prefixes to skip after built-in ignores.\npaths = []\n\n[ignore]\n# Built-in ignores already cover .git, node_modules, target, dist, build, out, coverage, vendor, .next, virtualenvs, caches, and .tessera.\nextra = []\n",
149 db_path.display()
150 )
151}
152
153fn codex_mcp(db_path: &Path) -> String {
154 format!(
155 "[[mcp_servers]]\nname = \"tessera\"\ncommand = \"tessera\"\nargs = [\"mcp\", \"--db\", \"{}\"]\n",
156 db_path.display()
157 )
158}
159
160fn claude_mcp(db_path: &Path) -> String {
161 format!(
162 "{{\n \"mcpServers\": {{\n \"tessera\": {{\n \"command\": \"tessera\",\n \"args\": [\"mcp\", \"--db\", \"{}\"]\n }}\n }}\n}}\n",
163 db_path.display()
164 )
165}
166
167fn cursor_mcp(db_path: &Path) -> String {
168 claude_mcp(db_path)
169}