Files
Kigi-CLI/crates/codegen/kigi-tui/src/trace_cmd.rs
T
ZacharyZhang-NY a02b555e66 docs(comments): rewrite comments across all crates to the guidelines
Sweep every first-party crate source (1956 .rs files) to the project comment
guidelines: delete redundant restatements, decorative banners, change
narration, and end-of-line comments; keep and tighten the crucial ones
(invariants, bug rationale, SAFETY blocks, ported-source attribution).

No functional code changed. Every edit is proven comment-only against the
prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a
separate doctest-fence check. Where removing a comment made rustfmt or clippy
want to re-lay-out adjacent code, the minimal triggering comment is restored so
code tokens stay byte-identical.

Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy
--workspace --all-targets (0 warnings).

Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for
these guidelines (flags banners, end-of-line comments, change narration, and
commented-out code).
2026-07-23 16:55:39 -04:00

218 lines
6.5 KiB
Rust

use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use kigi_shell::agent::config::Config as AgentConfig;
use kigi_shell::util::kigi_home::kigi_home;
#[derive(Debug, clap::Args, Clone)]
pub struct TraceArgs {
/// Session ID to export
pub session_id: String,
/// Output path (default: $KIGI_SHARE_DIR/trace-exports/<session-id>.tar.gz)
#[arg(short, long)]
pub output: Option<PathBuf>,
/// Emit machine-readable JSON output
#[arg(long)]
pub json: bool,
}
#[derive(serde::Serialize)]
struct TraceResult {
session_id: String,
status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
local_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
}
pub async fn run(args: TraceArgs, _agent_config: &AgentConfig) -> Result<()> {
run_export(&args.session_id, args.output.as_deref(), args.json).await
}
pub fn build_session_tar(session_dir: &Path, session_id: &str) -> Result<Vec<u8>> {
use flate2::Compression;
use flate2::write::GzEncoder;
tracing::info!(
session_id = %session_id,
session_dir = %session_dir.display(),
"trace_cmd: building session tar.gz archive"
);
let mut archive_data = Vec::new();
let mut file_count: u32 = 0;
{
let encoder = GzEncoder::new(&mut archive_data, Compression::default());
let mut archive = tar::Builder::new(encoder);
file_count += add_directory_to_tar(&mut archive, session_dir, session_id)?;
let metadata = ExportMetadata {
session_id: session_id.to_owned(),
kigi_version: env!("VERSION_WITH_COMMIT").to_owned(),
os: std::env::consts::OS.to_owned(),
arch: std::env::consts::ARCH.to_owned(),
exported_at: chrono::Utc::now().to_rfc3339(),
};
let meta_bytes = serde_json::to_vec_pretty(&metadata)?;
append_bytes(
&mut archive,
&format!("{session_id}/export_metadata.json"),
&meta_bytes,
);
file_count += 1;
archive
.into_inner()
.and_then(|encoder| encoder.finish())
.context("Failed to finalize tar.gz archive")?;
}
tracing::info!(
session_id = %session_id,
file_count,
archive_bytes = archive_data.len(),
"trace_cmd: archive built"
);
Ok(archive_data)
}
#[derive(serde::Serialize)]
struct ExportMetadata {
session_id: String,
kigi_version: String,
os: String,
arch: String,
exported_at: String,
}
fn append_bytes<W: std::io::Write>(archive: &mut tar::Builder<W>, path: &str, data: &[u8]) {
let mut header = tar::Header::new_gnu();
header.set_size(data.len() as u64);
header.set_mode(0o644);
set_mtime(&mut header);
if let Err(e) = archive.append_data(&mut header, path, data) {
tracing::warn!(error = %e, "trace_cmd: failed to add file to archive");
eprintln!(" Warning: failed to add {path}: {e}");
}
}
fn set_mtime(header: &mut tar::Header) {
header.set_mtime(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
);
}
/// Returns the number of files added.
fn add_directory_to_tar<W: std::io::Write>(
archive: &mut tar::Builder<W>,
dir: &Path,
prefix: &str,
) -> Result<u32> {
let entries =
std::fs::read_dir(dir).with_context(|| format!("Failed to read {}", dir.display()))?;
let mut count: u32 = 0;
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
let archive_path = format!("{prefix}/{name_str}");
if path.is_dir() {
count += add_directory_to_tar(archive, &path, &archive_path)?;
} else if path.is_file() {
match std::fs::read(&path) {
Ok(data) => {
append_bytes(archive, &archive_path, &data);
count += 1;
}
Err(e) => {
tracing::warn!(
path = %path.display(),
error = %e,
"trace_cmd: failed to read file for archive"
);
eprintln!(" Warning: failed to read {}: {}", path.display(), e);
}
}
}
}
Ok(count)
}
pub(crate) fn find_session_dir(session_id: &str) -> Result<PathBuf> {
kigi_shell::session::persistence::find_session_dir_by_id(session_id).with_context(|| {
format!(
"Session '{session_id}' not found under {}",
crate::util::display_user_kigi_path("sessions")
)
})
}
pub fn trace_exports_dir() -> PathBuf {
kigi_home().join("trace-exports")
}
/// Creates parent directory if needed.
pub fn save_local_bundle(
archive: &[u8],
session_id: &str,
output: Option<&Path>,
) -> Result<PathBuf> {
let output_path = match output {
Some(p) => p.to_path_buf(),
None => trace_exports_dir().join(format!("{session_id}.tar.gz")),
};
if let Some(parent) = output_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create {}", parent.display()))?;
}
std::fs::write(&output_path, archive)
.with_context(|| format!("Failed to write {}", output_path.display()))?;
tracing::info!(
session_id = %session_id,
path = %output_path.display(),
size_bytes = archive.len(),
"trace_cmd: local bundle saved"
);
Ok(output_path)
}
async fn run_export(session_id: &str, output: Option<&Path>, json: bool) -> Result<()> {
let session_dir = find_session_dir(session_id)?;
if !json {
eprintln!("Found session at: {}", session_dir.display());
eprintln!("Building session trace archive...");
}
let archive = build_session_tar(&session_dir, session_id)?;
let output_path = save_local_bundle(&archive, session_id, output)?;
if json {
let result = TraceResult {
session_id: session_id.to_owned(),
status: "exported",
local_path: Some(output_path.display().to_string()),
error: None,
};
println!("{}", serde_json::to_string(&result)?);
} else {
let size_kb = archive.len() / 1024;
eprintln!("Session trace exported ({size_kb} KB):");
eprintln!(" {}", output_path.display());
println!("{}", output_path.display());
}
Ok(())
}