feat(shuma-shell): matilda embebido — server admin desde la ventana
matilda deja de ser sólo un ejecutable aparte: el shell lo incorpora como herramienta. Meta-comando `:matilda plan|script|apply <inventario.json>` — reconcilia contra el estado real de la máquina (matilda-discover) y vuelca el resultado al feed del shell: - `plan`/`script` → una tarjeta sintética con el plan o el script. - `apply` → ejecuta el script de verdad; su salida fluye en una tarjeta como cualquier comando (streaming, captura acotada, kill). El panel [RUN] gana una sección [tools] con «⚙ matilda» que precarga el comando. Reusa todo lo del shell —feed, ejecución, sesión— sin panel nuevo ni peso extra: la herramienta es no invasiva. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Generated
+5
@@ -11489,8 +11489,13 @@ name = "shuma-shell"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"matilda-apply",
|
||||||
|
"matilda-core",
|
||||||
|
"matilda-discover",
|
||||||
|
"matilda-plan",
|
||||||
"nahual-launcher",
|
"nahual-launcher",
|
||||||
"nahual-theme",
|
"nahual-theme",
|
||||||
|
"serde_json",
|
||||||
"shuma-exec",
|
"shuma-exec",
|
||||||
"shuma-infer",
|
"shuma-infer",
|
||||||
"shuma-line",
|
"shuma-line",
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ shuma-session = { path = "../../modules/shuma/shuma-session" }
|
|||||||
shuma-exec = { path = "../../modules/shuma/shuma-exec" }
|
shuma-exec = { path = "../../modules/shuma/shuma-exec" }
|
||||||
shuma-infer = { path = "../../modules/shuma/shuma-infer" }
|
shuma-infer = { path = "../../modules/shuma/shuma-infer" }
|
||||||
shuma-sysmon = { path = "../../modules/shuma/shuma-sysmon" }
|
shuma-sysmon = { path = "../../modules/shuma/shuma-sysmon" }
|
||||||
|
# Herramienta matilda, embebida en la ventana del shell.
|
||||||
|
matilda-core = { path = "../../modules/matilda/matilda-core" }
|
||||||
|
matilda-plan = { path = "../../modules/matilda/matilda-plan" }
|
||||||
|
matilda-apply = { path = "../../modules/matilda/matilda-apply" }
|
||||||
|
matilda-discover = { path = "../../modules/matilda/matilda-discover" }
|
||||||
nahual-theme = { path = "../../modules/nahual/libs/theme" }
|
nahual-theme = { path = "../../modules/nahual/libs/theme" }
|
||||||
nahual-launcher = { path = "../../modules/nahual/libs/launcher" }
|
nahual-launcher = { path = "../../modules/nahual/libs/launcher" }
|
||||||
gpui = { workspace = true }
|
gpui = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
|||||||
@@ -600,6 +600,11 @@ impl Shell {
|
|||||||
.set_spill(matches!(arg.trim(), "on" | "si" | "sí" | "1" | "true"));
|
.set_spill(matches!(arg.trim(), "on" | "si" | "sí" | "1" | "true"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if let Some(args) = line.strip_prefix(":matilda ") {
|
||||||
|
// Herramienta matilda embebida — administración de servidores.
|
||||||
|
self.matilda_command(args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Los comandos anteriores que el usuario no fijó se autocolapsan
|
// Los comandos anteriores que el usuario no fijó se autocolapsan
|
||||||
// al aparecer uno nuevo abajo — orden de terminal tradicional.
|
// al aparecer uno nuevo abajo — orden de terminal tradicional.
|
||||||
@@ -641,13 +646,19 @@ impl Shell {
|
|||||||
/// Arma la `CommandSpec` de una línea: decide directo vs shell y
|
/// Arma la `CommandSpec` de una línea: decide directo vs shell y
|
||||||
/// aplica la política de captura de la sesión.
|
/// aplica la política de captura de la sesión.
|
||||||
fn build_spec(&self, line: &str, stdin: Option<String>, run_id: RunId) -> CommandSpec {
|
fn build_spec(&self, line: &str, stdin: Option<String>, run_id: RunId) -> CommandSpec {
|
||||||
|
self.build_spec_exec(plan_exec(line), stdin, run_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `build_spec` con el modo de ejecución ya decidido (lo usa la
|
||||||
|
/// herramienta matilda, que ejecuta un script de shell completo).
|
||||||
|
fn build_spec_exec(&self, exec: Exec, stdin: Option<String>, run_id: RunId) -> CommandSpec {
|
||||||
let policy = self.session.capture();
|
let policy = self.session.capture();
|
||||||
let spill_path = (policy.spill && policy.limit_bytes > 0).then(|| {
|
let spill_path = (policy.spill && policy.limit_bytes > 0).then(|| {
|
||||||
std::env::temp_dir()
|
std::env::temp_dir()
|
||||||
.join(format!("shuma-spill-{}-{run_id}.log", std::process::id()))
|
.join(format!("shuma-spill-{}-{run_id}.log", std::process::id()))
|
||||||
});
|
});
|
||||||
CommandSpec {
|
CommandSpec {
|
||||||
exec: plan_exec(line),
|
exec,
|
||||||
cwd: self.session.cwd().to_string(),
|
cwd: self.session.cwd().to_string(),
|
||||||
capture_limit: policy.limit_bytes,
|
capture_limit: policy.limit_bytes,
|
||||||
spill_path,
|
spill_path,
|
||||||
@@ -655,6 +666,116 @@ impl Shell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Registra un comando "sintético" ya terminado — su salida la
|
||||||
|
/// produce el shell mismo, no un proceso (la usa `:matilda plan`).
|
||||||
|
fn synthetic_run(&mut self, label: &str, output: Vec<String>, ok: bool) {
|
||||||
|
for ui in self.run_ui.values_mut() {
|
||||||
|
if !ui.user_touched {
|
||||||
|
ui.collapsed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let now = unix_now();
|
||||||
|
let id = self.session.begin_run(label, now);
|
||||||
|
self.run_ui.insert(id, RunUi::default());
|
||||||
|
for line in output {
|
||||||
|
self.session.append_output(id, Stream::Stdout, line);
|
||||||
|
}
|
||||||
|
self.session.finish_run(id, if ok { 0 } else { 1 }, now);
|
||||||
|
self.scroll.scroll_to_bottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ejecuta `exec_line` mostrando `label` en la tarjeta — el comando
|
||||||
|
/// real puede diferir de lo que se ve (matilda corre un script).
|
||||||
|
fn spawn_labeled(&mut self, label: String, exec_line: String) {
|
||||||
|
for ui in self.run_ui.values_mut() {
|
||||||
|
if !ui.user_touched {
|
||||||
|
ui.collapsed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let now = unix_now();
|
||||||
|
let id = self.session.begin_run(&label, now);
|
||||||
|
self.run_ui.insert(id, RunUi::default());
|
||||||
|
let exec = Exec::Shell { line: exec_line, program: "bash".into() };
|
||||||
|
let spec = self.build_spec_exec(exec, None, id);
|
||||||
|
self.active.push((id, exec_run(&spec)));
|
||||||
|
self.scroll.scroll_to_bottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Carga un inventario JSON, resolviendo la ruta contra el cwd.
|
||||||
|
fn load_inventory(&self, file: &str) -> Result<matilda_core::Inventory, String> {
|
||||||
|
let path = if file.starts_with('/') {
|
||||||
|
file.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", self.session.cwd(), file)
|
||||||
|
};
|
||||||
|
let text = std::fs::read_to_string(&path)
|
||||||
|
.map_err(|e| format!("no se pudo leer {path}: {e}"))?;
|
||||||
|
serde_json::from_str(&text).map_err(|e| format!("JSON inválido: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// La herramienta matilda, embebida: `:matilda plan|script|apply
|
||||||
|
/// <inventario.json>`. Reconcilia contra el estado real de la
|
||||||
|
/// máquina y vuelca el resultado al feed del shell.
|
||||||
|
fn matilda_command(&mut self, args: &str) {
|
||||||
|
let label = format!(":matilda {args}");
|
||||||
|
let parts: Vec<&str> = args.split_whitespace().collect();
|
||||||
|
let (sub, file) = match parts.as_slice() {
|
||||||
|
[s, f] => (*s, *f),
|
||||||
|
_ => {
|
||||||
|
self.synthetic_run(
|
||||||
|
&label,
|
||||||
|
vec!["uso: :matilda plan|script|apply <inventario.json>".into()],
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let desired = match self.load_inventory(file) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(e) => {
|
||||||
|
self.synthetic_run(&label, vec![e], false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Reconcilia contra el estado observado de esta máquina.
|
||||||
|
let current =
|
||||||
|
matilda_discover::observed_inventory(&matilda_discover::discover_local(), &desired);
|
||||||
|
let p = matilda_plan::plan(¤t, &desired);
|
||||||
|
|
||||||
|
match sub {
|
||||||
|
"plan" => {
|
||||||
|
let lines: Vec<String> = if p.is_empty() {
|
||||||
|
vec!["sin cambios: el servidor ya está al día".into()]
|
||||||
|
} else {
|
||||||
|
p.actions
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, a)| format!("{:>2}. {}", i + 1, a.describe()))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
self.synthetic_run(&label, lines, true);
|
||||||
|
}
|
||||||
|
"script" => {
|
||||||
|
let script = matilda_apply::steps_to_script(&matilda_apply::plan_to_steps(
|
||||||
|
&p, &desired,
|
||||||
|
));
|
||||||
|
self.synthetic_run(&label, script.lines().map(String::from).collect(), true);
|
||||||
|
}
|
||||||
|
"apply" => {
|
||||||
|
let steps = matilda_apply::plan_to_steps(&p, &desired);
|
||||||
|
if steps.is_empty() {
|
||||||
|
self.synthetic_run(&label, vec!["sin cambios: nada que aplicar".into()], true);
|
||||||
|
} else {
|
||||||
|
// El script se ejecuta de verdad — fluye al feed.
|
||||||
|
self.spawn_labeled(label, matilda_apply::steps_to_script(&steps));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
self.synthetic_run(&label, vec![format!("subcomando desconocido: {other}")], false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Reprocesa la salida capturada del comando `source`: ejecuta `line`
|
/// Reprocesa la salida capturada del comando `source`: ejecuta `line`
|
||||||
/// alimentándole esa salida por stdin, sin volver a correr el
|
/// alimentándole esa salida por stdin, sin volver a correr el
|
||||||
/// original. Así un resultado se filtra con distintas herramientas.
|
/// original. Así un resultado se filtra con distintas herramientas.
|
||||||
@@ -1290,6 +1411,26 @@ impl Render for Shell {
|
|||||||
.text_color(dim)
|
.text_color(dim)
|
||||||
.child("clic ejecuta · + guarda lo último"),
|
.child("clic ejecuta · + guarda lo último"),
|
||||||
)
|
)
|
||||||
|
.child(div().h(px(1.)).bg(theme.border))
|
||||||
|
.child(div().text_color(dim).text_size(px(12.)).child("[tools]"))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id("tool-matilda")
|
||||||
|
.px(px(8.))
|
||||||
|
.py(px(6.))
|
||||||
|
.bg(node_bg)
|
||||||
|
.rounded(px(4.))
|
||||||
|
.text_color(text)
|
||||||
|
.text_size(px(13.))
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|s| s.bg(theme.bg_row_hover))
|
||||||
|
.child("⚙ matilda")
|
||||||
|
.on_click(cx.listener(|shell, _, _, cx| {
|
||||||
|
// Precarga el comando para que el usuario nombre el inventario.
|
||||||
|
shell.line.set_text(":matilda plan ");
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Lienzo central: comandos ejecutados + su salida ---
|
// --- Lienzo central: comandos ejecutados + su salida ---
|
||||||
|
|||||||
Reference in New Issue
Block a user