feat: tui command on goose-cli (#9385)
This commit is contained in:
@@ -968,6 +968,27 @@ enum Command {
|
|||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: TermCommand,
|
command: TermCommand,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Launch the goose terminal UI (TUI)
|
||||||
|
#[command(
|
||||||
|
about = "Launch the goose terminal UI",
|
||||||
|
long_about = "Launch the goose terminal UI (the @aaif/goose npm package).\n\
|
||||||
|
\n\
|
||||||
|
Resolution order:\n \
|
||||||
|
1. GOOSE_TUI_SCRIPT, if set to an existing dist/tui.js\n \
|
||||||
|
2. A local checkout's ui/text/dist/tui.js (dev workflow)\n \
|
||||||
|
3. `npx --yes --package <spec> -- goose-tui` (deployed installs)\n\
|
||||||
|
\n\
|
||||||
|
Override the npm spec via GOOSE_TUI_NPM_SPEC (default: @aaif/goose@latest).\n\
|
||||||
|
Local script mode requires `node` on PATH; npx mode requires `npx` on PATH.\n\
|
||||||
|
Any extra arguments are passed through to the TUI."
|
||||||
|
)]
|
||||||
|
Tui {
|
||||||
|
/// Arguments forwarded to the TUI
|
||||||
|
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||||
|
args: Vec<String>,
|
||||||
|
},
|
||||||
|
|
||||||
/// Manage local inference models
|
/// Manage local inference models
|
||||||
#[cfg(feature = "local-inference")]
|
#[cfg(feature = "local-inference")]
|
||||||
#[command(about = "Manage local inference models", visible_alias = "lm")]
|
#[command(about = "Manage local inference models", visible_alias = "lm")]
|
||||||
@@ -1252,6 +1273,7 @@ fn get_command_name(command: &Option<Command>) -> &'static str {
|
|||||||
Some(Command::Recipe { .. }) => "recipe",
|
Some(Command::Recipe { .. }) => "recipe",
|
||||||
Some(Command::Plugin { .. }) => "plugin",
|
Some(Command::Plugin { .. }) => "plugin",
|
||||||
Some(Command::Term { .. }) => "term",
|
Some(Command::Term { .. }) => "term",
|
||||||
|
Some(Command::Tui { .. }) => "tui",
|
||||||
#[cfg(feature = "local-inference")]
|
#[cfg(feature = "local-inference")]
|
||||||
Some(Command::LocalModels { .. }) => "local-models",
|
Some(Command::LocalModels { .. }) => "local-models",
|
||||||
Some(Command::Completion { .. }) => "completion",
|
Some(Command::Completion { .. }) => "completion",
|
||||||
@@ -2087,6 +2109,7 @@ pub async fn cli() -> anyhow::Result<()> {
|
|||||||
Some(Command::Recipe { command }) => handle_recipe_subcommand(command),
|
Some(Command::Recipe { command }) => handle_recipe_subcommand(command),
|
||||||
Some(Command::Plugin { command }) => handle_plugin_subcommand(command),
|
Some(Command::Plugin { command }) => handle_plugin_subcommand(command),
|
||||||
Some(Command::Term { command }) => handle_term_subcommand(command).await,
|
Some(Command::Term { command }) => handle_term_subcommand(command).await,
|
||||||
|
Some(Command::Tui { args }) => crate::commands::tui::handle_tui(args),
|
||||||
#[cfg(feature = "local-inference")]
|
#[cfg(feature = "local-inference")]
|
||||||
Some(Command::LocalModels { command }) => handle_local_models_command(command).await,
|
Some(Command::LocalModels { command }) => handle_local_models_command(command).await,
|
||||||
Some(Command::Review {
|
Some(Command::Review {
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ pub mod review;
|
|||||||
pub mod schedule;
|
pub mod schedule;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod term;
|
pub mod term;
|
||||||
|
pub mod tui;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
const TUI_NPM_SPEC_ENV: &str = "GOOSE_TUI_NPM_SPEC";
|
||||||
|
const TUI_REL_PATH: &str = "ui/text/dist/tui.js";
|
||||||
|
const DEFAULT_NPM_SPEC: &str = "@aaif/goose@latest";
|
||||||
|
const NPM_BIN_NAME: &str = "goose-tui";
|
||||||
|
|
||||||
|
enum TuiSource {
|
||||||
|
LocalScript(PathBuf),
|
||||||
|
Npx(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_local_script() -> Option<PathBuf> {
|
||||||
|
let exe = std::env::current_exe().ok()?;
|
||||||
|
let exe_dir = exe.parent().unwrap_or_else(|| Path::new("."));
|
||||||
|
|
||||||
|
let mut dir = Some(exe_dir.to_path_buf());
|
||||||
|
for _ in 0..6 {
|
||||||
|
if let Some(d) = dir.clone() {
|
||||||
|
let candidate = d.join(TUI_REL_PATH);
|
||||||
|
if candidate.is_file() {
|
||||||
|
return Some(candidate);
|
||||||
|
}
|
||||||
|
dir = d.parent().map(Path::to_path_buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(cwd) = std::env::current_dir() {
|
||||||
|
let candidate = cwd.join(TUI_REL_PATH);
|
||||||
|
if candidate.is_file() {
|
||||||
|
return Some(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_source() -> TuiSource {
|
||||||
|
if let Some(script) = find_local_script() {
|
||||||
|
return TuiSource::LocalScript(script);
|
||||||
|
}
|
||||||
|
let spec = std::env::var(TUI_NPM_SPEC_ENV).unwrap_or_else(|_| DEFAULT_NPM_SPEC.to_string());
|
||||||
|
TuiSource::Npx(spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_command(source: &TuiSource, args: &[String]) -> Result<Command> {
|
||||||
|
match source {
|
||||||
|
TuiSource::LocalScript(script) => {
|
||||||
|
let mut cmd = Command::new("node");
|
||||||
|
cmd.arg(script).args(args);
|
||||||
|
Ok(cmd)
|
||||||
|
}
|
||||||
|
TuiSource::Npx(spec) => {
|
||||||
|
let mut cmd = Command::new("npx");
|
||||||
|
cmd.arg("--yes")
|
||||||
|
.arg("--package")
|
||||||
|
.arg(spec)
|
||||||
|
.arg("--")
|
||||||
|
.arg(NPM_BIN_NAME)
|
||||||
|
.args(args);
|
||||||
|
Ok(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_tui(args: Vec<String>) -> Result<()> {
|
||||||
|
let source = resolve_source();
|
||||||
|
|
||||||
|
let goose_binary = std::env::current_exe()
|
||||||
|
.context("could not determine current goose executable to expose as GOOSE_BINARY")?;
|
||||||
|
|
||||||
|
let mut cmd = build_command(&source, &args)?;
|
||||||
|
cmd.env("GOOSE_BINARY", &goose_binary);
|
||||||
|
|
||||||
|
let descriptor = match &source {
|
||||||
|
TuiSource::LocalScript(p) => format!("node {}", p.display()),
|
||||||
|
TuiSource::Npx(spec) => format!("npx --package {} -- {}", spec, NPM_BIN_NAME),
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
let err = cmd.exec();
|
||||||
|
Err(anyhow!("failed to exec TUI ({descriptor}): {err}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
{
|
||||||
|
let status = cmd
|
||||||
|
.status()
|
||||||
|
.with_context(|| format!("failed to run `{descriptor}`"))?;
|
||||||
|
if !status.success() {
|
||||||
|
std::process::exit(status.code().unwrap_or(1));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user