Files
notfiles/preflight.md

268 lines
9.4 KiB
Markdown
Raw Normal View History

# preflight.rs — Quick Context Surfacer
A `rust-script` preflight that surfaces repo context in one shot: shell detection, git state,
tracked-file tree, and HANDOFF document read/update.
````rust
#!/usr/bin/env rust-script
//! ```cargo
//! [dependencies]
//! anyhow = "1"
//! colored = "2"
//! chrono = { version = "0.4", features = ["clock"] }
//! ```
use anyhow::{Context, Result};
use colored::Colorize;
use std::{
env, fs,
io::Write,
path::{Path, PathBuf},
process::Command,
};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
fn run(cmd: &str, args: &[&str]) -> Option<String> {
Command::new(cmd)
.args(args)
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
}
fn run_or(cmd: &str, args: &[&str], fallback: &str) -> String {
run(cmd, args).unwrap_or_else(|| fallback.to_string())
}
fn section(title: &str) {
println!("\n{}", format!("── {title} ").bold().cyan());
}
// ---------------------------------------------------------------------------
// Shell detection
// ---------------------------------------------------------------------------
fn detect_shell() -> String {
// SHELL env is most reliable; fall back to $0 via ps
env::var("SHELL")
.ok()
.and_then(|s| Path::new(&s).file_name().map(|n| n.to_string_lossy().into_owned()))
.or_else(|| {
// Ask the parent process name via `ps`
let ppid = run("sh", &["-c", "echo $PPID"])?;
run("ps", &["-p", &ppid, "-o", "comm="])
})
.unwrap_or_else(|| "unknown".into())
}
// ---------------------------------------------------------------------------
// Git
// ---------------------------------------------------------------------------
fn git_root() -> Option<PathBuf> {
run("git", &["rev-parse", "--show-toplevel"]).map(PathBuf::from)
}
fn git_branch() -> String {
run_or("git", &["branch", "--show-current"], "(detached)")
}
fn git_status_short() -> String {
run_or("git", &["status", "--short"], "")
}
fn git_log_oneline(n: usize) -> String {
run_or(
"git",
&["log", "--oneline", &format!("-{n}"), "--decorate"],
"(no commits)",
)
}
// ---------------------------------------------------------------------------
// Tracked-file tree (git ls-files, depth-limited)
// ---------------------------------------------------------------------------
fn tracked_tree(root: &Path, max_depth: usize) -> String {
let files = run("git", &["ls-files"]).unwrap_or_default();
let mut tree: Vec<Vec<String>> = Vec::new();
for path in files.lines() {
let parts: Vec<String> = path.split('/').map(String::from).collect();
if parts.len() <= max_depth + 1 {
tree.push(parts);
}
}
// Deduplicate and build a simple indented representation
let mut seen = std::collections::BTreeSet::new();
let mut lines = Vec::new();
for parts in &tree {
for depth in 0..parts.len() {
let key = parts[..=depth].join("/");
if seen.insert(key) {
let indent = " ".repeat(depth);
let is_leaf = depth == parts.len() - 1;
let label = if is_leaf {
parts[depth].normal().to_string()
} else {
parts[depth].bold().blue().to_string()
};
lines.push(format!("{indent}{label}"));
}
}
}
if lines.is_empty() {
"(no tracked files)".to_string()
} else {
lines.join("\n")
}
}
// ---------------------------------------------------------------------------
// HANDOFF documents
// ---------------------------------------------------------------------------
fn find_handoff_files(root: &Path) -> Vec<PathBuf> {
let mut hits = Vec::new();
if let Ok(rd) = fs::read_dir(root) {
for entry in rd.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with("HANDOFF") {
hits.push(entry.path());
}
}
}
hits.sort();
hits
}
fn read_handoff(path: &Path) -> Result<String> {
fs::read_to_string(path)
.with_context(|| format!("reading {}", path.display()))
}
fn stamp_handoff(path: &Path) -> Result<()> {
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S %z").to_string();
let original = fs::read_to_string(path)
.with_context(|| format!("reading {}", path.display()))?;
// Insert or replace a `last_preflight:` line at the top of the YAML/MD doc
let stamp_line = format!("last_preflight: {now}");
let updated = if let Some(pos) = original.find("last_preflight:") {
let end = original[pos..].find('\n').map(|i| pos + i + 1).unwrap_or(original.len());
format!("{}{stamp_line}\n{}", &original[..pos], &original[end..])
} else {
// Prepend before first non-comment, non-empty line
format!("{stamp_line}\n{original}")
};
if updated != original {
fs::write(path, &updated)
.with_context(|| format!("writing {}", path.display()))?;
println!(" {} updated timestamp", path.display().to_string().dimmed());
}
Ok(())
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
fn main() -> Result<()> {
let cwd = env::current_dir()?;
let root = git_root().unwrap_or_else(|| cwd.clone());
println!("{}", "╔══ preflight ══╗".bold().yellow());
// ── Environment ──────────────────────────────────────────────────────────
section("Environment");
println!(" shell : {}", detect_shell().green());
println!(" cwd : {}", cwd.display().to_string().green());
println!(" git root : {}", root.display().to_string().dimmed());
// ── Git ──────────────────────────────────────────────────────────────────
section("Git — branch / status");
println!(" branch : {}", git_branch().yellow());
let status = git_status_short();
if status.is_empty() {
println!(" status : {}", "clean".green());
} else {
for line in status.lines() {
println!(" {line}");
}
}
section("Git — recent history");
for line in git_log_oneline(7).lines() {
println!(" {line}");
}
// ── Tracked files ─────────────────────────────────────────────────────
section("Tracked files (depth ≤ 3)");
println!("{}", tracked_tree(&root, 3));
// ── HANDOFF documents ────────────────────────────────────────────────
section("HANDOFF documents");
let handoffs = find_handoff_files(&root);
if handoffs.is_empty() {
println!(" (none found)");
} else {
for path in &handoffs {
println!("\n {}", path.display().to_string().bold());
match read_handoff(path) {
Ok(contents) => {
// Print first 40 lines to avoid flooding
for line in contents.lines().take(40) {
println!(" {line}");
}
let total = contents.lines().count();
if total > 40 {
println!(" {} … ({} more lines)", "↳".dimmed(), total - 40);
}
stamp_handoff(path)?;
}
Err(e) => println!(" {}", format!("error: {e}").red()),
}
}
}
println!("\n{}", "╚══ done ══╝".bold().yellow());
Ok(())
}
````
## Usage
```bash
# Make executable and run directly
chmod +x preflight.rs
./preflight.rs
# Or invoke via rust-script explicitly
rust-script preflight.rs
```
## What it surfaces
| Section | Details |
| ----------------- | ------------------------------------------------------------------------------------------------------ |
| Environment | Current shell (`$SHELL`), cwd, git root |
| Git status | Branch name, short status (clean or dirty file list) |
| Git history | Last 7 commits, one-line with decoration |
| Tracked file tree | `git ls-files` output rendered as an indented tree, depth ≤ 3 |
| HANDOFF docs | Full content (first 40 lines) of any `HANDOFF.*` file at repo root; stamps `last_preflight:` timestamp |
## Dependencies
- `rust-script``cargo install rust-script` or `mise use -g rust-script`
- `anyhow`, `colored`, `chrono` — fetched automatically by rust-script on first run