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(),
mounts: BTreeMap::new(),
},
)]),
};
let cancellation = CancellationToken::new();
let (tool, args, input) = match tool {
Tool::Read { path } => (Builtin::Read, Some(vec![logical(path)]), json!({})),
Tool::List { path } => (Builtin::List, Some(vec![logical(path)]), json!({})),
Tool::Section { path, heading } => (
Builtin::Section,
Some(vec![logical(path), heading]),
json!({}),
),
Tool::Help { args } => (Builtin::Help, Some(args), json!({})),
Tool::Glob { pattern, path } => (
Builtin::Glob,
None,
json!({"pattern":pattern,"path":logical(path)}),
),
Tool::Grep {
pattern,
path,
glob,
case_insensitive,
} => {
let mut input =
json!({"pattern":pattern,"path":logical(path),"caseInsensitive":case_insensitive});
if let Some(glob) = glob {
input["glob"] = json!(glob);
}
(Builtin::Grep, None, input)
}
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 = if let Some(args) = args {
let context = builtin::Context {
root: &root,
scope: &scope,
owner: &owner,
launcher: &crate::launch::Standalone,
};
builtin::call_fixed(tool, &args, input, context, &cancellation).await
} else {
let validator = crate::schema::compile(&builtin::input_schema(tool))?;
crate::schema::validate(&validator, &input)?;
builtin::call(
builtin::Input::parse(tool, 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(())
}
}