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:
@@ -0,0 +1,332 @@
|
||||
//! Filesystem scanner for discovering worktrees not yet tracked in the DB.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::db::{
|
||||
WorktreeKind, WorktreeRecord, WorktreeStatus, id_from_path, now_epoch_secs, repo_name_from_path,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DiscoveredWorktree {
|
||||
pub path: PathBuf,
|
||||
pub kind: WorktreeKind,
|
||||
pub creation_mode: &'static str,
|
||||
pub source_repo: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DiscoveryReport {
|
||||
pub found: Vec<DiscoveredWorktree>,
|
||||
pub skipped: u64,
|
||||
}
|
||||
|
||||
fn should_skip_entry(name: &str) -> bool {
|
||||
name.starts_with('.')
|
||||
|| name.ends_with(".ready")
|
||||
|| name.ends_with(".claimed")
|
||||
|| name.ends_with(".claiming")
|
||||
}
|
||||
|
||||
fn detect_creation_mode(worktree_path: &Path) -> &'static str {
|
||||
let git_entry = worktree_path.join(".git");
|
||||
if git_entry.is_file() {
|
||||
"linked"
|
||||
} else if git_entry.is_dir() {
|
||||
"standalone"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_source_repo(worktree_path: &Path) -> Option<PathBuf> {
|
||||
let git_entry = worktree_path.join(".git");
|
||||
if git_entry.is_file() {
|
||||
let content = std::fs::read_to_string(&git_entry).ok()?;
|
||||
let gitdir = content.trim().strip_prefix("gitdir: ")?;
|
||||
// Walk up from .git/worktrees/<name> → .git → repo root
|
||||
Path::new(gitdir)
|
||||
.parent()?
|
||||
.parent()?
|
||||
.parent()
|
||||
.map(|p| p.to_path_buf())
|
||||
} else if git_entry.is_dir() {
|
||||
Some(worktree_path.to_path_buf())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_two_level_dir(base_dir: &Path, kind: WorktreeKind, report: &mut DiscoveryReport) {
|
||||
let Ok(outer_entries) = std::fs::read_dir(base_dir) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for outer in outer_entries.flatten() {
|
||||
let outer_path = outer.path();
|
||||
if !outer_path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let outer_name = outer.file_name();
|
||||
if should_skip_entry(&outer_name.to_string_lossy()) {
|
||||
report.skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(inner_entries) = std::fs::read_dir(&outer_path) else {
|
||||
continue;
|
||||
};
|
||||
for inner in inner_entries.flatten() {
|
||||
let path = inner.path();
|
||||
if !path.is_dir() || should_skip_entry(&inner.file_name().to_string_lossy()) {
|
||||
report.skipped += 1;
|
||||
continue;
|
||||
}
|
||||
report.found.push(DiscoveredWorktree {
|
||||
creation_mode: detect_creation_mode(&path),
|
||||
source_repo: detect_source_repo(&path),
|
||||
path,
|
||||
kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn discover_worktrees(kigi_home: &Path) -> DiscoveryReport {
|
||||
let mut report = DiscoveryReport::default();
|
||||
scan_two_level_dir(
|
||||
&kigi_home.join("worktrees"),
|
||||
WorktreeKind::Session,
|
||||
&mut report,
|
||||
);
|
||||
scan_two_level_dir(
|
||||
&kigi_home.join("worktree_pool"),
|
||||
WorktreeKind::Pool,
|
||||
&mut report,
|
||||
);
|
||||
report
|
||||
}
|
||||
|
||||
fn fs_creation_time(path: &Path) -> i64 {
|
||||
std::fs::metadata(path)
|
||||
.and_then(|m| m.created())
|
||||
.ok()
|
||||
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or_else(now_epoch_secs)
|
||||
}
|
||||
|
||||
impl DiscoveredWorktree {
|
||||
pub fn into_record(self) -> WorktreeRecord {
|
||||
let repo_name = self
|
||||
.source_repo
|
||||
.as_deref()
|
||||
.map(repo_name_from_path)
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let source_repo = self.source_repo.unwrap_or_else(|| PathBuf::from("unknown"));
|
||||
let created_at = fs_creation_time(&self.path);
|
||||
|
||||
WorktreeRecord {
|
||||
id: id_from_path(&self.path),
|
||||
path: self.path,
|
||||
source_repo,
|
||||
repo_name,
|
||||
kind: self.kind,
|
||||
creation_mode: self.creation_mode.to_owned(),
|
||||
git_ref: None,
|
||||
head_commit: None,
|
||||
session_id: None,
|
||||
creator_pid: None,
|
||||
created_at,
|
||||
last_accessed_at: None,
|
||||
status: WorktreeStatus::Alive,
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct RebuildReport {
|
||||
pub discovered: u64,
|
||||
pub registered: u64,
|
||||
pub already_tracked: u64,
|
||||
}
|
||||
|
||||
pub fn rebuild_worktree_db(
|
||||
db: &crate::db::WorktreeDb,
|
||||
kigi_home: &Path,
|
||||
) -> anyhow::Result<RebuildReport> {
|
||||
let discovery = discover_worktrees(kigi_home);
|
||||
let mut report = RebuildReport {
|
||||
discovered: discovery.found.len() as u64,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for wt in discovery.found {
|
||||
let id = id_from_path(&wt.path);
|
||||
let path_str = wt.path.to_string_lossy();
|
||||
if db.get_by_id(&id)?.is_some() || db.get(&path_str)?.is_some() {
|
||||
report.already_tracked += 1;
|
||||
continue;
|
||||
}
|
||||
db.register(&wt.into_record())?;
|
||||
report.registered += 1;
|
||||
}
|
||||
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_fake_linked_worktree(path: &Path, gitdir_target: &str) {
|
||||
std::fs::create_dir_all(path).unwrap();
|
||||
std::fs::write(path.join(".git"), format!("gitdir: {gitdir_target}\n")).unwrap();
|
||||
}
|
||||
|
||||
fn make_fake_standalone_worktree(path: &Path) {
|
||||
std::fs::create_dir_all(path.join(".git")).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_session_worktrees() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let kigi_home = tmp.path();
|
||||
|
||||
let wt = kigi_home.join("worktrees/myrepo/worktree-abc123");
|
||||
make_fake_linked_worktree(&wt, "/repo/.git/worktrees/abc123");
|
||||
|
||||
let report = discover_worktrees(kigi_home);
|
||||
assert_eq!(report.found.len(), 1);
|
||||
assert_eq!(report.found[0].kind, WorktreeKind::Session);
|
||||
assert_eq!(report.found[0].creation_mode, "linked");
|
||||
assert_eq!(report.found[0].path, wt);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_pool_worktrees() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let kigi_home = tmp.path();
|
||||
|
||||
let wt = kigi_home.join("worktree_pool/inst-1/pool-a");
|
||||
make_fake_standalone_worktree(&wt);
|
||||
|
||||
let report = discover_worktrees(kigi_home);
|
||||
assert_eq!(report.found.len(), 1);
|
||||
assert_eq!(report.found[0].kind, WorktreeKind::Pool);
|
||||
assert_eq!(report.found[0].creation_mode, "standalone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_dot_prefixed_and_markers() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let kigi_home = tmp.path();
|
||||
|
||||
let base = kigi_home.join("worktrees/myrepo");
|
||||
std::fs::create_dir_all(&base).unwrap();
|
||||
|
||||
std::fs::create_dir_all(base.join(".tmp_creating")).unwrap();
|
||||
std::fs::create_dir_all(base.join(".hidden")).unwrap();
|
||||
std::fs::write(base.join("abc.ready"), "").unwrap();
|
||||
std::fs::write(base.join("abc.claimed"), "").unwrap();
|
||||
|
||||
make_fake_standalone_worktree(&base.join("real-session"));
|
||||
|
||||
let report = discover_worktrees(kigi_home);
|
||||
assert_eq!(report.found.len(), 1);
|
||||
assert_eq!(report.found[0].path, base.join("real-session"));
|
||||
assert!(report.skipped > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_empty_dirs_is_fine() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let report = discover_worktrees(tmp.path());
|
||||
assert!(report.found.is_empty());
|
||||
assert_eq!(report.skipped, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuild_registers_and_skips_duplicates() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let kigi_home = tmp.path();
|
||||
|
||||
let wt = kigi_home.join("worktrees/repo/worktree-sess1");
|
||||
make_fake_standalone_worktree(&wt);
|
||||
|
||||
let db = crate::db::WorktreeDb::open_in_memory().unwrap();
|
||||
|
||||
let r1 = rebuild_worktree_db(&db, kigi_home).unwrap();
|
||||
assert_eq!(r1.discovered, 1);
|
||||
assert_eq!(r1.registered, 1);
|
||||
assert_eq!(r1.already_tracked, 0);
|
||||
|
||||
let r2 = rebuild_worktree_db(&db, kigi_home).unwrap();
|
||||
assert_eq!(r2.discovered, 1);
|
||||
assert_eq!(r2.registered, 0);
|
||||
assert_eq!(r2.already_tracked, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuild_keeps_same_basename_worktrees_in_different_repos() {
|
||||
// The cross-repo eviction bug: two repos each have a `wt-abc`
|
||||
// worktree. Discovery + rebuild must register BOTH (distinct ids), not
|
||||
// collapse them into one and then permanently skip the other.
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
// Canonicalize the fake home: production `resolve_kigi_home()` always
|
||||
// canonicalizes, and `WorktreeDb::get` canonicalizes path queries. On
|
||||
// macOS TempDirs live under the `/var` → `/private/var` symlink, so a
|
||||
// raw path stored at registration would never match the query.
|
||||
let canon_home = dunce::canonicalize(tmp.path()).unwrap();
|
||||
let kigi_home = canon_home.as_path();
|
||||
|
||||
let wt_a = kigi_home.join("worktrees/repo-a/wt-abc");
|
||||
let wt_b = kigi_home.join("worktrees/repo-b/wt-abc");
|
||||
make_fake_standalone_worktree(&wt_a);
|
||||
make_fake_standalone_worktree(&wt_b);
|
||||
|
||||
let db = crate::db::WorktreeDb::open_in_memory().unwrap();
|
||||
let report = rebuild_worktree_db(&db, kigi_home).unwrap();
|
||||
assert_eq!(report.discovered, 2);
|
||||
assert_eq!(
|
||||
report.registered, 2,
|
||||
"both same-basename worktrees must register"
|
||||
);
|
||||
|
||||
let all = db.list(&crate::db::ListFilter::default()).unwrap();
|
||||
assert_eq!(all.len(), 2);
|
||||
assert!(db.get(&wt_a.to_string_lossy()).unwrap().is_some());
|
||||
assert!(db.get(&wt_b.to_string_lossy()).unwrap().is_some());
|
||||
|
||||
// Idempotent: a second rebuild finds both already tracked, skips neither.
|
||||
let report2 = rebuild_worktree_db(&db, kigi_home).unwrap();
|
||||
assert_eq!(report2.registered, 0);
|
||||
assert_eq!(report2.already_tracked, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_source_repo_from_linked() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let wt = tmp.path().join("wt");
|
||||
let gitdir = "/home/user/myrepo/.git/worktrees/wt";
|
||||
make_fake_linked_worktree(&wt, gitdir);
|
||||
|
||||
let source = detect_source_repo(&wt);
|
||||
assert_eq!(source, Some(PathBuf::from("/home/user/myrepo")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuild_report_serde_round_trip() {
|
||||
let report = RebuildReport {
|
||||
discovered: 5,
|
||||
registered: 3,
|
||||
already_tracked: 2,
|
||||
};
|
||||
let json = serde_json::to_string(&report).unwrap();
|
||||
let deser: RebuildReport = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(deser.discovered, 5);
|
||||
assert_eq!(deser.registered, 3);
|
||||
assert_eq!(deser.already_tracked, 2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user