cellstate/commands/
init.rs

1//! `cellstate init` — Scaffold a CELLSTATE project config in the current directory.
2//!
3//! Creates:
4//!   .cellstate/
5//!     config.toml      ← project settings
6//!     .env.example     ← API key template
7//!   cellstate.lock       ← agent definition checksums (empty on init)
8
9use crate::http;
10use crate::ui;
11use console::style;
12use dialoguer::{theme::ColorfulTheme, Input, Select};
13use std::fs;
14use std::path::Path;
15
16const CONFIG_TOML: &str = r#"# CELLSTATE Project Configuration
17# Docs: https://cellstate.batterypack.dev/docs/config
18
19[project]
20name = "{name}"
21
22# API connection
23[api]
24# Set via CELLSTATE_API_KEY env var or .env file.
25# Get a key at https://cellstate.batterypack.dev or run `cellstate start` for local dev.
26base_url = "{base_url}"
27
28# Memory defaults
29[memory]
30max_tokens = 4096
31default_user_id = "default"
32default_agent_id = "default"
33
34# Agent definitions live in cstate.toml files (see: cellstate.batterypack.dev/docs/packs)
35[packs]
36# path = "./agents"
37"#;
38
39const ENV_EXAMPLE: &str = r#"# CELLSTATE Environment Variables
40# Copy this to .env and fill in your values.
41# Never commit .env to version control.
42
43# API key from https://cellstate.batterypack.dev (or `cellstate start` for local dev)
44CELLSTATE_API_KEY=cst_...
45
46# API endpoint — defaults to local dev server
47CELLSTATE_BASE_URL=http://localhost:3000
48"#;
49
50const LOCK_INIT: &str = r#"# cellstate.lock — DO commit this file.
51# Auto-generated checksums of your agent definitions.
52# Run `cellstate pack emit all` to update.
53
54version = 1
55agents = []
56"#;
57
58const GITIGNORE_ADDITION: &str = r#"
59# CELLSTATE
60.cellstate/local/
61.env
62"#;
63
64pub fn run() -> anyhow::Result<()> {
65    ui::print_banner();
66
67    let cwd = std::env::current_dir()?;
68    let project_name = cwd
69        .file_name()
70        .and_then(|n| n.to_str())
71        .unwrap_or("my-project")
72        .to_string();
73
74    // Check if already initialized
75    if Path::new(".cellstate/config.toml").exists() {
76        ui::warn("Already initialized (.cellstate/config.toml exists).");
77        let reinit = dialoguer::Confirm::with_theme(&ColorfulTheme::default())
78            .with_prompt("Re-initialize? This will overwrite config.toml.")
79            .default(false)
80            .interact()?;
81        if !reinit {
82            return Ok(());
83        }
84    }
85
86    ui::step("Project setup");
87
88    let name: String = Input::with_theme(&ColorfulTheme::default())
89        .with_prompt("Project name")
90        .default(project_name)
91        .interact_text()?;
92
93    let local_label = format!("Local dev ({})", http::DEFAULT_BASE_URL);
94    let env_options = &[
95        local_label.as_str(),
96        "Managed cloud (https://cst.batterypack.dev)",
97        "Self-hosted (enter URL)",
98    ];
99    let env_choice = Select::with_theme(&ColorfulTheme::default())
100        .with_prompt("CELLSTATE server")
101        .items(env_options)
102        .default(0)
103        .interact()?;
104
105    let base_url = match env_choice {
106        0 => http::DEFAULT_BASE_URL.to_string(),
107        1 => "https://cst.batterypack.dev".to_string(),
108        _ => {
109            let url: String = Input::with_theme(&ColorfulTheme::default())
110                .with_prompt("Server URL")
111                .interact_text()?;
112            url
113        }
114    };
115
116    // Create .cellstate/ directory
117    ui::step("Creating project files...");
118    fs::create_dir_all(".cellstate")?;
119    fs::create_dir_all(".cellstate/local")?;
120
121    // Write config.toml
122    let config_content = CONFIG_TOML
123        .replace("{name}", &name)
124        .replace("{base_url}", &base_url);
125    fs::write(".cellstate/config.toml", &config_content)?;
126    ui::ok(".cellstate/config.toml");
127
128    // Write .env.example
129    fs::write(".cellstate/.env.example", ENV_EXAMPLE)?;
130    ui::ok(".cellstate/.env.example");
131
132    // Write cellstate.lock
133    if !Path::new("cellstate.lock").exists() {
134        fs::write("cellstate.lock", LOCK_INIT)?;
135        ui::ok("cellstate.lock");
136    } else {
137        ui::info("cellstate.lock already exists — skipping");
138    }
139
140    // Append to .gitignore if it exists, or create it
141    let gitignore_path = Path::new(".gitignore");
142    if gitignore_path.exists() {
143        let existing = fs::read_to_string(gitignore_path)?;
144        if !existing.contains(".cellstate/local") {
145            fs::write(
146                gitignore_path,
147                format!("{}{}", existing, GITIGNORE_ADDITION),
148            )?;
149            ui::ok(".gitignore updated");
150        } else {
151            ui::info(".gitignore already has CELLSTATE entries");
152        }
153    } else {
154        fs::write(gitignore_path, GITIGNORE_ADDITION.trim_start())?;
155        ui::ok(".gitignore created");
156    }
157
158    // Next steps
159    println!();
160    println!("{}", style("Next steps:").bold());
161    if base_url.contains("localhost") {
162        println!(
163            "  {} to start a local server",
164            style("cellstate start").cyan()
165        );
166    }
167    println!(
168        "  Copy {} to {} and add your API key",
169        style(".cellstate/.env.example").dim(),
170        style(".env").cyan()
171    );
172    println!(
173        "  Then: {}",
174        style(r#"from cellstate import SimpleMemory  # or import { SimpleMemory } from '@cellstate/sdk'"#).dim()
175    );
176    println!();
177
178    Ok(())
179}