jj workspaces over the network
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(cli): add tandem-cli crate structure

+281
+31
crates/tandem-cli/Cargo.toml
··· 1 + [package] 2 + name = "tandem-cli" 3 + version.workspace = true 4 + edition.workspace = true 5 + authors.workspace = true 6 + license.workspace = true 7 + repository.workspace = true 8 + 9 + [[bin]] 10 + name = "jjf" 11 + path = "src/main.rs" 12 + 13 + [dependencies] 14 + tandem-core.workspace = true 15 + 16 + clap.workspace = true 17 + tokio.workspace = true 18 + 19 + serde.workspace = true 20 + serde_json.workspace = true 21 + thiserror = "2.0.17" 22 + toml = "0.9.11" 23 + chrono.workspace = true 24 + tokio-tungstenite = "0.28.0" 25 + futures-util = "0.3.31" 26 + url = "2.5.4" 27 + tracing.workspace = true 28 + reqwest = { version = "0.13.1", features = ["rustls-native-certs"] } 29 + whoami = "2.0.2" 30 + jj-lib = "0.37.0" 31 + yrs.workspace = true
+12
crates/tandem-cli/src/lib.rs
··· 1 + //! Tandem CLI library 2 + //! 3 + //! This module provides the core functionality for the Tandem CLI 4 + 5 + pub mod repo; 6 + pub mod daemon; 7 + pub mod presence; 8 + pub mod link; 9 + pub mod clone; 10 + pub mod offline; 11 + pub mod alias; 12 + pub mod content;
+238
crates/tandem-cli/src/main.rs
··· 1 + //! Tandem CLI (jjf) 2 + //! 3 + //! Command-line interface for the Tandem Forge 4 + 5 + use clap::{Parser, Subcommand}; 6 + use tandem_cli::repo::{JjRepo, ForgeConfig, ForgeSettings}; 7 + use std::env; 8 + use std::path::PathBuf; 9 + 10 + #[derive(Parser)] 11 + #[command(name = "jjf")] 12 + #[command(about = "Jujutsu Forge CLI - Manage code reviews and changes", long_about = None)] 13 + struct Cli { 14 + #[command(subcommand)] 15 + command: Commands, 16 + } 17 + 18 + #[derive(Subcommand)] 19 + enum Commands { 20 + /// Initialize a new repository 21 + Init { 22 + /// Repository name 23 + #[arg(short, long)] 24 + name: String, 25 + }, 26 + /// List all changes 27 + List, 28 + /// Show status 29 + Status, 30 + /// Link this repository to a forge 31 + Link { 32 + /// Forge URL (e.g., https://forge.example.com/org/repo) 33 + url: String, 34 + 35 + /// Auth token (if not provided, will prompt or use keychain) 36 + #[arg(long)] 37 + token: Option<String>, 38 + }, 39 + /// Clone a repository from a forge 40 + Clone { 41 + /// Forge URL (e.g., https://forge.example.com/org/repo) 42 + url: String, 43 + 44 + /// Target directory (defaults to repo name) 45 + #[arg(short, long)] 46 + directory: Option<PathBuf>, 47 + 48 + /// Auth token 49 + #[arg(long)] 50 + token: Option<String>, 51 + }, 52 + /// Daemon management 53 + Daemon { 54 + #[command(subcommand)] 55 + action: DaemonAction, 56 + }, 57 + /// Wrapper for jj with presence warnings (use as: alias jj='jjf alias') 58 + Alias { 59 + /// Arguments to pass to jj 60 + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] 61 + args: Vec<String>, 62 + }, 63 + } 64 + 65 + #[derive(Subcommand)] 66 + enum DaemonAction { 67 + /// Start daemon in foreground 68 + Start, 69 + /// Check daemon status 70 + Status, 71 + } 72 + 73 + #[tokio::main] 74 + async fn main() { 75 + let cli = Cli::parse(); 76 + 77 + match cli.command { 78 + Commands::Alias { args } => { 79 + use tandem_cli::alias; 80 + match alias::run_alias(args).await { 81 + Ok(code) => std::process::exit(code), 82 + Err(e) => { 83 + eprintln!("Error: {}", e); 84 + std::process::exit(1); 85 + } 86 + } 87 + } 88 + _ => { 89 + let result = match cli.command { 90 + Commands::Init { name } => handle_init(&name), 91 + Commands::List => handle_list(), 92 + Commands::Status => handle_status(), 93 + Commands::Link { url, token } => handle_link(&url, token.as_deref()).await, 94 + Commands::Clone { url, directory, token } => handle_clone(&url, directory.as_deref(), token.as_deref()).await, 95 + Commands::Daemon { action } => handle_daemon(action).await, 96 + Commands::Alias { .. } => unreachable!(), 97 + }; 98 + 99 + if let Err(e) = result { 100 + eprintln!("Error: {}", e); 101 + std::process::exit(1); 102 + } 103 + } 104 + } 105 + } 106 + 107 + fn handle_init(name: &str) -> Result<(), Box<dyn std::error::Error>> { 108 + let current_dir = env::current_dir()?; 109 + 110 + // Check if .jj directory exists 111 + let jj_dir = current_dir.join(".jj"); 112 + if !jj_dir.exists() { 113 + return Err("Not a jj repository. Run 'jj init' or 'jj git clone' first.".into()); 114 + } 115 + 116 + let repo = JjRepo::open(&current_dir)?; 117 + 118 + // Check if forge is already configured 119 + if let Some(existing_config) = repo.forge_config()? { 120 + println!("Forge already configured: {}", existing_config.forge.url); 121 + return Ok(()); 122 + } 123 + 124 + // Create forge configuration 125 + let config = ForgeConfig { 126 + forge: ForgeSettings { 127 + url: format!("https://forge.example.com/{}", name), 128 + }, 129 + }; 130 + 131 + repo.set_forge_config(&config)?; 132 + println!("Initialized forge configuration for repository: {}", name); 133 + println!("Forge URL: {}", config.forge.url); 134 + 135 + Ok(()) 136 + } 137 + 138 + fn handle_list() -> Result<(), Box<dyn std::error::Error>> { 139 + let current_dir = env::current_dir()?; 140 + let repo = JjRepo::open(&current_dir)?; 141 + 142 + let changes = repo.list_changes()?; 143 + 144 + if changes.is_empty() { 145 + println!("No changes found."); 146 + } else { 147 + println!("Changes:"); 148 + for change in changes { 149 + println!(" {} - {}", change.id, change.description); 150 + } 151 + } 152 + 153 + Ok(()) 154 + } 155 + 156 + fn handle_status() -> Result<(), Box<dyn std::error::Error>> { 157 + let current_dir = env::current_dir()?; 158 + 159 + // Check if we're in a jj repository 160 + let jj_dir = current_dir.join(".jj"); 161 + if !jj_dir.exists() { 162 + println!("Not a jj repository"); 163 + return Ok(()); 164 + } 165 + 166 + let repo = JjRepo::open(&current_dir)?; 167 + 168 + // Check forge configuration 169 + match repo.forge_config()? { 170 + Some(config) => { 171 + println!("Repository: {}", repo.path().display()); 172 + println!("Forge URL: {}", config.forge.url); 173 + println!("Status: Connected"); 174 + } 175 + None => { 176 + println!("Repository: {}", repo.path().display()); 177 + println!("Forge: Not configured"); 178 + println!("Run 'jjf init --name <repo-name>' to configure"); 179 + } 180 + } 181 + 182 + Ok(()) 183 + } 184 + 185 + async fn handle_link(url: &str, token: Option<&str>) -> Result<(), Box<dyn std::error::Error>> { 186 + use tandem_cli::link; 187 + 188 + let cwd = env::current_dir()?; 189 + link::link_repo(&cwd, url, token).await?; 190 + Ok(()) 191 + } 192 + 193 + async fn handle_clone( 194 + url: &str, 195 + directory: Option<&std::path::Path>, 196 + token: Option<&str>, 197 + ) -> Result<(), Box<dyn std::error::Error>> { 198 + use tandem_cli::clone; 199 + 200 + clone::clone_repo(url, directory, token).await?; 201 + Ok(()) 202 + } 203 + 204 + async fn handle_daemon(action: DaemonAction) -> Result<(), Box<dyn std::error::Error>> { 205 + use tandem_cli::daemon; 206 + 207 + let current_dir = env::current_dir()?; 208 + let repo = JjRepo::open(&current_dir)?; 209 + 210 + let config = repo.forge_config()? 211 + .ok_or("Forge not configured. Run 'jjf init --name <repo-name>' first.")?; 212 + 213 + match action { 214 + DaemonAction::Start => { 215 + println!("Starting daemon..."); 216 + println!("Repo: {}", repo.path().display()); 217 + println!("Forge: {}", config.forge.url); 218 + 219 + let handle = daemon::spawn_daemon( 220 + repo.path().to_path_buf(), 221 + config.forge.url.clone() 222 + ); 223 + 224 + println!("Daemon started. Press Ctrl+C to stop."); 225 + 226 + tokio::signal::ctrl_c().await?; 227 + 228 + println!("\nShutting down daemon..."); 229 + handle.shutdown().await?; 230 + println!("Daemon stopped."); 231 + } 232 + DaemonAction::Status => { 233 + println!("Daemon status: Not implemented yet"); 234 + } 235 + } 236 + 237 + Ok(()) 238 + }