use crate::{manifest::Manifest, paths, proc};
use anyhow::{Context, Result};
fn artifact_filename(artifact: &str) -> Result<&'static str> {
match artifact {
"spec" => Ok("spec.md"),
"plan" => Ok("plan.md"),
"tasks" => Ok("tasks.md"),
other => {
anyhow::bail!(
"unknown artifact '{other}' — expected spec, plan, tasks, report, or diff"
)
}
}
}
const BOLD: &str = "\x1b[1m";
const DIM: &str = "\x1b[2m";
const CYAN: &str = "\x1b[36m";
const YELLOW: &str = "\x1b[33m";
const RESET: &str = "\x1b[0m";
fn highlight(markdown: &str) -> String {
let mut out = String::new();
let mut in_code_fence = false;
for line in markdown.lines() {
let trimmed = line.trim_start();
if trimmed.starts_with("```") {
in_code_fence = !in_code_fence;
out.push_str(DIM);
out.push_str(line);
out.push_str(RESET);
} else if in_code_fence {
out.push_str(DIM);
out.push_str(line);
out.push_str(RESET);
} else if trimmed.starts_with('#') {
out.push_str(BOLD);
out.push_str(CYAN);
out.push_str(line);
out.push_str(RESET);
} else if trimmed.starts_with("- [ ]") || trimmed.starts_with("- [x]") {
out.push_str(YELLOW);
out.push_str(line);
out.push_str(RESET);
} else {
out.push_str(line);
}
out.push('\n');
}
out
}
pub fn run(project: &str, slug: &str, artifact: &str) -> Result<()> {
if artifact == "diff" {
crate::manifest::validate_name(slug).context("spec name")?;
let m = Manifest::load(&paths::manifest_path(project)?)?;
let container = m.sandbox_container();
let status = proc::run_inherit(
"docker",
&[
"exec",
&container,
"git",
"--no-pager",
"diff",
"HEAD",
"--",
".",
":(exclude).keel",
],
)?;
let (_, new_files) = proc::run_capture(
"docker",
&[
"exec",
&container,
"git",
"ls-files",
"--others",
"--exclude-standard",
"--",
".",
":(exclude).keel",
],
)?;
for f in new_files.lines().filter(|l| !l.trim().is_empty()) {
let f: String = f.chars().filter(|c| !c.is_control()).collect();
println!("new file (not shown above): {f}");
}
return proc::require_success("showing the diff", status);
}
if artifact == "report" {
crate::manifest::validate_name(slug).context("spec name")?;
return super::keel_cmd::run(project, &["report".to_string(), slug.to_string()]);
}
let filename = artifact_filename(artifact)?;
let m = Manifest::load(&paths::manifest_path(project)?)?;
let container = m.sandbox_container();
let path = format!(".keel/specs/{slug}/{filename}");
let (status, out) = proc::run_capture("docker", &["exec", &container, "cat", &path])?;
if !status.success() {
anyhow::bail!(
"no {filename} found for spec '{slug}' in project '{project}' (looked for {path})"
);
}
print!("{}", highlight(&out));
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maps_known_artifacts_to_their_filename() {
assert_eq!(artifact_filename("spec").unwrap(), "spec.md");
assert_eq!(artifact_filename("plan").unwrap(), "plan.md");
assert_eq!(artifact_filename("tasks").unwrap(), "tasks.md");
}
#[test]
fn rejects_an_unknown_artifact() {
let err = artifact_filename("evidence").unwrap_err();
assert!(err.to_string().contains("evidence"));
}
#[test]
fn highlights_headings_bold_cyan() {
let out = highlight("# Tasks\nplain line");
assert_eq!(out, format!("{BOLD}{CYAN}# Tasks{RESET}\nplain line\n"));
}
#[test]
fn highlights_checkbox_lines_yellow() {
let out = highlight("- [ ] todo\n- [x] done");
assert_eq!(
out,
format!("{YELLOW}- [ ] todo{RESET}\n{YELLOW}- [x] done{RESET}\n")
);
}
#[test]
fn dims_code_fence_contents_including_the_fences() {
let out = highlight("```rust\nlet x = 1;\n```");
assert_eq!(
out,
format!("{DIM}```rust{RESET}\n{DIM}let x = 1;{RESET}\n{DIM}```{RESET}\n")
);
}
#[test]
fn leaves_ordinary_lines_untouched() {
let out = highlight("just a sentence.");
assert_eq!(out, "just a sentence.\n");
}
#[test]
fn a_heading_marker_inside_a_code_fence_is_dimmed_not_bolded() {
let out = highlight("```\n# not a heading\n```");
assert_eq!(
out,
format!("{DIM}```{RESET}\n{DIM}# not a heading{RESET}\n{DIM}```{RESET}\n")
);
}
}