Release / build (x86_64-unknown-linux-gnu) (push) Failing after 6s
Release / build (aarch64-apple-darwin) (push) Canceled after 0s
Release / build (aarch64-unknown-linux-gnu) (push) Canceled after 0s
Release / build (x86_64-apple-darwin) (push) Canceled after 0s
Release / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
Release / publish GitHub Release (push) Canceled after 0s
The GitHub account is restored, so github.com/ZacharyZhang-NY/Kigi-CLI is
the canonical repo again. Reverse the 2026-08 Gitea migration:
- .github/workflows/{release,warm-cache}.yml: restore the GitHub-hosted
pipeline dropped in 3e1652c, unchanged — hosted runners, rust-cache,
softprops release with every asset attached atomically.
- .gitea/workflows/release.yml: retire the Gitea edition (the runners are
being deregistered; git.zacharyzhang.com keeps the v0.1.12/v0.1.13
releases for old clients).
- kigi-env: update_base_url now defaults to the GitHub Releases API. The
updater already speaks GitHub wire format and sends a User-Agent; v0.1.14
is published to BOTH forges with identical assets so pre-0.1.14 installs
polling Gitea self-update onto the GitHub channel.
- install.sh / install.ps1: resolve releases from api.github.com.
- feedback, /docs, SECURITY.md, frontend worker + site: repo, issues and
raw-file links point at github.com / raw.githubusercontent.com.
- docs/RELEASE.md: GitHub edition of the checklist; Gitea runner sections
retired.
- version: 0.1.13 -> 0.1.14 (the cross-forge transition release).
Claude-Session: https://claude.ai/code/session_01A6RcwHhhsFYYrNdQB4ASuP
172 lines
5.7 KiB
Rust
172 lines
5.7 KiB
Rust
//! Backend endpoint defaults for the Kigi crate family.
|
|
//!
|
|
//! Kigi talks to exactly four first-party endpoints (PRD §8.2); everything
|
|
//! else (MCP servers, GitHub release assets) is user- or release-configured.
|
|
//! Each value resolves as an env-var override when set, else the compiled
|
|
//! production default.
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct KigiEndpoints {
|
|
/// Kimi Code subscription inference API (OpenAI chat/completions compatible).
|
|
pub coding_api_base_url: &'static str,
|
|
/// OAuth device-flow host for Kimi Code subscription login.
|
|
pub oauth_host: &'static str,
|
|
/// Releases API endpoint the self-updater polls (GitHub-Releases-shaped;
|
|
/// Gitea's `/api/v1` serves the same wire format).
|
|
pub update_base_url: &'static str,
|
|
/// Human-facing page for subscription upgrade guidance.
|
|
pub upgrade_page_url: &'static str,
|
|
}
|
|
|
|
pub const PRODUCTION_ENDPOINTS: KigiEndpoints = KigiEndpoints {
|
|
coding_api_base_url: "https://api.kimi.com/coding/v1",
|
|
oauth_host: "https://auth.kimi.com",
|
|
update_base_url: "https://api.github.com/repos/ZacharyZhang-NY/Kigi-CLI/releases",
|
|
upgrade_page_url: "https://www.kimi.com/code/",
|
|
};
|
|
|
|
/// Env var overriding [`coding_api_base_url`] (PRD F3).
|
|
pub const CODE_BASE_URL_ENV: &str = "KIGI_CODE_BASE_URL";
|
|
/// Env var overriding [`oauth_host`] (PRD F1).
|
|
pub const OAUTH_HOST_ENV: &str = "KIGI_OAUTH_HOST";
|
|
/// Env var overriding [`update_base_url`] (PRD F8). Points the self-updater
|
|
/// at an alternate GitHub/Gitea-Releases-shaped API (mirrors, tests).
|
|
pub const UPDATE_BASE_URL_ENV: &str = "KIGI_UPDATE_BASE_URL";
|
|
|
|
fn resolve(var: &str, compiled: &'static str) -> String {
|
|
match std::env::var(var) {
|
|
Ok(v) if !v.trim().is_empty() => v,
|
|
_ => compiled.to_string(),
|
|
}
|
|
}
|
|
|
|
pub fn coding_api_base_url() -> String {
|
|
resolve(CODE_BASE_URL_ENV, PRODUCTION_ENDPOINTS.coding_api_base_url)
|
|
}
|
|
|
|
pub fn oauth_host() -> String {
|
|
resolve(OAUTH_HOST_ENV, PRODUCTION_ENDPOINTS.oauth_host)
|
|
}
|
|
|
|
pub fn update_base_url() -> String {
|
|
resolve(UPDATE_BASE_URL_ENV, PRODUCTION_ENDPOINTS.update_base_url)
|
|
}
|
|
|
|
pub fn upgrade_page_url() -> &'static str {
|
|
PRODUCTION_ENDPOINTS.upgrade_page_url
|
|
}
|
|
|
|
/// Serializes env-var mutation across tests; `std::env` is process-global.
|
|
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
|
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
|
|
ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner())
|
|
}
|
|
|
|
/// RAII env-var override for tests: the constructors snapshot the prior value
|
|
/// under [`ENV_LOCK`] and `Drop` restores it, panics included.
|
|
pub struct EnvVarGuard {
|
|
key: &'static str,
|
|
prev: Option<String>,
|
|
_lock: std::sync::MutexGuard<'static, ()>,
|
|
}
|
|
|
|
impl EnvVarGuard {
|
|
pub fn set(key: &'static str, value: &str) -> Self {
|
|
let lock = env_lock();
|
|
let prev = std::env::var(key).ok();
|
|
unsafe { std::env::set_var(key, value) };
|
|
Self {
|
|
key,
|
|
prev,
|
|
_lock: lock,
|
|
}
|
|
}
|
|
|
|
pub fn remove(key: &'static str) -> Self {
|
|
let lock = env_lock();
|
|
let prev = std::env::var(key).ok();
|
|
unsafe { std::env::remove_var(key) };
|
|
Self {
|
|
key,
|
|
prev,
|
|
_lock: lock,
|
|
}
|
|
}
|
|
|
|
/// Overwrite the value without releasing [`ENV_LOCK`].
|
|
pub fn set_value(&self, value: &str) {
|
|
unsafe { std::env::set_var(self.key, value) };
|
|
}
|
|
}
|
|
|
|
impl Drop for EnvVarGuard {
|
|
fn drop(&mut self) {
|
|
match self.prev.take() {
|
|
Some(prev) => unsafe { std::env::set_var(self.key, prev) },
|
|
None => unsafe { std::env::remove_var(self.key) },
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn env_var_guard_set_value_updates_then_restores_on_drop() {
|
|
const KEY: &str = "KIGI_ENV_VAR_GUARD_SET_VALUE_PROBE";
|
|
let before = std::env::var(KEY).ok();
|
|
{
|
|
let guard = EnvVarGuard::set(KEY, "initial");
|
|
assert_eq!(std::env::var(KEY).ok().as_deref(), Some("initial"));
|
|
guard.set_value("updated");
|
|
assert_eq!(
|
|
std::env::var(KEY).ok().as_deref(),
|
|
Some("updated"),
|
|
"set_value must update the env var while the guard is live"
|
|
);
|
|
}
|
|
assert_eq!(
|
|
std::env::var(KEY).ok(),
|
|
before,
|
|
"Drop must restore the pre-guard snapshot (was {before:?})"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn override_env_vars_win_over_compiled_defaults() {
|
|
let _g = EnvVarGuard::set(CODE_BASE_URL_ENV, "https://example.test/v1");
|
|
assert_eq!(coding_api_base_url(), "https://example.test/v1");
|
|
drop(_g);
|
|
let _g2 = EnvVarGuard::set(OAUTH_HOST_ENV, "https://auth.example.test");
|
|
assert_eq!(oauth_host(), "https://auth.example.test");
|
|
drop(_g2);
|
|
let _g3 = EnvVarGuard::set(UPDATE_BASE_URL_ENV, "http://127.0.0.1:1/releases");
|
|
assert_eq!(update_base_url(), "http://127.0.0.1:1/releases");
|
|
drop(_g3);
|
|
let _unset = EnvVarGuard::remove(UPDATE_BASE_URL_ENV);
|
|
assert_eq!(update_base_url(), PRODUCTION_ENDPOINTS.update_base_url);
|
|
}
|
|
|
|
#[test]
|
|
fn blank_override_falls_back_to_compiled_default() {
|
|
let _g = EnvVarGuard::set(CODE_BASE_URL_ENV, " ");
|
|
assert_eq!(
|
|
coding_api_base_url(),
|
|
PRODUCTION_ENDPOINTS.coding_api_base_url
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn production_endpoints_are_https() {
|
|
for url in [
|
|
PRODUCTION_ENDPOINTS.coding_api_base_url,
|
|
PRODUCTION_ENDPOINTS.oauth_host,
|
|
PRODUCTION_ENDPOINTS.update_base_url,
|
|
PRODUCTION_ENDPOINTS.upgrade_page_url,
|
|
] {
|
|
assert!(url.starts_with("https://"), "{url} must be https");
|
|
}
|
|
}
|
|
}
|