Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions rust/src/cli/addon_cmd/authoring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::{AddonManifest, Path, flag_value, looks_like_path, positional, regist

/// `addon init [name]` — scaffold a ready-to-edit `lean-ctx-addon.toml` in the
/// current directory. `--http` for an HTTP addon, `--force` to overwrite.
pub(super) fn cmd_init(args: &[String]) {
pub(super) fn cmd_init(args: &[String]) -> i32 {
use crate::core::addons::scaffold;
use crate::core::mcp_catalog::TransportKind;

Expand All @@ -29,11 +29,11 @@ pub(super) fn cmd_init(args: &[String]) {
});
let Some(raw) = slug else {
eprintln!("Could not derive an addon name. Pass one: `lean-ctx addon init my-addon`.");
std::process::exit(1);
return 1;
};
let Some(slug) = scaffold::slugify(&raw) else {
eprintln!("`{raw}` has no usable slug characters ([a-z0-9-]).");
std::process::exit(1);
return 1;
};

let path = Path::new(scaffold::MANIFEST_FILENAME);
Expand All @@ -42,13 +42,13 @@ pub(super) fn cmd_init(args: &[String]) {
"{} already exists. Re-run with --force to overwrite.",
scaffold::MANIFEST_FILENAME
);
std::process::exit(1);
return 1;
}

let contents = scaffold::addon_manifest(&slug, transport, command.as_deref());
if let Err(e) = std::fs::write(path, contents) {
eprintln!("Error writing {}: {e}", scaffold::MANIFEST_FILENAME);
std::process::exit(1);
return 1;
}

println!("✓ Wrote {} (addon `{slug}`).", scaffold::MANIFEST_FILENAME);
Expand All @@ -63,25 +63,26 @@ pub(super) fn cmd_init(args: &[String]) {
scaffold::MANIFEST_FILENAME
);
println!(" 4. Get listed: see docs/guides/addons.md");
0
}

/// `addon registry validate [path]` — run the registry security/quality bar
/// (#864 + #403) against a registry JSON file, or the bundled + local registry
/// if no path is given. The dry-run harness an author / CI uses before opening a
/// merge request. Non-zero exit when problems are found.
pub(super) fn cmd_registry(args: &[String]) {
pub(super) fn cmd_registry(args: &[String]) -> i32 {
let sub = args.get(1).map_or("", String::as_str);
if sub != "validate" {
eprintln!("Usage: lean-ctx addon registry validate [path-to-registry.json]");
std::process::exit(1);
return 1;
}

let (entries, label) = match args.get(2).map(String::as_str) {
Some(path) if !path.starts_with('-') => match load_registry_file(path) {
Ok(e) => (e, path.to_string()),
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
return 1;
}
},
_ => (
Expand All @@ -97,13 +98,13 @@ pub(super) fn cmd_registry(args: &[String]) {
entries.len(),
if entries.len() == 1 { "y" } else { "ies" }
);
return;
return 0;
}
eprintln!("✗ {label}: {} problem(s):\n", problems.len());
for p in &problems {
eprintln!(" • {p}");
}
std::process::exit(1);
1
}

/// Parse a registry JSON file (`{ "addons": [ … ] }`) into manifests.
Expand All @@ -122,19 +123,19 @@ fn load_registry_file(path: &str) -> Result<Vec<AddonManifest>, String> {
/// `addon audit <name|path>` — run the publish/list gate (#403): wiring risk +
/// capability coherence + malware heuristics, then the verified/paid verdict.
/// Exits non-zero on a `fail` verdict so it is usable in CI / a publish hook.
pub(super) fn cmd_audit(target: &str) {
pub(super) fn cmd_audit(target: &str) -> i32 {
let manifest = if looks_like_path(target) {
match AddonManifest::from_path(Path::new(target)) {
Ok(m) => m,
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
return 1;
}
}
} else {
let Some(m) = registry::get(target) else {
eprintln!("Unknown addon `{target}`. Pass a name from the registry or a path.");
std::process::exit(1);
return 1;
};
m
};
Expand Down Expand Up @@ -220,6 +221,7 @@ pub(super) fn cmd_audit(target: &str) {
eprintln!(
"\nAudit failed — this addon must not be listed until the blocking findings are resolved."
);
std::process::exit(1);
return 1;
}
0
}
64 changes: 42 additions & 22 deletions rust/src/cli/addon_cmd/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,28 @@ use super::{
registry, resolve_declared_deps,
};

pub fn cmd_addon(args: &[String]) {
pub fn cmd_addon(args: &[String]) -> i32 {
let action = args.first().map_or("list", String::as_str);

match action {
"list" | "ls" => cmd_list(),
"list" | "ls" => {
cmd_list();
0
}
"init" | "new" => cmd_init(args),
"registry" => cmd_registry(args),
"categories" | "cats" => cmd_categories(),
"usage" | "stats" => cmd_usage(),
"search" | "browse" => cmd_search(args.get(1).map_or("", String::as_str)),
"categories" | "cats" => {
cmd_categories();
0
}
"usage" | "stats" => {
cmd_usage();
0
}
"search" | "browse" => {
cmd_search(args.get(1).map_or("", String::as_str));
0
}
"info" | "show" => match positional(args) {
Some(name) => cmd_info(&name),
None => usage_exit("lean-ctx addon info <name>"),
Expand All @@ -40,18 +52,24 @@ pub fn cmd_addon(args: &[String]) {
Some(name) => cmd_unrevoke(&name, args),
None => usage_exit("lean-ctx addon unrevoke <name>"),
},
"revocations" => cmd_revocations(),
"revocations" => {
cmd_revocations();
0
}
"verify" => cmd_verify(),
"audit" => match positional(args) {
Some(target) => cmd_audit(&target),
None => usage_exit("lean-ctx addon audit <name|path-to-lean-ctx-addon.toml>"),
},
"publish" => cmd_publish(args),
"help" | "--help" | "-h" => print_help(),
"help" | "--help" | "-h" => {
print_help();
0
}
_ => {
eprintln!("Unknown addon action: {action}");
print_help();
std::process::exit(1);
1
}
}
}
Expand All @@ -63,9 +81,9 @@ pub(super) fn positional(args: &[String]) -> Option<String> {
.filter(|s| !s.is_empty() && !s.starts_with('-'))
}

fn usage_exit(usage: &str) -> ! {
fn usage_exit(usage: &str) -> i32 {
eprintln!("Usage: {usage}");
std::process::exit(1);
1
}

fn cmd_list() {
Expand Down Expand Up @@ -212,7 +230,7 @@ fn cmd_usage() {
}
}

fn cmd_info(name: &str) {
fn cmd_info(name: &str) -> i32 {
let store = InstalledStore::load();
let Some(manifest) = registry::get(name).or_else(|| {
// Allow `info` on a local manifest path too.
Expand All @@ -229,13 +247,13 @@ fn cmd_info(name: &str) {
" Status: installed (gateway server `{}`, {})",
installed.gateway_server, installed.source
);
return;
return 0;
}
eprintln!(
"Addon `{name}` not found. Try `lean-ctx addon search`, or pass a path to a \
lean-ctx-addon.toml."
);
std::process::exit(1);
return 1;
};

println!("{} ({})", manifest.display_name(), manifest.addon.name);
Expand Down Expand Up @@ -268,9 +286,10 @@ fn cmd_info(name: &str) {
println!();
print_install_preview(&manifest);
}
0
}

fn cmd_add(target: &str, args: &[String]) {
fn cmd_add(target: &str, args: &[String]) -> i32 {
// Resolution order: local manifest file → hosted ctxpkg pack (`ns/slug`,
// GH #726) → bundled registry slug. A bare `ns/slug` that exists on disk
// is treated as the local path it names.
Expand All @@ -285,7 +304,7 @@ fn cmd_add(target: &str, args: &[String]) {
Ok(m) => (m, "local".to_string()),
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
return 1;
}
}
} else if let Some(remote_ref) = crate::core::context_package::remote::parse_remote_ref(target)
Expand All @@ -294,7 +313,7 @@ fn cmd_add(target: &str, args: &[String]) {
Ok((m, s)) => (m, s),
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
return 1;
}
}
} else {
Expand All @@ -305,14 +324,14 @@ fn cmd_add(target: &str, args: &[String]) {
`lean-ctx addon add <namespace>/<name>`, or pass a path to a \
lean-ctx-addon.toml."
);
std::process::exit(1);
return 1;
};
(m, "registry".to_string())
};

if let Err(e) = manifest.validate() {
eprintln!("Error: {e}");
std::process::exit(1);
return 1;
}

if !manifest.is_installable() {
Expand All @@ -327,7 +346,7 @@ fn cmd_add(target: &str, args: &[String]) {
&manifest.addon.homepage
}
);
std::process::exit(1);
return 1;
}

let force = args.iter().any(|a| a == "--force" || a == "-f");
Expand All @@ -341,7 +360,7 @@ fn cmd_add(target: &str, args: &[String]) {
// this resolution, so only the verdict matters here.)
if let Err(e) = install::preflight(&manifest, &cfg.addons, force) {
eprintln!("Error: {e}");
std::process::exit(1);
return 1;
}

println!("About to install `{}`:\n", manifest.addon.name);
Expand Down Expand Up @@ -373,7 +392,7 @@ fn cmd_add(target: &str, args: &[String]) {
super::prompt::wants_yes(args),
) {
println!("Aborted. Nothing was changed.");
return;
return 0;
}

// The slice wired into `[mcp.env]` must be the versions the install step
Expand Down Expand Up @@ -405,9 +424,10 @@ fn cmd_add(target: &str, args: &[String]) {
}
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
return 1;
}
}
0
}

/// The impure provisioning pipeline `add` and `update` share, run after user
Expand Down
Loading
Loading