feat: Loki can now update itself via .update and --update commands

This commit is contained in:
2026-05-19 14:29:44 -06:00
parent 1902e2d040
commit 6078072915
10 changed files with 698 additions and 9 deletions
Generated
+360 -2
View File
@@ -145,6 +145,15 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "arboard"
version = "3.6.1"
@@ -1267,6 +1276,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "convert_case"
version = "0.8.0"
@@ -1285,6 +1300,35 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "cookie_store"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
dependencies = [
"cookie",
"document-features",
"idna",
"indexmap 2.14.0",
"log",
"serde",
"serde_derive",
"serde_json",
"time",
"url",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
@@ -1435,6 +1479,33 @@ dependencies = [
"syn",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "darling"
version = "0.20.11"
@@ -1504,6 +1575,16 @@ dependencies = [
"syn",
]
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "deranged"
version = "0.5.8"
@@ -1514,6 +1595,17 @@ dependencies = [
"serde_core",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_more"
version = "0.99.20"
@@ -1685,6 +1777,31 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8",
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519",
"serde",
"sha2",
"signature",
"subtle",
"zeroize",
]
[[package]]
name = "ego-tree"
version = "0.10.0"
@@ -1703,6 +1820,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "enum-as-inner"
version = "0.6.1"
@@ -1831,6 +1957,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "filedescriptor"
version = "0.8.3"
@@ -1842,6 +1974,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "filetime"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
dependencies = [
"cfg-if",
"libc",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -1862,6 +2004,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
"zlib-rs",
]
[[package]]
@@ -2711,6 +2854,19 @@ dependencies = [
"serde_core",
]
[[package]]
name = "indicatif"
version = "0.18.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb"
dependencies = [
"console",
"portable-atomic",
"unicode-width",
"unit-prefix",
"web-time",
]
[[package]]
name = "indoc"
version = "2.0.7"
@@ -3114,6 +3270,7 @@ dependencies = [
"rmcp",
"rust-embed",
"scraper",
"self_update",
"serde",
"serde_json",
"serde_yaml",
@@ -3942,6 +4099,16 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "pkg-config"
version = "0.3.33"
@@ -3956,7 +4123,7 @@ checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"
dependencies = [
"base64",
"indexmap 2.14.0",
"quick-xml",
"quick-xml 0.39.4",
"serde",
"time",
]
@@ -4114,6 +4281,15 @@ dependencies = [
"prost",
]
[[package]]
name = "quick-xml"
version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.39.4"
@@ -4431,8 +4607,10 @@ checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0"
dependencies = [
"base64",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2 0.4.14",
"http 1.4.0",
"http-body 1.0.1",
"http-body-util",
@@ -4657,6 +4835,7 @@ dependencies = [
"aws-lc-rs",
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki 0.103.13",
"subtle",
@@ -4910,6 +5089,43 @@ dependencies = [
"smallvec",
]
[[package]]
name = "self-replace"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7"
dependencies = [
"fastrand",
"tempfile",
"windows-sys 0.52.0",
]
[[package]]
name = "self_update"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e79722b5a505d4ddc77527455a97244e9e8c4c07533ff44cf4421cce7bb6d17"
dependencies = [
"either",
"flate2",
"http 1.4.0",
"indicatif",
"log",
"quick-xml 0.38.4",
"regex",
"reqwest 0.13.3",
"self-replace",
"semver",
"serde",
"serde_json",
"tar",
"tempfile",
"ureq",
"urlencoding",
"zip",
"zipsign-api",
]
[[package]]
name = "semver"
version = "1.0.28"
@@ -5170,6 +5386,7 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core 0.6.4",
]
@@ -5251,6 +5468,27 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "socks"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b"
dependencies = [
"byteorder",
"libc",
"winapi",
]
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "sse-stream"
version = "0.2.3"
@@ -5440,6 +5678,17 @@ dependencies = [
"windows 0.62.2",
]
[[package]]
name = "tar"
version = "0.4.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -6031,6 +6280,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unit-prefix"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3"
[[package]]
name = "universal-hash"
version = "0.5.1"
@@ -6068,6 +6323,40 @@ version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "ureq"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
dependencies = [
"base64",
"cookie_store",
"encoding_rs",
"flate2",
"log",
"percent-encoding",
"rustls 0.23.40",
"rustls-pki-types",
"serde",
"serde_json",
"socks",
"ureq-proto",
"utf8-zero",
"webpki-roots",
]
[[package]]
name = "ureq-proto"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
dependencies = [
"base64",
"http 1.4.0",
"httparse",
"log",
]
[[package]]
name = "url"
version = "2.5.8"
@@ -6092,6 +6381,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8-zero"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -6387,7 +6682,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a"
dependencies = [
"proc-macro2",
"quick-xml",
"quick-xml 0.39.4",
"quote",
]
@@ -6429,6 +6724,15 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "which"
version = "8.0.2"
@@ -6951,6 +7255,16 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "xattr"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
"rustix",
]
[[package]]
name = "xml5ever"
version = "0.18.1"
@@ -7097,8 +7411,52 @@ dependencies = [
"syn",
]
[[package]]
name = "zip"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b"
dependencies = [
"arbitrary",
"crc32fast",
"flate2",
"indexmap 2.14.0",
"memchr",
"time",
"zopfli",
]
[[package]]
name = "zipsign-api"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dba6063ff82cdbd9a765add16d369abe81e520f836054e997c2db217ceca40c0"
dependencies = [
"base64",
"ed25519-dalek",
"thiserror 2.0.18",
]
[[package]]
name = "zlib-rs"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zopfli"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
]
+8
View File
@@ -96,6 +96,14 @@ clap_complete_nushell = "4.5.9"
open = "5"
rand = { version = "0.10.0", features = ["default"] }
url = "2.5.8"
self_update = { version = "0.44", default-features = false, features = [
"reqwest",
"rustls",
"archive-tar",
"compression-flate2",
"archive-zip",
"compression-zip-deflate",
] }
[dependencies.reqwest]
version = "0.13.3"
+23
View File
@@ -138,6 +138,29 @@ To use a binary from the releases page on Linux/MacOS, do the following:
3. Extract the binary with `tar -C /usr/local/bin -xzf loki-<arch>.tar.gz` (Note: This may require `sudo`)
4. Now you can run `loki`!
## Updating
Loki can update itself in place to the latest GitHub release. Run `loki --update`
for the newest release, or `loki --update v0.4.0` for a specific version:
```shell
loki --update
loki --update v0.4.0
```
The same is available from within the REPL via `.update` and `.update v0.4.0`.
If Loki was installed with a package manager, prefer that package manager so its
records stay in sync with the binary on disk; i.e. `brew upgrade loki` for Homebrew,
or `cargo install --locked loki-ai` for Cargo.
When Loki detects a package-manager install it prints a warning and asks for
confirmation. In a non-interactive shell (no TTY), pass `--force` to update
anyway:
```shell
loki --update --force
```
## Getting Started
After installation, you can generate the configuration files and directories by simply running:
+30
View File
@@ -137,6 +137,12 @@ pub struct Cli {
/// Generate static shell completion scripts
#[arg(long, value_name = "SHELL", value_enum)]
pub completions: Option<ShellCompletion>,
/// Update Loki to the latest release, or to a specific version
#[arg(long, value_name = "VERSION")]
pub update: Option<Option<String>>,
/// With --update, update even if Loki was installed via a package manager
#[arg(long, requires = "update")]
pub force: bool,
}
impl Cli {
@@ -396,4 +402,28 @@ mod tests {
let cli = parse(&["--macro", "my-macro"]);
assert_eq!(cli.macro_name, Some("my-macro".to_string()));
}
#[test]
fn parse_update_flag_no_value() {
let cli = parse(&["--update"]);
assert_eq!(cli.update, Some(None));
}
#[test]
fn parse_update_flag_with_version() {
let cli = parse(&["--update", "v0.4.0"]);
assert_eq!(cli.update, Some(Some("v0.4.0".to_string())));
}
#[test]
fn parse_update_with_force() {
let cli = parse(&["--update", "--force"]);
assert_eq!(cli.update, Some(None));
assert!(cli.force);
}
#[test]
fn parse_force_without_update_fails() {
assert!(Cli::try_parse_from(["loki", "--force"]).is_err());
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ use crate::utils::AbortSignal;
use anyhow::{Context, Result, anyhow, bail};
use eventsource_stream::Eventsource;
use futures_util::{Stream, StreamExt};
use reqwest::{header, RequestBuilder};
use reqwest::{RequestBuilder, header};
use serde_json::Value;
use tokio::sync::mpsc::UnboundedSender;
+2
View File
@@ -12,6 +12,7 @@ mod role;
mod session;
pub(crate) mod todo;
mod tool_scope;
mod update;
pub use self::agent::{Agent, AgentVariables, complete_agent_variables, list_agents};
#[allow(unused_imports)]
@@ -25,6 +26,7 @@ pub use self::role::{
CODE_ROLE, CREATE_TITLE_ROLE, EXPLAIN_SHELL_ROLE, Role, RoleLike, SHELL_ROLE,
};
use self::session::Session;
pub use self::update::run_self_update;
use crate::client::{
ClientConfig, MessageContentToolCalls, Model, ModelType, OPENAI_COMPATIBLE_PROVIDERS,
ProviderModels, create_client_config, list_client_types,
+248
View File
@@ -0,0 +1,248 @@
use std::fs::OpenOptions;
use anyhow::{Context, Result, bail};
use inquire::Confirm;
use is_terminal::IsTerminal;
use std::path::Path;
use std::{env, fs, io, process};
use dunce::canonicalize;
use self_update::backends::github::Update;
use self_update::Status;
use crate::utils::warning_text;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InstallSource {
Cargo,
Homebrew,
Manual,
}
impl InstallSource {
fn is_package_managed(self) -> bool {
matches!(self, InstallSource::Cargo | InstallSource::Homebrew)
}
fn label(self) -> &'static str {
match self {
InstallSource::Cargo => "Cargo",
InstallSource::Homebrew => "Homebrew",
InstallSource::Manual => "manually-installed",
}
}
}
fn classify_install_path(path: &Path) -> InstallSource {
let components: Vec<&str> = path
.components()
.filter_map(|c| c.as_os_str().to_str())
.collect();
if components
.windows(2)
.any(|w| w[0] == ".cargo" && w[1] == "bin")
{
return InstallSource::Cargo;
}
if components.contains(&"Cellar") {
return InstallSource::Homebrew;
}
let path_str = path.to_string_lossy();
if path_str.starts_with("/opt/homebrew/") || path_str.starts_with("/home/linuxbrew/.linuxbrew/")
{
return InstallSource::Homebrew;
}
InstallSource::Manual
}
fn normalize_version(requested: Option<String>) -> Option<String> {
let raw = requested?;
let trimmed = raw.trim();
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("latest") {
return None;
}
match trimmed.chars().next() {
Some('v' | 'V') => Some(trimmed.to_string()),
Some(c) if c.is_ascii_digit() => Some(format!("v{trimmed}")),
_ => Some(trimmed.to_string()),
}
}
fn is_dir_writable(dir: &Path) -> bool {
let probe = dir.join(format!(".loki-update-write-test-{}", process::id()));
match OpenOptions::new()
.write(true)
.create_new(true)
.open(&probe)
{
Ok(_) => {
let _ = fs::remove_file(&probe);
true
}
Err(_) => false,
}
}
pub fn run_self_update(requested: Option<String>, force: bool) -> Result<()> {
let target_tag = normalize_version(requested);
let exe_path = env::current_exe()
.context("Could not determine the path of the running loki executable")?;
let resolved = canonicalize(&exe_path).unwrap_or_else(|_| exe_path.clone());
let source = classify_install_path(&resolved);
if source.is_package_managed() {
let body = match source {
InstallSource::Homebrew => format!(
"Loki appears to be installed via Homebrew ({}).\n\
Updating in place replaces the binary inside Homebrew's Cellar; `brew` will\n\
then report a version that no longer matches the file on disk, and a later\n\
`brew upgrade`/`brew reinstall` may overwrite it or fail.\n\
The clean way to update is: brew upgrade loki",
exe_path.display()
),
InstallSource::Cargo => format!(
"Loki appears to be installed via `cargo install` ({}).\n\
Updating in place leaves Cargo's records out of sync with the binary on disk.\n\
The clean way to update is: cargo install --locked loki-ai",
exe_path.display()
),
InstallSource::Manual => unreachable!("Manual installs are not package-managed"),
};
println!("{} {body}", warning_text("WARNING:"));
if force {
println!("--force specified; updating anyway.");
} else if io::stdin().is_terminal() {
let proceed = Confirm::new("Update anyway?")
.with_default(false)
.prompt()?;
if !proceed {
println!("Update cancelled.");
return Ok(());
}
} else {
bail!(
"Refusing to update a {} install. Re-run with --force to override.",
source.label()
);
}
}
if let Some(parent) = exe_path.parent()
&& !is_dir_writable(parent)
{
bail!(
"No write permission for '{}'. Re-run with elevated permissions (e.g. sudo), \
or update Loki through your package manager.",
parent.display()
);
}
let interactive = io::stdin().is_terminal();
let mut builder = Update::configure();
builder
.repo_owner("Dark-Alex-17")
.repo_name("loki")
.bin_name("loki")
.current_version(env!("CARGO_PKG_VERSION"))
.no_confirm(true)
.show_download_progress(interactive);
if let Some(tag) = &target_tag {
builder.target_version_tag(tag.as_str());
}
let status = builder
.build()
.context("Failed to configure the self-update")?
.update()
.context("Self-update failed")?;
match status {
Status::UpToDate(version) => {
println!("Loki is already up to date (v{version}).");
}
Status::Updated(version) => {
println!("Loki updated to v{version}. Restart loki to use the new version.");
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn classify_cargo_install() {
assert_eq!(
classify_install_path(&PathBuf::from("/home/u/.cargo/bin/loki")),
InstallSource::Cargo
);
}
#[test]
fn classify_homebrew_opt_prefix() {
assert_eq!(
classify_install_path(&PathBuf::from("/opt/homebrew/bin/loki")),
InstallSource::Homebrew
);
}
#[test]
fn classify_homebrew_cellar() {
assert_eq!(
classify_install_path(&PathBuf::from("/usr/local/Cellar/loki/0.3.0/bin/loki")),
InstallSource::Homebrew
);
}
#[test]
fn classify_homebrew_linuxbrew() {
assert_eq!(
classify_install_path(&PathBuf::from("/home/linuxbrew/.linuxbrew/bin/loki")),
InstallSource::Homebrew
);
}
#[test]
fn classify_manual_usr_local_bin() {
assert_eq!(
classify_install_path(&PathBuf::from("/usr/local/bin/loki")),
InstallSource::Manual
);
}
#[test]
fn classify_manual_local_bin() {
assert_eq!(
classify_install_path(&PathBuf::from("/home/u/.local/bin/loki")),
InstallSource::Manual
);
}
#[test]
fn normalize_version_latest_and_empty_are_none() {
assert_eq!(normalize_version(None), None);
assert_eq!(normalize_version(Some(String::new())), None);
assert_eq!(normalize_version(Some(" ".to_string())), None);
assert_eq!(normalize_version(Some("latest".to_string())), None);
assert_eq!(normalize_version(Some("LATEST".to_string())), None);
}
#[test]
fn normalize_version_prepends_v_for_bare_semver() {
assert_eq!(
normalize_version(Some("0.4.0".to_string())),
Some("v0.4.0".to_string())
);
assert_eq!(
normalize_version(Some("v0.4.0".to_string())),
Some("v0.4.0".to_string())
);
assert_eq!(
normalize_version(Some(" v0.4.0 ".to_string())),
Some("v0.4.0".to_string())
);
}
}
+7
View File
@@ -83,6 +83,13 @@ async fn main() -> Result<()> {
let log_path = setup_logger()?;
if let Some(version) = &cli.update {
let version = version.clone();
let force = cli.force;
return tokio::task::spawn_blocking(move || config::run_self_update(version, force))
.await?;
}
install_builtins()?;
if let Some(client_arg) = &cli.authenticate {
+2 -3
View File
@@ -2,6 +2,7 @@ use anyhow::{Context, Result, anyhow};
use eventsource_stream::{EventStream, Eventsource};
use fmt::{Display, Formatter};
use futures_util::StreamExt;
use futures_util::stream::BoxStream;
use mpsc::error::SendError;
use mpsc::{OwnedPermit, Receiver, Sender, channel};
use reqwest::Client;
@@ -13,13 +14,11 @@ use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::task::Poll;
use futures_util::stream::BoxStream;
use tokio::sync::mpsc;
use tokio::time::Duration;
use url::Url;
type SseEventStream =
EventStream<BoxStream<'static, reqwest::Result<bytes::Bytes>>>;
type SseEventStream = EventStream<BoxStream<'static, reqwest::Result<bytes::Bytes>>>;
const CHANNEL_BUF: usize = 64;
+17 -3
View File
@@ -33,6 +33,7 @@ use reedline::{
use reedline::{MenuBuilder, Signal};
use std::sync::LazyLock;
use std::{env, process, sync::Arc};
use tokio::task;
const MENU_NAME: &str = "completion_menu";
@@ -45,7 +46,7 @@ pub const DEFAULT_CONTINUATION_PROMPT: &str = indoc! {"
4. Continue with the next pending item now. Call tools immediately."
};
static REPL_COMMANDS: LazyLock<[ReplCommand; 41]> = LazyLock::new(|| {
static REPL_COMMANDS: LazyLock<[ReplCommand; 42]> = LazyLock::new(|| {
[
ReplCommand::new(".help", "Show this help guide", AssertState::pass()),
ReplCommand::new(".info", "Show system info", AssertState::pass()),
@@ -222,6 +223,11 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 41]> = LazyLock::new(|| {
"Reinstall bundled agents, macros, functions, or MCP config (overwrites local changes)",
AssertState::pass(),
),
ReplCommand::new(
".update",
"Update Loki to the latest release (or a specified version)",
AssertState::pass(),
),
ReplCommand::new(".exit", "Exit REPL", AssertState::pass()),
]
});
@@ -542,6 +548,14 @@ pub async fn run_repl_command(
},
_ => println!("Usage: .install <{}>", AssetCategory::NAMES.join("|")),
},
".update" => {
if ctx.macro_flag {
bail!("Cannot perform this operation because you are in a macro")
}
let version = args.map(|s| s.trim().to_string());
task::spawn_blocking(move || config::run_self_update(version, false))
.await??;
}
".rag" => {
ctx.use_rag(args, abort_signal.clone()).await?;
}
@@ -1241,8 +1255,8 @@ mod tests {
}
#[test]
fn repl_commands_has_41_entries() {
assert_eq!(REPL_COMMANDS.len(), 41);
fn repl_commands_has_42_entries() {
assert_eq!(REPL_COMMANDS.len(), 42);
}
#[test]