forklaunch 1.18.0

Launch faster with forklaunch
use std::{collections::HashMap, io::Write};

use anyhow::{Context, Result, bail};
use clap::{Arg, ArgAction, ArgMatches, Command};
use serde_json::{Value, json};
use termcolor::{Color, ColorChoice, StandardStream, WriteColor};

use crate::{
    CliCommand,
    constants::get_platform_management_api_url,
    core::{
        command::command,
        http_client,
        validate::{require_auth, require_manifest},
    },
};

#[derive(Debug)]
pub(crate) struct ConnectCommand;

impl ConnectCommand {
    pub(crate) fn new() -> Self {
        Self {}
    }
}

impl CliCommand for ConnectCommand {
    fn command(&self) -> Command {
        command(
            "connect",
            "Connect this application to a GitHub repository and configure autodeploy",
        )
        .arg(
            Arg::new("repo")
                .long("repo")
                .short('r')
                .required(true)
                .help("Repository URL (e.g. https://github.com/org/repo)"),
        )
        .arg(
            Arg::new("default_branch")
                .long("default-branch")
                .default_value("main")
                .help("Default branch of the repository"),
        )
        .arg(
            Arg::new("auto_deploy")
                .long("auto-deploy")
                .action(ArgAction::SetTrue)
                .help("Automatically release and deploy on push"),
        )
        .arg(
            Arg::new("release_environment")
                .long("release-environment")
                .help("Environment autodeployed releases target (e.g. production)"),
        )
        .arg(
            Arg::new("region")
                .long("region")
                .help("Region autodeployed releases target (e.g. us-east-2)"),
        )
        .arg(
            Arg::new("branch_mapping")
                .long("branch-mapping")
                .action(ArgAction::Append)
                .value_name("BRANCH=ENVIRONMENT")
                .help("Map a branch to an environment (repeatable, e.g. --branch-mapping main=production)"),
        )
        .arg(
            Arg::new("base_path")
                .long("path")
                .short('p')
                .help("Path to application root (optional)"),
        )
    }

    fn handler(&self, matches: &ArgMatches) -> Result<()> {
        let mut stdout = StandardStream::stdout(ColorChoice::Always);
        require_auth()?;
        let (_, manifest) = require_manifest(matches)?;

        let Some(app_id) = &manifest.platform_application_id else {
            bail!(
                "This application is not integrated with the platform. Run `forklaunch integrate --app <application-id>` first."
            );
        };

        let repo = matches
            .get_one::<String>("repo")
            .expect("repo is required by clap");
        let default_branch = matches
            .get_one::<String>("default_branch")
            .expect("default_branch has a default");

        let mut branch_mapping: HashMap<String, String> = HashMap::new();
        if let Some(mappings) = matches.get_many::<String>("branch_mapping") {
            for mapping in mappings {
                let Some((branch, environment)) = mapping.split_once('=') else {
                    bail!(
                        "Invalid --branch-mapping '{}': expected BRANCH=ENVIRONMENT",
                        mapping
                    );
                };
                branch_mapping.insert(branch.to_string(), environment.to_string());
            }
        }

        let mut cicd_config = serde_json::Map::new();
        if matches.get_flag("auto_deploy") {
            cicd_config.insert("autoDeploy".into(), json!(true));
            cicd_config.insert("autoDeployReleases".into(), json!(true));
        }
        if let Some(env) = matches.get_one::<String>("release_environment") {
            cicd_config.insert("releaseEnvironment".into(), json!(env));
        }
        if let Some(region) = matches.get_one::<String>("region") {
            cicd_config.insert("region".into(), json!(region));
        }
        if !branch_mapping.is_empty() {
            cicd_config.insert("branchMapping".into(), json!(branch_mapping));
        }

        let mut body = json!({
            "repositoryUrl": repo,
            "defaultBranch": default_branch,
        });
        if !cicd_config.is_empty() {
            body["cicdConfig"] = Value::Object(cicd_config);
        }

        log_info!(stdout, "Connecting {} to {}...", app_id, repo);
        let url = format!(
            "{}/applications/{}/github/connect",
            get_platform_management_api_url(),
            app_id
        );
        let response = http_client::post(&url, body)?;
        let status = response.status();
        if !status.is_success() {
            let detail = response.text().unwrap_or_default();
            bail!(
                "Failed to connect repository (Status: {}): {}",
                status,
                detail
            );
        }
        let result: Value = response
            .json()
            .with_context(|| "Failed to parse connect response")?;

        log_header!(stdout, Color::Green, "Repository connected!");
        if let Some(message) = result.get("message").and_then(Value::as_str) {
            log_info!(stdout, "{}", message);
        }
        if matches.get_flag("auto_deploy") {
            log_info!(
                stdout,
                "Autodeploy is on: pushes to {} will release and deploy automatically.",
                default_branch
            );
        }
        log_info!(
            stdout,
            "Inspect the connection any time with `forklaunch github status`."
        );

        Ok(())
    }
}