chub_core/team/
project.rs1use std::fs;
2use std::path::{Path, PathBuf};
3
4use serde::{Deserialize, Serialize};
5
6use crate::config::SourceConfig;
7use crate::error::{Error, Result};
8
9pub fn find_project_root(start: Option<&Path>) -> Option<PathBuf> {
11 let start = start
12 .map(|p| p.to_path_buf())
13 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
14
15 let mut current = start.as_path();
16 loop {
17 let candidate = current.join(".chub");
18 if candidate.is_dir() {
19 return Some(current.to_path_buf());
20 }
21 current = current.parent()?;
22 }
23}
24
25pub fn project_chub_dir() -> Option<PathBuf> {
27 find_project_root(None).map(|root| root.join(".chub"))
28}
29
30#[derive(Debug, Clone, Default, Deserialize)]
32pub struct ProjectFileConfig {
33 #[serde(default)]
34 pub sources: Option<Vec<SourceConfig>>,
35 #[serde(default)]
36 pub cdn_url: Option<String>,
37 #[serde(default)]
38 pub output_dir: Option<String>,
39 #[serde(default)]
40 pub refresh_interval: Option<u64>,
41 #[serde(default)]
42 pub output_format: Option<String>,
43 #[serde(default)]
44 pub source: Option<String>,
45 #[serde(default)]
46 pub telemetry: Option<bool>,
47 #[serde(default)]
48 pub feedback: Option<bool>,
49 #[serde(default)]
50 pub telemetry_url: Option<String>,
51 #[serde(default)]
52 pub agent_rules: Option<AgentRules>,
53 #[serde(default)]
54 pub auto_profile: Option<Vec<AutoProfileEntry>>,
55}
56
57#[derive(Debug, Clone, Default, Serialize, Deserialize)]
59pub struct AgentRules {
60 #[serde(default)]
61 pub global: Vec<String>,
62 #[serde(default)]
63 pub modules: std::collections::HashMap<String, ModuleRules>,
64 #[serde(default)]
65 pub include_pins: bool,
66 #[serde(default)]
67 pub include_context: bool,
68 #[serde(default)]
69 pub targets: Vec<String>,
70}
71
72#[derive(Debug, Clone, Default, Serialize, Deserialize)]
74pub struct ModuleRules {
75 pub path: String,
76 #[serde(default)]
77 pub rules: Vec<String>,
78}
79
80#[derive(Debug, Clone, Deserialize, Serialize)]
82pub struct AutoProfileEntry {
83 pub path: String,
84 pub profile: String,
85}
86
87pub fn load_project_config() -> Option<ProjectFileConfig> {
89 let chub_dir = project_chub_dir()?;
90 let config_path = chub_dir.join("config.yaml");
91 let raw = fs::read_to_string(&config_path).ok()?;
92 serde_yaml::from_str(&raw).ok()
93}
94
95pub fn init_project(from_deps: bool, monorepo: bool) -> Result<PathBuf> {
97 let cwd = std::env::current_dir().map_err(|e| Error::Config(e.to_string()))?;
98 let chub_dir = cwd.join(".chub");
99
100 if chub_dir.exists() {
101 return Err(Error::Config(format!(
102 ".chub/ directory already exists at {}",
103 cwd.display()
104 )));
105 }
106
107 fs::create_dir_all(&chub_dir)?;
108 fs::create_dir_all(chub_dir.join("annotations"))?;
109 fs::create_dir_all(chub_dir.join("context"))?;
110 fs::create_dir_all(chub_dir.join("profiles"))?;
111
112 let config_content = r#"# Chub project configuration
114# This file is shared with the team via git.
115# It overrides personal settings in ~/.chub/config.yaml.
116
117# sources:
118# - name: official
119# url: https://cdn.aichub.org/v1
120# - name: company
121# url: https://docs.internal.company.com/chub
122
123# Agent rules for generating CLAUDE.md, .cursorrules, etc.
124# agent_rules:
125# global:
126# - "Follow the project coding conventions"
127# modules: {}
128# include_pins: true
129# include_context: true
130# targets:
131# - claude.md
132"#;
133 fs::write(chub_dir.join("config.yaml"), config_content)?;
134
135 fs::write(chub_dir.join("pins.yaml"), "pins: []\n")?;
137
138 let base_profile = r#"name: Base
140description: "Shared rules for all roles"
141rules: []
142context: []
143"#;
144 fs::write(chub_dir.join("profiles").join("base.yaml"), base_profile)?;
145
146 let example_context = r#"---
148name: Project Architecture
149description: "High-level architecture overview"
150tags: architecture
151---
152
153# Architecture Overview
154
155Describe your project architecture here.
156"#;
157 fs::write(
158 chub_dir.join("context").join("architecture.md"),
159 example_context,
160 )?;
161
162 fs::write(chub_dir.join(".gitignore"), "# .chub/ is git-tracked\n")?;
165
166 if monorepo {
167 let monorepo_config = r#"# Chub project configuration (monorepo)
169
170# auto_profile:
171# - path: "packages/api/**"
172# profile: backend
173# - path: "packages/web/**"
174# profile: frontend
175
176agent_rules:
177 global:
178 - "Follow the project coding conventions"
179 modules: {}
180 include_pins: true
181 include_context: true
182 targets:
183 - claude.md
184"#;
185 fs::write(chub_dir.join("config.yaml"), monorepo_config)?;
186 }
187
188 if from_deps {
189 fs::write(chub_dir.join(".init_from_deps"), "")?;
192 }
193
194 Ok(chub_dir)
195}