1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
use std::{env, process::Command};

use clap::{crate_version, Args};
use color_eyre::eyre::{bail, Ok, Result};
use fluent_templates::Loader;
use semver::Version;
use tracing::error;
use url::Url;

use crate::{
    utils::{self, CurrentDir},
    LANG_ID, LOCALES,
};

#[must_use]
#[derive(Args)]
#[command(about = LOCALES.lookup(&LANG_ID, "update_command"))]
pub struct Update {
    #[arg(long, num_args = 0..=1, default_missing_value = super::DEFAULT_PROXY_SURGE,
        help = LOCALES.lookup(&LANG_ID, "proxy"))]
    pub proxy: Option<Url>,
}

pub async fn execute(config: Update) -> Result<()> {
    utils::ensure_executable_exists("gh")?;

    if let Some(proxy) = config.proxy {
        env::set_var("HTTP_PROXY", proxy.to_string());
        env::set_var("HTTPS_PROXY", proxy.to_string());
    }

    let current_version = Version::parse(crate_version!())?;
    println!("Checking current version: {}", current_version);

    let latest_released_version = latest_released_version()?;
    println!(
        "Checking latest released version: {}",
        latest_released_version
    );

    if current_version >= latest_released_version {
        println!("Already up-to-date");
        return Ok(());
    }

    let zip_name = if cfg!(target_os = "windows") {
        "novel-cli-x86_64-pc-windows-msvc.zip"
    } else if cfg!(target_os = "macos") {
        "novel-cli-aarch64-apple-darwin.zip"
    } else if cfg!(target_os = "linux") {
        "novel-cli-x86_64-unknown-linux-gnu.zip"
    } else {
        bail!("Unsupported operating system");
    };

    let temp_dir = tempfile::tempdir()?;
    let current_dir = CurrentDir::new(temp_dir.path())?;

    let mut child = Command::new("gh")
        .arg("release")
        .arg("download")
        .arg("--clobber")
        .args(["--pattern", zip_name])
        .args(["--repo", "novel-rs/cli"])
        .spawn()?;

    if !child.wait()?.success() {
        bail!("`gh` failed to execute");
    }

    let zip_path = temp_dir.path().join(zip_name);
    super::unzip(&zip_path)?;

    let file_name = if cfg!(target_os = "windows") {
        "novel-cli.exe"
    } else {
        "novel-cli"
    };
    let file_path = temp_dir
        .path()
        .join(zip_path.with_extension(""))
        .join(file_name);

    self_replace::self_replace(file_path)?;

    current_dir.restore()?;

    println!("Update Successful");

    Ok(())
}

fn latest_released_version() -> Result<Version> {
    let output = Command::new("gh")
        .arg("release")
        .arg("list")
        .args(["--repo", "novel-rs/cli"])
        .args(["--json", "tagName"])
        .args(["--order", "desc"])
        .args(["--jq", ".[0].tagName"])
        .output()?;

    if !output.status.success() {
        error!("{}", simdutf8::basic::from_utf8(&output.stderr)?);
        bail!("`gh` failed to execute");
    }

    Ok(Version::parse(
        simdutf8::basic::from_utf8(&output.stdout)?.trim(),
    )?)
}