feat(cli): Add global robot mode for machine-readable output
Introduces a unified robot mode that enables JSON output across all commands, designed for AI agent and script consumption. Robot mode activation (any of): - --robot flag: Explicit opt-in - GI_ROBOT=1 env var: For persistent configuration - Non-TTY stdout: Auto-detect when piped (e.g., gi list issues | jq) Implementation: - Cli::is_robot_mode(): Centralized detection logic - All command handlers receive robot_mode boolean - Errors emit structured JSON to stderr with exit codes - Success responses emit JSON to stdout Behavior changes in robot mode: - No color/emoji output (no ANSI escapes) - No progress spinners or interactive prompts - Timestamps as ISO 8601 strings (not relative "2 hours ago") - Full content (no truncation of descriptions/notes) - Structured error objects with code, message, suggestion This enables reliable parsing by Claude Code, shell scripts, and automation pipelines. The auto-detect on non-TTY means simple piping "just works" without explicit flags. Per-command --json flags remain for explicit control and override robot mode when needed for human-friendly terminal + JSON file output. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
pub mod commands;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::io::IsTerminal;
|
||||
|
||||
/// GitLab Inbox - Unified notification management
|
||||
#[derive(Parser)]
|
||||
@@ -13,11 +14,23 @@ pub struct Cli {
|
||||
#[arg(short, long, global = true)]
|
||||
pub config: Option<String>,
|
||||
|
||||
/// Machine-readable JSON output (auto-enabled when piped)
|
||||
#[arg(long, global = true, env = "GI_ROBOT")]
|
||||
pub robot: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
/// Check if robot mode is active (explicit flag, env var, or non-TTY stdout)
|
||||
pub fn is_robot_mode(&self) -> bool {
|
||||
self.robot || !std::io::stdout().is_terminal()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Commands {
|
||||
/// Initialize configuration and database
|
||||
Init {
|
||||
@@ -62,7 +75,7 @@ pub enum Commands {
|
||||
/// Ingest data from GitLab
|
||||
Ingest {
|
||||
/// Resource type to ingest
|
||||
#[arg(long, value_parser = ["issues", "merge_requests"])]
|
||||
#[arg(long, value_parser = ["issues", "mrs"])]
|
||||
r#type: String,
|
||||
|
||||
/// Filter to single project
|
||||
@@ -92,8 +105,8 @@ pub enum Commands {
|
||||
#[arg(long)]
|
||||
project: Option<String>,
|
||||
|
||||
/// Filter by state
|
||||
#[arg(long, value_parser = ["opened", "closed", "all"])]
|
||||
/// Filter by state (opened|closed|all for issues; opened|merged|closed|locked|all for MRs)
|
||||
#[arg(long)]
|
||||
state: Option<String>,
|
||||
|
||||
/// Filter by author username
|
||||
@@ -108,7 +121,7 @@ pub enum Commands {
|
||||
#[arg(long)]
|
||||
label: Option<Vec<String>>,
|
||||
|
||||
/// Filter by milestone title
|
||||
/// Filter by milestone title (issues only)
|
||||
#[arg(long)]
|
||||
milestone: Option<String>,
|
||||
|
||||
@@ -116,11 +129,11 @@ pub enum Commands {
|
||||
#[arg(long)]
|
||||
since: Option<String>,
|
||||
|
||||
/// Filter by due date (before this date, YYYY-MM-DD)
|
||||
/// Filter by due date (before this date, YYYY-MM-DD) (issues only)
|
||||
#[arg(long)]
|
||||
due_before: Option<String>,
|
||||
|
||||
/// Show only issues with a due date
|
||||
/// Show only issues with a due date (issues only)
|
||||
#[arg(long)]
|
||||
has_due_date: bool,
|
||||
|
||||
@@ -132,13 +145,33 @@ pub enum Commands {
|
||||
#[arg(long, value_parser = ["desc", "asc"], default_value = "desc")]
|
||||
order: String,
|
||||
|
||||
/// Open first matching issue in browser
|
||||
/// Open first matching item in browser
|
||||
#[arg(long)]
|
||||
open: bool,
|
||||
|
||||
/// Output as JSON
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
|
||||
/// Show only draft MRs (MRs only)
|
||||
#[arg(long, conflicts_with = "no_draft")]
|
||||
draft: bool,
|
||||
|
||||
/// Exclude draft MRs (MRs only)
|
||||
#[arg(long, conflicts_with = "draft")]
|
||||
no_draft: bool,
|
||||
|
||||
/// Filter by reviewer username (MRs only)
|
||||
#[arg(long)]
|
||||
reviewer: Option<String>,
|
||||
|
||||
/// Filter by target branch (MRs only)
|
||||
#[arg(long)]
|
||||
target_branch: Option<String>,
|
||||
|
||||
/// Filter by source branch (MRs only)
|
||||
#[arg(long)]
|
||||
source_branch: Option<String>,
|
||||
},
|
||||
|
||||
/// Count entities in local database
|
||||
@@ -164,5 +197,9 @@ pub enum Commands {
|
||||
/// Filter by project path (required if iid is ambiguous)
|
||||
#[arg(long)]
|
||||
project: Option<String>,
|
||||
|
||||
/// Output as JSON
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
}
|
||||
|
||||
419
src/main.rs
419
src/main.rs
@@ -3,21 +3,26 @@
|
||||
use clap::Parser;
|
||||
use console::style;
|
||||
use dialoguer::{Confirm, Input};
|
||||
use serde::Serialize;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
use gi::Config;
|
||||
use gi::cli::commands::{
|
||||
InitInputs, InitOptions, ListFilters, open_issue_in_browser, print_count,
|
||||
print_doctor_results, print_ingest_summary, print_list_issues, print_list_issues_json,
|
||||
print_show_issue, print_sync_status, run_auth_test, run_count, run_doctor, run_ingest,
|
||||
run_init, run_list_issues, run_show_issue, run_sync_status,
|
||||
InitInputs, InitOptions, ListFilters, MrListFilters, open_issue_in_browser, open_mr_in_browser,
|
||||
print_count, print_count_json, print_doctor_results, print_ingest_summary,
|
||||
print_ingest_summary_json, print_list_issues, print_list_issues_json, print_list_mrs,
|
||||
print_list_mrs_json, print_show_issue, print_show_issue_json, print_show_mr,
|
||||
print_show_mr_json, print_sync_status, print_sync_status_json, run_auth_test, run_count,
|
||||
run_doctor, run_ingest, run_init, run_list_issues, run_list_mrs, run_show_issue, run_show_mr,
|
||||
run_sync_status,
|
||||
};
|
||||
use gi::core::db::{create_connection, get_schema_version, run_migrations};
|
||||
use gi::core::paths::get_db_path;
|
||||
use gi::cli::{Cli, Commands};
|
||||
use gi::core::db::{create_connection, get_schema_version, run_migrations};
|
||||
use gi::core::error::{GiError, RobotErrorOutput};
|
||||
use gi::core::paths::get_config_path;
|
||||
use gi::core::paths::get_db_path;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -39,34 +44,36 @@ async fn main() {
|
||||
.init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
let robot_mode = cli.is_robot_mode();
|
||||
|
||||
let result = match cli.command {
|
||||
Commands::Init {
|
||||
force,
|
||||
non_interactive,
|
||||
} => handle_init(cli.config.as_deref(), force, non_interactive).await,
|
||||
Commands::AuthTest => handle_auth_test(cli.config.as_deref()).await,
|
||||
Commands::Doctor { json } => handle_doctor(cli.config.as_deref(), json).await,
|
||||
Commands::Version => {
|
||||
println!("gi version {}", env!("CARGO_PKG_VERSION"));
|
||||
Ok(())
|
||||
}
|
||||
Commands::Backup => {
|
||||
println!("gi backup - not yet implemented");
|
||||
Ok(())
|
||||
}
|
||||
Commands::Reset { confirm: _ } => {
|
||||
println!("gi reset - not yet implemented");
|
||||
Ok(())
|
||||
}
|
||||
Commands::Migrate => handle_migrate(cli.config.as_deref()).await,
|
||||
Commands::SyncStatus => handle_sync_status(cli.config.as_deref()).await,
|
||||
} => handle_init(cli.config.as_deref(), force, non_interactive, robot_mode).await,
|
||||
Commands::AuthTest => handle_auth_test(cli.config.as_deref(), robot_mode).await,
|
||||
Commands::Doctor { json } => handle_doctor(cli.config.as_deref(), json || robot_mode).await,
|
||||
Commands::Version => handle_version(robot_mode),
|
||||
Commands::Backup => handle_backup(robot_mode),
|
||||
Commands::Reset { confirm: _ } => handle_reset(robot_mode),
|
||||
Commands::Migrate => handle_migrate(cli.config.as_deref(), robot_mode).await,
|
||||
Commands::SyncStatus => handle_sync_status(cli.config.as_deref(), robot_mode).await,
|
||||
Commands::Ingest {
|
||||
r#type,
|
||||
project,
|
||||
force,
|
||||
full,
|
||||
} => handle_ingest(cli.config.as_deref(), &r#type, project.as_deref(), force, full).await,
|
||||
} => {
|
||||
handle_ingest(
|
||||
cli.config.as_deref(),
|
||||
&r#type,
|
||||
project.as_deref(),
|
||||
force,
|
||||
full,
|
||||
robot_mode,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Commands::List {
|
||||
entity,
|
||||
limit,
|
||||
@@ -83,6 +90,11 @@ async fn main() {
|
||||
order,
|
||||
open,
|
||||
json,
|
||||
draft,
|
||||
no_draft,
|
||||
reviewer,
|
||||
target_branch,
|
||||
source_branch,
|
||||
} => {
|
||||
handle_list(
|
||||
cli.config.as_deref(),
|
||||
@@ -100,30 +112,106 @@ async fn main() {
|
||||
&sort,
|
||||
&order,
|
||||
open,
|
||||
json,
|
||||
json || robot_mode,
|
||||
draft,
|
||||
no_draft,
|
||||
reviewer.as_deref(),
|
||||
target_branch.as_deref(),
|
||||
source_branch.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
Commands::Count { entity, r#type } => {
|
||||
handle_count(cli.config.as_deref(), &entity, r#type.as_deref()).await
|
||||
handle_count(cli.config.as_deref(), &entity, r#type.as_deref(), robot_mode).await
|
||||
}
|
||||
Commands::Show {
|
||||
entity,
|
||||
iid,
|
||||
project,
|
||||
} => handle_show(cli.config.as_deref(), &entity, iid, project.as_deref()).await,
|
||||
json,
|
||||
} => {
|
||||
handle_show(
|
||||
cli.config.as_deref(),
|
||||
&entity,
|
||||
iid,
|
||||
project.as_deref(),
|
||||
json || robot_mode,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
eprintln!("{} {}", style("Error:").red(), e);
|
||||
std::process::exit(1);
|
||||
handle_error(e, robot_mode);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fallback error output for non-GiError errors in robot mode.
|
||||
#[derive(Serialize)]
|
||||
struct FallbackErrorOutput {
|
||||
error: FallbackError,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FallbackError {
|
||||
code: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
fn handle_error(e: Box<dyn std::error::Error>, robot_mode: bool) -> ! {
|
||||
// Try to downcast to GiError for structured output
|
||||
if let Some(gi_error) = e.downcast_ref::<GiError>() {
|
||||
if robot_mode {
|
||||
let output = RobotErrorOutput::from(gi_error);
|
||||
// Use serde_json for safe serialization; fallback constructs JSON safely
|
||||
eprintln!(
|
||||
"{}",
|
||||
serde_json::to_string(&output).unwrap_or_else(|_| {
|
||||
// Fallback uses serde to ensure proper escaping
|
||||
let fallback = FallbackErrorOutput {
|
||||
error: FallbackError {
|
||||
code: "INTERNAL_ERROR".to_string(),
|
||||
message: gi_error.to_string(),
|
||||
},
|
||||
};
|
||||
serde_json::to_string(&fallback)
|
||||
.unwrap_or_else(|_| r#"{"error":{"code":"INTERNAL_ERROR","message":"Serialization failed"}}"#.to_string())
|
||||
})
|
||||
);
|
||||
std::process::exit(gi_error.exit_code());
|
||||
} else {
|
||||
eprintln!("{} {}", style("Error:").red(), gi_error);
|
||||
if let Some(suggestion) = gi_error.suggestion() {
|
||||
eprintln!("{} {}", style("Hint:").yellow(), suggestion);
|
||||
}
|
||||
std::process::exit(gi_error.exit_code());
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for non-GiError errors - use serde for proper JSON escaping
|
||||
if robot_mode {
|
||||
let output = FallbackErrorOutput {
|
||||
error: FallbackError {
|
||||
code: "INTERNAL_ERROR".to_string(),
|
||||
message: e.to_string(),
|
||||
},
|
||||
};
|
||||
eprintln!(
|
||||
"{}",
|
||||
serde_json::to_string(&output)
|
||||
.unwrap_or_else(|_| r#"{"error":{"code":"INTERNAL_ERROR","message":"Serialization failed"}}"#.to_string())
|
||||
);
|
||||
} else {
|
||||
eprintln!("{} {}", style("Error:").red(), e);
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
async fn handle_init(
|
||||
config_override: Option<&str>,
|
||||
force: bool,
|
||||
non_interactive: bool,
|
||||
_robot_mode: bool, // TODO: Add robot mode support for init (requires non-interactive implementation)
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config_path = get_config_path(config_override);
|
||||
let mut confirmed_overwrite = force;
|
||||
@@ -244,16 +332,57 @@ async fn handle_init(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_auth_test(config_override: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// JSON output for auth-test command.
|
||||
#[derive(Serialize)]
|
||||
struct AuthTestOutput {
|
||||
ok: bool,
|
||||
data: AuthTestData,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AuthTestData {
|
||||
authenticated: bool,
|
||||
username: String,
|
||||
name: String,
|
||||
gitlab_url: String,
|
||||
}
|
||||
|
||||
async fn handle_auth_test(
|
||||
config_override: Option<&str>,
|
||||
robot_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match run_auth_test(config_override).await {
|
||||
Ok(result) => {
|
||||
println!("Authenticated as @{} ({})", result.username, result.name);
|
||||
println!("GitLab: {}", result.base_url);
|
||||
if robot_mode {
|
||||
let output = AuthTestOutput {
|
||||
ok: true,
|
||||
data: AuthTestData {
|
||||
authenticated: true,
|
||||
username: result.username.clone(),
|
||||
name: result.name.clone(),
|
||||
gitlab_url: result.base_url.clone(),
|
||||
},
|
||||
};
|
||||
println!("{}", serde_json::to_string(&output)?);
|
||||
} else {
|
||||
println!("Authenticated as @{} ({})", result.username, result.name);
|
||||
println!("GitLab: {}", result.base_url);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}", style(format!("Error: {e}")).red());
|
||||
std::process::exit(1);
|
||||
if robot_mode {
|
||||
let output = FallbackErrorOutput {
|
||||
error: FallbackError {
|
||||
code: "AUTH_FAILED".to_string(),
|
||||
message: e.to_string(),
|
||||
},
|
||||
};
|
||||
eprintln!("{}", serde_json::to_string(&output)?);
|
||||
} else {
|
||||
eprintln!("{}", style(format!("Error: {e}")).red());
|
||||
}
|
||||
std::process::exit(5); // AUTH_FAILED exit code
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,12 +412,17 @@ async fn handle_ingest(
|
||||
project_filter: Option<&str>,
|
||||
force: bool,
|
||||
full: bool,
|
||||
robot_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = Config::load(config_override)?;
|
||||
|
||||
match run_ingest(&config, resource_type, project_filter, force, full).await {
|
||||
match run_ingest(&config, resource_type, project_filter, force, full, robot_mode).await {
|
||||
Ok(result) => {
|
||||
print_ingest_summary(&result);
|
||||
if robot_mode {
|
||||
print_ingest_summary_json(&result);
|
||||
} else {
|
||||
print_ingest_summary(&result);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -298,6 +432,7 @@ async fn handle_ingest(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn handle_list(
|
||||
config_override: Option<&str>,
|
||||
entity: &str,
|
||||
@@ -315,6 +450,11 @@ async fn handle_list(
|
||||
order: &str,
|
||||
open_browser: bool,
|
||||
json_output: bool,
|
||||
draft: bool,
|
||||
no_draft: bool,
|
||||
reviewer_filter: Option<&str>,
|
||||
target_branch_filter: Option<&str>,
|
||||
source_branch_filter: Option<&str>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = Config::load(config_override)?;
|
||||
|
||||
@@ -348,7 +488,33 @@ async fn handle_list(
|
||||
Ok(())
|
||||
}
|
||||
"mrs" => {
|
||||
println!("MR listing not yet implemented. Only 'issues' is supported in CP1.");
|
||||
let filters = MrListFilters {
|
||||
limit,
|
||||
project: project_filter,
|
||||
state: state_filter,
|
||||
author: author_filter,
|
||||
assignee: assignee_filter,
|
||||
reviewer: reviewer_filter,
|
||||
labels: label_filter,
|
||||
since: since_filter,
|
||||
draft,
|
||||
no_draft,
|
||||
target_branch: target_branch_filter,
|
||||
source_branch: source_branch_filter,
|
||||
sort,
|
||||
order,
|
||||
};
|
||||
|
||||
let result = run_list_mrs(&config, filters)?;
|
||||
|
||||
if open_browser {
|
||||
open_mr_in_browser(&result);
|
||||
} else if json_output {
|
||||
print_list_mrs_json(&result);
|
||||
} else {
|
||||
print_list_mrs(&result);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
@@ -362,21 +528,31 @@ async fn handle_count(
|
||||
config_override: Option<&str>,
|
||||
entity: &str,
|
||||
type_filter: Option<&str>,
|
||||
robot_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = Config::load(config_override)?;
|
||||
|
||||
let result = run_count(&config, entity, type_filter)?;
|
||||
print_count(&result);
|
||||
if robot_mode {
|
||||
print_count_json(&result);
|
||||
} else {
|
||||
print_count(&result);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_sync_status(
|
||||
config_override: Option<&str>,
|
||||
robot_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = Config::load(config_override)?;
|
||||
|
||||
let result = run_sync_status(&config)?;
|
||||
print_sync_status(&result);
|
||||
if robot_mode {
|
||||
print_sync_status_json(&result);
|
||||
} else {
|
||||
print_sync_status(&result);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -385,17 +561,27 @@ async fn handle_show(
|
||||
entity: &str,
|
||||
iid: i64,
|
||||
project_filter: Option<&str>,
|
||||
json: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = Config::load(config_override)?;
|
||||
|
||||
match entity {
|
||||
"issue" => {
|
||||
let result = run_show_issue(&config, iid, project_filter)?;
|
||||
print_show_issue(&result);
|
||||
if json {
|
||||
print_show_issue_json(&result);
|
||||
} else {
|
||||
print_show_issue(&result);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
"mr" => {
|
||||
println!("MR details not yet implemented. Only 'issue' is supported in CP1.");
|
||||
let result = run_show_mr(&config, iid, project_filter)?;
|
||||
if json {
|
||||
print_show_mr_json(&result);
|
||||
} else {
|
||||
print_show_mr(&result);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
@@ -405,32 +591,159 @@ async fn handle_show(
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_migrate(config_override: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// JSON output for version command.
|
||||
#[derive(Serialize)]
|
||||
struct VersionOutput {
|
||||
ok: bool,
|
||||
data: VersionData,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct VersionData {
|
||||
version: String,
|
||||
}
|
||||
|
||||
fn handle_version(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let version = env!("CARGO_PKG_VERSION").to_string();
|
||||
if robot_mode {
|
||||
let output = VersionOutput {
|
||||
ok: true,
|
||||
data: VersionData { version },
|
||||
};
|
||||
println!("{}", serde_json::to_string(&output)?);
|
||||
} else {
|
||||
println!("gi version {}", version);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// JSON output for not-implemented commands.
|
||||
#[derive(Serialize)]
|
||||
struct NotImplementedOutput {
|
||||
ok: bool,
|
||||
data: NotImplementedData,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct NotImplementedData {
|
||||
status: String,
|
||||
command: String,
|
||||
}
|
||||
|
||||
fn handle_backup(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if robot_mode {
|
||||
let output = NotImplementedOutput {
|
||||
ok: true,
|
||||
data: NotImplementedData {
|
||||
status: "not_implemented".to_string(),
|
||||
command: "backup".to_string(),
|
||||
},
|
||||
};
|
||||
println!("{}", serde_json::to_string(&output)?);
|
||||
} else {
|
||||
println!("gi backup - not yet implemented");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_reset(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if robot_mode {
|
||||
let output = NotImplementedOutput {
|
||||
ok: true,
|
||||
data: NotImplementedData {
|
||||
status: "not_implemented".to_string(),
|
||||
command: "reset".to_string(),
|
||||
},
|
||||
};
|
||||
println!("{}", serde_json::to_string(&output)?);
|
||||
} else {
|
||||
println!("gi reset - not yet implemented");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// JSON output for migrate command.
|
||||
#[derive(Serialize)]
|
||||
struct MigrateOutput {
|
||||
ok: bool,
|
||||
data: MigrateData,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MigrateData {
|
||||
before_version: i32,
|
||||
after_version: i32,
|
||||
migrated: bool,
|
||||
}
|
||||
|
||||
/// JSON error output with suggestion field.
|
||||
#[derive(Serialize)]
|
||||
struct RobotErrorWithSuggestion {
|
||||
error: RobotErrorSuggestionData,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RobotErrorSuggestionData {
|
||||
code: String,
|
||||
message: String,
|
||||
suggestion: String,
|
||||
}
|
||||
|
||||
async fn handle_migrate(
|
||||
config_override: Option<&str>,
|
||||
robot_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = Config::load(config_override)?;
|
||||
let db_path = get_db_path(config.storage.db_path.as_deref());
|
||||
|
||||
if !db_path.exists() {
|
||||
eprintln!(
|
||||
"{}",
|
||||
style(format!("Database not found at {}", db_path.display())).red()
|
||||
);
|
||||
eprintln!("{}", style("Run 'gi init' first to create the database.").yellow());
|
||||
std::process::exit(1);
|
||||
if robot_mode {
|
||||
let output = RobotErrorWithSuggestion {
|
||||
error: RobotErrorSuggestionData {
|
||||
code: "DB_ERROR".to_string(),
|
||||
message: format!("Database not found at {}", db_path.display()),
|
||||
suggestion: "Run 'gi init' first".to_string(),
|
||||
},
|
||||
};
|
||||
eprintln!("{}", serde_json::to_string(&output)?);
|
||||
} else {
|
||||
eprintln!(
|
||||
"{}",
|
||||
style(format!("Database not found at {}", db_path.display())).red()
|
||||
);
|
||||
eprintln!(
|
||||
"{}",
|
||||
style("Run 'gi init' first to create the database.").yellow()
|
||||
);
|
||||
}
|
||||
std::process::exit(10); // DB_ERROR exit code
|
||||
}
|
||||
|
||||
let conn = create_connection(&db_path)?;
|
||||
let before_version = get_schema_version(&conn);
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
style(format!("Current schema version: {}", before_version)).blue()
|
||||
);
|
||||
if !robot_mode {
|
||||
println!(
|
||||
"{}",
|
||||
style(format!("Current schema version: {}", before_version)).blue()
|
||||
);
|
||||
}
|
||||
|
||||
run_migrations(&conn)?;
|
||||
|
||||
let after_version = get_schema_version(&conn);
|
||||
|
||||
if after_version > before_version {
|
||||
if robot_mode {
|
||||
let output = MigrateOutput {
|
||||
ok: true,
|
||||
data: MigrateData {
|
||||
before_version,
|
||||
after_version,
|
||||
migrated: after_version > before_version,
|
||||
},
|
||||
};
|
||||
println!("{}", serde_json::to_string(&output)?);
|
||||
} else if after_version > before_version {
|
||||
println!(
|
||||
"{}",
|
||||
style(format!(
|
||||
|
||||
Reference in New Issue
Block a user