#![forbid(unsafe_code)]
mod bootstrap;
mod commands;
mod config;
mod digest;
mod driver;
mod evidence;
mod export;
mod memory_tool;
mod projects;
mod render;
mod repl;
mod settings;
#[cfg(feature = "tui")]
mod tui_hook;
use std::path::PathBuf;
use anyhow::Result;
use clap::{CommandFactory, Parser, Subcommand};
use niuse_core::Store;
#[cfg(feature = "web")]
use serde_json::json;
#[derive(Parser, Debug)]
#[command(
name = "niuse",
version,
about = "A personal engineering companion built on niucode"
)]
struct Cli {
#[arg(long, global = true)]
preset: Option<String>,
#[arg(long, global = true)]
model: Option<String>,
#[arg(long, global = true)]
no_thinking: bool,
#[arg(long, global = true)]
cwd: Option<PathBuf>,
#[arg(long, global = true)]
sandbox: Option<String>,
#[arg(long, global = true)]
approval: Option<String>,
#[arg(long, global = true)]
network: Option<String>,
#[arg(long, global = true)]
home: Option<PathBuf>,
#[arg(long, global = true)]
thread: Option<PathBuf>,
#[arg(long, global = true)]
max_steps: Option<usize>,
#[arg(long, global = true)]
plan: bool,
#[arg(long, global = true)]
allow_mcp_write: bool,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand, Debug, Clone)]
enum McpCommand {
Init {
#[arg(long)]
notes: Option<PathBuf>,
#[arg(long)]
calendar: Option<PathBuf>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
packs: Option<PathBuf>,
#[arg(long)]
force: bool,
},
}
#[derive(Subcommand, Debug, Clone)]
enum PlanCommand {
List {
#[arg(long)]
all: bool,
#[arg(long)]
json: bool,
},
Add { text: String },
Done {
id: u32,
#[arg(long)]
thread: Option<PathBuf>,
},
}
#[derive(Subcommand, Debug, Clone)]
enum Command {
Exec { prompt: String },
Mem {
#[command(subcommand)]
command: MemCommand,
},
Report {
#[arg(long, default_value_t = 7)]
days: u64,
#[arg(long)]
dogfood: Option<PathBuf>,
#[arg(long)]
json: bool,
},
Dogfood {
#[arg(long)]
file: Option<PathBuf>,
#[arg(long)]
note: Option<String>,
#[arg(long)]
json: bool,
},
Loops {
#[command(subcommand)]
command: LoopsCommand,
},
Goals {
#[command(subcommand)]
command: GoalsCommand,
},
Export {
#[arg(long)]
out: Option<PathBuf>,
#[arg(long)]
session: Option<PathBuf>,
},
Threads {
#[arg(long)]
json: bool,
},
#[cfg(feature = "web")]
Web {
#[arg(long, default_value_t = 8788)]
port: u16,
},
#[cfg(feature = "tui")]
Tui {
#[arg(long)]
replay: Option<PathBuf>,
},
Completions {
shell: clap_complete::Shell,
},
Sessions {
#[arg(long)]
json: bool,
},
Journal {
#[arg(long, default_value_t = 7)]
days: usize,
#[arg(long)]
search: Option<String>,
#[arg(long)]
add: Option<String>,
#[arg(long)]
json: bool,
},
Identity {
#[command(subcommand)]
command: IdentityCommand,
},
Bootstrap {
#[arg(long, default_value_t = 40)]
limit: usize,
#[arg(long, default_value_t = 90)]
since_days: i64,
#[arg(long)]
dry_run: bool,
#[arg(long)]
profile: bool,
#[arg(long)]
force_profile: bool,
#[arg(long)]
profile_only: bool,
},
Review,
Digest {
#[arg(long)]
thread: Option<PathBuf>,
#[arg(long)]
dry_run: bool,
#[arg(long, default_value_t = 40)]
limit: usize,
},
Doctor {
#[arg(long)]
json: bool,
},
User {
#[command(subcommand)]
command: UserCommand,
},
Config {
#[command(subcommand)]
command: ConfigCommand,
},
Stats {
#[arg(long, default_value_t = 7)]
days: u64,
#[arg(long)]
trend: bool,
#[arg(long)]
json: bool,
},
Mcp {
#[command(subcommand)]
command: McpCommand,
},
Plan {
#[command(subcommand)]
command: PlanCommand,
},
Verify {
#[arg(long)]
thread: Option<PathBuf>,
#[arg(long)]
json: bool,
#[arg(long)]
step: Option<String>,
#[arg(last = true, required = true)]
command: Vec<String>,
},
Evidence {
#[arg(long)]
thread: Option<PathBuf>,
#[arg(long)]
json: bool,
},
Brief {
#[arg(long)]
json: bool,
},
Recall {
query: String,
#[arg(long, default_value_t = 20)]
limit: usize,
#[arg(long)]
json: bool,
},
Projects {
#[arg(long, default_value_t = 10)]
limit: usize,
#[arg(long)]
all: bool,
#[arg(long)]
json: bool,
},
Status {
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand, Debug, Clone)]
enum MemCommand {
List {
#[arg(long)]
all: bool,
#[arg(long)]
json: bool,
#[arg(long)]
kind: Option<String>,
#[arg(long)]
tag: Option<String>,
},
Add {
text: String,
#[arg(long, default_value = "preference")]
kind: String,
#[arg(long, value_delimiter = ',')]
tags: Vec<String>,
#[arg(long)]
force: bool,
},
Search {
query: String,
#[arg(long)]
fresh: bool,
#[arg(long)]
json: bool,
#[arg(long)]
kind: Option<String>,
#[arg(long)]
tag: Option<String>,
},
Show {
id: String,
#[arg(long)]
json: bool,
},
Edit { id: String },
Forget { id: String },
Confirm { id: String },
Review,
Prune {
#[arg(long)]
apply: bool,
},
}
#[derive(Subcommand, Debug, Clone)]
enum GoalsCommand {
List {
#[arg(long)]
all: bool,
#[arg(long)]
json: bool,
},
Add { text: String },
Done { id: String },
}
#[derive(Subcommand, Debug, Clone)]
enum LoopsCommand {
List {
#[arg(long)]
all: bool,
#[arg(long)]
json: bool,
#[arg(long)]
overdue: bool,
},
Add {
text: String,
#[arg(long)]
due: Option<String>,
},
Done { id: String },
}
#[derive(Subcommand, Debug, Clone)]
enum UserCommand {
Show {
#[arg(long)]
json: bool,
},
Edit,
}
#[derive(Subcommand, Debug, Clone)]
enum ConfigCommand {
Init {
#[arg(long)]
force: bool,
},
Show,
Effective {
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand, Debug, Clone)]
enum IdentityCommand {
Init {
#[arg(long)]
force: bool,
},
Show {
#[arg(long)]
json: bool,
},
Edit,
}
fn install_pipe_guard() {
let default = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let broken = info
.payload()
.downcast_ref::<&str>()
.is_some_and(|message| message.contains("Broken pipe"))
|| info
.payload()
.downcast_ref::<String>()
.is_some_and(|message| message.contains("Broken pipe"));
if broken {
std::process::exit(0);
}
default(info);
}));
}
#[tokio::main]
async fn main() -> Result<()> {
install_pipe_guard();
let cli = Cli::parse();
let command = cli.command.clone();
if let Some(Command::Completions { shell }) = &command {
let mut command = Cli::command();
clap_complete::generate(*shell, &mut command, "niuse", &mut std::io::stdout());
return Ok(());
}
let store = match &cli.home {
Some(path) => Store::at(path.clone()),
None => Store::discover()?,
};
store.init()?;
match command {
#[cfg(feature = "web")]
Some(Command::Web { port }) => run_web(&cli, &store, port).await,
#[cfg(feature = "tui")]
Some(Command::Tui { replay }) => run_tui(&cli, &store, replay).await,
Some(Command::Completions { .. }) => Ok(()),
None => {
let settings = settings::resolve(&cli, &store)?;
let app = repl::App::new(&cli, &settings, store)?;
app.run().await
}
Some(Command::Exec { prompt }) => {
let settings = settings::resolve(&cli, &store)?;
let app = repl::App::new(&cli, &settings, store)?;
repl::run_exec(app, prompt).await
}
Some(Command::Mem { command }) => match command {
MemCommand::List {
all,
json,
kind,
tag,
} => commands::mem_list(&store, all, json, kind.as_deref(), tag.as_deref()),
MemCommand::Add {
text,
kind,
tags,
force,
} => commands::mem_add(&store, &text, &kind, tags, force),
MemCommand::Search {
query,
fresh,
json,
kind,
tag,
} => commands::mem_search(&store, &query, fresh, json, kind.as_deref(), tag.as_deref()),
MemCommand::Show { id, json } => commands::mem_show(&store, &id, json),
MemCommand::Edit { id } => commands::mem_edit(&store, &id),
MemCommand::Forget { id } => commands::mem_forget(&store, &id),
MemCommand::Confirm { id } => commands::mem_confirm(&store, &id),
MemCommand::Review => commands::mem_review(&store),
MemCommand::Prune { apply } => commands::mem_prune(&store, apply),
},
Some(Command::Report {
days,
dogfood,
json,
}) => {
let dogfood = dogfood.or_else(|| {
let path = PathBuf::from("docs/dogfood.md");
path.is_file().then_some(path)
});
commands::report(&store, days, dogfood, json)
}
Some(Command::Dogfood { file, note, json }) => commands::dogfood(&store, file, note, json),
Some(Command::Loops { command }) => match command {
LoopsCommand::List { all, json, overdue } => {
commands::loops_list(&store, all, json, overdue)
}
LoopsCommand::Add { text, due } => commands::loops_add(&store, &text, due.as_deref()),
LoopsCommand::Done { id } => commands::loops_done(&store, &id),
},
Some(Command::Goals { command }) => match command {
GoalsCommand::List { all, json } => commands::goals_list(&store, all, json),
GoalsCommand::Add { text } => commands::goals_add(&store, &text),
GoalsCommand::Done { id } => commands::goals_done(&store, &id),
},
Some(Command::Sessions { json }) => commands::sessions(&store, json),
Some(Command::Threads { json }) => {
let active = config::resolve_thread(cli.thread.clone(), &store)?;
commands::threads(&store, &active, json)
}
Some(Command::Export { out, session }) => {
commands::export(&store, out.as_deref(), session.as_deref())
}
Some(Command::Journal {
days,
search,
add,
json,
}) => {
if let Some(text) = add {
commands::journal_add(&store, &text)
} else if let Some(query) = search {
commands::journal_search(&store, &query, json)
} else {
commands::journal_show(&store, days, json)
}
}
Some(Command::Identity { command }) => match command {
IdentityCommand::Init { force } => commands::identity_init(&store, force),
IdentityCommand::Show { json } => commands::identity_show(&store, json),
IdentityCommand::Edit => commands::identity_edit(&store),
},
Some(Command::Bootstrap {
limit,
since_days,
dry_run,
profile,
force_profile,
profile_only,
}) => {
let settings = settings::resolve(&cli, &store)?;
let app = repl::App::new(&cli, &settings, store)?;
bootstrap::run(
&app.store,
app.provider.as_ref(),
&app.model,
bootstrap::BootstrapOptions {
limit,
since_days,
dry_run,
profile,
force_profile,
profile_only,
},
)
.await
}
Some(Command::Review) => bootstrap::review(&store),
Some(Command::Digest {
thread,
dry_run,
limit,
}) => {
let settings = settings::resolve(&cli, &store)?;
let thread = config::resolve_thread(thread.or(cli.thread.clone()), &store)?;
let cwd = config::resolve_cwd(cli.cwd.clone())?;
let project = cwd
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| "unknown".to_string());
let app = repl::App::new(&cli, &settings, store)?;
digest::run(
&app.store,
app.provider.as_ref(),
&app.model,
&thread,
&project,
digest::DigestOptions { dry_run, limit },
)
.await
}
Some(Command::Doctor { json }) => {
let settings = settings::resolve(&cli, &store)?;
commands::doctor(&store, &settings.provider, json)
}
Some(Command::User { command }) => match command {
UserCommand::Show { json } => commands::user_show(&store, json),
UserCommand::Edit => commands::user_edit(&store),
},
Some(Command::Config { command }) => match command {
ConfigCommand::Init { force } => {
if settings::init(&store, force)? {
println!("wrote {}", store.config_path().display());
} else {
println!(
"{} already exists; pass --force to overwrite",
store.config_path().display()
);
}
Ok(())
}
ConfigCommand::Show => settings::show(&store),
ConfigCommand::Effective { json } => {
let settings = settings::resolve(&cli, &store)?;
if json {
let payload = serde_json::json!({
"provider": settings.provider,
"model": settings.model,
"sandbox": settings.sandbox,
"approval": settings.approval,
"network": settings.network,
"max_steps": settings.max_steps,
});
println!("{}", serde_json::to_string_pretty(&payload)?);
return Ok(());
}
println!("provider {}", settings.provider);
println!(
"model {}",
settings.model.as_deref().unwrap_or("(provider default)")
);
println!("sandbox {}", settings.sandbox);
println!("approval {}", settings.approval);
println!("network {}", settings.network);
println!(
"max_steps {}",
if settings.max_steps == 0 {
"unlimited".to_string()
} else {
settings.max_steps.to_string()
}
);
Ok(())
}
},
Some(Command::Stats { days, trend, json }) => commands::stats(&store, days, trend, json),
Some(Command::Mcp { command }) => match command {
McpCommand::Init {
notes,
calendar,
config,
packs,
force,
} => commands::mcp_init(
&store,
commands::McpInitOptions {
notes,
calendar,
config,
packs,
force,
},
),
},
Some(Command::Plan { command }) => match command {
PlanCommand::List { all, json } => commands::plan_list(&store, all, json),
PlanCommand::Add { text } => {
let step = commands::plan_add(&store, &text)?;
println!("added {}", step.label());
Ok(())
}
PlanCommand::Done { id, thread } => {
let thread = config::resolve_thread(thread.or(cli.thread.clone()), &store)?;
let step = commands::plan_done(&store, &thread, id)?;
println!("done {}", step.label());
Ok(())
}
},
Some(Command::Verify {
thread,
json,
step,
command,
}) => {
let thread = config::resolve_thread(thread.or(cli.thread.clone()), &store)?;
let outcome = commands::verify_with_step(&thread, &command, step.as_deref(), json)?;
std::process::exit(outcome.code);
}
Some(Command::Evidence { thread, json }) => {
let thread = config::resolve_thread(thread.or(cli.thread.clone()), &store)?;
commands::evidence(&thread, &store, json)
}
Some(Command::Brief { json }) => {
let cwd = config::resolve_cwd(cli.cwd.clone())?;
commands::brief(&store, &cwd, json)
}
Some(Command::Recall { query, limit, json }) => {
commands::recall(&store, &query, limit, json)
}
Some(Command::Projects { limit, all, json }) => commands::projects(limit, all, json),
Some(Command::Status { json }) => {
let thread = config::resolve_thread(cli.thread.clone(), &store)?;
let cwd = config::resolve_cwd(cli.cwd.clone())?;
commands::status(&store, &thread, &cwd, json)
}
}
}
#[cfg(feature = "web")]
async fn run_web(cli: &Cli, store: &Store, port: u16) -> Result<()> {
let settings = settings::resolve(cli, store)?;
let cwd = config::resolve_cwd(cli.cwd.clone())?;
let model = settings
.model
.clone()
.unwrap_or_else(|| "deepseek-flash".to_string());
let identity = store.identity()?;
let user = store.user()?;
let memories = store.memories()?;
let goals = store.goals()?;
let plan = store.plan()?;
let loops = store.loops()?;
let journal = store.recent_journal(7)?;
let composition = niuse_core::prompt::compose_with_plan(
identity.as_deref(),
user.as_deref(),
&goals,
&plan,
&memories,
&loops,
&journal,
niuse_core::date::Date::today(),
);
let context = web_context(store)?;
niucode::web::serve(niucode::web::Options {
session: store.session_path(),
port,
preset: settings.provider.clone(),
model: Some(model.clone()),
cwd: cwd.clone(),
max_steps: settings.max_steps,
system_prompt: Some(composition.system),
brand: Some("niuse".into()),
context: Some(context),
context_provider: Some(std::sync::Arc::new({
let context_store = store.clone();
move || web_context(&context_store).unwrap_or_else(|_| json!({"brand": "niuse"}))
})),
allow_new: true,
switch_session_provider: Some(std::sync::Arc::new({
let store = store.clone();
let cwd = cwd.clone();
let model = model.clone();
let provider = settings.provider.clone();
move |id| web_switch_thread(&store, id, &cwd, &model, &provider)
})),
new_session_provider: Some(std::sync::Arc::new({
let store = store.clone();
let provider = settings.provider.clone();
let model = model.clone();
let cwd = cwd.clone();
move || web_new_thread(&store, &cwd, &model, &provider)
})),
})
.await
}
#[cfg(feature = "web")]
fn web_new_thread(
store: &Store,
cwd: &std::path::Path,
model: &str,
provider: &str,
) -> Result<PathBuf> {
let active = store.session_path();
driver::archive_thread(&active, &store.threads_dir())?;
let thread = driver::open_thread(
&active,
&driver::SessionMeta {
cwd,
model,
provider,
policy: niucode_tools::SandboxPolicy::new(
niucode_tools::SandboxMode::WorkspaceWrite,
niucode_tools::NetworkMode::Off,
),
approval: niucode_agent::ApprovalMode::Ask,
},
)?;
Ok(thread.path.clone())
}
#[cfg(feature = "web")]
fn web_switch_thread(
store: &Store,
id: &str,
cwd: &std::path::Path,
model: &str,
provider: &str,
) -> Result<PathBuf> {
if id.is_empty()
|| !id
.chars()
.all(|character| character.is_ascii_alphanumeric() || matches!(character, '-' | '_'))
{
anyhow::bail!("invalid thread id");
}
let selected = store.threads_dir().join(format!("{id}.jsonl"));
if !selected.exists() {
anyhow::bail!("thread not found");
}
let active = store.session_path();
driver::archive_thread(&active, &store.threads_dir())?;
std::fs::rename(&selected, &active)
.map_err(|error| anyhow::anyhow!("switch {}: {error}", selected.display()))?;
let thread = driver::open_thread(
&active,
&driver::SessionMeta {
cwd,
model,
provider,
policy: niucode_tools::SandboxPolicy::new(
niucode_tools::SandboxMode::WorkspaceWrite,
niucode_tools::NetworkMode::Off,
),
approval: niucode_agent::ApprovalMode::Ask,
},
)?;
Ok(thread.path.clone())
}
#[cfg(feature = "web")]
fn web_context(store: &Store) -> Result<serde_json::Value> {
let identity = store.identity()?;
let user = store.user()?;
let memories = store.memories()?;
let goals = store.goals()?;
let plan = store.plan()?;
let loops = store.loops()?;
let mut sections = Vec::new();
sections.push(json!({
"title": "identity",
"lines": identity
.as_deref()
.map(|text| text.lines().take(10).collect::<Vec<_>>())
.unwrap_or_else(|| vec!["not set"]),
}));
sections.push(json!({
"title": "user",
"lines": user
.as_deref()
.map(|text| text.lines().take(10).collect::<Vec<_>>())
.unwrap_or_else(|| vec!["not set"]),
}));
sections.push(json!({
"title": "goals",
"lines": goals
.iter()
.map(|goal| format!("{} · {}", goal.id, goal.text))
.collect::<Vec<_>>(),
}));
sections.push(json!({
"title": "plan",
"lines": plan
.iter()
.map(|step| format!("{} {} · {}", if step.done { "x" } else { " " }, step.id, step.text))
.collect::<Vec<_>>(),
}));
sections.push(json!({
"title": "memory",
"lines": memories
.iter()
.take(8)
.map(|memory| format!("{} · {}", memory.id, memory.text))
.collect::<Vec<_>>(),
}));
sections.push(json!({
"title": "loops",
"lines": loops
.iter()
.filter(|item| !item.done)
.take(8)
.map(|item| format!("{} · {}", item.id, item.text))
.collect::<Vec<_>>(),
}));
let active = store.session_path();
sections.push(json!({
"title": "thread",
"lines": [format!(
"active · {}",
active
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("thread.jsonl")
)],
}));
let mut threads: Vec<(std::time::SystemTime, String)> = std::fs::read_dir(store.threads_dir())
.map(|entries| {
entries
.filter_map(Result::ok)
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "jsonl"))
.filter_map(|entry| {
let modified = entry.metadata().ok()?.modified().ok()?;
let name = entry.path().file_stem()?.to_string_lossy().to_string();
Some((modified, name))
})
.collect()
})
.unwrap_or_default();
threads.sort_by_key(|right| std::cmp::Reverse(right.0));
sections.push(json!({
"title": "threads",
"kind": "threads",
"lines": threads
.into_iter()
.take(8)
.map(|(_, name)| name)
.collect::<Vec<_>>(),
}));
let report =
commands::report_text(store, 7, None).unwrap_or_else(|_| "report unavailable".to_string());
sections.push(json!({
"title": "report · 7d",
"lines": report.lines().take(18).collect::<Vec<_>>(),
}));
Ok(json!({
"brand": "niuse",
"title": "niuse · web",
"sections": sections,
}))
}
#[cfg(feature = "tui")]
async fn run_tui(cli: &Cli, store: &Store, replay: Option<PathBuf>) -> Result<()> {
let settings = settings::resolve(cli, store)?;
let cwd = config::resolve_cwd(cli.cwd.clone())?;
let (policy, approval, _) =
config::resolve_policy(&settings.sandbox, &settings.approval, &settings.network)?;
let model = settings
.model
.clone()
.unwrap_or_else(|| "deepseek-flash".to_string());
let thread = config::resolve_thread(cli.thread.clone(), store)?;
let meta = driver::SessionMeta {
cwd: &cwd,
model: &model,
provider: &settings.provider,
policy,
approval,
};
drop(driver::open_thread(&thread, &meta)?);
let hooks =
tui_hook::CompanionHooks::new_with_thread(store, thread.clone())?.with_cwd(cwd.clone());
let options = niucode::TuiOptions {
preset: settings.provider,
model: Some(model),
max_steps: settings.max_steps,
no_thinking: cli.no_thinking,
cwd: Some(cwd),
session_dir: Some(store.root().to_path_buf()),
resume: Some(thread),
sandbox: settings.sandbox,
approval: settings.approval,
network: settings.network,
replay,
plan: cli.plan,
approval_keys: niucode::ApprovalKeys::custom('y', 'n', 's')
.expect("distinct approval keys"),
extra_tools: memory_tool::tools(store.root().to_path_buf()),
allow_mcp_write: cli.allow_mcp_write,
..niucode::TuiOptions::default()
};
niucode::run_tui_with_hooks(options, Box::new(hooks)).await
}
#[cfg(test)]
mod docs_tests {
use super::*;
use clap::CommandFactory;
use std::collections::HashSet;
const README: &str = include_str!("../../../README.md");
#[cfg(feature = "web")]
#[test]
fn web_new_thread_archives_and_recreates_the_active_thread() {
let directory = tempfile::tempdir().unwrap();
let store = Store::at(directory.path().to_path_buf());
store.init().unwrap();
std::fs::write(store.session_path(), "").unwrap();
let path = web_new_thread(&store, directory.path(), "m", "deepseek").unwrap();
assert_eq!(path, store.session_path());
let text = std::fs::read_to_string(&path).unwrap();
assert!(text.contains("session_start"), "{text}");
let archived = std::fs::read_dir(store.threads_dir()).unwrap().count();
assert!(archived >= 1);
}
#[cfg(feature = "web")]
#[test]
fn web_switch_thread_moves_an_archived_chapter_back() {
let directory = tempfile::tempdir().unwrap();
let store = Store::at(directory.path().to_path_buf());
store.init().unwrap();
let active = web_new_thread(&store, directory.path(), "m", "deepseek").unwrap();
std::fs::create_dir_all(store.threads_dir()).unwrap();
let archived = store.threads_dir().join("thread-1.jsonl");
std::fs::copy(&active, &archived).unwrap();
let expected = std::fs::read_to_string(&archived).unwrap();
let switched =
web_switch_thread(&store, "thread-1", directory.path(), "m", "deepseek").unwrap();
assert_eq!(switched, active);
assert_eq!(std::fs::read_to_string(&switched).unwrap(), expected);
}
#[cfg(feature = "web")]
#[test]
fn web_context_contains_product_sections() {
let directory = tempfile::tempdir().unwrap();
let store = Store::at(directory.path().to_path_buf());
store.init().unwrap();
let context = web_context(&store).unwrap();
assert_eq!(context["brand"], "niuse");
let sections = context["sections"].as_array().unwrap();
assert!(sections.len() >= 9);
assert!(sections
.iter()
.any(|section| section["title"] == "report · 7d"));
assert!(sections.iter().any(|section| section["title"] == "threads"));
assert!(sections.iter().any(|section| section["title"] == "thread"));
}
#[test]
fn readme_cli_examples_name_real_subcommands() {
let mut known: HashSet<String> = Cli::command()
.get_subcommands()
.map(|command| command.get_name().to_string())
.collect();
known.insert("tui".to_string());
let mut checked = 0;
let mut fenced = false;
for line in README.lines() {
let line = line.trim();
if line.starts_with("```") {
fenced = !fenced;
continue;
}
if !fenced {
continue;
}
let Some(rest) = line.strip_prefix("niuse ") else {
continue;
};
if rest.starts_with(char::is_whitespace) {
continue;
}
let Some(first) = rest.split_whitespace().next() else {
continue;
};
if first.starts_with('-') || first.starts_with('#') {
continue;
}
assert!(
known.contains(first),
"README example `niuse {first}` is not a subcommand; known: {known:?}"
);
checked += 1;
}
assert!(
checked >= 10,
"expected the README command table, saw {checked}"
);
}
#[test]
fn readme_cli_flags_exist_on_the_documented_command() {
let root = Cli::command();
let mut checked = 0;
let mut fenced = false;
for line in README.lines() {
let line = line.trim();
if line.starts_with("```") {
fenced = !fenced;
continue;
}
if !fenced {
continue;
}
let Some(rest) = line.strip_prefix("niuse ") else {
continue;
};
if rest.starts_with(char::is_whitespace) {
continue;
}
let tokens: Vec<&str> = rest.split_whitespace().collect();
let Some(first) = tokens.first() else {
continue;
};
if first.starts_with('-') || first.starts_with('#') || *first == "tui" {
continue;
}
let Some(command) = root.find_subcommand(first).cloned() else {
continue;
};
let mut commands = vec![command.clone()];
commands.extend(command.get_subcommands().cloned());
let mut valid: HashSet<String> = commands
.iter()
.flat_map(|command| command.get_arguments())
.filter_map(|argument| argument.get_long().map(ToString::to_string))
.collect();
valid.extend(
root.get_arguments()
.filter_map(|argument| argument.get_long().map(ToString::to_string)),
);
for flag in flags_in(line) {
if flag == "help" || flag == "version" {
continue;
}
assert!(
valid.contains(&flag),
"README flag `--{flag}` is not valid for `niuse {first}`; valid: {valid:?}"
);
checked += 1;
}
}
assert!(checked >= 5, "expected documented flags, saw {checked}");
}
fn flags_in(line: &str) -> Vec<String> {
let mut flags = Vec::new();
let mut chars = line.chars().peekable();
while let Some(character) = chars.next() {
if character != '-' || chars.peek() != Some(&'-') {
continue;
}
chars.next();
let mut flag = String::new();
while let Some(next) = chars.peek().copied() {
if next.is_ascii_alphanumeric() || next == '-' {
flag.push(next);
chars.next();
} else {
break;
}
}
if !flag.is_empty() {
flags.push(flag);
}
}
flags
}
#[test]
fn readme_repl_commands_are_registered() {
let line = README
.lines()
.find(|line| line.starts_with("Inside the REPL:"))
.expect("README has an inside-the-REPL line");
let mut checked = 0;
for token in line.split('`') {
let Some(command) = token.split_whitespace().next() else {
continue;
};
if !command.starts_with('/') {
continue;
}
assert!(
repl::SLASH_COMMANDS.contains(&command),
"README REPL command {command} is not registered"
);
checked += 1;
}
assert!(checked >= 10, "expected REPL commands, saw {checked}");
}
}