offline-cli 0.3.3

The Offline Protocol developer CLI
//! `offline plan`: workflow/package/architecture plan without scaffolding.

use std::path::PathBuf;

use anyhow::Result;
use clap::Args;
use offline_registry::{architecture_markdown, implementation_plan_markdown, Registry};
use serde_json::json;

use crate::output;

#[derive(Args)]
pub struct PlanArgs {
    /// What you want to build, e.g. "a local event app"
    pub prompt: String,
    /// Target platform: react-native | nextjs | react-vite (inferred if omitted)
    #[arg(long)]
    pub platform: Option<String>,
    /// Write architecture.md and implementation-plan.md into this directory
    #[arg(long)]
    pub out: Option<PathBuf>,
    /// Output JSON
    #[arg(long)]
    pub json: bool,
}

pub fn run(args: PlanArgs) -> Result<()> {
    let registry = Registry::load_default()?;
    let plan = registry.plan(&args.prompt, args.platform.as_deref())?;
    let architecture = architecture_markdown(&plan);
    let implementation = implementation_plan_markdown(&plan);

    if let Some(out) = &args.out {
        std::fs::create_dir_all(out)?;
        std::fs::write(out.join("architecture.md"), &architecture)?;
        std::fs::write(out.join("implementation-plan.md"), &implementation)?;
    }

    if args.json {
        return output::json(&json!({
            "plan": plan,
            "documents": {
                "architecture.md": architecture,
                "implementation-plan.md": implementation,
            }
        }));
    }

    output::success(&format!(
        "{} ({}) on {}",
        plan.workflow_title, plan.workflow, plan.platform
    ));
    output::heading("Packages");
    for p in &plan.resolution.packages {
        output::item(&p.name, &format!("v{} — {}", p.version, p.reason));
    }
    for w in &plan.resolution.warnings {
        output::warn(w);
    }
    output::heading("Install");
    for cmd in &plan.resolution.install_commands {
        println!("  {cmd}");
    }
    if let Some(t) = &plan.template {
        output::heading("Scaffold");
        println!(
            "  offline create {} --platform {} --name <name>",
            plan.workflow, plan.platform
        );
        output::note(&format!("template: {t}"));
    }
    if !plan.alternatives.is_empty() {
        output::heading("Alternatives considered");
        for alt in &plan.alternatives {
            output::item(&alt.workflow, &format!("score {:.1}", alt.score));
        }
    }
    if let Some(out) = &args.out {
        output::note(&format!(
            "architecture.md and implementation-plan.md written to {}",
            out.display()
        ));
    } else {
        output::note("add --out <dir> to write architecture.md / implementation-plan.md");
    }
    Ok(())
}