From 5ce03098bdd13eea850cb63f6678fd0fadcdd806 Mon Sep 17 00:00:00 2001 From: syui Date: Thu, 12 Jun 2025 19:59:19 +0900 Subject: [PATCH] fix stream env --- oauth/.env.production | 8 +- run.zsh | 2 +- src/commands/mod.rs | 3 +- src/commands/oauth.rs | 190 +++++++++++++++++++++++++++++++++++++++++ src/commands/stream.rs | 85 +++++++++++++++++- src/main.rs | 27 +++++- 6 files changed, 304 insertions(+), 11 deletions(-) create mode 100644 src/commands/oauth.rs diff --git a/oauth/.env.production b/oauth/.env.production index c471f07..d03edc3 100644 --- a/oauth/.env.production +++ b/oauth/.env.production @@ -4,8 +4,10 @@ VITE_OAUTH_CLIENT_ID=https://log.syui.ai/client-metadata.json VITE_OAUTH_REDIRECT_URI=https://log.syui.ai/oauth/callback VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn -# Optional: Override collection names (if not set, auto-generated from host) -# VITE_COLLECTION_COMMENT=ai.syui.log -# VITE_COLLECTION_USER=ai.syui.log.user +# Collection names for OAuth app +VITE_COLLECTION_COMMENT=ai.syui.log +VITE_COLLECTION_USER=ai.syui.log.user + +# Collection names for ailog (backward compatibility) AILOG_COLLECTION_COMMENT=ai.syui.log AILOG_COLLECTION_USER=ai.syui.log.user diff --git a/run.zsh b/run.zsh index bdb3506..133f679 100755 --- a/run.zsh +++ b/run.zsh @@ -6,6 +6,7 @@ function _env() { oauth=$d/oauth myblog=$d/my-blog port=4173 + source $oauth/.env.production case $OSTYPE in darwin*) export NVM_DIR="$HOME/.nvm" @@ -34,7 +35,6 @@ function _oauth_build() { cd $oauth nvm use 21 npm i - source .env.production npm run build rm -rf $myblog/static/assets cp -rf dist/* $myblog/static/ diff --git a/src/commands/mod.rs b/src/commands/mod.rs index ac6d2e7..0f90635 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -5,4 +5,5 @@ pub mod serve; pub mod clean; pub mod doc; pub mod auth; -pub mod stream; \ No newline at end of file +pub mod stream; +pub mod oauth; \ No newline at end of file diff --git a/src/commands/oauth.rs b/src/commands/oauth.rs new file mode 100644 index 0000000..5160e4e --- /dev/null +++ b/src/commands/oauth.rs @@ -0,0 +1,190 @@ +use anyhow::{Result, Context}; +use std::path::{Path, PathBuf}; +use std::fs; +use std::process::Command; +use toml::Value; + +pub async fn build(project_dir: PathBuf) -> Result<()> { + println!("Building OAuth app for project: {}", project_dir.display()); + + // 1. Read config.toml from project directory + let config_path = project_dir.join("config.toml"); + if !config_path.exists() { + anyhow::bail!("config.toml not found in {}", project_dir.display()); + } + + let config_content = fs::read_to_string(&config_path) + .with_context(|| format!("Failed to read config.toml from {}", config_path.display()))?; + + let config: Value = config_content.parse() + .with_context(|| "Failed to parse config.toml")?; + + // 2. Extract [oauth] section + let oauth_config = config.get("oauth") + .and_then(|v| v.as_table()) + .ok_or_else(|| anyhow::anyhow!("No [oauth] section found in config.toml"))?; + + let site_config = config.get("site") + .and_then(|v| v.as_table()) + .ok_or_else(|| anyhow::anyhow!("No [site] section found in config.toml"))?; + + // 3. Generate environment variables + let base_url = site_config.get("base_url") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("No base_url found in [site] section"))?; + + let client_id_path = oauth_config.get("json") + .and_then(|v| v.as_str()) + .unwrap_or("client-metadata.json"); + + let redirect_path = oauth_config.get("redirect") + .and_then(|v| v.as_str()) + .unwrap_or("oauth/callback"); + + let admin_did = oauth_config.get("admin") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("No admin DID found in [oauth] section"))?; + + let collection_comment = oauth_config.get("collection_comment") + .and_then(|v| v.as_str()) + .unwrap_or("ai.syui.log"); + + let collection_user = oauth_config.get("collection_user") + .and_then(|v| v.as_str()) + .unwrap_or("ai.syui.log.user"); + + // 4. Create .env.production content + let env_content = format!( + r#"# Production environment variables +VITE_APP_HOST={} +VITE_OAUTH_CLIENT_ID={}/{} +VITE_OAUTH_REDIRECT_URI={}/{} +VITE_ADMIN_DID={} + +# Collection names for OAuth app +VITE_COLLECTION_COMMENT={} +VITE_COLLECTION_USER={} + +# Collection names for ailog (backward compatibility) +AILOG_COLLECTION_COMMENT={} +AILOG_COLLECTION_USER={} +"#, + base_url, + base_url, client_id_path, + base_url, redirect_path, + admin_did, + collection_comment, + collection_user, + collection_comment, + collection_user + ); + + // 5. Find oauth directory (relative to current working directory) + let oauth_dir = Path::new("oauth"); + if !oauth_dir.exists() { + anyhow::bail!("oauth directory not found in current working directory"); + } + + let env_path = oauth_dir.join(".env.production"); + fs::write(&env_path, env_content) + .with_context(|| format!("Failed to write .env.production to {}", env_path.display()))?; + + println!("Generated .env.production"); + + // 6. Build OAuth app + build_oauth_app(&oauth_dir).await?; + + // 7. Copy build artifacts to project directory + copy_build_artifacts(&oauth_dir, &project_dir).await?; + + println!("OAuth app built successfully!"); + Ok(()) +} + +async fn build_oauth_app(oauth_dir: &Path) -> Result<()> { + println!("Installing dependencies..."); + + // Check if node is available + let node_check = Command::new("node") + .arg("--version") + .output(); + + if node_check.is_err() { + anyhow::bail!("Node.js not found. Please install Node.js or ensure it's in PATH"); + } + + // Install dependencies + let npm_install = Command::new("npm") + .arg("install") + .current_dir(oauth_dir) + .status() + .with_context(|| "Failed to run npm install")?; + + if !npm_install.success() { + anyhow::bail!("npm install failed"); + } + + println!("Building OAuth app..."); + + // Build the app + let npm_build = Command::new("npm") + .arg("run") + .arg("build") + .current_dir(oauth_dir) + .status() + .with_context(|| "Failed to run npm run build")?; + + if !npm_build.success() { + anyhow::bail!("npm run build failed"); + } + + println!("OAuth app build completed"); + Ok(()) +} + +async fn copy_build_artifacts(oauth_dir: &Path, project_dir: &Path) -> Result<()> { + let dist_dir = oauth_dir.join("dist"); + let static_dir = project_dir.join("static"); + let templates_dir = project_dir.join("templates"); + + // Remove old assets + let assets_dir = static_dir.join("assets"); + if assets_dir.exists() { + fs::remove_dir_all(&assets_dir) + .with_context(|| format!("Failed to remove old assets directory: {}", assets_dir.display()))?; + } + + // Copy all files from dist to static + copy_dir_recursive(&dist_dir, &static_dir) + .with_context(|| "Failed to copy dist files to static directory")?; + + // Copy index.html to oauth-assets.html template + let index_html = dist_dir.join("index.html"); + let oauth_assets = templates_dir.join("oauth-assets.html"); + + fs::copy(&index_html, &oauth_assets) + .with_context(|| "Failed to copy index.html to oauth-assets.html")?; + + println!("Copied build artifacts to project directory"); + Ok(()) +} + +fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> { + if !dst.exists() { + fs::create_dir_all(dst)?; + } + + for entry in fs::read_dir(src)? { + let entry = entry?; + let path = entry.path(); + let dst_path = dst.join(entry.file_name()); + + if path.is_dir() { + copy_dir_recursive(&path, &dst_path)?; + } else { + fs::copy(&path, &dst_path)?; + } + } + + Ok(()) +} \ No newline at end of file diff --git a/src/commands/stream.rs b/src/commands/stream.rs index 897c950..76d7d41 100644 --- a/src/commands/stream.rs +++ b/src/commands/stream.rs @@ -5,13 +5,74 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::collections::HashSet; use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use tokio::time::{sleep, Duration, interval}; use tokio_tungstenite::{connect_async, tungstenite::Message}; +use toml; use super::auth::{load_config, load_config_with_refresh, AuthConfig}; +// Load collection config with priority: env vars > project config.toml > defaults +fn load_collection_config(project_dir: Option<&Path>) -> Result<(String, String)> { + // 1. Check environment variables first (highest priority) + if let (Ok(comment), Ok(user)) = ( + std::env::var("AILOG_COLLECTION_COMMENT"), + std::env::var("AILOG_COLLECTION_USER") + ) { + println!("{}", "📂 Using collection config from environment variables".cyan()); + return Ok((comment, user)); + } + + // 2. Try to load from project config.toml (second priority) + if let Some(project_path) = project_dir { + match load_collection_config_from_project(project_path) { + Ok(config) => { + println!("{}", format!("📂 Using collection config from: {}", project_path.display()).cyan()); + return Ok(config); + } + Err(e) => { + println!("{}", format!("⚠️ Failed to load project config: {}", e).yellow()); + println!("{}", "📂 Falling back to default collections".cyan()); + } + } + } + + // 3. Use defaults (lowest priority) + println!("{}", "📂 Using default collection configuration".cyan()); + Ok(("ai.syui.log".to_string(), "ai.syui.log.user".to_string())) +} + +// Load collection config from project's config.toml +fn load_collection_config_from_project(project_dir: &Path) -> Result<(String, String)> { + let config_path = project_dir.join("config.toml"); + if !config_path.exists() { + return Err(anyhow::anyhow!("config.toml not found in {}", project_dir.display())); + } + + let config_content = fs::read_to_string(&config_path) + .with_context(|| format!("Failed to read config.toml from {}", config_path.display()))?; + + let config: toml::Value = config_content.parse() + .with_context(|| "Failed to parse config.toml")?; + + let oauth_config = config.get("oauth") + .and_then(|v| v.as_table()) + .ok_or_else(|| anyhow::anyhow!("No [oauth] section found in config.toml"))?; + + let collection_comment = oauth_config.get("collection_comment") + .and_then(|v| v.as_str()) + .unwrap_or("ai.syui.log") + .to_string(); + + let collection_user = oauth_config.get("collection_user") + .and_then(|v| v.as_str()) + .unwrap_or("ai.syui.log.user") + .to_string(); + + Ok((collection_comment, collection_user)) +} + #[derive(Debug, Serialize, Deserialize)] struct JetstreamMessage { collection: Option, @@ -57,8 +118,17 @@ fn get_pid_file() -> Result { Ok(pid_dir.join("stream.pid")) } -pub async fn start(daemon: bool) -> Result<()> { - let config = load_config_with_refresh().await?; +pub async fn start(project_dir: Option, daemon: bool) -> Result<()> { + let mut config = load_config_with_refresh().await?; + + // Load collection config with priority: env vars > project config > defaults + let (collection_comment, collection_user) = load_collection_config(project_dir.as_deref())?; + + // Update config with loaded collections + config.collections.comment = collection_comment.clone(); + config.collections.user = collection_user; + config.jetstream.collections = vec![collection_comment]; + let pid_file = get_pid_file()?; // Check if already running @@ -74,8 +144,15 @@ pub async fn start(daemon: bool) -> Result<()> { // Fork process for daemon mode let current_exe = std::env::current_exe()?; + let mut args = vec!["stream".to_string(), "start".to_string()]; + + // Add project_dir argument if provided + if let Some(project_path) = &project_dir { + args.push(project_path.to_string_lossy().to_string()); + } + let child = Command::new(current_exe) - .args(&["stream", "start"]) + .args(&args) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) diff --git a/src/main.rs b/src/main.rs index 07d7fff..232725a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -85,6 +85,11 @@ enum Commands { #[command(subcommand)] command: StreamCommands, }, + /// OAuth app management + Oauth { + #[command(subcommand)] + command: OauthCommands, + }, } #[derive(Subcommand)] @@ -101,6 +106,8 @@ enum AuthCommands { enum StreamCommands { /// Start monitoring ATProto streams Start { + /// Path to the blog project directory + project_dir: Option, /// Run as daemon #[arg(short, long)] daemon: bool, @@ -113,6 +120,15 @@ enum StreamCommands { Test, } +#[derive(Subcommand)] +enum OauthCommands { + /// Build OAuth app + Build { + /// Path to the blog project directory + project_dir: PathBuf, + }, +} + #[tokio::main] async fn main() -> Result<()> { let cli = Cli::parse(); @@ -159,8 +175,8 @@ async fn main() -> Result<()> { } Commands::Stream { command } => { match command { - StreamCommands::Start { daemon } => { - commands::stream::start(daemon).await?; + StreamCommands::Start { project_dir, daemon } => { + commands::stream::start(project_dir, daemon).await?; } StreamCommands::Stop => { commands::stream::stop().await?; @@ -173,6 +189,13 @@ async fn main() -> Result<()> { } } } + Commands::Oauth { command } => { + match command { + OauthCommands::Build { project_dir } => { + commands::oauth::build(project_dir).await?; + } + } + } } Ok(())