greentic-dev 0.4.73

Developer CLI and local tooling for Greentic flows, packs, and components
Documentation
use anyhow::{Result, bail};
use std::path::PathBuf;

use crate::wizard::plan::{
    RunCommandStep, WizardAnswers, WizardFrontend, WizardPlan, WizardPlanMetadata, WizardStep,
};

pub trait WizardProvider {
    fn build_plan(&self, req: &ProviderRequest) -> Result<WizardPlan>;
}

#[derive(Debug, Clone)]
pub struct ProviderRequest {
    pub frontend: WizardFrontend,
    pub locale: String,
    pub dry_run: bool,
    pub answers: WizardAnswers,
    pub delegated_answers_path: Option<PathBuf>,
}

pub struct ShellWizardProvider;

impl WizardProvider for ShellWizardProvider {
    fn build_plan(&self, req: &ProviderRequest) -> Result<WizardPlan> {
        let selected_action = selected_action(&req.answers)?;
        let args = delegated_command_args(req);

        let (program, semantic_step) = match selected_action {
            "pack" => ("greentic-pack".to_string(), WizardStep::LaunchPackWizard),
            "bundle" => (
                "greentic-bundle".to_string(),
                WizardStep::LaunchBundleWizard,
            ),
            other => bail!("unsupported selected_action `{other}`; expected `pack` or `bundle`"),
        };

        Ok(WizardPlan {
            plan_version: 1,
            created_at: None,
            metadata: WizardPlanMetadata {
                target: "launcher".to_string(),
                mode: "main".to_string(),
                locale: req.locale.clone(),
                frontend: req.frontend.clone(),
            },
            inputs: Default::default(),
            steps: vec![
                semantic_step,
                WizardStep::RunCommand(RunCommandStep {
                    program,
                    args,
                    destructive: false,
                }),
            ],
        })
    }
}

fn selected_action(answers: &WizardAnswers) -> Result<&str> {
    let Some(action) = answers.data.get("selected_action").and_then(|v| v.as_str()) else {
        bail!("missing required answers.selected_action (`pack` or `bundle`)");
    };
    Ok(action)
}

fn delegated_command_args(req: &ProviderRequest) -> Vec<String> {
    let mut args = vec!["wizard".to_string()];
    if let Some(path) = &req.delegated_answers_path {
        args.extend([
            "apply".to_string(),
            "--answers".to_string(),
            path.display().to_string(),
        ]);
    }
    if req.dry_run {
        args.push("--dry-run".to_string());
    }
    args
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::{ProviderRequest, ShellWizardProvider, WizardProvider};
    use crate::wizard::plan::{WizardAnswers, WizardFrontend, WizardStep};

    #[test]
    fn build_plan_pack_apply() {
        let provider = ShellWizardProvider;
        let plan = provider
            .build_plan(&ProviderRequest {
                frontend: WizardFrontend::Json,
                locale: "en-US".to_string(),
                dry_run: false,
                answers: WizardAnswers {
                    data: json!({"selected_action":"pack"}),
                },
                delegated_answers_path: None,
            })
            .expect("build plan");

        assert_eq!(plan.metadata.target, "launcher");
        assert_eq!(plan.metadata.mode, "main");
        let cmd = match plan.steps.last().expect("run step") {
            WizardStep::RunCommand(cmd) => cmd,
            other => panic!("expected RunCommand step, got {other:?}"),
        };
        assert_eq!(cmd.program, "greentic-pack");
        assert_eq!(cmd.args, vec!["wizard"]);
    }

    #[test]
    fn build_plan_bundle_dry_run() {
        let provider = ShellWizardProvider;
        let plan = provider
            .build_plan(&ProviderRequest {
                frontend: WizardFrontend::Json,
                locale: "en-US".to_string(),
                dry_run: true,
                answers: WizardAnswers {
                    data: json!({"selected_action":"bundle"}),
                },
                delegated_answers_path: None,
            })
            .expect("build plan");

        let cmd = match plan.steps.last().expect("run step") {
            WizardStep::RunCommand(cmd) => cmd,
            other => panic!("expected RunCommand step, got {other:?}"),
        };
        assert_eq!(cmd.program, "greentic-bundle");
        assert_eq!(cmd.args, vec!["wizard", "--dry-run"]);
    }

    #[test]
    fn build_plan_uses_delegated_answers_when_present() {
        let provider = ShellWizardProvider;
        let plan = provider
            .build_plan(&ProviderRequest {
                frontend: WizardFrontend::Json,
                locale: "en-US".to_string(),
                dry_run: false,
                answers: WizardAnswers {
                    data: json!({"selected_action":"bundle"}),
                },
                delegated_answers_path: Some("/tmp/delegated-answers.json".into()),
            })
            .expect("build plan");

        let cmd = match plan.steps.last().expect("run step") {
            WizardStep::RunCommand(cmd) => cmd,
            other => panic!("expected RunCommand step, got {other:?}"),
        };
        assert_eq!(cmd.program, "greentic-bundle");
        assert_eq!(
            cmd.args,
            vec![
                "wizard",
                "apply",
                "--answers",
                "/tmp/delegated-answers.json",
            ]
        );
    }
}