mod app;
mod entities;
mod features;
mod screens;
mod shared;
mod widgets;
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, anyhow, bail};
use tokio::sync::mpsc::unbounded_channel;
use crate::app::events::{AppCommand, AppEvent};
use crate::app::orchestrator::{self, OrchestratorDeps};
use crate::app::supervisor::{DemoSupervisor, LlamaSupervisor, ServerSupervisor};
use crate::features::backup::{self, RestoreOutcome};
use crate::features::cli::{self, CliCommand};
use crate::shared::config::{AppConfig, ServerMode};
use crate::shared::i18n::{self, Lang, Locale};
use crate::shared::paths::{Locations, Paths};
use crate::shared::storage::{JsonStore, Storage};
use crate::shared::{instance, logging};
fn main() -> ExitCode {
let (paths, lang) = match Paths::resolve() {
Ok(paths) => {
let settings_lang = try_settings_language(&paths.settings_file());
let lang = cli_lang(settings_lang, paths.default_language());
(paths, lang)
}
Err(err) => {
print_error(Lang::En, &err);
shared::console::hold_if_sole_owner(i18n::locale(Lang::En));
return ExitCode::FAILURE;
}
};
let locale_warnings = i18n::init(&paths.locales_dir());
let loc = i18n::locale(lang);
let args: Vec<String> = std::env::args().skip(1).collect();
let command = match cli::parse(&args, loc) {
Ok(cmd) => cmd,
Err(msg) => {
eprintln!("{msg}");
shared::console::hold_if_sole_owner(loc);
return ExitCode::from(2);
}
};
match command {
CliCommand::Help { topic } => {
println!("{}", cli::render_help(topic, loc));
return ExitCode::SUCCESS;
}
CliCommand::Version => {
println!(
"{}",
loc.tf(
"cli.version.line",
&[("version", env!("CARGO_PKG_VERSION"))]
)
);
return ExitCode::SUCCESS;
}
_ => {}
}
match real_main(command, &paths, loc, &locale_warnings) {
Ok(code) => code,
Err(err) => {
print_error(lang, &err);
shared::console::hold_if_sole_owner(loc);
ExitCode::FAILURE
}
}
}
fn real_main(
command: CliCommand,
paths: &Paths,
loc: &Locale,
locale_warnings: &[String],
) -> anyhow::Result<ExitCode> {
if refuses_tui_launch(&command, std::io::stdout().is_terminal()) {
let refusal = if matches!(command, CliCommand::Keys { .. }) {
"cli.keys.no_terminal"
} else {
"cli.tui.no_terminal"
};
eprintln!("{}", loc.t(refusal));
return Ok(ExitCode::from(2));
}
if let CliCommand::Keys { output } = &command {
return app::key_echo::run(output.as_deref(), loc);
}
if matches!(command, CliCommand::Demo) {
return run_demo(loc, locale_warnings);
}
if let CliCommand::Stats {
archive,
compare,
password,
json,
} = command
{
return run_stats(
paths,
archive.as_deref(),
compare.as_deref(),
password,
json,
loc,
);
}
paths.ensure_dirs(loc).with_context(|| {
loc.tf(
"cli.ctx.ensure_dirs",
&[("path", &paths.root().display().to_string())],
)
})?;
let _logging =
logging::init(paths, loc).with_context(|| loc.t("cli.ctx.init_logging").to_string())?;
for w in locale_warnings {
tracing::warn!("{w}");
}
match command {
CliCommand::Import { file } => {
run_import(paths, &file, loc)?;
Ok(ExitCode::SUCCESS)
}
CliCommand::Backup {
output,
compression,
password,
} => {
run_backup(paths, output, compression, password, loc)?;
Ok(ExitCode::SUCCESS)
}
CliCommand::Restore { archive, password } => {
run_restore(paths, &archive, password, loc)?;
Ok(ExitCode::SUCCESS)
}
CliCommand::SandboxSetup {
force,
enable_python,
} => {
run_sandbox_setup(paths, force, enable_python, loc)?;
Ok(ExitCode::SUCCESS)
}
CliCommand::LlamaBackends { build } => {
run_llama_backends(paths, build.as_deref(), loc)?;
Ok(ExitCode::SUCCESS)
}
CliCommand::LlamaSetup {
backend,
build,
cudart,
force,
set_binary,
} => run_llama_setup(paths, backend, build, cudart, force, set_binary, loc),
CliCommand::LlamaInstalled => {
run_llama_installed(paths, loc);
Ok(ExitCode::SUCCESS)
}
CliCommand::Setup(args) => run_setup(paths, &args, loc),
CliCommand::LlamaRemove { id, force } => {
run_llama_remove(paths, &id, force, loc)?;
Ok(ExitCode::SUCCESS)
}
CliCommand::LocalesExport { code, output } => {
run_locales_export(&code, &output, loc)?;
Ok(ExitCode::SUCCESS)
}
CliCommand::ThemesExport { name, output } => {
run_themes_export(paths, &name, &output, loc)?;
Ok(ExitCode::SUCCESS)
}
CliCommand::ThemesCheck { target } => run_themes_check(paths, target.as_deref(), loc),
CliCommand::Run => run_tui(paths, loc),
CliCommand::Demo | CliCommand::Stats { .. } | CliCommand::Keys { .. } => {
unreachable!("handled above, before the real root is touched")
}
CliCommand::Help { .. } | CliCommand::Version => unreachable!("handled in main"),
}
}
fn refuses_tui_launch(command: &CliCommand, stdout_is_terminal: bool) -> bool {
matches!(
command,
CliCommand::Run | CliCommand::Demo | CliCommand::Keys { .. }
) && !stdout_is_terminal
}
fn run_tui(paths: &Paths, loc: &Locale) -> anyhow::Result<ExitCode> {
let _instance = match instance::acquire() {
Ok(guard) => guard,
Err(instance::InstanceError::AlreadyRunning) => {
eprintln!("{}", loc.t("cli.instance.already_running"));
tracing::warn!("startup refused: another instance of the app is already running");
shared::console::hold_if_sole_owner(loc);
return Ok(ExitCode::from(2));
}
Err(instance::InstanceError::Init(e)) => {
return Err(anyhow!(
"{}",
loc.tf("cli.instance.init_failed", &[("err", e.as_str())])
));
}
};
tracing::info!(
version = env!("CARGO_PKG_VERSION"),
root = %paths.root().display(),
"mindfork starting"
);
features::data_migration::run(paths, loc)?;
launch_tui(
paths,
Arc::new(LlamaSupervisor::new(paths)),
true,
paths.locations(),
loc,
)
}
fn run_demo(loc: &Locale, locale_warnings: &[String]) -> anyhow::Result<ExitCode> {
let root = std::env::temp_dir().join(format!("mindfork-demo-{}", std::process::id()));
let paths = Paths::with_root(&root);
paths.ensure_dirs(loc).with_context(|| {
loc.tf(
"cli.ctx.ensure_dirs",
&[("path", &root.display().to_string())],
)
})?;
let _logging =
logging::init(&paths, loc).with_context(|| loc.t("cli.ctx.init_logging").to_string())?;
for w in locale_warnings {
tracing::warn!("{w}");
}
tracing::info!(
version = env!("CARGO_PKG_VERSION"),
root = %root.display(),
"mindfork demo starting"
);
{
let storage = Storage::open(paths.clone())
.with_context(|| loc.t("cli.ctx.open_storage").to_string())?;
features::demo::provision(&storage)
.with_context(|| loc.t("cli.ctx.provision_demo").to_string())?;
}
let backend = Arc::new(crate::shared::api::mock::MockBackend::cycling(
features::demo::demo_replies(),
DEMO_STREAM_DELAY_MS,
));
let result = launch_tui(
&paths,
Arc::new(DemoSupervisor::new(backend)),
false,
features::demo::locations(),
loc,
);
let _ = std::fs::remove_dir_all(&root);
result
}
const DEMO_STREAM_DELAY_MS: u64 = 18;
fn launch_tui(
paths: &Paths,
supervisor: Arc<dyn ServerSupervisor>,
apply_env: bool,
locations: Locations,
loc: &Locale,
) -> anyhow::Result<ExitCode> {
let background_query = crate::shared::osc11::begin();
crate::shared::config::set_environment_mode(crate::shared::config::ThemeMode::for_environment(
std::env::var_os("NO_COLOR").as_deref(),
));
let depth = crate::shared::colour_depth::ColourDepth::for_environment(
std::env::var_os("TERM_PROGRAM").as_deref(),
std::env::var_os("COLORTERM").as_deref(),
);
if depth != crate::shared::colour_depth::ColourDepth::TrueColour {
tracing::info!("colour depth: {depth:?} (TERM_PROGRAM, COLORTERM)");
}
crate::shared::colour_depth::set(depth);
for warning in crate::shared::user_theme::init(&paths.themes_dir()) {
tracing::warn!("{warning}");
}
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.with_context(|| loc.t("cli.ctx.build_runtime").to_string())?;
let storage = Arc::new(
Storage::open(paths.clone()).with_context(|| loc.t("cli.ctx.open_storage").to_string())?,
);
let (cmd_tx, cmd_rx) = unbounded_channel::<AppCommand>();
let (evt_tx, evt_rx) = unbounded_channel::<AppEvent>();
let config = startup_config(paths, storage.json(), apply_env, loc)?;
let orchestrator = runtime.spawn(orchestrator::run(OrchestratorDeps {
cmd_rx,
evt_tx: evt_tx.clone(),
storage,
config,
supervisor,
default_language: paths.default_language(),
extra_tools: Vec::new(),
}));
let dict_dir = paths.dictionaries_dir();
let bundled_dict_dir = paths.bundled_dictionaries_dir();
let personal = paths.personal_dictionary();
let result = app::runtime::run(
cmd_tx.clone(),
evt_rx,
dict_dir,
bundled_dict_dir,
personal,
paths.log_dir(),
locations,
background_query,
);
if result.is_err() && orchestrator.is_finished() {
match runtime.block_on(orchestrator) {
Err(join) if join.is_panic() => {
tracing::error!(error = %join, "the orchestrator task panicked");
}
Err(join) => tracing::error!(error = %join, "the orchestrator task ended abnormally"),
Ok(()) => tracing::error!("the orchestrator task ended while the session was running"),
}
}
let _ = cmd_tx.send(AppCommand::Quit);
runtime.shutdown_timeout(Duration::from_secs(2));
match &result {
Ok(()) => tracing::info!("mindfork exited cleanly"),
Err(err) => tracing::error!(error = %err, "mindfork exited with error"),
}
result.map(|()| ExitCode::SUCCESS)
}
fn startup_config(
paths: &Paths,
store: &JsonStore,
apply_env: bool,
loc: &Locale,
) -> anyhow::Result<AppConfig> {
let fresh_config = !paths.settings_file().exists();
let mut config = store
.load_config()
.with_context(|| loc.t("cli.ctx.load_settings").to_string())?;
if fresh_config {
config.interface.language = paths.default_language();
}
if apply_env {
apply_env_overrides(&mut config);
}
Ok(config)
}
fn cli_lang(settings_language: Option<Lang>, default_language: Lang) -> Lang {
settings_language.unwrap_or(default_language)
}
fn try_settings_language(settings_file: &Path) -> Option<Lang> {
let bytes = std::fs::read(settings_file).ok()?;
let raw: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
let unset = || Some(crate::shared::config::InterfaceSettings::default().language);
let Some(interface) = raw.as_object()?.get("interface") else {
return unset();
};
match interface.as_object()?.get("language") {
None => unset(),
Some(code) => code.as_str().map(Lang::from_code),
}
}
fn print_error(lang: Lang, err: &anyhow::Error) {
eprintln!("{}", cli_error_line(i18n::locale(lang), err));
}
fn cli_error_line(loc: &Locale, err: &anyhow::Error) -> String {
format!("{}: {err:#}", loc.t("cli.err.prefix"))
}
fn acquire_cli_guard(loc: &Locale, action: &str) -> anyhow::Result<instance::InstanceGuard> {
match instance::acquire() {
Ok(guard) => Ok(guard),
Err(instance::InstanceError::AlreadyRunning) => {
bail!("{}", loc.tf("cli.guard.busy", &[("action", action)]))
}
Err(instance::InstanceError::Init(e)) => Err(anyhow!(
"{}",
loc.tf("cli.instance.init_failed", &[("err", e.as_str())])
)),
}
}
fn backup_config(paths: &Paths, loc: &Locale) -> backup::BackupSettings {
backup::backup_settings(paths).unwrap_or_else(|err| {
tracing::warn!(error = %format!("{err:#}"),
"the settings could not be read for a backup command");
eprintln!(
"{}",
loc.tf(
"cli.backup.settings_unreadable",
&[("path", &paths.settings_file().display().to_string())]
)
);
backup::BackupSettings::default()
})
}
fn effective_password(arg: Option<String>, stored: Option<String>) -> Option<String> {
arg.or(stored).filter(|p| !p.is_empty())
}
fn run_backup(
paths: &Paths,
output: Option<PathBuf>,
compression: i64,
password: Option<String>,
loc: &Locale,
) -> anyhow::Result<()> {
let _instance = acquire_cli_guard(loc, loc.t("cli.guard.action.backup"))?;
let cfg = backup_config(paths, loc);
let password = effective_password(password, cfg.stored_password);
let out = backup::create_backup(
paths,
output,
compression,
cfg.fs_root.as_deref(),
password.as_deref(),
loc,
|msg| println!("{msg}"),
);
features::terminal_input::discard_type_ahead();
let out = out.with_context(|| loc.t("cli.ctx.backup").to_string())?;
if password.is_some() {
println!("{}", loc.t("cli.backup.encrypted"));
}
println!(
"{}",
loc.tf(
"cli.backup.created",
&[("path", &out.display().to_string())]
)
);
Ok(())
}
fn run_stats(
paths: &Paths,
archive: Option<&Path>,
compare: Option<&Path>,
password: Option<String>,
json: bool,
loc: &Locale,
) -> anyhow::Result<ExitCode> {
use crate::features::data_stats::{self, OtherCopy};
let stats = match archive {
None => data_stats::collect_root(paths, loc)?,
Some(archive) => stats_of_archive(paths, archive, password.clone(), loc)?,
};
let Some(other) = compare else {
if json {
println!("{}", data_stats::render_json(&stats));
} else {
println!("{}", data_stats::render_text(&stats, loc));
}
return Ok(ExitCode::SUCCESS);
};
let (there, snapshot) = match data_stats::other_copy(other, loc)? {
OtherCopy::Archive => (stats_of_archive(paths, other, password, loc)?, None),
OtherCopy::Snapshot => (
data_stats::read_snapshot(other, loc)?,
Some(other.display().to_string()),
),
};
let comparison = data_stats::compare(&stats, &there, snapshot);
if json {
println!("{}", data_stats::render_comparison_json(&comparison));
} else {
println!("{}", data_stats::render_comparison_text(&comparison, loc));
}
Ok(ExitCode::SUCCESS)
}
fn stats_of_archive(
paths: &Paths,
archive: &Path,
password: Option<String>,
loc: &Locale,
) -> anyhow::Result<features::data_stats::DataStats> {
let stored = backup_config(paths, loc).stored_password;
eprintln!(
"{}",
loc.tf(
"cli.stats.reading_archive",
&[("path", &archive.display().to_string())]
)
);
let password = resolve_restore_password(archive, password, stored, loc)?;
features::data_stats::collect_archive(archive, password.as_deref(), loc)
}
const PASSWORD_ATTEMPTS: usize = 3;
fn resolve_restore_password(
archive: &Path,
argument: Option<String>,
stored: Option<String>,
loc: &Locale,
) -> anyhow::Result<Option<String>> {
use crate::features::backup::ArchivePassword;
let from_settings = argument.is_none();
let mut current = effective_password(argument, stored);
for attempt in 0..PASSWORD_ATTEMPTS {
let Ok(status) = backup::check_password(archive, current.as_deref()) else {
return Ok(current);
};
if matches!(status, ArchivePassword::NotNeeded | ArchivePassword::Ok) {
return Ok(current);
}
let stored_failed = stored_password_failed(from_settings, attempt, status);
match ask_for_password(status, stored_failed, loc)? {
Some(entered) => current = Some(entered),
None => return Ok(current),
}
}
Ok(current)
}
fn ask_for_password(
status: crate::features::backup::ArchivePassword,
stored_failed: bool,
loc: &Locale,
) -> anyhow::Result<Option<String>> {
use crate::features::backup::ArchivePassword;
use crate::features::terminal_input;
if !terminal_input::is_interactive() {
if stored_failed {
bail!("{}", loc.t("cli.restore.stored_password_wrong"));
}
return Ok(None);
}
if stored_failed {
eprintln!("{}", loc.t("cli.restore.stored_password_wrong"));
} else if status == ArchivePassword::Wrong {
eprintln!("{}", loc.t("backup.err.wrong_password"));
}
Ok(terminal_input::read_password(
loc.t("cli.restore.password_prompt"),
)?)
}
fn stored_password_failed(
from_settings: bool,
attempt: usize,
status: crate::features::backup::ArchivePassword,
) -> bool {
from_settings && attempt == 0 && status == crate::features::backup::ArchivePassword::Wrong
}
fn run_restore(
paths: &Paths,
archive: &Path,
password: Option<String>,
loc: &Locale,
) -> anyhow::Result<()> {
let _instance = acquire_cli_guard(loc, loc.t("cli.guard.action.restore"))?;
let cfg = backup_config(paths, loc);
let password = resolve_restore_password(archive, password, cfg.stored_password, loc)?;
if let Ok(Some(m)) = backup::read_manifest(archive)
&& m.is_newer_than_current()
{
eprintln!(
"{}",
loc.tf("backup.warn.newer_manifest", &[("version", &m.app_version)])
);
}
let outcome = backup::restore_backup(
paths,
archive,
cfg.fs_root.as_deref(),
password.as_deref(),
loc,
|msg| println!("{msg}"),
);
features::terminal_input::discard_type_ahead();
let outcome = outcome?;
match outcome {
RestoreOutcome::Restored { pre_restore } => {
if let Some(pre) = pre_restore {
println!(
"{}",
loc.tf(
"cli.restore.pre_saved",
&[("path", &pre.display().to_string())]
)
);
}
println!(
"{}",
loc.tf(
"cli.restore.done",
&[("path", &archive.display().to_string())]
)
);
Ok(())
}
RestoreOutcome::RolledBack {
pre_restore,
restore_error,
} => {
eprintln!(
"{}",
loc.tf(
"cli.restore.failed",
&[
("path", &archive.display().to_string()),
("err", &format!("{restore_error:#}")),
],
)
);
eprintln!(
"{}",
loc.tf(
"cli.restore.rolled_back",
&[("path", &pre_restore.display().to_string())]
)
);
bail!("{}", loc.t("cli.restore.err_rolled_back"))
}
RestoreOutcome::Failed {
pre_restore,
restore_error,
rollback_error,
} => {
eprintln!(
"{}",
loc.tf(
"cli.restore.failed",
&[
("path", &archive.display().to_string()),
("err", &format!("{restore_error:#}")),
],
)
);
if let Some(rb) = rollback_error {
eprintln!(
"{}",
loc.tf(
"cli.restore.rollback_failed",
&[("err", &format!("{rb:#}"))]
)
);
}
match pre_restore {
Some(pre) => bail!(
"{}",
loc.tf(
"cli.restore.err_inconsistent",
&[("path", &pre.display().to_string())]
)
),
None => bail!("{}", loc.t("cli.restore.err_failed")),
}
}
}
}
fn run_sandbox_setup(
paths: &Paths,
force: bool,
enable_python: bool,
loc: &Locale,
) -> anyhow::Result<()> {
let _instance = acquire_cli_guard(loc, loc.t("cli.guard.action.sandbox"))?;
let dir = paths.sandbox_dir();
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.with_context(|| loc.t("cli.ctx.build_runtime").to_string())?;
runtime.block_on(features::sandbox_setup::setup(
&dir,
&features::sandbox_setup::SetupOptions { force },
loc,
|msg| println!("{msg}"),
))?;
if enable_python {
enable_python_tool(paths, loc)?;
println!("{}", loc.t("cli.sandbox.python_enabled"));
}
Ok(())
}
fn run_setup(paths: &Paths, args: &cli::SetupArgs, loc: &Locale) -> anyhow::Result<ExitCode> {
let loc: &'static Locale = i18n::locale(loc.lang());
if args.is_empty() {
println!("{}", cli::render_help(Some(cli::HelpTopic::Setup), loc));
return Ok(ExitCode::from(2));
}
let _instance = acquire_cli_guard(loc, loc.t("cli.guard.action.setup"))?;
Ok(ExitCode::from(setup_under_guard(paths, args, loc)?))
}
fn setup_under_guard(
paths: &Paths,
args: &cli::SetupArgs,
loc: &'static Locale,
) -> anyhow::Result<u8> {
let (store, mut config) = open_config_for_cli_write(paths, loc, SETUP_CTX)?;
let before = config.clone();
let wrote = features::provision::apply_settings(&mut config, args, loc)?;
let mut run = SetupRun {
paths,
loc,
runtime: cli_runtime(loc)?,
config,
wrote,
failed: Vec::new(),
};
if args.sandbox {
run.sandbox();
}
if let Some(backend) = &args.llama {
run.llama(backend, args.llama_build.clone());
}
run.write(&store, &before)?;
if args.verify {
run.verify();
}
Ok(run.summary())
}
const SETUP_CTX: &str = "cli.ctx.setup";
struct SetupRun<'a> {
paths: &'a Paths,
loc: &'static Locale,
runtime: tokio::runtime::Runtime,
config: AppConfig,
wrote: Vec<String>,
failed: Vec<&'static str>,
}
impl SetupRun<'_> {
fn fail(&mut self, step: &'static str, err: &anyhow::Error) {
let name = self.loc.t(step);
eprintln!(
"{}",
self.loc.tf(
"setup.step.failed",
&[("step", name), ("reason", &format!("{err:#}"))]
)
);
self.failed.push(name);
}
fn sandbox(&mut self) {
println!("{}", self.loc.t("setup.step.sandbox.header"));
let done = self.runtime.block_on(features::sandbox_setup::setup(
&self.paths.sandbox_dir(),
&features::sandbox_setup::SetupOptions { force: false },
self.loc,
|msg| println!("{msg}"),
));
match done {
Ok(()) if !self.config.tools.python_enabled => {
self.config.tools.python_enabled = true;
self.wrote.push(self.loc.tf(
"setup.settings.line",
&[("key", "tools.python_enabled"), ("value", "true")],
));
}
Ok(()) => {}
Err(e) => self.fail("setup.step.sandbox", &e),
}
}
fn llama(&mut self, backend: &str, build: Option<String>) {
println!("{}", self.loc.t("setup.step.llama.header"));
let done = self.runtime.block_on(features::llama_setup::setup(
&self.paths.llama_dir(),
&features::llama_setup::SetupOptions {
backend: backend.to_string(),
build,
force: false,
cudart: true,
},
self.loc,
|msg| println!("{msg}"),
));
match done {
Ok(_) => {
let cleared = features::provision::clear_dead_binaries(&mut self.config);
let loc = self.loc;
self.wrote.extend(
cleared
.into_iter()
.map(|key| loc.tf("setup.settings.cleared", &[("key", key)])),
);
}
Err(e) => self.fail("setup.step.llama", &e),
}
}
fn write(&self, store: &JsonStore, before: &AppConfig) -> anyhow::Result<()> {
if self.wrote.is_empty() {
return Ok(());
}
println!("{}", self.loc.t("setup.settings.header"));
for line in &self.wrote {
println!("{line}");
}
if self.config == *before {
println!("{}", self.loc.t("setup.settings.unchanged"));
return Ok(());
}
store
.save_config(&self.config)
.with_context(|| self.loc.t(SETUP_CTX).to_string())
}
fn verify(&mut self) {
println!("{}", self.loc.t("setup.step.verify.header"));
let lookup = app::supervisor::BinaryLookup::from_paths(self.paths);
let ok = self
.runtime
.block_on(app::verify::run(&self.config, &lookup, self.loc, |msg| {
println!("{msg}")
}));
if ok {
return;
}
self.failed.push(self.loc.t("setup.step.verify"));
println!(
"{}",
self.loc.tf(
"setup.verify.see_log",
&[("path", &self.paths.log_dir().display().to_string())]
)
);
}
fn summary(&self) -> u8 {
if self.failed.is_empty() {
println!("{}", self.loc.t("setup.summary.ok"));
return 0;
}
eprintln!(
"{}",
self.loc.tf(
"setup.summary.failed",
&[("steps", &self.failed.join(", "))]
)
);
1
}
}
fn cli_runtime(loc: &Locale) -> anyhow::Result<tokio::runtime::Runtime> {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.with_context(|| loc.t("cli.ctx.build_runtime").to_string())
}
fn run_llama_backends(paths: &Paths, build: Option<&str>, loc: &Locale) -> anyhow::Result<()> {
let runtime = cli_runtime(loc)?;
let listing = runtime.block_on(features::llama_setup::list_backends(build, loc))?;
for line in features::llama_setup::render_backends(&listing, &paths.llama_dir(), loc) {
println!("{line}");
}
Ok(())
}
fn run_llama_setup(
paths: &Paths,
backend: Option<String>,
build: Option<String>,
cudart: bool,
force: bool,
set_binary: bool,
loc: &Locale,
) -> anyhow::Result<ExitCode> {
let root = paths.llama_dir();
let runtime = cli_runtime(loc)?;
let Some(backend) = backend else {
let listing =
runtime.block_on(features::llama_setup::list_backends(build.as_deref(), loc))?;
for line in features::llama_setup::render_backends(&listing, &root, loc) {
println!("{line}");
}
eprintln!("{}", loc.t("cli.llama.pick_backend"));
return Ok(ExitCode::from(2));
};
let _instance = acquire_cli_guard(loc, loc.t("cli.guard.action.llama"))?;
let installed = runtime.block_on(features::llama_setup::setup(
&root,
&features::llama_setup::SetupOptions {
backend,
build,
force,
cudart,
},
loc,
|msg| println!("{msg}"),
))?;
if set_binary {
for line in set_engine_binary(paths, &installed.binary, loc)? {
println!("{line}");
}
} else {
println!(
"{}",
loc.tf(
"cli.llama.binary_hint",
&[("path", &installed.binary.display().to_string())],
)
);
}
Ok(ExitCode::SUCCESS)
}
fn run_llama_remove(paths: &Paths, id: &str, force: bool, loc: &Locale) -> anyhow::Result<()> {
use crate::features::llama_setup as llama;
let root = paths.llama_dir();
let _instance = acquire_cli_guard(loc, loc.t("cli.guard.action.llama_remove"))?;
let all = llama::installed(&root);
let Some(install) = llama::find_install(&root, id) else {
let names: Vec<String> = all
.iter()
.map(|i| llama::install_name(&i.backend, &i.tag))
.collect();
if names.is_empty() {
bail!(
"{}",
loc.tf(
"llamacpp.remove.none",
&[("path", &root.display().to_string())]
)
);
}
let ambiguous: Vec<String> = all
.iter()
.filter(|i| i.backend == id)
.map(|i| llama::install_name(&i.backend, &i.tag))
.collect();
if ambiguous.len() > 1 {
bail!(
"{}",
loc.tf(
"llamacpp.remove.ambiguous",
&[("id", id), ("list", &ambiguous.join(", "))],
)
);
}
bail!(
"{}",
loc.tf(
"llamacpp.remove.unknown",
&[
("id", id),
("path", &root.display().to_string()),
("list", &names.join(", ")),
],
)
);
};
if !force {
refuse_a_build_in_use(paths, &install, loc)?;
}
println!(
"{}",
loc.tf(
"llamacpp.remove.removing",
&[
("path", &install.dir.display().to_string()),
("size", &(install.bytes >> 20).to_string()),
],
)
);
llama::remove_install(&install.dir, loc)?;
for line in llama::render_removed(&install, &root, paths.exe_dir(), loc) {
println!("{line}");
}
Ok(())
}
fn refuse_a_build_in_use(
paths: &Paths,
install: &features::llama_setup::Install,
loc: &Locale,
) -> anyhow::Result<()> {
use crate::features::llama_setup as llama;
let config = JsonStore::new(paths.clone()).load_config().map_err(|e| {
anyhow!(
"{}",
loc.tf(
"llamacpp.remove.settings_unreadable",
&[("err", &format!("{e:#}"))]
)
)
})?;
let uses = llama::binary_uses(&config, &install.dir);
if !uses.is_empty() {
bail!("{}", llama::render_in_use(&uses, install, loc));
}
Ok(())
}
fn set_engine_binary(paths: &Paths, binary: &Path, loc: &Locale) -> anyhow::Result<Vec<String>> {
let ctx = "cli.ctx.set_binary";
let (store, mut config) = open_config_for_cli_write(paths, loc, ctx)?;
let targets = features::llama_setup::set_engine_binary(&mut config, binary);
store
.save_config(&config)
.with_context(|| loc.t(ctx).to_string())?;
Ok(features::llama_setup::render_binary_targets(
&targets, binary, loc,
))
}
fn open_config_for_cli_write(
paths: &Paths,
loc: &Locale,
ctx_key: &str,
) -> anyhow::Result<(JsonStore, AppConfig)> {
features::data_migration::run(paths, loc)?;
let store = JsonStore::new(paths.clone());
let fresh = !paths.settings_file().exists();
let mut config = store
.load_config()
.with_context(|| loc.t(ctx_key).to_string())?;
if fresh {
config.interface.language = paths.default_language();
}
Ok((store, config))
}
fn run_llama_installed(paths: &Paths, loc: &Locale) {
let root = paths.llama_dir();
let found = features::llama_setup::installed(&root);
for line in features::llama_setup::render_installed(&found, &root, loc) {
println!("{line}");
}
}
fn enable_python_tool(paths: &Paths, loc: &Locale) -> anyhow::Result<()> {
let fresh = !paths.settings_file().exists();
let (store, mut config) = open_config_for_cli_write(paths, loc, "cli.ctx.enable_python")?;
if config.tools.python_enabled && !fresh {
return Ok(()); }
config.tools.python_enabled = true;
store
.save_config(&config)
.with_context(|| loc.t("cli.ctx.enable_python").to_string())
}
fn run_locales_export(code: &str, output: &Path, loc: &Locale) -> anyhow::Result<()> {
if output.exists() {
bail!(
"{}",
loc.tf(
"cli.locales.file_exists",
&[("path", &output.display().to_string())]
)
);
}
let lang = Lang::from_code(code);
let content = i18n::export_bundle(lang);
if let Some(parent) = output.parent().filter(|p| !p.as_os_str().is_empty()) {
std::fs::create_dir_all(parent).with_context(|| {
loc.tf(
"cli.ctx.create_dir",
&[("path", &parent.display().to_string())],
)
})?;
}
std::fs::write(output, content).with_context(|| {
loc.tf(
"cli.ctx.write_file",
&[("path", &output.display().to_string())],
)
})?;
println!(
"{}",
loc.tf(
"cli.locales.exported",
&[("code", code), ("path", &output.display().to_string())]
)
);
Ok(())
}
fn run_themes_export(paths: &Paths, name: &str, output: &Path, loc: &Locale) -> anyhow::Result<()> {
if output.exists() {
bail!(
"{}",
loc.tf(
"cli.themes.file_exists",
&[("path", &output.display().to_string())]
)
);
}
let content = features::themes::export(name, &paths.themes_dir(), loc)
.map_err(|e| anyhow::anyhow!("{e}"))?;
if let Some(parent) = output.parent().filter(|p| !p.as_os_str().is_empty()) {
std::fs::create_dir_all(parent).with_context(|| {
loc.tf(
"cli.ctx.create_dir",
&[("path", &parent.display().to_string())],
)
})?;
}
std::fs::write(output, content).with_context(|| {
loc.tf(
"cli.ctx.write_file",
&[("path", &output.display().to_string())],
)
})?;
println!(
"{}",
loc.tf(
"cli.themes.exported",
&[("name", name), ("path", &output.display().to_string())]
)
);
Ok(())
}
fn run_themes_check(paths: &Paths, target: Option<&str>, loc: &Locale) -> anyhow::Result<ExitCode> {
let checked = features::themes::check(target, &paths.themes_dir(), loc)
.map_err(|e| anyhow::anyhow!("{e}"))?;
let clean = checked.iter().all(|c| c.clean);
let texts: Vec<&str> = checked.iter().map(|c| c.text.as_str()).collect();
println!("{}", texts.join("\n\n"));
Ok(if clean {
ExitCode::SUCCESS
} else {
ExitCode::from(1)
})
}
fn run_import(paths: &Paths, file: &Path, loc: &Locale) -> anyhow::Result<()> {
features::data_migration::run(paths, loc)?;
let storage =
Storage::open(paths.clone()).with_context(|| loc.t("cli.ctx.open_storage").to_string())?;
let result = features::import::import_file(file, loc)
.with_context(|| loc.tf("cli.ctx.import", &[("file", &file.display().to_string())]))?;
for profile in &result.profiles {
storage.json().upsert_profile(profile)?;
}
for chat in &result.chats {
storage.json().save_chat(chat)?;
}
let mut config = storage
.json()
.load_config()
.with_context(|| loc.t("cli.ctx.load_settings").to_string())?;
if let Some(sampling) = result.sampling {
config.default_sampling = sampling;
}
if let Some(interface) = result.interface {
if let Some(v) = interface.spellcheck_enabled {
config.interface.spellcheck_enabled = v;
}
if let Some(v) = interface.dictionaries {
config.interface.selected_dictionaries = v;
}
if let Some(v) = interface.theme {
config.interface.theme = v;
}
}
storage.json().save_config(&config)?;
println!(
"{}",
loc.tf(
"cli.import.done",
&[
("profiles", &result.profiles.len().to_string()),
("chats", &result.chats.len().to_string()),
]
)
);
Ok(())
}
fn apply_env_overrides(config: &mut AppConfig) {
apply_engine_env(config);
apply_embed_env(config);
}
fn apply_engine_env(config: &mut AppConfig) {
if let Ok(url) = std::env::var("MINDFORK_ENGINE_URL") {
config.engine.mode = ServerMode::External;
config.engine.external.url = Some(url);
} else if let Ok(bin) = std::env::var("MINDFORK_LLAMA_BIN") {
config.engine.mode = ServerMode::Managed;
config.engine.managed.binary = Some(bin);
if let Ok(m) = std::env::var("MINDFORK_MODEL") {
config.engine.managed.model_path = Some(m);
}
if let Ok(p) = std::env::var("MINDFORK_MMPROJ") {
config.engine.managed.mmproj = Some(p);
}
if let Some(ngl) = std::env::var("MINDFORK_NGL")
.ok()
.and_then(|v| v.parse().ok())
{
config.engine.managed.gpu_layers = ngl;
}
if let Some(ctx) = std::env::var("MINDFORK_CTX")
.ok()
.and_then(|v| v.parse().ok())
{
config.engine.managed.context_size = ctx;
}
if let Some(port) = std::env::var("MINDFORK_PORT")
.ok()
.and_then(|p| p.parse().ok())
{
config.engine.managed.port = port;
}
}
}
fn apply_embed_env(config: &mut AppConfig) {
if let Ok(url) = std::env::var("MINDFORK_EMBED_URL") {
config.embed.mode = ServerMode::External;
config.embed.external.url = Some(url);
} else if let Ok(bin) = std::env::var("MINDFORK_EMBED_BIN") {
config.embed.mode = ServerMode::Managed;
config.embed.managed.binary = Some(bin);
if let Ok(m) = std::env::var("MINDFORK_EMBED_MODEL") {
config.embed.managed.model_path = Some(m);
}
if let Some(port) = std::env::var("MINDFORK_EMBED_PORT")
.ok()
.and_then(|p| p.parse().ok())
{
config.embed.managed.port = port;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enable_python_on_a_fresh_root_keeps_the_installer_language() {
let tmp = tempfile::tempdir().unwrap();
let paths = Paths::with_root(tmp.path()).with_default_language(Lang::En);
assert!(!paths.settings_file().exists());
enable_python_tool(&paths, i18n::locale(Lang::En)).unwrap();
let cfg = JsonStore::new(paths.clone()).load_config().unwrap();
assert!(cfg.tools.python_enabled, "the tool was not enabled");
assert_eq!(
cfg.interface.language,
Lang::En,
"the installer's language choice was lost by creating settings.json"
);
}
fn setup_args_for(model: &Path) -> cli::SetupArgs {
cli::SetupArgs {
model: Some(model.to_path_buf()),
ctx: Some(4096),
set: vec![("engine.managed.sessions".into(), "2".into())],
..Default::default()
}
}
#[test]
fn setup_writes_once_and_a_rerun_touches_nothing() {
let tmp = tempfile::tempdir().unwrap();
let paths = Paths::with_root(tmp.path()).with_default_language(Lang::En);
let model = tmp.path().join("chat.gguf");
std::fs::write(&model, b"GGUF").unwrap();
let loc = i18n::locale(Lang::En);
let args = setup_args_for(&model);
assert_eq!(setup_under_guard(&paths, &args, loc).unwrap(), 0);
let cfg = JsonStore::new(paths.clone()).load_config().unwrap();
assert_eq!(cfg.engine.mode, ServerMode::Managed);
assert_eq!(
cfg.engine.managed.model_path.as_deref(),
Some(model.to_str().unwrap())
);
assert_eq!(cfg.engine.managed.context_size, 4096);
assert_eq!(cfg.engine.managed.sessions, 2);
assert_eq!(cfg.engine.managed.binary, None, "no binary path is written");
assert_eq!(cfg.interface.language, Lang::En, "the language was seeded");
let written = std::fs::read(paths.settings_file()).unwrap();
let bak = paths.settings_file().with_extension("bak");
assert!(!bak.exists(), "a first write has nothing to back up");
assert_eq!(setup_under_guard(&paths, &args, loc).unwrap(), 0);
assert!(!bak.exists(), "the re-run wrote: it rotated a backup");
assert_eq!(std::fs::read(paths.settings_file()).unwrap(), written);
}
#[test]
fn setup_refuses_a_typo_before_anything_is_written() {
let tmp = tempfile::tempdir().unwrap();
let paths = Paths::with_root(tmp.path());
let loc = i18n::locale(Lang::En);
let args = setup_args_for(&tmp.path().join("nope.gguf"));
let err = setup_under_guard(&paths, &args, loc).unwrap_err();
assert!(err.to_string().contains("nope.gguf"), "{err}");
assert!(!paths.settings_file().exists());
let args = cli::SetupArgs {
set: vec![("engine.managed.modle_path".into(), "x".into())],
..Default::default()
};
let err = setup_under_guard(&paths, &args, loc).unwrap_err();
assert!(err.to_string().contains("modle_path"), "{err}");
assert!(!paths.settings_file().exists());
}
#[test]
fn setup_with_nothing_to_do_exits_2_and_writes_nothing() {
let tmp = tempfile::tempdir().unwrap();
let paths = Paths::with_root(tmp.path());
let code = run_setup(&paths, &cli::SetupArgs::default(), i18n::locale(Lang::En)).unwrap();
assert_eq!(format!("{code:?}"), format!("{:?}", ExitCode::from(2)));
assert!(!paths.settings_file().exists());
}
#[test]
fn setup_verify_skips_a_cloud_engine_and_fails_an_empty_managed_one() {
let loc = i18n::locale(Lang::En);
let cloud = tempfile::tempdir().unwrap();
let paths = Paths::with_root(cloud.path());
let args = cli::SetupArgs {
set: vec![
("engine.mode".into(), "claude".into()),
("embed.mode".into(), "openai".into()),
],
verify: true,
..Default::default()
};
assert_eq!(setup_under_guard(&paths, &args, loc).unwrap(), 0);
let empty = tempfile::tempdir().unwrap();
let paths = Paths::with_root(empty.path());
let args = cli::SetupArgs {
ctx: Some(2048),
verify: true,
..Default::default()
};
assert_eq!(setup_under_guard(&paths, &args, loc).unwrap(), 1);
let cfg = JsonStore::new(paths.clone()).load_config().unwrap();
assert_eq!(cfg.engine.managed.context_size, 2048);
}
#[test]
fn a_failed_setup_step_is_remembered_not_raised() {
let tmp = tempfile::tempdir().unwrap();
let paths = Paths::with_root(tmp.path());
let loc = i18n::locale(Lang::En);
let mut run = SetupRun {
paths: &paths,
loc,
runtime: cli_runtime(loc).unwrap(),
config: AppConfig::default(),
wrote: Vec::new(),
failed: Vec::new(),
};
assert_eq!(run.summary(), 0);
run.fail("setup.step.sandbox", &anyhow!("the network went away"));
assert_eq!(run.failed, [loc.t("setup.step.sandbox")]);
assert_eq!(run.summary(), 1);
run.write(&JsonStore::new(paths.clone()), &AppConfig::default())
.unwrap();
assert!(!paths.settings_file().exists());
}
#[test]
fn enable_python_preserves_an_existing_config() {
let tmp = tempfile::tempdir().unwrap();
let paths = Paths::with_root(tmp.path());
let store = JsonStore::new(paths.clone());
let mut cfg = AppConfig::default();
cfg.interface.language = Lang::En;
cfg.max_tool_rounds = 7;
store.save_config(&cfg).unwrap();
enable_python_tool(&paths, i18n::locale(Lang::Ru)).unwrap();
let after = store.load_config().unwrap();
assert!(after.tools.python_enabled);
assert_eq!(after.max_tool_rounds, 7, "an unrelated setting was reset");
assert_eq!(
after.interface.language,
Lang::En,
"an existing language was overwritten by the defaults.json seeding"
);
}
#[test]
fn enable_python_is_a_no_op_when_already_enabled() {
let tmp = tempfile::tempdir().unwrap();
let paths = Paths::with_root(tmp.path());
let minimal = format!(
r#"{{"schema_version":{},"tools":{{"python_enabled":true}}}}"#,
crate::shared::config::SCHEMA_VERSION
)
.into_bytes();
std::fs::write(paths.settings_file(), &minimal).unwrap();
enable_python_tool(&paths, i18n::locale(Lang::Ru)).unwrap();
assert_eq!(
std::fs::read(paths.settings_file()).unwrap(),
minimal,
"settings.json was rewritten (and a hand-edited file expanded) for nothing"
);
}
#[test]
fn enable_python_refuses_a_corrupt_config_without_touching_it() {
let tmp = tempfile::tempdir().unwrap();
let paths = Paths::with_root(tmp.path());
std::fs::write(paths.settings_file(), b"{ this is not json").unwrap();
assert!(enable_python_tool(&paths, i18n::locale(Lang::Ru)).is_err());
assert_eq!(
std::fs::read(paths.settings_file()).unwrap(),
b"{ this is not json",
"a corrupt config was overwritten instead of being reported"
);
}
#[test]
fn enable_python_refuses_a_config_from_a_newer_version() {
let tmp = tempfile::tempdir().unwrap();
let paths = Paths::with_root(tmp.path());
let from_the_future = format!(
r#"{{"schema_version":{},"tools":{{"python_enabled":false}},"a_setting_we_do_not_know":42}}"#,
crate::shared::storage::schema::SETTINGS_SCHEMA + 1
);
std::fs::write(paths.settings_file(), &from_the_future).unwrap();
assert!(enable_python_tool(&paths, i18n::locale(Lang::Ru)).is_err());
assert_eq!(
std::fs::read_to_string(paths.settings_file()).unwrap(),
from_the_future,
"a newer config was rewritten, dropping the fields this build cannot see"
);
}
fn settings_with_a_typo() -> String {
format!(
r#"{{
"schema_version": {},
"max_tool_rounds": 3,
"engine": {{ "mode": "external", "external": {{ "url": "http://127.0.0.1:8000/v1" }} }},
"interface": {{ "theme": "drak", "language": "en", "input_max_rows": 12 }},
"tools": {{ "python_enabled": true }}
}}"#,
crate::shared::config::SCHEMA_VERSION
)
}
fn root_with_a_typo() -> (tempfile::TempDir, Paths, String) {
let tmp = tempfile::tempdir().unwrap();
let paths = Paths::with_root(tmp.path());
let original = settings_with_a_typo();
std::fs::write(paths.settings_file(), &original).unwrap();
(tmp, paths, original)
}
fn assert_settings_untouched(paths: &Paths, original: &str) {
assert_eq!(
std::fs::read_to_string(paths.settings_file()).unwrap(),
original
);
assert!(!paths.settings_file().with_extension("bak").exists());
}
#[test]
fn one_unreadable_value_refuses_the_start_and_costs_nothing() {
let (_tmp, paths, original) = root_with_a_typo();
let loc = i18n::locale(Lang::En);
let gate = features::data_migration::run(&paths, loc).unwrap_err();
assert!(
gate.to_string().contains("unknown variant `drak`"),
"{gate}"
);
let store = JsonStore::new(paths.clone());
let load = startup_config(&paths, &store, false, loc).unwrap_err();
let said = format!("{load:#}");
assert!(said.contains("reading the settings"), "{said}");
assert!(said.contains("unknown variant `drak`"), "{said}");
assert_settings_untouched(&paths, &original);
}
#[test]
fn the_same_settings_without_the_typo_start_with_the_users_values() {
let (_tmp, paths, original) = root_with_a_typo();
let corrected = original.replace("drak", "dark");
std::fs::write(paths.settings_file(), &corrected).unwrap();
let loc = i18n::locale(Lang::En);
features::data_migration::run(&paths, loc).unwrap();
let store = JsonStore::new(paths.clone());
let config = startup_config(&paths, &store, false, loc).unwrap();
assert_eq!(config.max_tool_rounds, 3);
assert_eq!(config.engine.mode, ServerMode::External);
assert_eq!(
config.engine.external.url.as_deref(),
Some("http://127.0.0.1:8000/v1")
);
assert_eq!(config.interface.theme, crate::shared::config::Theme::Dark);
assert_eq!(config.interface.language, Lang::En);
assert_eq!(config.interface.input_max_rows, 12);
assert!(config.tools.python_enabled);
assert_settings_untouched(&paths, &corrected);
}
#[test]
fn a_fresh_root_starts_on_the_defaults_in_the_installers_language() {
let tmp = tempfile::tempdir().unwrap();
let paths = Paths::with_root(tmp.path()).with_default_language(Lang::En);
let store = JsonStore::new(paths.clone());
let config = startup_config(&paths, &store, false, i18n::locale(Lang::En)).unwrap();
let mut expected = AppConfig::default();
expected.interface.language = Lang::En;
assert_eq!(config, expected);
assert!(!paths.settings_file().exists());
}
#[test]
fn the_cli_language_survives_a_value_the_config_cannot_read() {
let (_tmp, paths, _) = root_with_a_typo();
let file = paths.settings_file();
assert_eq!(try_settings_language(&file), Some(Lang::En));
let cases: [(&str, Option<Lang>); 6] = [
(r#"{"max_tool_rounds": 3}"#, Some(Lang::Ru)),
(r#"{"interface": {"theme": "dark"}}"#, Some(Lang::Ru)),
(r#"{"interface": {"language": "ru"}}"#, Some(Lang::Ru)),
(r#"{"interface": {"language": 7}}"#, None),
(r#"{"interface": "en"}"#, None),
("{ this is not json", None),
];
for (text, expected) in cases {
std::fs::write(&file, text).unwrap();
assert_eq!(try_settings_language(&file), expected, "{text}");
}
std::fs::remove_file(&file).unwrap();
assert_eq!(try_settings_language(&file), None);
}
#[test]
fn import_refuses_settings_it_cannot_read_and_writes_nothing() {
let (tmp, paths, original) = root_with_a_typo();
let err = run_import(
&paths,
&tmp.path().join("never-read.json"),
i18n::locale(Lang::En),
)
.unwrap_err();
assert!(err.to_string().contains("unknown variant `drak`"), "{err}");
assert_settings_untouched(&paths, &original);
assert!(!paths.profiles_file().exists());
}
fn a_build_in(dir: &Path) -> features::llama_setup::Install {
features::llama_setup::Install {
backend: "cpu".into(),
tag: "b1".into(),
dir: dir.to_path_buf(),
bytes: 0,
binary_ok: true,
}
}
#[test]
fn llama_remove_refuses_when_the_settings_cannot_be_asked() {
let (tmp, paths, original) = root_with_a_typo();
let install = a_build_in(&tmp.path().join("llama").join("cpu-b1"));
let err = refuse_a_build_in_use(&paths, &install, i18n::locale(Lang::En)).unwrap_err();
let said = err.to_string();
assert!(said.contains("unknown variant `drak`"), "{said}");
assert!(said.contains("--force"), "{said}");
assert_settings_untouched(&paths, &original);
}
#[test]
fn llama_remove_refuses_a_build_the_settings_name() {
let tmp = tempfile::tempdir().unwrap();
let paths = Paths::with_root(tmp.path());
let loc = i18n::locale(Lang::En);
let named = tmp.path().join("llama").join("cpu-b1");
let config = AppConfig {
engine: crate::shared::config::EngineSettings {
managed: crate::shared::config::ManagedSettings {
binary: Some(named.join("llama-server").to_string_lossy().into_owned()),
..Default::default()
},
..Default::default()
},
..Default::default()
};
JsonStore::new(paths.clone()).save_config(&config).unwrap();
let err = refuse_a_build_in_use(&paths, &a_build_in(&named), loc).unwrap_err();
assert!(err.to_string().contains("cpu-b1"), "{err}");
let other = tmp.path().join("llama").join("cpu-b2");
refuse_a_build_in_use(&paths, &a_build_in(&other), loc).unwrap();
}
#[test]
fn a_backup_command_runs_on_settings_that_are_not_json() {
let tmp = tempfile::tempdir().unwrap();
let paths = Paths::with_root(tmp.path());
std::fs::write(paths.settings_file(), b"{ this is not json").unwrap();
assert_eq!(
backup_config(&paths, i18n::locale(Lang::En)),
backup::BackupSettings::default()
);
}
#[test]
fn stats_is_answered_before_the_data_root_is_created() {
let tmp = tempfile::tempdir().unwrap();
let paths = Paths::with_root(tmp.path().join("never-created"));
let command = CliCommand::Stats {
archive: None,
compare: None,
password: None,
json: true,
};
real_main(command, &paths, i18n::locale(Lang::En), &[]).unwrap();
assert!(!paths.root().exists());
assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
}
#[test]
fn only_an_untyped_first_attempt_blames_the_stored_password() {
use crate::features::backup::ArchivePassword::{Required, Wrong};
assert!(stored_password_failed(true, 0, Wrong));
assert!(
!stored_password_failed(false, 0, Wrong),
"--password was given"
);
assert!(
!stored_password_failed(true, 1, Wrong),
"typed at the prompt"
);
assert!(
!stored_password_failed(true, 0, Required),
"nothing was tried"
);
}
#[test]
fn effective_password_prefers_the_argument_then_the_setting() {
let arg = || Some("from-arg".to_string());
let stored = || Some("from-settings".to_string());
assert_eq!(
effective_password(arg(), stored()).as_deref(),
Some("from-arg")
);
assert_eq!(
effective_password(None, stored()).as_deref(),
Some("from-settings")
);
assert_eq!(effective_password(arg(), None).as_deref(), Some("from-arg"));
assert_eq!(effective_password(None, None), None);
assert_eq!(effective_password(Some(String::new()), None), None);
assert_eq!(effective_password(None, Some(String::new())), None);
assert_eq!(effective_password(Some(String::new()), stored()), None);
}
#[test]
fn only_a_full_screen_launch_without_a_terminal_is_refused() {
for command in [
CliCommand::Run,
CliCommand::Demo,
CliCommand::Keys { output: None },
] {
assert!(refuses_tui_launch(&command, false), "{command:?}");
assert!(!refuses_tui_launch(&command, true), "{command:?}");
}
for command in [
CliCommand::LlamaInstalled,
CliCommand::Import {
file: PathBuf::from("chats.json"),
},
] {
assert!(!refuses_tui_launch(&command, false), "{command:?}");
}
}
#[test]
fn cli_lang_prefers_settings_then_resolved_default() {
assert_eq!(cli_lang(Some(Lang::Ru), Lang::En), Lang::Ru);
assert_eq!(cli_lang(Some(Lang::En), Lang::Ru), Lang::En);
assert_eq!(cli_lang(None, Lang::Ru), Lang::Ru);
assert_eq!(cli_lang(None, Lang::En), Lang::En);
}
#[test]
fn cli_error_line_is_localized_single_line() {
let err = anyhow!("outer").context("wrapper");
let en = cli_error_line(i18n::locale(Lang::En), &err);
assert!(en.starts_with("Error: "), "{en}");
assert!(en.contains("wrapper") && en.contains("outer"), "{en}");
assert!(!en.contains('\n'), "{en}");
let ru = cli_error_line(i18n::locale(Lang::Ru), &err);
assert!(ru.starts_with("Ошибка: "), "{ru}");
}
}