diff --git a/README.md b/README.md index e732a8d..ca2205c 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,20 @@ curl -fsSL https://git.hananakick.cc/Autotrade/openstock/raw/branch/main/scripts 삭제 스크립트는 설치된 바이너리만 제거하고 `~/.config/openstock`의 설정/캐시는 보존합니다. +업데이트: + +```bash +openstock update +``` + +원라인 업데이트: + +```bash +curl -fsSL https://git.hananakick.cc/Autotrade/openstock/raw/branch/main/scripts/update.sh | sh +``` + +`openstock update`는 현재 실행 중인 바이너리의 디렉터리를 `OPENSTOCK_INSTALL_DIR`로 사용해 설치 스크립트를 다시 실행합니다. 설치 위치를 직접 지정하려면 `OPENSTOCK_INSTALL_DIR=/path openstock update`를 사용합니다. 설치 스크립트 URL은 `OPENSTOCK_INSTALL_SCRIPT_URL`로 바꿀 수 있습니다. + ## Release Gitea Actions가 활성화된 저장소에서는 push 시 자동으로 CI가 실행됩니다. `main` branch push는 테스트와 release 빌드 검증만 수행하고, `v*` tag push는 Gitea Release를 만들고 Linux x86_64 바이너리를 asset으로 등록합니다. @@ -169,6 +183,16 @@ docker compose logs --tail=120 openstock-runner | `name` | string | 프로그램 이름입니다. Cargo package name과 같습니다. | | `version` | string | 현재 실행 중인 openstock 버전입니다. | +#### `update` + +| Field | Type | Meaning | +| --- | --- | --- | +| `installer_url` | string | 업데이트에 사용한 원격 설치 스크립트 URL입니다. | +| `install_dir` | string | 업데이트 대상 바이너리를 설치한 디렉터리입니다. | +| `status` | string | 업데이트 명령 실행 결과입니다. 성공 시 `updated`입니다. | +| `stdout` | string | 설치 스크립트의 표준 출력입니다. | +| `stderr` | string | 설치 스크립트의 표준 오류 출력입니다. | + #### `api list` | Field | Type | Meaning | @@ -554,6 +578,19 @@ openstock cache prune | Output fields | `name`, `version` | | Side effect | 없음 | +### `openstock update` + +원격 설치 스크립트를 다시 실행해 현재 openstock 바이너리를 업데이트합니다. + +| Direction | Data | +| --- | --- | +| Input | `OPENSTOCK_INSTALL_DIR`, `OPENSTOCK_INSTALL_SCRIPT_URL` 선택 지정 | +| External IO | `GET https://git.hananakick.cc/Autotrade/openstock/raw/branch/main/scripts/install.sh`; install script가 source archive를 내려받을 수 있음 | +| File write | 현재 실행 바이너리 디렉터리 또는 `OPENSTOCK_INSTALL_DIR`의 `openstock` 바이너리 | +| Output fields | `installer_url`, `install_dir`, `status`, `stdout`, `stderr` | +| Raw | `null` | +| Side effect | release build 후 설치된 바이너리를 교체합니다. `~/.config/openstock` 설정/캐시는 보존합니다. | + ### `openstock api list` 등록된 증권사 API와 capability catalog를 반환합니다. diff --git a/scripts/update.sh b/scripts/update.sh new file mode 100755 index 0000000..a192401 --- /dev/null +++ b/scripts/update.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +set -eu + +SCRIPT_URL="${OPENSTOCK_INSTALL_SCRIPT_URL:-https://git.hananakick.cc/Autotrade/openstock/raw/branch/main/scripts/install.sh}" + +curl -fsSL "$SCRIPT_URL" | sh diff --git a/src/apis/kis/main.rs b/src/apis/kis/main.rs index 18bd29d..5abbc50 100644 --- a/src/apis/kis/main.rs +++ b/src/apis/kis/main.rs @@ -26,7 +26,6 @@ impl KisApi { token: None, } } - } impl TraderApi for KisApi { diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 5298915..65734f0 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,6 +6,7 @@ pub mod market; pub mod order; pub mod search; pub mod universe; +pub mod update; mod version; pub use account::handle_account; @@ -16,4 +17,5 @@ pub use market::handle_market; pub use order::handle_order; pub use search::handle_search; pub use universe::handle_universe; +pub use update::handle_update; pub use version::handle_version; diff --git a/src/commands/update.rs b/src/commands/update.rs new file mode 100644 index 0000000..c097e0e --- /dev/null +++ b/src/commands/update.rs @@ -0,0 +1,113 @@ +use crate::core::output; +use std::path::PathBuf; +use std::process::Command; + +const DEFAULT_INSTALL_SCRIPT_URL: &str = + "https://git.hananakick.cc/Autotrade/openstock/raw/branch/main/scripts/install.sh"; + +pub fn handle_update() { + match update() { + Ok(result) => { + println!( + "{}", + output::explained( + "update", + "openstock 바이너리 업데이트 실행 결과", + vec![ + output::field( + "installer_url", + "업데이트에 사용한 원격 설치 스크립트 URL", + serde_json::json!(result.installer_url), + ), + output::field( + "install_dir", + "업데이트 대상 바이너리를 설치한 디렉터리", + serde_json::json!(result.install_dir.display().to_string()), + ), + output::field( + "status", + "업데이트 명령 실행 결과", + serde_json::json!("updated"), + ), + output::field( + "stdout", + "설치 스크립트 표준 출력", + serde_json::json!(result.stdout), + ), + output::field( + "stderr", + "설치 스크립트 표준 오류 출력", + serde_json::json!(result.stderr), + ), + ], + ) + ); + } + Err(err) => eprintln!( + "{}", + output::error("update", "openstock 업데이트 실패", &err) + ), + } +} + +fn update() -> Result { + let install_dir = install_dir()?; + let installer_url = installer_url(); + let command = format!("curl -fsSL {} | sh", shell_quote(&installer_url)); + let output = Command::new("sh") + .arg("-c") + .arg(command) + .env("OPENSTOCK_INSTALL_DIR", &install_dir) + .output() + .map_err(|err| format!("업데이트 스크립트 실행 실패: {}", err))?; + + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + if !output.status.success() { + return Err(format!( + "업데이트 스크립트가 실패했습니다. status={}, stdout={}, stderr={}", + output.status, stdout, stderr + )); + } + + Ok(UpdateResult { + installer_url, + install_dir, + stdout, + stderr, + }) +} + +fn install_dir() -> Result { + if let Some(value) = std::env::var_os("OPENSTOCK_INSTALL_DIR") { + return Ok(PathBuf::from(value)); + } + + let exe = + std::env::current_exe().map_err(|err| format!("현재 실행 파일 경로 확인 실패: {}", err))?; + exe.parent().map(PathBuf::from).ok_or_else(|| { + format!( + "현재 실행 파일의 부모 디렉터리를 확인할 수 없습니다: {}", + exe.display() + ) + }) +} + +fn installer_url() -> String { + std::env::var("OPENSTOCK_INSTALL_SCRIPT_URL") + .ok() + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| DEFAULT_INSTALL_SCRIPT_URL.to_string()) +} + +fn shell_quote(value: &str) -> String { + format!("'{}'", value.replace('\'', "'\\''")) +} + +struct UpdateResult { + installer_url: String, + install_dir: PathBuf, + stdout: String, + stderr: String, +} diff --git a/src/main.rs b/src/main.rs index d01dd54..80118fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,9 @@ enum Commands { /// 애플리케이션 버젼 표시 Version, + /// 설치된 openstock 바이너리 업데이트 + Update, + /// 증권사 API 리스트 조회 및 API 설정 Api { #[command(subcommand)] @@ -80,6 +83,7 @@ fn main() { match &cli.command { Some(Commands::Version) => commands::handle_version(), + Some(Commands::Update) => commands::handle_update(), Some(Commands::Api { sub }) => commands::handle_api(sub), Some(Commands::Dart { sub }) => commands::handle_dart(sub), Some(Commands::Account { sub }) => commands::handle_account(sub),