2026-04-01 01:14:44 +00:00
use std ::fs ;
use std ::path ::{ Path , PathBuf } ;
const STARTER_CLAUDE_JSON : & str = concat! (
" { \n " ,
" \" permissions \" : { \n " ,
2026-04-01 02:42:49 +00:00
" \" defaultMode \" : \" dontAsk \" \n " ,
2026-04-01 01:14:44 +00:00
" } \n " ,
" } \n " ,
) ;
const GITIGNORE_COMMENT : & str = " # Claude Code local artifacts " ;
const GITIGNORE_ENTRIES : [ & str ; 2 ] = [ " .claude/settings.local.json " , " .claude/sessions/ " ] ;
#[ derive(Debug, Clone, Copy, PartialEq, Eq) ]
pub ( crate ) enum InitStatus {
Created ,
Updated ,
Skipped ,
}
impl InitStatus {
#[ must_use ]
pub ( crate ) fn label ( self ) -> & 'static str {
match self {
Self ::Created = > " created " ,
Self ::Updated = > " updated " ,
Self ::Skipped = > " skipped (already exists) " ,
}
}
}
#[ derive(Debug, Clone, PartialEq, Eq) ]
pub ( crate ) struct InitArtifact {
pub ( crate ) name : & 'static str ,
pub ( crate ) status : InitStatus ,
}
#[ derive(Debug, Clone, PartialEq, Eq) ]
pub ( crate ) struct InitReport {
pub ( crate ) project_root : PathBuf ,
pub ( crate ) artifacts : Vec < InitArtifact > ,
}
impl InitReport {
#[ must_use ]
pub ( crate ) fn render ( & self ) -> String {
let mut lines = vec! [
" Init " . to_string ( ) ,
format! ( " Project {} " , self . project_root . display ( ) ) ,
] ;
for artifact in & self . artifacts {
lines . push ( format! (
" {:<16} {} " ,
artifact . name ,
artifact . status . label ( )
) ) ;
}
lines . push ( " Next step Review and tailor the generated guidance " . to_string ( ) ) ;
lines . join ( " \n " )
}
}
#[ derive(Debug, Clone, Default, PartialEq, Eq) ]
#[ allow(clippy::struct_excessive_bools) ]
struct RepoDetection {
rust_workspace : bool ,
rust_root : bool ,
python : bool ,
package_json : bool ,
typescript : bool ,
nextjs : bool ,
react : bool ,
vite : bool ,
nest : bool ,
src_dir : bool ,
tests_dir : bool ,
rust_dir : bool ,
}
pub ( crate ) fn initialize_repo ( cwd : & Path ) -> Result < InitReport , Box < dyn std ::error ::Error > > {
let mut artifacts = Vec ::new ( ) ;
let claude_dir = cwd . join ( " .claude " ) ;
artifacts . push ( InitArtifact {
name : " .claude/ " ,
status : ensure_dir ( & claude_dir ) ? ,
} ) ;
let claude_json = cwd . join ( " .claude.json " ) ;
artifacts . push ( InitArtifact {
name : " .claude.json " ,
status : write_file_if_missing ( & claude_json , STARTER_CLAUDE_JSON ) ? ,
} ) ;
let gitignore = cwd . join ( " .gitignore " ) ;
artifacts . push ( InitArtifact {
name : " .gitignore " ,
status : ensure_gitignore_entries ( & gitignore ) ? ,
} ) ;
let claude_md = cwd . join ( " CLAUDE.md " ) ;
let content = render_init_claude_md ( cwd ) ;
artifacts . push ( InitArtifact {
name : " CLAUDE.md " ,
status : write_file_if_missing ( & claude_md , & content ) ? ,
} ) ;
Ok ( InitReport {
project_root : cwd . to_path_buf ( ) ,
artifacts ,
} )
}
fn ensure_dir ( path : & Path ) -> Result < InitStatus , std ::io ::Error > {
if path . is_dir ( ) {
return Ok ( InitStatus ::Skipped ) ;
}
fs ::create_dir_all ( path ) ? ;
Ok ( InitStatus ::Created )
}
fn write_file_if_missing ( path : & Path , content : & str ) -> Result < InitStatus , std ::io ::Error > {
if path . exists ( ) {
return Ok ( InitStatus ::Skipped ) ;
}
fs ::write ( path , content ) ? ;
Ok ( InitStatus ::Created )
}
fn ensure_gitignore_entries ( path : & Path ) -> Result < InitStatus , std ::io ::Error > {
if ! path . exists ( ) {
let mut lines = vec! [ GITIGNORE_COMMENT . to_string ( ) ] ;
lines . extend ( GITIGNORE_ENTRIES . iter ( ) . map ( | entry | ( * entry ) . to_string ( ) ) ) ;
fs ::write ( path , format! ( " {} \n " , lines . join ( " \n " ) ) ) ? ;
return Ok ( InitStatus ::Created ) ;
}
let existing = fs ::read_to_string ( path ) ? ;
let mut lines = existing . lines ( ) . map ( ToOwned ::to_owned ) . collect ::< Vec < _ > > ( ) ;
let mut changed = false ;
if ! lines . iter ( ) . any ( | line | line = = GITIGNORE_COMMENT ) {
lines . push ( GITIGNORE_COMMENT . to_string ( ) ) ;
changed = true ;
}
for entry in GITIGNORE_ENTRIES {
if ! lines . iter ( ) . any ( | line | line = = entry ) {
lines . push ( entry . to_string ( ) ) ;
changed = true ;
}
}
if ! changed {
return Ok ( InitStatus ::Skipped ) ;
}
fs ::write ( path , format! ( " {} \n " , lines . join ( " \n " ) ) ) ? ;
Ok ( InitStatus ::Updated )
}
pub ( crate ) fn render_init_claude_md ( cwd : & Path ) -> String {
let detection = detect_repo ( cwd ) ;
let mut lines = vec! [
" # CLAUDE.md " . to_string ( ) ,
String ::new ( ) ,
" This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. " . to_string ( ) ,
String ::new ( ) ,
] ;
let detected_languages = detected_languages ( & detection ) ;
let detected_frameworks = detected_frameworks ( & detection ) ;
lines . push ( " ## Detected stack " . to_string ( ) ) ;
if detected_languages . is_empty ( ) {
lines . push ( " - No specific language markers were detected yet; document the primary language and verification commands once the project structure settles. " . to_string ( ) ) ;
} else {
lines . push ( format! ( " - Languages: {} . " , detected_languages . join ( " , " ) ) ) ;
}
if detected_frameworks . is_empty ( ) {
lines . push ( " - Frameworks: none detected from the supported starter markers. " . to_string ( ) ) ;
} else {
lines . push ( format! (
" - Frameworks/tooling markers: {}. " ,
detected_frameworks . join ( " , " )
) ) ;
}
lines . push ( String ::new ( ) ) ;
let verification_lines = verification_lines ( cwd , & detection ) ;
if ! verification_lines . is_empty ( ) {
lines . push ( " ## Verification " . to_string ( ) ) ;
lines . extend ( verification_lines ) ;
lines . push ( String ::new ( ) ) ;
}
let structure_lines = repository_shape_lines ( & detection ) ;
if ! structure_lines . is_empty ( ) {
lines . push ( " ## Repository shape " . to_string ( ) ) ;
lines . extend ( structure_lines ) ;
lines . push ( String ::new ( ) ) ;
}
let framework_lines = framework_notes ( & detection ) ;
if ! framework_lines . is_empty ( ) {
lines . push ( " ## Framework notes " . to_string ( ) ) ;
lines . extend ( framework_lines ) ;
lines . push ( String ::new ( ) ) ;
}
lines . push ( " ## Working agreement " . to_string ( ) ) ;
lines . push ( " - Prefer small, reviewable changes and keep generated bootstrap files aligned with actual repo workflows. " . to_string ( ) ) ;
lines . push ( " - Keep shared defaults in `.claude.json`; reserve `.claude/settings.local.json` for machine-local overrides. " . to_string ( ) ) ;
lines . push ( " - Do not overwrite existing `CLAUDE.md` content automatically; update it intentionally when repo workflows change. " . to_string ( ) ) ;
lines . push ( String ::new ( ) ) ;
lines . join ( " \n " )
}
fn detect_repo ( cwd : & Path ) -> RepoDetection {
let package_json_contents = fs ::read_to_string ( cwd . join ( " package.json " ) )
. unwrap_or_default ( )
. to_ascii_lowercase ( ) ;
RepoDetection {
rust_workspace : cwd . join ( " rust " ) . join ( " Cargo.toml " ) . is_file ( ) ,
rust_root : cwd . join ( " Cargo.toml " ) . is_file ( ) ,
python : cwd . join ( " pyproject.toml " ) . is_file ( )
| | cwd . join ( " requirements.txt " ) . is_file ( )
| | cwd . join ( " setup.py " ) . is_file ( ) ,
package_json : cwd . join ( " package.json " ) . is_file ( ) ,
typescript : cwd . join ( " tsconfig.json " ) . is_file ( )
| | package_json_contents . contains ( " typescript " ) ,
nextjs : package_json_contents . contains ( " \" next \" " ) ,
react : package_json_contents . contains ( " \" react \" " ) ,
vite : package_json_contents . contains ( " \" vite \" " ) ,
nest : package_json_contents . contains ( " @nestjs " ) ,
src_dir : cwd . join ( " src " ) . is_dir ( ) ,
tests_dir : cwd . join ( " tests " ) . is_dir ( ) ,
rust_dir : cwd . join ( " rust " ) . is_dir ( ) ,
}
}
fn detected_languages ( detection : & RepoDetection ) -> Vec < & 'static str > {
let mut languages = Vec ::new ( ) ;
if detection . rust_workspace | | detection . rust_root {
languages . push ( " Rust " ) ;
}
if detection . python {
languages . push ( " Python " ) ;
}
if detection . typescript {
languages . push ( " TypeScript " ) ;
} else if detection . package_json {
languages . push ( " JavaScript/Node.js " ) ;
}
languages
}
fn detected_frameworks ( detection : & RepoDetection ) -> Vec < & 'static str > {
let mut frameworks = Vec ::new ( ) ;
if detection . nextjs {
frameworks . push ( " Next.js " ) ;
}
if detection . react {
frameworks . push ( " React " ) ;
}
if detection . vite {
frameworks . push ( " Vite " ) ;
}
if detection . nest {
frameworks . push ( " NestJS " ) ;
}
frameworks
}
fn verification_lines ( cwd : & Path , detection : & RepoDetection ) -> Vec < String > {
let mut lines = Vec ::new ( ) ;
if detection . rust_workspace {
lines . push ( " - Run Rust verification from `rust/`: `cargo fmt`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace` " . to_string ( ) ) ;
} else if detection . rust_root {
lines . push ( " - Run Rust verification from the repo root: `cargo fmt`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace` " . to_string ( ) ) ;
}
if detection . python {
if cwd . join ( " pyproject.toml " ) . is_file ( ) {
lines . push ( " - Run the Python project checks declared in `pyproject.toml` (for example: `pytest`, `ruff check`, and `mypy` when configured). " . to_string ( ) ) ;
} else {
lines . push (
" - Run the repo's Python test/lint commands before shipping changes. " . to_string ( ) ,
) ;
}
}
if detection . package_json {
lines . push ( " - Run the JavaScript/TypeScript checks from `package.json` before shipping changes (`npm test`, `npm run lint`, `npm run build`, or the repo equivalent). " . to_string ( ) ) ;
}
if detection . tests_dir & & detection . src_dir {
lines . push ( " - `src/` and `tests/` are both present; update both surfaces together when behavior changes. " . to_string ( ) ) ;
}
lines
}
fn repository_shape_lines ( detection : & RepoDetection ) -> Vec < String > {
let mut lines = Vec ::new ( ) ;
if detection . rust_dir {
lines . push (
" - `rust/` contains the Rust workspace and active CLI/runtime implementation. "
. to_string ( ) ,
) ;
}
if detection . src_dir {
lines . push ( " - `src/` contains source files that should stay consistent with generated guidance and tests. " . to_string ( ) ) ;
}
if detection . tests_dir {
lines . push ( " - `tests/` contains validation surfaces that should be reviewed alongside code changes. " . to_string ( ) ) ;
}
lines
}
fn framework_notes ( detection : & RepoDetection ) -> Vec < String > {
let mut lines = Vec ::new ( ) ;
if detection . nextjs {
lines . push ( " - Next.js detected: preserve routing/data-fetching conventions and verify production builds after changing app structure. " . to_string ( ) ) ;
}
if detection . react & & ! detection . nextjs {
lines . push ( " - React detected: keep component behavior covered with focused tests and avoid unnecessary prop/API churn. " . to_string ( ) ) ;
}
if detection . vite {
lines . push ( " - Vite detected: validate the production bundle after changing build-sensitive configuration or imports. " . to_string ( ) ) ;
}
if detection . nest {
lines . push ( " - NestJS detected: keep module/provider boundaries explicit and verify controller/service wiring after refactors. " . to_string ( ) ) ;
}
lines
}
#[ cfg(test) ]
mod tests {
use super ::{ initialize_repo , render_init_claude_md } ;
use std ::fs ;
use std ::path ::Path ;
use std ::time ::{ SystemTime , UNIX_EPOCH } ;
fn temp_dir ( ) -> std ::path ::PathBuf {
let nanos = SystemTime ::now ( )
. duration_since ( UNIX_EPOCH )
. expect ( " time should be after epoch " )
. as_nanos ( ) ;
std ::env ::temp_dir ( ) . join ( format! ( " rusty-claude-init- {nanos} " ) )
}
#[ test ]
fn initialize_repo_creates_expected_files_and_gitignore_entries ( ) {
let root = temp_dir ( ) ;
fs ::create_dir_all ( root . join ( " rust " ) ) . expect ( " create rust dir " ) ;
fs ::write ( root . join ( " rust " ) . join ( " Cargo.toml " ) , " [workspace] \n " ) . expect ( " write cargo " ) ;
let report = initialize_repo ( & root ) . expect ( " init should succeed " ) ;
let rendered = report . render ( ) ;
assert! ( rendered . contains ( " .claude/ created " ) ) ;
assert! ( rendered . contains ( " .claude.json created " ) ) ;
assert! ( rendered . contains ( " .gitignore created " ) ) ;
assert! ( rendered . contains ( " CLAUDE.md created " ) ) ;
assert! ( root . join ( " .claude " ) . is_dir ( ) ) ;
assert! ( root . join ( " .claude.json " ) . is_file ( ) ) ;
assert! ( root . join ( " CLAUDE.md " ) . is_file ( ) ) ;
assert_eq! (
fs ::read_to_string ( root . join ( " .claude.json " ) ) . expect ( " read claude json " ) ,
concat! (
" { \n " ,
" \" permissions \" : { \n " ,
2026-04-01 02:42:49 +00:00
" \" defaultMode \" : \" dontAsk \" \n " ,
2026-04-01 01:14:44 +00:00
" } \n " ,
" } \n " ,
)
) ;
let gitignore = fs ::read_to_string ( root . join ( " .gitignore " ) ) . expect ( " read gitignore " ) ;
assert! ( gitignore . contains ( " .claude/settings.local.json " ) ) ;
assert! ( gitignore . contains ( " .claude/sessions/ " ) ) ;
let claude_md = fs ::read_to_string ( root . join ( " CLAUDE.md " ) ) . expect ( " read claude md " ) ;
assert! ( claude_md . contains ( " Languages: Rust. " ) ) ;
assert! ( claude_md . contains ( " cargo clippy --workspace --all-targets -- -D warnings " ) ) ;
fs ::remove_dir_all ( root ) . expect ( " cleanup temp dir " ) ;
}
#[ test ]
fn initialize_repo_is_idempotent_and_preserves_existing_files ( ) {
let root = temp_dir ( ) ;
fs ::create_dir_all ( & root ) . expect ( " create root " ) ;
fs ::write ( root . join ( " CLAUDE.md " ) , " custom guidance \n " ) . expect ( " write existing claude md " ) ;
fs ::write ( root . join ( " .gitignore " ) , " .claude/settings.local.json \n " )
. expect ( " write gitignore " ) ;
let first = initialize_repo ( & root ) . expect ( " first init should succeed " ) ;
assert! ( first
. render ( )
. contains ( " CLAUDE.md skipped (already exists) " ) ) ;
let second = initialize_repo ( & root ) . expect ( " second init should succeed " ) ;
let second_rendered = second . render ( ) ;
assert! ( second_rendered . contains ( " .claude/ skipped (already exists) " ) ) ;
assert! ( second_rendered . contains ( " .claude.json skipped (already exists) " ) ) ;
assert! ( second_rendered . contains ( " .gitignore skipped (already exists) " ) ) ;
assert! ( second_rendered . contains ( " CLAUDE.md skipped (already exists) " ) ) ;
assert_eq! (
fs ::read_to_string ( root . join ( " CLAUDE.md " ) ) . expect ( " read existing claude md " ) ,
" custom guidance \n "
) ;
let gitignore = fs ::read_to_string ( root . join ( " .gitignore " ) ) . expect ( " read gitignore " ) ;
assert_eq! ( gitignore . matches ( " .claude/settings.local.json " ) . count ( ) , 1 ) ;
assert_eq! ( gitignore . matches ( " .claude/sessions/ " ) . count ( ) , 1 ) ;
fs ::remove_dir_all ( root ) . expect ( " cleanup temp dir " ) ;
}
#[ test ]
fn render_init_template_mentions_detected_python_and_nextjs_markers ( ) {
let root = temp_dir ( ) ;
fs ::create_dir_all ( & root ) . expect ( " create root " ) ;
fs ::write ( root . join ( " pyproject.toml " ) , " [project] \n name = \" demo \" \n " )
. expect ( " write pyproject " ) ;
fs ::write (
root . join ( " package.json " ) ,
r # "{"dependencies":{"next":"14.0.0","react":"18.0.0"},"devDependencies":{"typescript":"5.0.0"}}"# ,
)
. expect ( " write package json " ) ;
let rendered = render_init_claude_md ( Path ::new ( & root ) ) ;
assert! ( rendered . contains ( " Languages: Python, TypeScript. " ) ) ;
assert! ( rendered . contains ( " Frameworks/tooling markers: Next.js, React. " ) ) ;
assert! ( rendered . contains ( " pyproject.toml " ) ) ;
assert! ( rendered . contains ( " Next.js detected " ) ) ;
fs ::remove_dir_all ( root ) . expect ( " cleanup temp dir " ) ;
}
}