mod launch;
mod verify;
use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context as _, bail};
use serde_json::json;
use tracing::{error, info};
use crate::cli::MetaAction;
pub(crate) const DEFAULT_TIMEOUT_SECS: u64 = 120;
pub(crate) async fn meta(action: MetaAction) -> anyhow::Result<()> {
match action {
MetaAction::Run {
demo,
project,
no_provision,
timeout_secs,
} => run(demo, project, no_provision, timeout_secs).await,
}
}
pub(crate) async fn run(
demo: bool,
project: Option<PathBuf>,
no_provision: bool,
timeout_secs: Option<u64>,
) -> anyhow::Result<()> {
init_meta_tracing();
let project = resolve_project(project)?;
let timeout = Duration::from_secs(timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS));
if !no_provision {
}
info!(
demo,
no_provision,
project = %project.display(),
timeout_secs = timeout.as_secs(),
"meta run: launching a real Claude Code session via the existing machinery"
);
let state_dir = launch::state_dir(&project);
std::fs::create_dir_all(&state_dir)
.with_context(|| format!("failed to create state dir: {}", state_dir.display()))?;
let mgr = launch::new_session_manager(&state_dir).await?;
if demo {
run_demo(&mgr, &project, timeout).await
} else {
run_plain(&mgr, &project, timeout).await
}
}
async fn run_plain(
mgr: &trusty_mpm::session_manager::SessionManager,
project: &Path,
timeout: Duration,
) -> anyhow::Result<()> {
let report = launch::launch_and_wait(mgr, project, None, timeout).await?;
let summary = json!({
"status": "launched",
"demo": false,
"project": project.display().to_string(),
"tmux": report.tmux_name,
"outcome": report.outcome.status(),
});
println!("{}", serde_json::to_string(&summary)?);
Ok(())
}
async fn run_demo(
mgr: &trusty_mpm::session_manager::SessionManager,
project: &Path,
timeout: Duration,
) -> anyhow::Result<()> {
let run_id = run_id();
let expected = verify::expected_content(&run_id);
let task = verify::demo_task(&run_id);
info!(run_id = %run_id, "meta run --demo: bundled task prepared");
let report = launch::launch_and_wait(mgr, project, Some(&task), timeout).await?;
let verdict = verify::verify_artifact(project, &expected);
let summary = json!({
"status": "demo",
"demo": true,
"project": project.display().to_string(),
"run_id": run_id,
"tmux": report.tmux_name,
"launch_outcome": report.outcome.status(),
"artifact": verify::DEMO_ARTIFACT,
"verdict": verdict.status(),
"pass": verdict.is_pass(),
});
println!("{}", serde_json::to_string(&summary)?);
if verdict.is_pass() {
info!(
run_id = %run_id,
"meta run --demo: PASS — session wrote and verified {}",
verify::DEMO_ARTIFACT
);
Ok(())
} else {
error!(
run_id = %run_id,
verdict = verdict.status(),
launch_outcome = report.outcome.status(),
"meta run --demo: FAIL — captured tmux transcript follows:\n{}",
report.transcript
);
bail!(
"meta run --demo failed: artifact verdict '{}' (launch {}); see the captured \
transcript above for diagnostics",
verdict.status(),
report.outcome.status()
)
}
}
static RUN_ID_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
pub(crate) fn run_id() -> String {
let millis = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
let seq = RUN_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
format!("{millis}{seq:06}")
}
pub(crate) fn resolve_project(project: Option<PathBuf>) -> anyhow::Result<PathBuf> {
let raw = match project {
Some(p) => p,
None => std::env::current_dir().context("failed to resolve current directory")?,
};
let resolved = std::fs::canonicalize(&raw)
.with_context(|| format!("project path does not exist: {}", raw.display()))?;
Ok(resolved)
}
fn init_meta_tracing() {
let filter =
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into());
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.try_init();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn meta_resolve_project_accepts_existing_dir() {
let tmp = std::env::temp_dir();
let resolved = resolve_project(Some(tmp.clone())).expect("temp dir resolves");
assert!(resolved.is_absolute());
assert!(resolved.exists());
}
#[test]
fn meta_resolve_project_rejects_missing_path() {
let missing = PathBuf::from("/nonexistent-meta-bootstrap-xyz-12345");
let err = resolve_project(Some(missing)).expect_err("missing path must error");
let msg = format!("{err}");
assert!(
msg.contains("does not exist"),
"error should name the missing path, got: {msg}"
);
}
#[test]
fn meta_resolve_project_defaults_to_cwd() {
let cwd = std::env::current_dir().expect("cwd available");
let resolved = resolve_project(None).expect("cwd resolves");
assert_eq!(
resolved,
std::fs::canonicalize(&cwd).expect("cwd canonicalises")
);
}
#[test]
fn meta_default_timeout_is_120s() {
assert_eq!(DEFAULT_TIMEOUT_SECS, 120);
}
#[test]
fn meta_run_id_is_numeric() {
let id = run_id();
assert!(
id.chars().all(|c| c.is_ascii_digit()),
"run id must be numeric, got: {id}"
);
}
#[test]
fn meta_run_id_is_unique() {
let a = run_id();
let b = run_id();
assert_ne!(a, b, "run ids must be unique across back-to-back calls");
}
}