e2da24239e
Cierra el círculo del Card: el flow.input "chart-request" y output
"chart-result" declarados desde fase 1 ahora tienen data plane real.
Otros módulos brahman (incluyendo el CLI nuevo) pueden conectar al
Unix socket de Tahuantinsuyu y pedir cómputos de cartas natales sin
abrir la GUI.
## Protocolo y server
Nuevo módulo `tahuantinsuyu_card::service` con:
- `ComputeRequest { Ping, Natal { birth, config, offset_minutes,
label } }` — postcard-serializable
- `ComputeResponse { Pong, Render { render }, Error { message } }`
- `serve(socket_path)` — async loop sobre tokio::UnixListener,
spawn por conexión, frame `u32 length` LE + postcard payload
(mismo molde que brahman-handshake). Cap defensivo a 1 MiB por
frame.
- `request(&socket, &req) -> Response` — cliente async one-shot
(abre, envía, recibe, cierra)
- `spawn_service_thread(path)` — thread dedicado con tokio
current_thread runtime; loggea warn si bind falla, la app sigue
standalone.
La Card ahora declara `service_socket: Some(default_service_socket())`
— el broker brahman puede revelar este path a consumidores que
matcheen el flow `chart-request`. Path canónico:
`$XDG_CACHE_HOME/tahuantinsuyu/service.sock` (con fallback a
/tmp).
## CLI nuevo
`crates/apps/tahuantinsuyu-cli` — binario standalone que usa el
helper cliente. Comandos:
- `tahuantinsuyu-cli ping` — health check
- `tahuantinsuyu-cli natal --year ... --month ... --day ... --hour
... --minute ... --tz-min ... --lat ... --lon ... [--alt ...]
[--label "..."] [--offset-minutes N]` — pide compute y emite
RenderModel como JSON pretty-print en stdout
Útil para:
- Smoke tests del data plane (CI puede levantar la app + ping)
- Scripts batch (computar 100 cartas y exportar JSON)
- Integraciones con otros tools del fractal brahman vía broker
## Cambios accesorios
- apps/tahuantinsuyu/main.rs: spawn_service_thread al boot junto al
sidecar. Loggea el path del socket a stderr para debug.
- Cargo workspace: agrega tahuantinsuyu-cli como member.
- tahuantinsuyu-card Cargo: agrega deps (engine, model, postcard,
tokio, tracing, directories, thiserror) para soportar el server.
Lo que falta para integración brahman 100%:
- Suscripción al broker como "consumer-aware" para detectar cuando
otros módulos publican `chart-request`s
- Publishing de eventos al broker cuando se crean/borran cartas
- Ambos requieren protocolo handshake bidireccional sobre el Init
socket (no service_socket) — fase posterior.
cargo check verde, 8 tests engine + 1 modules verdes. CLI compila;
prueba end-to-end (ping + natal) queda a manos del usuario que
levante la GUI.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
165 lines
5.2 KiB
Rust
165 lines
5.2 KiB
Rust
//! `tahuantinsuyu-cli` — cliente del service socket de Tahuantinsuyu.
|
|
//!
|
|
//! Pide cómputos de cartas sin abrir la GUI. Útil para integraciones,
|
|
//! scripts y para verificar end-to-end que el data plane brahman está
|
|
//! sirviendo. Conecta al socket que la app GUI expone (default
|
|
//! `$XDG_CACHE_HOME/tahuantinsuyu/service.sock`).
|
|
//!
|
|
//! ## Comandos
|
|
//!
|
|
//! - `ping` — verifica que el server responde.
|
|
//! - `natal --year N --month M --day D --hour H --minute MIN
|
|
//! --tz-min TZ --lat LAT --lon LON [--alt ALT] [--label TEXT]`
|
|
//! — pide una carta natal y la imprime como JSON.
|
|
//!
|
|
//! ## Ejemplo
|
|
//!
|
|
//! ```bash
|
|
//! cargo run -p tahuantinsuyu-cli -- natal \
|
|
//! --year 1987 --month 3 --day 14 \
|
|
//! --hour 5 --minute 22 --tz-min -240 \
|
|
//! --lat 10.4806 --lon -66.9036 \
|
|
//! --label "Sergio"
|
|
//! ```
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use anyhow::{anyhow, Context, Result};
|
|
use clap::{Parser, Subcommand};
|
|
use tahuantinsuyu_card::service::{self, ComputeRequest, ComputeResponse};
|
|
use tahuantinsuyu_model::{StoredBirthData, StoredChartConfig};
|
|
|
|
#[derive(Parser)]
|
|
#[command(
|
|
name = "tahuantinsuyu-cli",
|
|
version,
|
|
about = "Cliente del service socket de Tahuantinsuyu."
|
|
)]
|
|
struct Cli {
|
|
/// Path al service socket. Default: el resuelto por
|
|
/// `service::default_service_socket()`.
|
|
#[arg(long, global = true)]
|
|
socket: Option<PathBuf>,
|
|
|
|
#[command(subcommand)]
|
|
command: Command,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum Command {
|
|
/// Health check — verifica que el server responde con Pong.
|
|
Ping,
|
|
/// Pide el cómputo de una carta natal e imprime el RenderModel
|
|
/// como JSON.
|
|
Natal {
|
|
#[arg(long)]
|
|
year: i32,
|
|
#[arg(long)]
|
|
month: u32,
|
|
#[arg(long)]
|
|
day: u32,
|
|
#[arg(long)]
|
|
hour: u32,
|
|
#[arg(long)]
|
|
minute: u32,
|
|
#[arg(long, default_value_t = 0.0)]
|
|
second: f64,
|
|
/// Offset de zona horaria del lugar de nacimiento, en minutos.
|
|
/// Ej: Argentina = -180, UTC = 0, Madrid = 60.
|
|
#[arg(long = "tz-min")]
|
|
tz_offset_minutes: i32,
|
|
#[arg(long)]
|
|
lat: f64,
|
|
#[arg(long)]
|
|
lon: f64,
|
|
#[arg(long, default_value_t = 0.0)]
|
|
alt: f64,
|
|
/// Etiqueta del chart para el title del RenderModel.
|
|
#[arg(long)]
|
|
label: Option<String>,
|
|
/// Offset adicional en minutos sobre el instante natal (útil
|
|
/// para rectificación rápida sin guardar variantes).
|
|
#[arg(long, default_value_t = 0)]
|
|
offset_minutes: i64,
|
|
},
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let cli = Cli::parse();
|
|
let socket = cli
|
|
.socket
|
|
.unwrap_or_else(service::default_service_socket);
|
|
|
|
let rt = tokio::runtime::Builder::new_current_thread()
|
|
.enable_io()
|
|
.build()
|
|
.context("crear tokio runtime")?;
|
|
|
|
rt.block_on(async {
|
|
match cli.command {
|
|
Command::Ping => {
|
|
let response = service::request(&socket, &ComputeRequest::Ping)
|
|
.await
|
|
.with_context(|| format!("ping a {}", socket.display()))?;
|
|
match response {
|
|
ComputeResponse::Pong => {
|
|
println!("pong");
|
|
Ok(())
|
|
}
|
|
other => Err(anyhow!("respuesta inesperada al ping: {:?}", other)),
|
|
}
|
|
}
|
|
Command::Natal {
|
|
year,
|
|
month,
|
|
day,
|
|
hour,
|
|
minute,
|
|
second,
|
|
tz_offset_minutes,
|
|
lat,
|
|
lon,
|
|
alt,
|
|
label,
|
|
offset_minutes,
|
|
} => {
|
|
let request = ComputeRequest::Natal {
|
|
birth: StoredBirthData {
|
|
year,
|
|
month,
|
|
day,
|
|
hour,
|
|
minute,
|
|
second,
|
|
tz_offset_minutes,
|
|
latitude_deg: lat,
|
|
longitude_deg: lon,
|
|
altitude_m: alt,
|
|
time_certainty: Default::default(),
|
|
subject_name: label.clone(),
|
|
birthplace_label: None,
|
|
},
|
|
config: StoredChartConfig::default(),
|
|
offset_minutes,
|
|
label,
|
|
};
|
|
let response = service::request(&socket, &request)
|
|
.await
|
|
.with_context(|| format!("natal request a {}", socket.display()))?;
|
|
match response {
|
|
ComputeResponse::Render { render } => {
|
|
let json = serde_json::to_string_pretty(&render)
|
|
.context("serializar RenderModel a JSON")?;
|
|
println!("{}", json);
|
|
Ok(())
|
|
}
|
|
ComputeResponse::Error { message } => {
|
|
Err(anyhow!("server reportó error: {}", message))
|
|
}
|
|
other => Err(anyhow!("respuesta inesperada al natal: {:?}", other)),
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|