mirror of
https://github.com/alphaXiv/OpenResearch.git
synced 2026-11-12 10:05:24 +08:00
- `orx --version` (clap) and `orx version [--check] [--json]`: prints the CLI version and compares it to the latest GitHub release. `--json` is the agent-facing form (stable stdout object, exit 0 either way). - `orx update [--dry-run] [--force]`: updates in place by re-running the cargo-dist release installer pinned to the existing install prefix (CARGO_DIST_FORCE_INSTALL_DIR), the same mechanism as axoupdater/uv but without the native-tls dependency that conflicts with our musl builds. Guarded by the cargo-dist install receipt (refuses cargo-install copies, path/version mismatches, Nix/Homebrew paths, unwritable bin dirs), an exclusive flock against concurrent updates, and run via `sh script` so noexec /tmp mounts don't break it. - Passive nudge: at most one background latest-version check per 24h (cached in the config dir), printed to stderr only on interactive terminals, never in CI/pipes, never altering exit codes. Opt out with ORX_NO_UPDATE_CHECK, NO_UPDATE_NOTIFIER, or OPENRESEARCH_CLI_DISABLE_UPDATE=1. Latest-version discovery uses the releases/latest/download/dist-manifest.json permalink (CDN redirect, no GitHub API rate limit) instead of the REST API, whose 60 req/h/IP unauthenticated cap is routinely exhausted on the datacenter IPs agents run from.
102 lines
3.6 KiB
Rust
102 lines
3.6 KiB
Rust
//! Credential storage, XDG paths, and the default API URL.
|
|
//!
|
|
//! Credentials live at
|
|
//! `$XDG_CONFIG_HOME/openresearch/credentials.json` (falling back to
|
|
//! `~/.config/openresearch/credentials.json`), written owner-only (mode 0600).
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use tokio::fs;
|
|
|
|
use crate::error::Result;
|
|
|
|
/// Base URL of the API. Defaults to prod (`https://api.openresearch.sh`), so a
|
|
/// plain `orx login` just works. Override per-invocation with `--api-url` (handled
|
|
/// in the `login` command) or set the `OPENRESEARCH_API_URL` env var to point at
|
|
/// local dev, e.g. `OPENRESEARCH_API_URL=http://localhost:4000 orx login`.
|
|
pub fn default_api_url() -> String {
|
|
std::env::var("OPENRESEARCH_API_URL")
|
|
.unwrap_or_else(|_| "https://api.openresearch.sh".to_string())
|
|
}
|
|
|
|
/// Base URL for the alphaXiv JSON API (full-text literature search). Unlike the
|
|
/// OpenResearch API these endpoints are public — no token — and live on a
|
|
/// different host. Override with `ALPHAXIV_API_URL`.
|
|
pub fn alphaxiv_api_url() -> String {
|
|
std::env::var("ALPHAXIV_API_URL").unwrap_or_else(|_| "https://api.alphaxiv.org".to_string())
|
|
}
|
|
|
|
/// Base URL for the alphaXiv web app, which serves the per-paper `.md` routes
|
|
/// (`/overview/<id>.md` report, `/abs/<id>.md` full text). Default is the `www.`
|
|
/// host so we skip the apex→www 301. Override with `ALPHAXIV_WEB_URL`.
|
|
pub fn alphaxiv_web_url() -> String {
|
|
std::env::var("ALPHAXIV_WEB_URL").unwrap_or_else(|_| "https://www.alphaxiv.org".to_string())
|
|
}
|
|
|
|
/// Stored credentials: the API base URL and the bearer token.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Credentials {
|
|
#[serde(rename = "apiUrl")]
|
|
pub api_url: String,
|
|
pub token: String,
|
|
}
|
|
|
|
pub(crate) fn config_dir() -> PathBuf {
|
|
let base = std::env::var_os("XDG_CONFIG_HOME")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|| {
|
|
dirs::home_dir()
|
|
.unwrap_or_else(|| PathBuf::from("."))
|
|
.join(".config")
|
|
});
|
|
base.join("openresearch")
|
|
}
|
|
|
|
fn credentials_path() -> PathBuf {
|
|
config_dir().join("credentials.json")
|
|
}
|
|
|
|
/// Reads stored credentials. Returns `Ok(None)` when the file is missing,
|
|
/// unreadable, malformed, or missing required fields — matching the TS
|
|
/// `loadCredentials`, which swallows all errors and returns `null`.
|
|
pub async fn load_credentials() -> Result<Option<Credentials>> {
|
|
let path = credentials_path();
|
|
let raw = match fs::read_to_string(&path).await {
|
|
Ok(raw) => raw,
|
|
Err(_) => return Ok(None),
|
|
};
|
|
match serde_json::from_str::<Credentials>(&raw) {
|
|
Ok(creds) if !creds.api_url.is_empty() && !creds.token.is_empty() => Ok(Some(creds)),
|
|
_ => Ok(None),
|
|
}
|
|
}
|
|
|
|
/// Persists credentials as pretty JSON with a trailing newline, mode 0600.
|
|
pub async fn save_credentials(creds: &Credentials) -> Result<()> {
|
|
let path = credentials_path();
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent).await?;
|
|
}
|
|
let body = format!("{}\n", serde_json::to_string_pretty(creds)?);
|
|
fs::write(&path, body).await?;
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let perms = std::fs::Permissions::from_mode(0o600);
|
|
fs::set_permissions(&path, perms).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Removes the credentials file. Succeeds even if it does not exist (`force`).
|
|
pub async fn clear_credentials() -> Result<()> {
|
|
match fs::remove_file(credentials_path()).await {
|
|
Ok(()) => Ok(()),
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
|
Err(e) => Err(e.into()),
|
|
}
|
|
}
|