···11+//! Tandem CLI library
22+//!
33+//! This module provides the core functionality for the Tandem CLI
44+55+pub mod repo;
66+pub mod daemon;
77+pub mod presence;
88+pub mod link;
99+pub mod clone;
1010+pub mod offline;
1111+pub mod alias;
1212+pub mod content;
+238
crates/tandem-cli/src/main.rs
···11+//! Tandem CLI (jjf)
22+//!
33+//! Command-line interface for the Tandem Forge
44+55+use clap::{Parser, Subcommand};
66+use tandem_cli::repo::{JjRepo, ForgeConfig, ForgeSettings};
77+use std::env;
88+use std::path::PathBuf;
99+1010+#[derive(Parser)]
1111+#[command(name = "jjf")]
1212+#[command(about = "Jujutsu Forge CLI - Manage code reviews and changes", long_about = None)]
1313+struct Cli {
1414+ #[command(subcommand)]
1515+ command: Commands,
1616+}
1717+1818+#[derive(Subcommand)]
1919+enum Commands {
2020+ /// Initialize a new repository
2121+ Init {
2222+ /// Repository name
2323+ #[arg(short, long)]
2424+ name: String,
2525+ },
2626+ /// List all changes
2727+ List,
2828+ /// Show status
2929+ Status,
3030+ /// Link this repository to a forge
3131+ Link {
3232+ /// Forge URL (e.g., https://forge.example.com/org/repo)
3333+ url: String,
3434+3535+ /// Auth token (if not provided, will prompt or use keychain)
3636+ #[arg(long)]
3737+ token: Option<String>,
3838+ },
3939+ /// Clone a repository from a forge
4040+ Clone {
4141+ /// Forge URL (e.g., https://forge.example.com/org/repo)
4242+ url: String,
4343+4444+ /// Target directory (defaults to repo name)
4545+ #[arg(short, long)]
4646+ directory: Option<PathBuf>,
4747+4848+ /// Auth token
4949+ #[arg(long)]
5050+ token: Option<String>,
5151+ },
5252+ /// Daemon management
5353+ Daemon {
5454+ #[command(subcommand)]
5555+ action: DaemonAction,
5656+ },
5757+ /// Wrapper for jj with presence warnings (use as: alias jj='jjf alias')
5858+ Alias {
5959+ /// Arguments to pass to jj
6060+ #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
6161+ args: Vec<String>,
6262+ },
6363+}
6464+6565+#[derive(Subcommand)]
6666+enum DaemonAction {
6767+ /// Start daemon in foreground
6868+ Start,
6969+ /// Check daemon status
7070+ Status,
7171+}
7272+7373+#[tokio::main]
7474+async fn main() {
7575+ let cli = Cli::parse();
7676+7777+ match cli.command {
7878+ Commands::Alias { args } => {
7979+ use tandem_cli::alias;
8080+ match alias::run_alias(args).await {
8181+ Ok(code) => std::process::exit(code),
8282+ Err(e) => {
8383+ eprintln!("Error: {}", e);
8484+ std::process::exit(1);
8585+ }
8686+ }
8787+ }
8888+ _ => {
8989+ let result = match cli.command {
9090+ Commands::Init { name } => handle_init(&name),
9191+ Commands::List => handle_list(),
9292+ Commands::Status => handle_status(),
9393+ Commands::Link { url, token } => handle_link(&url, token.as_deref()).await,
9494+ Commands::Clone { url, directory, token } => handle_clone(&url, directory.as_deref(), token.as_deref()).await,
9595+ Commands::Daemon { action } => handle_daemon(action).await,
9696+ Commands::Alias { .. } => unreachable!(),
9797+ };
9898+9999+ if let Err(e) = result {
100100+ eprintln!("Error: {}", e);
101101+ std::process::exit(1);
102102+ }
103103+ }
104104+ }
105105+}
106106+107107+fn handle_init(name: &str) -> Result<(), Box<dyn std::error::Error>> {
108108+ let current_dir = env::current_dir()?;
109109+110110+ // Check if .jj directory exists
111111+ let jj_dir = current_dir.join(".jj");
112112+ if !jj_dir.exists() {
113113+ return Err("Not a jj repository. Run 'jj init' or 'jj git clone' first.".into());
114114+ }
115115+116116+ let repo = JjRepo::open(¤t_dir)?;
117117+118118+ // Check if forge is already configured
119119+ if let Some(existing_config) = repo.forge_config()? {
120120+ println!("Forge already configured: {}", existing_config.forge.url);
121121+ return Ok(());
122122+ }
123123+124124+ // Create forge configuration
125125+ let config = ForgeConfig {
126126+ forge: ForgeSettings {
127127+ url: format!("https://forge.example.com/{}", name),
128128+ },
129129+ };
130130+131131+ repo.set_forge_config(&config)?;
132132+ println!("Initialized forge configuration for repository: {}", name);
133133+ println!("Forge URL: {}", config.forge.url);
134134+135135+ Ok(())
136136+}
137137+138138+fn handle_list() -> Result<(), Box<dyn std::error::Error>> {
139139+ let current_dir = env::current_dir()?;
140140+ let repo = JjRepo::open(¤t_dir)?;
141141+142142+ let changes = repo.list_changes()?;
143143+144144+ if changes.is_empty() {
145145+ println!("No changes found.");
146146+ } else {
147147+ println!("Changes:");
148148+ for change in changes {
149149+ println!(" {} - {}", change.id, change.description);
150150+ }
151151+ }
152152+153153+ Ok(())
154154+}
155155+156156+fn handle_status() -> Result<(), Box<dyn std::error::Error>> {
157157+ let current_dir = env::current_dir()?;
158158+159159+ // Check if we're in a jj repository
160160+ let jj_dir = current_dir.join(".jj");
161161+ if !jj_dir.exists() {
162162+ println!("Not a jj repository");
163163+ return Ok(());
164164+ }
165165+166166+ let repo = JjRepo::open(¤t_dir)?;
167167+168168+ // Check forge configuration
169169+ match repo.forge_config()? {
170170+ Some(config) => {
171171+ println!("Repository: {}", repo.path().display());
172172+ println!("Forge URL: {}", config.forge.url);
173173+ println!("Status: Connected");
174174+ }
175175+ None => {
176176+ println!("Repository: {}", repo.path().display());
177177+ println!("Forge: Not configured");
178178+ println!("Run 'jjf init --name <repo-name>' to configure");
179179+ }
180180+ }
181181+182182+ Ok(())
183183+}
184184+185185+async fn handle_link(url: &str, token: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
186186+ use tandem_cli::link;
187187+188188+ let cwd = env::current_dir()?;
189189+ link::link_repo(&cwd, url, token).await?;
190190+ Ok(())
191191+}
192192+193193+async fn handle_clone(
194194+ url: &str,
195195+ directory: Option<&std::path::Path>,
196196+ token: Option<&str>,
197197+) -> Result<(), Box<dyn std::error::Error>> {
198198+ use tandem_cli::clone;
199199+200200+ clone::clone_repo(url, directory, token).await?;
201201+ Ok(())
202202+}
203203+204204+async fn handle_daemon(action: DaemonAction) -> Result<(), Box<dyn std::error::Error>> {
205205+ use tandem_cli::daemon;
206206+207207+ let current_dir = env::current_dir()?;
208208+ let repo = JjRepo::open(¤t_dir)?;
209209+210210+ let config = repo.forge_config()?
211211+ .ok_or("Forge not configured. Run 'jjf init --name <repo-name>' first.")?;
212212+213213+ match action {
214214+ DaemonAction::Start => {
215215+ println!("Starting daemon...");
216216+ println!("Repo: {}", repo.path().display());
217217+ println!("Forge: {}", config.forge.url);
218218+219219+ let handle = daemon::spawn_daemon(
220220+ repo.path().to_path_buf(),
221221+ config.forge.url.clone()
222222+ );
223223+224224+ println!("Daemon started. Press Ctrl+C to stop.");
225225+226226+ tokio::signal::ctrl_c().await?;
227227+228228+ println!("\nShutting down daemon...");
229229+ handle.shutdown().await?;
230230+ println!("Daemon stopped.");
231231+ }
232232+ DaemonAction::Status => {
233233+ println!("Daemon status: Not implemented yet");
234234+ }
235235+ }
236236+237237+ Ok(())
238238+}