use crate::error::ProjectsError;
use crate::stripe::{CommandRunner, StripeProjects};
const TRAVERSAL: &[&[&str]] = &[
&[],
&["init"],
&["build"],
&["list"],
&["pull"],
&["status"],
&["services"],
&["services", "list"],
&["catalog"],
&["search"],
&["switch-account"],
&["add"],
&["update"],
&["upgrade"],
&["downgrade"],
&["remove"],
&["rotate"],
&["link"],
&["unlink"],
&["open"],
&["env"],
&["env", "list"],
&["env", "show"],
&["env", "create"],
&["env", "use"],
&["env", "update"],
&["env", "delete"],
&["env", "add"],
&["env", "remove"],
&["variables"],
&["variables", "set"],
&["variables", "list"],
&["variables", "delete"],
&["llm-context"],
&["billing"],
&["billing", "show"],
&["billing", "add"],
&["billing", "update"],
&["spend"],
&["feedback"],
];
const HEADER_VERSION_PREFIX: &str = "# plugin-version:";
pub async fn plugin_version<R: CommandRunner>(
stripe: &StripeProjects<R>,
) -> Result<String, ProjectsError> {
let out = stripe.plain(&["--version"]).await?;
out.stdout
.lines()
.map(str::trim)
.find(|line| line.starts_with(|c: char| c.is_ascii_digit()))
.map(str::to_owned)
.ok_or_else(|| ProjectsError::Unavailable {
detail: format!(
"`stripe projects --version` printed no version line: {:?}",
out.stdout
),
})
}
pub async fn command_surface<R: CommandRunner>(
stripe: &StripeProjects<R>,
) -> Result<String, ProjectsError> {
let mut out = String::new();
for path in TRAVERSAL {
let mut args: Vec<&str> = path.to_vec();
args.extend_from_slice(&["--help", "--color", "off"]);
let result = stripe.plain(&args).await?;
out.push_str(&format!("===== {} =====\n", label(path)));
out.push_str(&normalize_block(&result.stdout));
out.push_str("\n\n");
}
Ok(out.trim_end().to_owned())
}
pub fn surface_header(version: &str) -> String {
format!(
"# stripe projects command surface\n\
{HEADER_VERSION_PREFIX} {version}\n\
# DO NOT EDIT — regenerated by CI (mise run stripe-refresh)\n\n"
)
}
pub fn render_surface(version: &str, body: &str) -> String {
format!("{}{}\n", surface_header(version), body)
}
pub fn parse_header_version(text: &str) -> Option<String> {
text.lines()
.find_map(|line| line.trim().strip_prefix(HEADER_VERSION_PREFIX))
.map(|value| value.trim().to_owned())
}
pub fn command_separators() -> Vec<String> {
TRAVERSAL
.iter()
.map(|path| format!("===== {} =====", label(path)))
.collect()
}
fn label(path: &[&str]) -> String {
if path.is_empty() {
"stripe projects --help".to_owned()
} else {
format!("stripe projects {} --help", path.join(" "))
}
}
fn normalize_block(raw: &str) -> String {
let mut lines: Vec<&str> = raw.lines().map(str::trim_end).collect();
while lines.first().is_some_and(|line| line.is_empty()) {
lines.remove(0);
}
while lines.last().is_some_and(|line| line.is_empty()) {
lines.pop();
}
lines.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::stripe::CommandOutput;
use async_trait::async_trait;
use std::path::Path;
struct EchoRunner;
#[async_trait]
impl CommandRunner for EchoRunner {
async fn run(&self, args: &[String], _cwd: &Path) -> Result<CommandOutput, ProjectsError> {
Ok(CommandOutput {
status: 0,
stdout: format!("\n\nhelp for: {} \n\n", args.join(" ")),
stderr: String::new(),
})
}
}
fn driver() -> StripeProjects<EchoRunner> {
StripeProjects::new(EchoRunner, std::env::temp_dir())
}
#[test]
fn label_handles_top_level_and_nested() {
assert_eq!(label(&[]), "stripe projects --help");
assert_eq!(label(&["add"]), "stripe projects add --help");
assert_eq!(
label(&["billing", "update"]),
"stripe projects billing update --help"
);
}
#[test]
fn normalize_strips_trailing_ws_and_blank_edges() {
assert_eq!(normalize_block("\n\n body \n\n"), " body");
assert_eq!(normalize_block("a \nb\t\n"), "a\nb");
}
#[test]
fn header_round_trips_version() {
let text = render_surface("0.19.0", "===== stripe projects --help =====\nbody");
assert_eq!(parse_header_version(&text).as_deref(), Some("0.19.0"));
assert!(text.ends_with("body\n"));
}
#[tokio::test]
async fn surface_has_one_block_per_traversal_entry_in_order() {
let body = command_surface(&driver()).await.unwrap();
let seps: Vec<&str> = body.lines().filter(|l| l.starts_with("===== ")).collect();
assert_eq!(seps, command_separators());
assert!(body.starts_with("===== stripe projects --help =====\n"));
assert!(body.contains("help for: --help --color off"));
assert!(body.contains("help for: env list --help --color off"));
assert!(!body.ends_with('\n'));
}
}