use std::{collections::BTreeMap, io::Write, path::PathBuf};
use crate::{
Builtin, Content, builtin,
scope::{Artifact, ArtifactId, ArtifactKind, Scope},
};
use clap::{Parser, Subcommand};
use serde_json::json;
use tokio_util::sync::CancellationToken;
#[derive(Parser)]
#[command(
disable_help_subcommand = true,
version,
about = "Portable scoped tools for artifactize",
long_about = "Portable scoped tools for artifactize. Filesystem targets must be relative to the current directory and stay below it. Parent components, absolute paths, and symlinks are refused. Use '.' for the current directory. Only open accepts HTTP(S) URLs. help resolves a literal program through the system's program search path."
)]
pub struct Cli {
#[command(subcommand)]
pub command: Tool,
}
#[derive(Debug, Subcommand)]
pub enum Tool {
Read { path: String },
List {
#[arg(default_value = ".")]
path: String,
},
Glob {
pattern: String,
#[arg(default_value = ".")]
path: String,
},
Grep {
pattern: String,
#[arg(default_value = ".")]
path: String,
#[arg(long)]
glob: Option<String>,
#[arg(long)]
case_insensitive: bool,
},
Section { path: String, heading: String },
Help {
#[arg(required = true, num_args = 1.., trailing_var_arg = true)]
args: Vec<String>,
},
Open { target: String },
}
pub async fn run(tool: Tool) -> u8 {
match execute(tool).await {
Ok(()) => 0,
Err(error) => {
let _ = writeln!(std::io::stderr(), "{error}");
1
}
}
}
fn logical(path: String) -> String {
if path == "." { String::new() } else { path }
}
async fn execute(tool: Tool) -> Result<(), String> {
let root = crate::files::current_directory().map_err(|error| error.to_string())?;
let owner = ArtifactId::new("cwd").expect("static identifier");
let scope = Scope {
artifacts: BTreeMap::from([(
owner.clone(),
Artifact {
path: PathBuf::new(),
kind: ArtifactKind::Folder,
name: "cwd".into(),
children: BTreeMap::new(),
},
)]),
};
let cancellation = CancellationToken::new();
enum Invocation {
Fixed(Builtin, Vec<String>),
Dynamic(builtin::Input),
}
let invocation = match tool {
Tool::Read { path } => Invocation::Fixed(Builtin::Read, vec![logical(path)]),
Tool::List { path } => Invocation::Fixed(Builtin::List, vec![logical(path)]),
Tool::Section { path, heading } => {
Invocation::Fixed(Builtin::Section, vec![logical(path), heading])
}
Tool::Help { args } => Invocation::Fixed(Builtin::Help, args),
Tool::Glob { pattern, path } => {
Invocation::Dynamic(builtin::Input::glob(&pattern, logical(path))?)
}
Tool::Grep {
pattern,
path,
glob,
case_insensitive,
} => Invocation::Dynamic(builtin::Input::grep(
&pattern,
logical(path),
glob.as_deref(),
case_insensitive,
)?),
Tool::Open { target } => {
let target = if builtin::is_url(&target) {
target.into()
} else {
scope
.resolve_input(&root, &owner, &logical(target))
.map_err(|error| error.to_string())?
.into_os_string()
};
return crate::opener::open(&target)
.await
.map_err(|error| error.to_string());
}
};
let result = match invocation {
Invocation::Fixed(tool, args) => {
builtin::call_fixed(
tool,
&args,
json!({}),
builtin::Context {
root: &root,
scope: &scope,
owner: &owner,
launcher: &crate::launch::Standalone,
},
&cancellation,
)
.await
}
Invocation::Dynamic(input) => builtin::call(input, &root, &scope, &owner, &cancellation),
};
let (mut stdout, mut stderr) = (std::io::stdout().lock(), std::io::stderr().lock());
for content in result.content() {
match content {
Content::Text { text } if result.is_error() => writeln!(stderr, "{text}"),
Content::Text { text } => write!(stdout, "{text}"),
Content::Json { data } => writeln!(
stdout,
"{}",
serde_json::to_string_pretty(&data).expect("builtin JSON")
),
Content::Image { .. } => unreachable!("CLI tools produce no images"),
}
.map_err(|error| error.to_string())?;
}
if result.is_error() {
Err("Tool failed.".into())
} else {
Ok(())
}
}