diff --git a/crates/cdcx-cli/src/cli_builder.rs b/crates/cdcx-cli/src/cli_builder.rs index 4ad8d18..23bcb0f 100644 --- a/crates/cdcx-cli/src/cli_builder.rs +++ b/crates/cdcx-cli/src/cli_builder.rs @@ -395,7 +395,7 @@ mod tests { assert!(sub_names.contains(&"mcp".to_string())); } - /// Regression: `schemas/trade.toml` used to carry `default = "MARKET"` for + /// Regression: `schemas/apis/trade.toml` used to carry `default = "MARKET"` for /// the `type` param, which silently turned `cdcx trade order BUY INST QTY /// --price P` into a MARKET order with an ignored price field. This test /// pins the fix: without `--type`, the CLI must reject the command; with diff --git a/crates/cdcx-cli/src/groups/setup.rs b/crates/cdcx-cli/src/groups/setup.rs index 644e091..7bda7c2 100644 --- a/crates/cdcx-cli/src/groups/setup.rs +++ b/crates/cdcx-cli/src/groups/setup.rs @@ -322,8 +322,10 @@ pub async fn run_setup() -> Result<(), CdcxError> { } }; - toml::to_string_pretty(&config_file) - .map_err(|e| CdcxError::Config(format!("Failed to serialize config: {}", e)))? + let body = toml::to_string_pretty(&config_file) + .map_err(|e| CdcxError::Config(format!("Failed to serialize config: {}", e)))?; + let schema_url = cdcx_core::github::raw("main", "schemas/configs/config.json"); + format!("#:schema {}\n\n{}", schema_url, body) }; // Ensure directory exists with owner-only permissions diff --git a/crates/cdcx-core/src/github.rs b/crates/cdcx-core/src/github.rs new file mode 100644 index 0000000..d1f979e --- /dev/null +++ b/crates/cdcx-core/src/github.rs @@ -0,0 +1,73 @@ +const REPO: &str = env!("CARGO_PKG_REPOSITORY"); + +fn owner_repo() -> &'static str { + REPO.strip_prefix("https://github.com/").unwrap() +} + +pub fn html(path: &str) -> String { + format!("{}/{}", REPO, path.trim_start_matches('/')) +} + +pub fn api(path: &str) -> String { + format!( + "https://api.github.com/repos/{}/{}", + owner_repo(), + path.trim_start_matches('/') + ) +} + +pub fn raw(branch: &str, path: &str) -> String { + format!( + "https://raw.githubusercontent.com/{}/refs/heads/{}/{}", + owner_repo(), + branch, + path.trim_start_matches('/') + ) +} + +pub fn release_download(tag: &str, asset: &str) -> String { + format!( + "{}/releases/download/{}/{}", + REPO, + tag, + asset.trim_start_matches('/') + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_html() { + assert_eq!(html("releases/latest"), format!("{}/releases/latest", REPO)); + } + + #[test] + fn test_api() { + let url = api("releases/latest"); + assert!(url.starts_with("https://api.github.com/repos/")); + assert!(url.ends_with("/releases/latest")); + } + + #[test] + fn test_raw() { + let url = raw("main", "schemas/configs/tui.json"); + assert!(url.contains("/refs/heads/main/")); + assert!(url.ends_with("/schemas/configs/tui.json")); + } + + #[test] + fn test_release_download() { + let url = release_download("v1.0.0", "cdcx-linux.tar.gz"); + assert!(url.contains("/releases/download/v1.0.0/")); + assert!(url.ends_with("cdcx-linux.tar.gz")); + } + + #[test] + fn test_no_double_slash() { + assert!(!raw("main", "/schemas/foo.json").contains("main//")); + assert!(!api("/releases/latest").contains("repos//")); + assert!(!html("/releases").contains("cli//")); + } +} diff --git a/crates/cdcx-core/src/lib.rs b/crates/cdcx-core/src/lib.rs index 7294103..ef141e2 100644 --- a/crates/cdcx-core/src/lib.rs +++ b/crates/cdcx-core/src/lib.rs @@ -3,6 +3,7 @@ pub mod auth; pub mod config; pub mod env; pub mod error; +pub mod github; pub mod openapi; pub mod origin; pub mod output; diff --git a/crates/cdcx-core/src/schema.rs b/crates/cdcx-core/src/schema.rs index 766c583..2616825 100644 --- a/crates/cdcx-core/src/schema.rs +++ b/crates/cdcx-core/src/schema.rs @@ -113,7 +113,7 @@ impl SchemaRegistry { } /// Same as `from_fixture`, but also applies the real production schema - /// overlays (schemas/*.toml). Used by tests that need to exercise the + /// overlays (schemas/apis/*.toml). Used by tests that need to exercise the /// fixture + overlay merge path — e.g. to assert that safety-critical /// overlay decisions (required params, enum defaults) behave correctly. pub fn from_fixture_with_overlays() -> Result { @@ -124,15 +124,15 @@ impl SchemaRegistry { fn load_overlays() -> Vec { [ - include_str!("../../../schemas/market.toml"), - include_str!("../../../schemas/account.toml"), - include_str!("../../../schemas/trade.toml"), - include_str!("../../../schemas/advanced.toml"), - include_str!("../../../schemas/wallet.toml"), - include_str!("../../../schemas/fiat.toml"), - include_str!("../../../schemas/staking.toml"), - include_str!("../../../schemas/margin.toml"), - include_str!("../../../schemas/history.toml"), + include_str!("../../../schemas/apis/market.toml"), + include_str!("../../../schemas/apis/account.toml"), + include_str!("../../../schemas/apis/trade.toml"), + include_str!("../../../schemas/apis/advanced.toml"), + include_str!("../../../schemas/apis/wallet.toml"), + include_str!("../../../schemas/apis/fiat.toml"), + include_str!("../../../schemas/apis/staking.toml"), + include_str!("../../../schemas/apis/margin.toml"), + include_str!("../../../schemas/apis/history.toml"), ] .iter() .map(|s| { diff --git a/crates/cdcx-core/src/update.rs b/crates/cdcx-core/src/update.rs index 218d5bb..69afc44 100644 --- a/crates/cdcx-core/src/update.rs +++ b/crates/cdcx-core/src/update.rs @@ -3,16 +3,11 @@ use std::path::{Path, PathBuf}; use std::time::Duration; fn releases_api_url() -> String { - let repo = env!("CARGO_PKG_REPOSITORY"); - let owner_repo = repo.strip_prefix("https://github.com/").unwrap(); - format!( - "https://api.github.com/repos/{}/releases/latest", - owner_repo - ) + crate::github::api("releases/latest") } fn releases_html_url() -> String { - format!("{}/releases/latest", env!("CARGO_PKG_REPOSITORY")) + crate::github::html("releases/latest") } pub struct UpdateChecker { diff --git a/crates/cdcx-tui/src/setup.rs b/crates/cdcx-tui/src/setup.rs index b49dac8..11b67e5 100644 --- a/crates/cdcx-tui/src/setup.rs +++ b/crates/cdcx-tui/src/setup.rs @@ -523,8 +523,10 @@ impl SetupState { .join(", "); let tick_ms = self.tick_rates[self.tick_rate_idx].0; + let schema_url = cdcx_core::github::raw("main", "schemas/configs/tui.json"); format!( - "theme = \"{}\"\ntick_rate_ms = {}\nwatchlist = [{}]\n", + "#:schema {}\n\ntheme = \"{}\"\ntick_rate_ms = {}\nwatchlist = [{}]\n", + schema_url, self.selected_theme_name(), tick_ms, watchlist_str, diff --git a/crates/cdcx-tui/src/widgets/settings.rs b/crates/cdcx-tui/src/widgets/settings.rs index 1548635..1b7acc2 100644 --- a/crates/cdcx-tui/src/widgets/settings.rs +++ b/crates/cdcx-tui/src/widgets/settings.rs @@ -374,7 +374,9 @@ pub fn save_settings( let toml_str = toml::to_string_pretty(&config) .map_err(|e| format!("Failed to serialize config: {}", e))?; - std::fs::write(&path, toml_str).map_err(|e| format!("Failed to write config: {}", e))?; + let schema_url = cdcx_core::github::raw("main", "schemas/configs/tui.json"); + let output = format!("#:schema {}\n\n{}", schema_url, toml_str); + std::fs::write(&path, output).map_err(|e| format!("Failed to write config: {}", e))?; Ok(()) } diff --git a/schemas/account.toml b/schemas/apis/account.toml similarity index 100% rename from schemas/account.toml rename to schemas/apis/account.toml diff --git a/schemas/advanced.toml b/schemas/apis/advanced.toml similarity index 100% rename from schemas/advanced.toml rename to schemas/apis/advanced.toml diff --git a/schemas/fiat.toml b/schemas/apis/fiat.toml similarity index 100% rename from schemas/fiat.toml rename to schemas/apis/fiat.toml diff --git a/schemas/history.toml b/schemas/apis/history.toml similarity index 100% rename from schemas/history.toml rename to schemas/apis/history.toml diff --git a/schemas/margin.toml b/schemas/apis/margin.toml similarity index 100% rename from schemas/margin.toml rename to schemas/apis/margin.toml diff --git a/schemas/market.toml b/schemas/apis/market.toml similarity index 100% rename from schemas/market.toml rename to schemas/apis/market.toml diff --git a/schemas/staking.toml b/schemas/apis/staking.toml similarity index 100% rename from schemas/staking.toml rename to schemas/apis/staking.toml diff --git a/schemas/trade.toml b/schemas/apis/trade.toml similarity index 100% rename from schemas/trade.toml rename to schemas/apis/trade.toml diff --git a/schemas/wallet.toml b/schemas/apis/wallet.toml similarity index 100% rename from schemas/wallet.toml rename to schemas/apis/wallet.toml diff --git a/schemas/configs/config.json b/schemas/configs/config.json new file mode 100644 index 0000000..ce3f953 --- /dev/null +++ b/schemas/configs/config.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/crypto-com/cdcx-cli/refs/heads/main/schemas/configs/config.json", + "title": "cdcx config", + "description": "API credentials and profile configuration (~/.config/cdcx/config.toml)", + "type": "object", + "properties": { + "default": { + "$ref": "#/$defs/profile", + "description": "Default profile used when no --profile flag is given" + }, + "profiles": { + "type": "object", + "description": "Named profiles, accessed via --profile ", + "additionalProperties": { + "$ref": "#/$defs/profile" + } + } + }, + "$defs": { + "profile": { + "type": "object", + "description": "API credential set for one environment", + "properties": { + "api_key": { + "type": "string", + "description": "Crypto.com Exchange API key" + }, + "api_secret": { + "type": "string", + "description": "Crypto.com Exchange API secret" + }, + "environment": { + "type": "string", + "enum": ["production", "uat"], + "description": "Target environment" + } + }, + "required": ["api_key", "api_secret", "environment"], + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/schemas/configs/tui.json b/schemas/configs/tui.json new file mode 100644 index 0000000..14265ba --- /dev/null +++ b/schemas/configs/tui.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/crypto-com/cdcx-cli/refs/heads/main/schemas/configs/tui.json", + "title": "cdcx TUI config", + "description": "TUI dashboard preferences (~/.config/cdcx/tui.toml)", + "type": "object", + "properties": { + "theme": { + "type": "string", + "description": "Builtin theme name, or a key from the custom themes map", + "examples": ["terminal-pro", "cyber-midnight", "monochrome", "neon", "micky-d", "amber"] + }, + "tick_rate_ms": { + "type": "integer", + "minimum": 1, + "default": 250, + "description": "Event loop tick rate in milliseconds" + }, + "ticker_speed": { + "type": "string", + "enum": ["slow", "medium", "fast"], + "default": "medium", + "description": "Ticker tape scroll speed (slow=divisor 4, medium=2, fast=1)" + }, + "default_instrument": { + "type": "string", + "description": "Instrument to select on launch", + "examples": ["BTC_USDT", "ETH_USDT"] + }, + "watchlist": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "Instruments shown in the watchlist tab", + "examples": [["BTC_USDT", "ETH_USDT", "SOL_USDT", "CRO_USDT"]] + }, + "themes": { + "type": "object", + "description": "Custom user-defined themes keyed by name", + "additionalProperties": { + "$ref": "#/$defs/customTheme" + }, + "default": {} + } + }, + "$defs": { + "customTheme": { + "type": "object", + "description": "Custom theme colors; unset fields inherit from the base theme (terminal-pro)", + "properties": { + "bg": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "description": "Background color" }, + "fg": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "description": "Foreground / text color" }, + "accent": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "description": "Accent color" }, + "positive": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "description": "Price-up / positive color" }, + "negative": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "description": "Price-down / negative color" }, + "border": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "description": "Border color" }, + "header": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "description": "Header text color" }, + "selected_bg": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "description": "Selected row background" }, + "selected_fg": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "description": "Selected row foreground" }, + "muted": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "description": "Muted / secondary text color" }, + "volume": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "description": "Volume bar color" }, + "status_bar_bg": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "description": "Status bar background" }, + "status_bar_fg": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "description": "Status bar foreground" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +}