Files
Kigi-CLI/crates/codegen/kigi-workspace/src/project_config.rs
T
ZacharyZhang-NY 6f31415ed6 §9 acceptance: grep-zero sweep — every internal x.ai/grok identifier renamed
The PRD's first acceptance gate now holds: grep -RinE '\bx\.ai\b|grok'
crates/ --include='*.rs' → 0 matches (exempt: NOTICE and third-party
license archives, README provenance, and the required 'Based on Grok
Build Open Source' attribution, now sourced from version_attribution.txt).

Wire-visible renames (both sides in this repo, changed in lockstep):
- Auth method id 'grok.com' → 'kimi-code' (AuthMethodKind::KimiCode).
- Every x.ai/* and _x.ai/* ACP ext method and meta key → kigi/* /
  _kigi/* (~200 names; grokShell → kigiShell). Session-file replay keeps
  a read-side alias for the legacy '_x.ai/session/update' method so
  existing updates.jsonl histories load; writes emit only the new name
  (both directions test-pinned).
- Agent types grok-build* → kigi* with a documented legacy-prefix alias
  at resolution time so persisted sessions keep resolving.
- ToolNamespace/BuiltinAgentName GrokBuild* → Kigi* (wire snake_case
  kigi/kigi_concise/kigi_hashline; schema regenerated); grok_build
  implementation dirs renamed to kigi*.
- x-grok-* headers → x-kigi-*, __GROK_* sentinels → __KIGI_*, themes
  grokday/groknight → kigiday/kiginight (old persisted values fall back
  to the default theme), web_fetch allowlist xAI hosts → kimi.com +
  moonshot platforms, changelog CDN → this repo, grok-build changelog
  archives deleted.
- BYOK default endpoint removed: [endpoints] api_base_url is now truly
  optional with NO default — consumers fail fast with the flag name when
  unset (no silent x.ai egress). Mock harnesses inject it explicitly.
- System-prompt identity fixed: 'released by xAI' → 'an unofficial
  community CLI for Kimi' (template + regenerated encrypted form).

Also repaired pre-existing grok-era test debt found by the sweep: the
stale trace_classify default-model pin, the grok-pager UA label test,
pty-harness stale-binary reuse and non-hermetic moonshot routing (a PTY
test could previously reach the real api.moonshot.cn), and the outdated
oauth fixture scope key.

Gates: §9 grep 0; fmt clean; workspace check/clippy 0/0 (-D warnings);
FULL cargo test --workspace: 234 suites, 21,961 passed, 0 failed;
deny advisories ok.
2026-07-18 02:48:46 -04:00

116 lines
4.6 KiB
Rust

//! Project config-file discovery: locating repo-local `.mcp.json` and
//! `.kigi/config.toml` files by walking from `cwd` up to the git root.
//!
//! These pure `git2` + filesystem walks are shared by the shell's config
//! loaders and the folder-trust gate's `repo_configs_present`.
use std::path::{Path, PathBuf};
use kigi_agent::repo::RepoDirChain;
/// Filename of the project-local MCP server config.
pub const MCP_JSON_FILENAME: &str = ".mcp.json";
/// Candidate `.mcp.json` paths from repo root to `cwd`, whether or not they exist.
/// Useful for file watching so newly created files are detected after startup.
pub fn mcp_json_candidate_paths(cwd: &Path) -> Vec<PathBuf> {
mcp_json_candidate_paths_in(&RepoDirChain::resolve(cwd).dirs)
}
/// [`mcp_json_candidate_paths`] over a precomputed cwd→git-root dir chain
/// ([`RepoDirChain`]), repo-root-first. Private: only this file's two callers.
fn mcp_json_candidate_paths_in(chain_dirs: &[PathBuf]) -> Vec<PathBuf> {
chain_dirs
.iter()
.rev()
.map(|dir| dir.join(MCP_JSON_FILENAME))
.collect()
}
/// Find existing `.mcp.json` files from `cwd` up to the git root (repo-root-first order).
pub fn find_mcp_json_files(cwd: &Path) -> Vec<PathBuf> {
find_mcp_json_files_in(&RepoDirChain::resolve(cwd).dirs)
}
/// [`find_mcp_json_files`] over a precomputed dir chain. See [`RepoDirChain`].
/// `pub(crate)` — the gate (`repo_configs_present`) reaches it within this crate.
pub(crate) fn find_mcp_json_files_in(chain_dirs: &[PathBuf]) -> Vec<PathBuf> {
mcp_json_candidate_paths_in(chain_dirs)
.into_iter()
.filter(|path| path.is_file())
.collect()
}
/// True when `config_path` is `$KIGI_SHARE_DIR/config.toml` (user tier, not project).
fn is_user_kigi_config_file(config_path: &Path) -> bool {
let Some(user_home) = kigi_config::user_kigi_home() else {
return false;
};
let user_config = user_home.join("config.toml");
if config_path == user_config.as_path() {
return true;
}
let Ok(canonical_config) = dunce::canonicalize(config_path) else {
return false;
};
let canonical_user = dunce::canonicalize(&user_config).unwrap_or(user_config);
canonical_config == canonical_user
}
/// Find all `.kigi/config.toml` files from `cwd` upward to the git repo root.
/// Returns paths ordered from repo root (lowest priority) to cwd (highest priority),
/// matching the convention used by skills and AGENTS.md discovery.
///
/// If no git repo is found, only checks `cwd/.kigi/config.toml`. Excludes the
/// user-global config so `cwd == $HOME` does not treat `~/.kigi/config.toml` as
/// a project overlay.
pub fn find_project_configs(cwd: &Path) -> Vec<PathBuf> {
find_project_configs_in(&RepoDirChain::resolve(cwd).dirs)
}
/// [`find_project_configs`] over a precomputed cwd→git-root dir chain
/// ([`RepoDirChain`]), repo-root-first. Excludes the user-global config so
/// `cwd == $HOME` does not treat `~/.kigi/config.toml` as a project overlay.
/// `pub(crate)` — the gate (`repo_configs_present`) reaches it within this crate.
pub(crate) fn find_project_configs_in(chain_dirs: &[PathBuf]) -> Vec<PathBuf> {
// `dirs` is cwd-first; reverse so repo root comes first (lowest priority)
// and cwd last (highest), matching skills/AGENTS.md discovery order.
chain_dirs
.iter()
.rev()
.map(|dir| dir.join(".kigi").join("config.toml"))
.filter(|config_path| config_path.is_file() && !is_user_kigi_config_file(config_path))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_project_configs_excludes_user_kigi_config_file() {
let Some(user_home) = kigi_config::user_kigi_home() else {
return;
};
let user_config = user_home.join("config.toml");
if user_config.is_file() {
#[allow(deprecated)]
let home = std::env::home_dir().expect("home dir");
let from_home = find_project_configs(&home);
assert!(
!from_home.iter().any(|p| is_user_kigi_config_file(p)),
"user config leaked into project configs: {from_home:?}"
);
assert!(is_user_kigi_config_file(&user_config));
}
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path().join("repo");
std::fs::create_dir_all(project.join(".kigi")).unwrap();
std::fs::write(project.join(".kigi/config.toml"), "# project\n").unwrap();
let found = find_project_configs(&project);
assert_eq!(found.len(), 1);
assert!(!is_user_kigi_config_file(&found[0]));
}
}