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
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AnnotationServerConfig {
12 pub url: String,
14 #[serde(default)]
16 pub auto_push: bool,
17 #[serde(default)]
19 pub cache_ttl_secs: Option<u64>,
20}
21
22pub fn find_project_root(start: Option<&Path>) -> Option<PathBuf> {
25 if let Ok(dir) = std::env::var("CHUB_PROJECT_DIR") {
27 let path = PathBuf::from(dir);
28 if path.join(".chub").is_dir() {
29 return Some(path);
30 }
31 }
32
33 let start = start
34 .map(|p| p.to_path_buf())
35 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
36
37 let mut current = start.as_path();
38 loop {
39 let candidate = current.join(".chub");
40 if candidate.is_dir() {
41 return Some(current.to_path_buf());
42 }
43 current = current.parent()?;
44 }
45}
46
47pub fn project_chub_dir() -> Option<PathBuf> {
49 find_project_root(None).map(|root| root.join(".chub"))
50}
51
52#[derive(Debug, Clone, Default, Deserialize)]
54pub struct ProjectFileConfig {
55 #[serde(default)]
56 pub sources: Option<Vec<SourceConfig>>,
57 #[serde(default)]
58 pub cdn_url: Option<String>,
59 #[serde(default)]
60 pub output_dir: Option<String>,
61 #[serde(default)]
62 pub refresh_interval: Option<u64>,
63 #[serde(default)]
64 pub output_format: Option<String>,
65 #[serde(default)]
66 pub source: Option<String>,
67 #[serde(default)]
68 pub telemetry: Option<bool>,
69 #[serde(default)]
70 pub feedback: Option<bool>,
71 #[serde(default)]
72 pub telemetry_url: Option<String>,
73 #[serde(default)]
74 pub agent_rules: Option<AgentRules>,
75 #[serde(default)]
76 pub auto_profile: Option<Vec<AutoProfileEntry>>,
77 #[serde(default)]
78 pub annotation_server: Option<AnnotationServerConfig>,
79}
80
81#[derive(Debug, Clone, Default, Serialize, Deserialize)]
83pub struct AgentRules {
84 #[serde(default)]
85 pub global: Vec<String>,
86 #[serde(default)]
87 pub modules: std::collections::HashMap<String, ModuleRules>,
88 #[serde(default)]
89 pub include_pins: bool,
90 #[serde(default)]
91 pub include_context: bool,
92 #[serde(default)]
95 pub include_annotation_policy: bool,
96 #[serde(default)]
97 pub targets: Vec<String>,
98}
99
100#[derive(Debug, Clone, Default, Serialize, Deserialize)]
102pub struct ModuleRules {
103 pub path: String,
104 #[serde(default)]
105 pub rules: Vec<String>,
106}
107
108#[derive(Debug, Clone, Deserialize, Serialize)]
110pub struct AutoProfileEntry {
111 pub path: String,
112 pub profile: String,
113}
114
115pub fn load_project_config() -> Option<ProjectFileConfig> {
117 let chub_dir = project_chub_dir()?;
118 let config_path = chub_dir.join("config.yaml");
119 let raw = fs::read_to_string(&config_path).ok()?;
120 serde_yaml::from_str(&raw).ok()
121}
122
123pub fn init_project(from_deps: bool, monorepo: bool) -> Result<PathBuf> {
125 let cwd = std::env::current_dir().map_err(|e| Error::Config(e.to_string()))?;
126 let chub_dir = cwd.join(".chub");
127
128 if chub_dir.exists() {
129 return Err(Error::Config(format!(
130 ".chub/ directory already exists at {}",
131 cwd.display()
132 )));
133 }
134
135 fs::create_dir_all(&chub_dir)?;
136 fs::create_dir_all(chub_dir.join("annotations"))?;
137 fs::create_dir_all(chub_dir.join("context"))?;
138 fs::create_dir_all(chub_dir.join("profiles"))?;
139
140 let config_content = r#"# Chub project configuration
142# This file is shared with the team via git.
143# It overrides personal settings in ~/.chub/config.yaml.
144
145# sources:
146# - name: official
147# url: https://cdn.aichub.org/v1
148# - name: company
149# url: https://docs.internal.company.com/chub
150
151# Agent rules for generating CLAUDE.md, .cursorrules, etc.
152# agent_rules:
153# global:
154# - "Follow the project coding conventions"
155# modules: {}
156# include_pins: true # list pinned docs so agents know what to fetch
157# include_context: true # list project context docs so agents know what to fetch
158# include_annotation_policy: true # add standing instructions for agents to write annotations
159# targets:
160# - claude.md
161
162# annotation_server:
163# url: https://annotations.internal.company.com
164# auto_push: false # set true to mirror team writes to org tier
165# cache_ttl_secs: 3600
166"#;
167 fs::write(chub_dir.join("config.yaml"), config_content)?;
168
169 fs::write(chub_dir.join("pins.yaml"), "pins: []\n")?;
171
172 let base_profile = r#"name: Base
174description: "Shared rules for all roles"
175rules: []
176context: []
177"#;
178 fs::write(chub_dir.join("profiles").join("base.yaml"), base_profile)?;
179
180 let example_context = r#"---
182name: Project Architecture
183description: "High-level architecture overview"
184tags: architecture
185---
186
187# Architecture Overview
188
189Describe your project architecture here.
190"#;
191 fs::write(
192 chub_dir.join("context").join("architecture.md"),
193 example_context,
194 )?;
195
196 fs::write(chub_dir.join(".gitignore"), "# .chub/ is git-tracked\n")?;
199
200 if monorepo {
201 let monorepo_config = r#"# Chub project configuration (monorepo)
203
204# auto_profile:
205# - path: "packages/api/**"
206# profile: backend
207# - path: "packages/web/**"
208# profile: frontend
209
210agent_rules:
211 global:
212 - "Follow the project coding conventions"
213 modules: {}
214 include_pins: true
215 include_context: true
216 include_annotation_policy: true
217 targets:
218 - claude.md
219"#;
220 fs::write(chub_dir.join("config.yaml"), monorepo_config)?;
221 }
222
223 if from_deps {
224 fs::write(chub_dir.join(".init_from_deps"), "")?;
227 }
228
229 Ok(chub_dir)
230}