Skip to main content

tessera_codegraph/
init.rs

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}