M0: compilable skeleton — Kigi 0.1.0 fork surgery

Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.

Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
  kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
  ptyctl, ptyctl-cli, third_party/ unchanged; proto package
  xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
  KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
  (templates re-encrypted)

Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
  trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
  module & dc_log, heap-profile uploader, auth-diagnostics uploader,
  session-analytics halves of feedback; local zero-egress observability
  preserved in new kigi-log crate (unified log, --debug firehose,
  subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
  direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
  relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
  ~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
  kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
  session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
  shell util

Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
  https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
  https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
  Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted

Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
  workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
  all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
  exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
  insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean

Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
  (new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
  fast-worktree); RSS measurement tests serialized via serial_test

Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
  notices sustained; kigi-tools ported-code notices extended; README,
  CONTRIBUTING, SECURITY, AGENTS.md rewritten

Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
This commit is contained in:
2026-07-17 05:31:01 -04:00
commit d6c20fc13f
2612 changed files with 1353757 additions and 0 deletions
+285
View File
@@ -0,0 +1,285 @@
//! Parse .envrc files and extract environment variables.
//!
//! This module provides a way to load environment variables from `.envrc` files.
//! It uses a two-tier approach:
//!
//! 1. **Try `direnv export json`** - If direnv is installed, use it for full compatibility
//! 2. **Fallback to bash** - Run .envrc in a bash subshell with direnv stubs
//!
//! This approach handles:
//! - Variable expansion ($HOME, ${VAR:-default})
//! - Command substitution ($(git rev-parse ...))
//! - Conditional logic (if/then/else)
//! - direnv helper functions (source_up_if_exists, PATH_add, etc.)
use std::collections::HashMap;
use std::path::Path;
use std::process::Command;
/// Stub implementations of common direnv helper functions.
/// These are prepended to the .envrc before execution when direnv is not available.
const DIRENV_STUBS: &str = r#"
# Stub direnv helper functions
source_up_if_exists() { :; }
source_up() { :; }
source_env_if_exists() {
if [ -f "$1" ]; then
. "$1"
fi
}
source_env() {
if [ -f "$1" ]; then
. "$1"
fi
}
PATH_add() {
export PATH="$PWD/$1:$PATH"
}
path_add() {
PATH_add "$@"
}
layout() { :; }
use() { :; }
watch_file() { :; }
"#;
/// Load environment variables from .envrc file in the given directory.
///
/// Returns a HashMap of environment variables that were set/modified by the .envrc.
/// Returns None if no .envrc exists or if parsing fails.
pub fn load_envrc(dir: &Path) -> Option<HashMap<String, String>> {
let envrc_path = dir.join(".envrc");
if !envrc_path.exists() {
tracing::debug!(?dir, ".envrc not found");
return None;
}
// Try direnv first (most reliable if installed)
if let Some(env) = try_direnv_export(dir) {
return Some(env);
}
// Fall back to bash subshell approach
load_envrc_via_bash(dir)
}
/// Try to use `direnv export json` to load environment variables.
/// Returns None if direnv is not installed or fails.
fn try_direnv_export(dir: &Path) -> Option<HashMap<String, String>> {
let mut cmd = Command::new("direnv");
cmd.args(["export", "json"])
.current_dir(dir)
.stdin(std::process::Stdio::null());
kigi_tools::util::detach_std_command(&mut cmd);
let output = cmd.output().ok()?;
if !output.status.success() {
// direnv not allowed, or other error
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.contains("not allowed") {
tracing::debug!(?dir, %stderr, "direnv export failed");
}
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.trim().is_empty() {
// No changes from direnv
return None;
}
// Parse JSON output: {"VAR": "value", ...}
// Note: direnv also outputs null for vars to unset, but we ignore those
match serde_json::from_str::<HashMap<String, serde_json::Value>>(&stdout) {
Ok(json) => {
let env: HashMap<String, String> = json
.into_iter()
.filter_map(|(k, v)| {
if let serde_json::Value::String(s) = v {
Some((k, s))
} else {
None // Skip null values (unset)
}
})
.collect();
if env.is_empty() {
None
} else {
tracing::info!(?dir, count = env.len(), "Loaded environment via direnv");
Some(env)
}
}
Err(e) => {
tracing::warn!(?dir, ?e, "Failed to parse direnv JSON output");
None
}
}
}
/// Load environment by running .envrc in a bash subshell.
/// This is the fallback when direnv is not installed.
fn load_envrc_via_bash(dir: &Path) -> Option<HashMap<String, String>> {
let envrc_path = dir.join(".envrc");
// Build a script that:
// 1. Includes direnv stubs
// 2. Sources the .envrc
// 3. Outputs all env vars as KEY=VALUE pairs (null-separated for safety)
let script = format!(
r#"
set -e
cd "{dir}"
{stubs}
. "{envrc}"
# Output all environment variables, null-separated
env -0
"#,
dir = dir.display(),
stubs = DIRENV_STUBS,
envrc = envrc_path.display(),
);
// Capture baseline environment (before running .envrc)
let baseline: HashMap<String, String> = std::env::vars().collect();
// Run the script and capture output
let mut bash_cmd = Command::new("/bin/bash");
bash_cmd
.arg("-c")
.arg(&script)
.current_dir(dir)
.stdin(std::process::Stdio::null());
kigi_tools::util::detach_std_command(&mut bash_cmd);
let output = bash_cmd.output();
let output = match output {
Ok(o) if o.status.success() => o,
Ok(_) => {
tracing::warn!(?envrc_path, "Failed to execute .envrc via bash");
return None;
}
Err(e) => {
tracing::warn!(?envrc_path, ?e, "Failed to run bash for .envrc");
return None;
}
};
// Parse the null-separated KEY=VALUE pairs
let stdout = String::from_utf8_lossy(&output.stdout);
let mut result: HashMap<String, String> = HashMap::new();
for entry in stdout.split('\0') {
if entry.is_empty() {
continue;
}
if let Some((key, value)) = entry.split_once('=') {
// Skip internal/noise variables
let ignored_keys = ["_", "SHLVL", "PWD", "OLDPWD"];
if ignored_keys.contains(&key) {
continue;
}
// Only include vars that are new or changed from baseline
match baseline.get(key) {
Some(baseline_value) if baseline_value == value => {
// Unchanged, skip
}
_ => {
// New or changed
result.insert(key.to_string(), value.to_string());
}
}
}
}
if result.is_empty() {
tracing::debug!(?envrc_path, "No environment changes from .envrc");
None
} else {
tracing::info!(
?envrc_path,
count = result.len(),
"Loaded environment from .envrc via bash"
);
Some(result)
}
}
/// Load .envrc and return the environment, or empty HashMap on failure.
pub fn load_envrc_or_empty(dir: &Path) -> HashMap<String, String> {
load_envrc(dir).unwrap_or_default()
}
/// [`load_envrc_or_empty`] gated on folder-trust: loads the repo-local `.envrc`
/// (executed in a bash subshell) only when `trusted`, else returns an empty map.
/// The shell call sites pass the `project_scope_allowed` verdict so the "run a
/// cloned repo's `.envrc` only when the folder is trusted" rule lives in ONE
/// place (mirrors `permission::claude_settings::load_claude_env_with_project`).
pub fn load_envrc_or_empty_when_trusted(dir: &Path, trusted: bool) -> HashMap<String, String> {
if trusted {
load_envrc_or_empty(dir)
} else {
HashMap::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_simple_export() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join(".envrc"), "export FOO=bar\n").unwrap();
let env = load_envrc(dir.path()).unwrap();
assert_eq!(env.get("FOO"), Some(&"bar".to_string()));
}
#[test]
fn test_variable_expansion() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join(".envrc"), "export MY_DIR=$PWD/subdir\n").unwrap();
let env = load_envrc(dir.path()).unwrap();
let expected = format!("{}/subdir", dir.path().display());
assert_eq!(env.get("MY_DIR"), Some(&expected));
}
#[test]
fn test_no_envrc() {
let dir = TempDir::new().unwrap();
assert!(load_envrc(dir.path()).is_none());
}
#[test]
fn test_path_add() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join(".envrc"), "PATH_add bin\n").unwrap();
let env = load_envrc(dir.path()).unwrap();
let path = env.get("PATH").unwrap();
assert!(path.contains(&format!("{}/bin", dir.path().display())));
}
#[test]
fn test_conditional() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join(".envrc"),
r#"
if [ -d "$PWD" ]; then
export EXISTS=yes
else
export EXISTS=no
fi
"#,
)
.unwrap();
let env = load_envrc(dir.path()).unwrap();
assert_eq!(env.get("EXISTS"), Some(&"yes".to_string()));
}
}