merge: plugin cli inspection
This commit is contained in:
commit
71ca05c899
|
|
@ -436,7 +436,7 @@ pub struct ResolvedPluginRecord {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResolvedPluginRecord {
|
impl ResolvedPluginRecord {
|
||||||
fn from_resolved(resolved: &ResolvedPlugin) -> Self {
|
pub fn from_resolved(resolved: &ResolvedPlugin) -> Self {
|
||||||
Self {
|
Self {
|
||||||
identity: resolved.identity.clone(),
|
identity: resolved.identity.clone(),
|
||||||
source: resolved.source,
|
source: resolved.source,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ use manifest::plugin::{
|
||||||
PluginConfig, PluginDiscoveryLimits, PluginHostApi, PluginPermission, PluginSurface,
|
PluginConfig, PluginDiscoveryLimits, PluginHostApi, PluginPermission, PluginSurface,
|
||||||
PluginToolManifest, ResolvedPluginRecord, read_resolved_plugin_runtime_module,
|
PluginToolManifest, ResolvedPluginRecord, read_resolved_plugin_runtime_module,
|
||||||
};
|
};
|
||||||
|
use serde::Serialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
|
|
@ -76,6 +77,204 @@ impl PluginToolFeature {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Static, read-only eligibility information for a resolved plugin package.
|
||||||
|
///
|
||||||
|
/// This inspection mirrors the registration-time permission checks without
|
||||||
|
/// loading the WASM module, calling a plugin Tool, or executing plugin code.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct PluginStaticInspection {
|
||||||
|
pub runtime: PluginRuntimeEligibility,
|
||||||
|
pub host_apis: Vec<PluginPermissionEligibility>,
|
||||||
|
pub tools: Vec<PluginToolEligibility>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginStaticInspection {
|
||||||
|
pub fn statically_eligible(&self) -> bool {
|
||||||
|
self.runtime.eligible
|
||||||
|
&& self.host_apis.iter().all(|api| api.eligible)
|
||||||
|
&& self.tools.iter().all(|tool| tool.eligible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct PluginRuntimeEligibility {
|
||||||
|
pub eligible: bool,
|
||||||
|
pub status: String,
|
||||||
|
pub diagnostic: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct PluginPermissionEligibility {
|
||||||
|
pub permission: String,
|
||||||
|
pub requested: bool,
|
||||||
|
pub granted: bool,
|
||||||
|
pub eligible: bool,
|
||||||
|
pub diagnostic: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct PluginToolEligibility {
|
||||||
|
pub name: String,
|
||||||
|
pub permission: String,
|
||||||
|
pub requested: bool,
|
||||||
|
pub granted: bool,
|
||||||
|
pub eligible: bool,
|
||||||
|
pub external_write: bool,
|
||||||
|
pub diagnostic: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inspect static plugin runtime/tool eligibility without executing plugin code.
|
||||||
|
pub fn inspect_resolved_plugin_static(record: &ResolvedPluginRecord) -> PluginStaticInspection {
|
||||||
|
let runtime = match &record.manifest.runtime {
|
||||||
|
Some(runtime)
|
||||||
|
if runtime.kind == "wasm" && runtime.abi.as_deref() == Some("yoi-plugin-wasm-1") =>
|
||||||
|
{
|
||||||
|
PluginRuntimeEligibility {
|
||||||
|
eligible: true,
|
||||||
|
status: "wasm/yoi-plugin-wasm-1".to_string(),
|
||||||
|
diagnostic: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(runtime) if runtime.kind == "wasm" => {
|
||||||
|
let status = runtime
|
||||||
|
.abi
|
||||||
|
.as_deref()
|
||||||
|
.map(|abi| format!("wasm/{abi}"))
|
||||||
|
.unwrap_or_else(|| "wasm/<missing-abi>".to_string());
|
||||||
|
PluginRuntimeEligibility {
|
||||||
|
eligible: false,
|
||||||
|
status,
|
||||||
|
diagnostic: Some("unsupported or missing plugin runtime ABI".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(runtime) => PluginRuntimeEligibility {
|
||||||
|
eligible: false,
|
||||||
|
status: runtime.kind.clone(),
|
||||||
|
diagnostic: Some(format!(
|
||||||
|
"unsupported plugin runtime kind `{}`",
|
||||||
|
runtime.kind
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
None => PluginRuntimeEligibility {
|
||||||
|
eligible: false,
|
||||||
|
status: "none".to_string(),
|
||||||
|
diagnostic: Some("plugin runtime is not declared".to_string()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let host_apis = [PluginHostApi::Https, PluginHostApi::Fs]
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|api| {
|
||||||
|
let permission = PluginPermission::host_api(api);
|
||||||
|
let requested = permission_requested(record, &permission);
|
||||||
|
let granted = grant_allows(record, &permission);
|
||||||
|
if !requested && !granted {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let diagnostic = authorize_plugin_host_api(record, api)
|
||||||
|
.err()
|
||||||
|
.map(|error| error.bounded_message());
|
||||||
|
Some(PluginPermissionEligibility {
|
||||||
|
permission: permission.label(),
|
||||||
|
requested,
|
||||||
|
granted,
|
||||||
|
eligible: diagnostic.is_none(),
|
||||||
|
diagnostic,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let duplicate_tool_names = duplicate_tool_names(record);
|
||||||
|
let tools = record
|
||||||
|
.manifest
|
||||||
|
.tools
|
||||||
|
.iter()
|
||||||
|
.map(|tool| {
|
||||||
|
let permission = PluginPermission::tool(&tool.name);
|
||||||
|
let requested = permission_requested(record, &permission);
|
||||||
|
let granted = grant_allows(record, &permission);
|
||||||
|
let mut diagnostics = validate_plugin_tool_definition(tool, &duplicate_tool_names);
|
||||||
|
if let Err(error) = authorize_plugin_tool(record, tool) {
|
||||||
|
diagnostics.push(error.bounded_message());
|
||||||
|
}
|
||||||
|
let diagnostic = join_tool_diagnostics(diagnostics);
|
||||||
|
PluginToolEligibility {
|
||||||
|
name: tool.name.clone(),
|
||||||
|
permission: permission.label(),
|
||||||
|
requested,
|
||||||
|
granted,
|
||||||
|
eligible: diagnostic.is_none(),
|
||||||
|
external_write: tool.external_write,
|
||||||
|
diagnostic,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
PluginStaticInspection {
|
||||||
|
runtime,
|
||||||
|
host_apis,
|
||||||
|
tools,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn permission_requested(record: &ResolvedPluginRecord, permission: &PluginPermission) -> bool {
|
||||||
|
record
|
||||||
|
.manifest
|
||||||
|
.permissions
|
||||||
|
.iter()
|
||||||
|
.any(|requested| requested == permission)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grant_allows(record: &ResolvedPluginRecord, permission: &PluginPermission) -> bool {
|
||||||
|
record
|
||||||
|
.grants
|
||||||
|
.permissions
|
||||||
|
.iter()
|
||||||
|
.any(|granted| granted == permission)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn duplicate_tool_names(record: &ResolvedPluginRecord) -> HashSet<String> {
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
let mut duplicates = HashSet::new();
|
||||||
|
for tool in &record.manifest.tools {
|
||||||
|
if !seen.insert(tool.name.clone()) {
|
||||||
|
duplicates.insert(tool.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
duplicates
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_plugin_tool_definition(
|
||||||
|
tool: &PluginToolManifest,
|
||||||
|
duplicate_tool_names: &HashSet<String>,
|
||||||
|
) -> Vec<String> {
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
if duplicate_tool_names.contains(&tool.name) {
|
||||||
|
diagnostics.push(format!(
|
||||||
|
"tool `{}` has duplicate name within plugin manifest",
|
||||||
|
tool.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Err(reason) = validate_tool_name(&tool.name) {
|
||||||
|
diagnostics.push(format!("tool `{}` has invalid name: {reason}", tool.name));
|
||||||
|
}
|
||||||
|
if let Err(reason) = validate_input_schema(&tool.input_schema) {
|
||||||
|
diagnostics.push(format!(
|
||||||
|
"tool `{}` has invalid input_schema: {reason}",
|
||||||
|
tool.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join_tool_diagnostics(diagnostics: Vec<String>) -> Option<String> {
|
||||||
|
if diagnostics.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(bounded_message(diagnostics.join("; ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FeatureModule for PluginToolFeature {
|
impl FeatureModule for PluginToolFeature {
|
||||||
fn descriptor(&self) -> FeatureDescriptor {
|
fn descriptor(&self) -> FeatureDescriptor {
|
||||||
let mut descriptor =
|
let mut descriptor =
|
||||||
|
|
@ -1665,6 +1864,111 @@ input_schema = { type = "object", additionalProperties = true }
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn static_inspection_does_not_read_or_execute_package() {
|
||||||
|
let mut record = record(vec![tool("Echo")]);
|
||||||
|
record.package_path = std::path::PathBuf::from("/no/such/plugin.wasm");
|
||||||
|
record.manifest.runtime = Some(PluginRuntimeManifest {
|
||||||
|
kind: "wasm".to_string(),
|
||||||
|
entry: "plugin.wasm".to_string(),
|
||||||
|
abi: Some("yoi-plugin-wasm-1".to_string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let inspection = inspect_resolved_plugin_static(&record);
|
||||||
|
|
||||||
|
assert!(inspection.runtime.eligible);
|
||||||
|
assert_eq!(inspection.tools.len(), 1);
|
||||||
|
assert!(inspection.tools[0].eligible);
|
||||||
|
assert!(inspection.statically_eligible());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn static_inspection_reports_missing_tool_grant() {
|
||||||
|
let mut record = record(vec![tool("Echo")]);
|
||||||
|
record.manifest.runtime = Some(PluginRuntimeManifest {
|
||||||
|
kind: "wasm".to_string(),
|
||||||
|
entry: "plugin.wasm".to_string(),
|
||||||
|
abi: Some("yoi-plugin-wasm-1".to_string()),
|
||||||
|
});
|
||||||
|
record.grants.permissions = vec![PluginPermission::surface(PluginSurface::Tool)];
|
||||||
|
|
||||||
|
let inspection = inspect_resolved_plugin_static(&record);
|
||||||
|
|
||||||
|
assert!(!inspection.statically_eligible());
|
||||||
|
assert!(!inspection.tools[0].eligible);
|
||||||
|
assert!(
|
||||||
|
inspection.tools[0]
|
||||||
|
.diagnostic
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("grant")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn static_inspection_reports_invalid_tool_definition() {
|
||||||
|
let mut bad_schema = tool("Echo");
|
||||||
|
bad_schema.input_schema = json!({"type":"string"});
|
||||||
|
let mut record = record(vec![bad_schema]);
|
||||||
|
record.manifest.runtime = Some(PluginRuntimeManifest {
|
||||||
|
kind: "wasm".to_string(),
|
||||||
|
entry: "plugin.wasm".to_string(),
|
||||||
|
abi: Some("yoi-plugin-wasm-1".to_string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let inspection = inspect_resolved_plugin_static(&record);
|
||||||
|
|
||||||
|
assert!(!inspection.statically_eligible());
|
||||||
|
assert!(!inspection.tools[0].eligible);
|
||||||
|
let diagnostic = inspection.tools[0]
|
||||||
|
.diagnostic
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_default();
|
||||||
|
assert!(diagnostic.contains("invalid input_schema"));
|
||||||
|
assert!(diagnostic.contains("root schema type must be `object`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn static_inspection_reports_invalid_and_duplicate_tool_names() {
|
||||||
|
let mut invalid = tool("Bad Tool");
|
||||||
|
invalid.input_schema = json!({"type":"object"});
|
||||||
|
let mut first_duplicate = tool("Echo");
|
||||||
|
let mut second_duplicate = tool("Echo");
|
||||||
|
first_duplicate.input_schema = json!({"type":"object"});
|
||||||
|
second_duplicate.input_schema = json!({"type":"object"});
|
||||||
|
let mut record = record(vec![invalid, first_duplicate, second_duplicate]);
|
||||||
|
record.manifest.runtime = Some(PluginRuntimeManifest {
|
||||||
|
kind: "wasm".to_string(),
|
||||||
|
entry: "plugin.wasm".to_string(),
|
||||||
|
abi: Some("yoi-plugin-wasm-1".to_string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let inspection = inspect_resolved_plugin_static(&record);
|
||||||
|
|
||||||
|
assert!(!inspection.statically_eligible());
|
||||||
|
assert!(
|
||||||
|
inspection.tools[0]
|
||||||
|
.diagnostic
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("invalid name")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
inspection.tools[1]
|
||||||
|
.diagnostic
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("duplicate name")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
inspection.tools[2]
|
||||||
|
.diagnostic
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("duplicate name")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn write_stored_zip(path: &Path, files: &[(&str, &[u8])]) {
|
fn write_stored_zip(path: &Path, files: &[(&str, &[u8])]) {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let mut central = Vec::new();
|
let mut central = Vec::new();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
mod memory_lint;
|
mod memory_lint;
|
||||||
mod objective_cli;
|
mod objective_cli;
|
||||||
|
mod plugin_cli;
|
||||||
mod session_cli;
|
mod session_cli;
|
||||||
mod ticket_cli;
|
mod ticket_cli;
|
||||||
|
|
||||||
|
|
@ -17,6 +18,7 @@ enum Mode {
|
||||||
Help,
|
Help,
|
||||||
MemoryLintHelp,
|
MemoryLintHelp,
|
||||||
MemoryLint(LintCliOptions),
|
MemoryLint(LintCliOptions),
|
||||||
|
Plugin(plugin_cli::PluginCliCommand),
|
||||||
Objective(objective_cli::ObjectiveCli),
|
Objective(objective_cli::ObjectiveCli),
|
||||||
Session(session_cli::SessionCli),
|
Session(session_cli::SessionCli),
|
||||||
Ticket(ticket_cli::TicketCli),
|
Ticket(ticket_cli::TicketCli),
|
||||||
|
|
@ -68,6 +70,13 @@ async fn main() -> ExitCode {
|
||||||
ExitCode::FAILURE
|
ExitCode::FAILURE
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Mode::Plugin(command) => match plugin_cli::run(command) {
|
||||||
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("yoi plugin: {e}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
},
|
||||||
Mode::Objective(cli) => match objective_cli::run(cli) {
|
Mode::Objective(cli) => match objective_cli::run(cli) {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
print!("{}", output.stdout);
|
print!("{}", output.stdout);
|
||||||
|
|
@ -173,6 +182,10 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
|
||||||
ticket_cli::parse_ticket_args(&args[1..]).map_err(|e| ParseError(e.to_string()))?;
|
ticket_cli::parse_ticket_args(&args[1..]).map_err(|e| ParseError(e.to_string()))?;
|
||||||
return Ok(Mode::Ticket(ticket_cli));
|
return Ok(Mode::Ticket(ticket_cli));
|
||||||
}
|
}
|
||||||
|
"plugin" => {
|
||||||
|
let plugin_cli = parse_plugin_args(&args[1..])?;
|
||||||
|
return Ok(Mode::Plugin(plugin_cli));
|
||||||
|
}
|
||||||
"panel" => {
|
"panel" => {
|
||||||
return Ok(Mode::Tui {
|
return Ok(Mode::Tui {
|
||||||
mode: LaunchMode::Panel,
|
mode: LaunchMode::Panel,
|
||||||
|
|
@ -413,6 +426,97 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_plugin_args(args: &[String]) -> Result<plugin_cli::PluginCliCommand, ParseError> {
|
||||||
|
let Some((subcommand, rest)) = args.split_first() else {
|
||||||
|
return Err(ParseError(
|
||||||
|
"yoi plugin requires `list` or `show <ref>`".to_string(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
match subcommand.as_str() {
|
||||||
|
"list" => {
|
||||||
|
let (plugin_args, positional) = parse_plugin_common_args(rest)?;
|
||||||
|
if !positional.is_empty() {
|
||||||
|
return Err(ParseError(
|
||||||
|
"yoi plugin list does not accept positional arguments".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(plugin_cli::PluginCliCommand::List(plugin_args))
|
||||||
|
}
|
||||||
|
"show" => {
|
||||||
|
let (plugin_args, positional) = parse_plugin_common_args(rest)?;
|
||||||
|
match positional.as_slice() {
|
||||||
|
[reference] => Ok(plugin_cli::PluginCliCommand::Show {
|
||||||
|
reference: reference.clone(),
|
||||||
|
args: plugin_args,
|
||||||
|
}),
|
||||||
|
[] => Err(ParseError(
|
||||||
|
"yoi plugin show requires a plugin ref".to_string(),
|
||||||
|
)),
|
||||||
|
_ => Err(ParseError(
|
||||||
|
"yoi plugin show accepts exactly one plugin ref".to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"--help" | "-h" => Err(ParseError(plugin_usage().to_string())),
|
||||||
|
other => Err(ParseError(format!(
|
||||||
|
"unknown yoi plugin subcommand `{other}`"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_plugin_common_args(
|
||||||
|
args: &[String],
|
||||||
|
) -> Result<(plugin_cli::PluginCliArgs, Vec<String>), ParseError> {
|
||||||
|
let mut parsed = plugin_cli::PluginCliArgs::default();
|
||||||
|
let mut positional = Vec::new();
|
||||||
|
let mut index = 0;
|
||||||
|
while index < args.len() {
|
||||||
|
let arg = &args[index];
|
||||||
|
match arg.as_str() {
|
||||||
|
"--json" => parsed.json = true,
|
||||||
|
"--workspace" => {
|
||||||
|
index += 1;
|
||||||
|
let Some(value) = args.get(index) else {
|
||||||
|
return Err(ParseError("--workspace requires a value".to_string()));
|
||||||
|
};
|
||||||
|
parsed.workspace = Some(PathBuf::from(value));
|
||||||
|
}
|
||||||
|
"--profile" => {
|
||||||
|
index += 1;
|
||||||
|
let Some(value) = args.get(index) else {
|
||||||
|
return Err(ParseError("--profile requires a value".to_string()));
|
||||||
|
};
|
||||||
|
parsed.profile = Some(value.clone());
|
||||||
|
}
|
||||||
|
"--help" | "-h" => return Err(ParseError(plugin_usage().to_string())),
|
||||||
|
_ if arg.starts_with("--workspace=") => {
|
||||||
|
let value = arg.trim_start_matches("--workspace=");
|
||||||
|
if value.is_empty() {
|
||||||
|
return Err(ParseError("--workspace requires a value".to_string()));
|
||||||
|
}
|
||||||
|
parsed.workspace = Some(PathBuf::from(value));
|
||||||
|
}
|
||||||
|
_ if arg.starts_with("--profile=") => {
|
||||||
|
let value = arg.trim_start_matches("--profile=");
|
||||||
|
if value.is_empty() {
|
||||||
|
return Err(ParseError("--profile requires a value".to_string()));
|
||||||
|
}
|
||||||
|
parsed.profile = Some(value.to_string());
|
||||||
|
}
|
||||||
|
_ if arg.starts_with('-') => {
|
||||||
|
return Err(ParseError(format!("unknown yoi plugin option `{arg}`")));
|
||||||
|
}
|
||||||
|
_ => positional.push(arg.clone()),
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
Ok((parsed, positional))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plugin_usage() -> &'static str {
|
||||||
|
"usage: yoi plugin list [--workspace PATH] [--profile REF] [--json]\n yoi plugin show <ref> [--workspace PATH] [--profile REF] [--json]"
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_panel_workspace(args: &[String]) -> Result<PathBuf, ParseError> {
|
fn parse_panel_workspace(args: &[String]) -> Result<PathBuf, ParseError> {
|
||||||
match args {
|
match args {
|
||||||
[] => std::env::current_dir()
|
[] => std::env::current_dir()
|
||||||
|
|
@ -443,7 +547,7 @@ fn parse_session_id(value: &str) -> Result<SegmentId, ParseError> {
|
||||||
|
|
||||||
fn print_help() {
|
fn print_help() {
|
||||||
println!(
|
println!(
|
||||||
"yoi\n\nUsage:\n yoi [OPTIONS] [POD_NAME]\n yoi panel [--workspace <PATH>]\n yoi keys\n yoi setup-model\n yoi pod [POD_OPTIONS]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi ticket <COMMAND> [OPTIONS]\n yoi memory lint [OPTIONS]\n\nOptions:\n -r, --resume Open the Pod picker and resume/attach a Pod\n --workspace <PATH> Runtime workspace root (defaults to cwd)\n --pod <NAME> Attach/restore/create a Pod by name\n --socket <PATH> Attach to a specific Pod socket with --pod\n --session <UUID> Resume a specific session segment\n --profile <REF> Select a reusable Profile recipe\n -h, --help Print help\n"
|
"yoi\n\nUsage:\n yoi [OPTIONS] [POD_NAME]\n yoi panel [--workspace <PATH>]\n yoi keys\n yoi setup-model\n yoi pod [POD_OPTIONS]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi ticket <COMMAND> [OPTIONS]\n yoi plugin list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi plugin show <REF> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi memory lint [OPTIONS]\n\nOptions:\n -r, --resume Open the Pod picker and resume/attach a Pod\n --workspace <PATH> Runtime workspace root (defaults to cwd)\n --pod <NAME> Attach/restore/create a Pod by name\n --socket <PATH> Attach to a specific Pod socket with --pod\n --session <UUID> Resume a specific session segment\n --profile <REF> Select a reusable Profile recipe\n -h, --help Print help\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -607,6 +711,33 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_plugin_list_and_show() {
|
||||||
|
match parse_args_from(["plugin", "list", "--workspace=/tmp/ws", "--json"]).unwrap() {
|
||||||
|
Mode::Plugin(plugin_cli::PluginCliCommand::List(options)) => {
|
||||||
|
assert_eq!(options.workspace, Some(PathBuf::from("/tmp/ws")));
|
||||||
|
assert!(options.json);
|
||||||
|
}
|
||||||
|
_ => panic!("expected Plugin list mode"),
|
||||||
|
}
|
||||||
|
|
||||||
|
match parse_args_from([
|
||||||
|
"plugin",
|
||||||
|
"show",
|
||||||
|
"project:echo",
|
||||||
|
"--profile",
|
||||||
|
"project:inspect",
|
||||||
|
])
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
Mode::Plugin(plugin_cli::PluginCliCommand::Show { reference, args }) => {
|
||||||
|
assert_eq!(reference, "project:echo");
|
||||||
|
assert_eq!(args.profile.as_deref(), Some("project:inspect"));
|
||||||
|
}
|
||||||
|
_ => panic!("expected Plugin show mode"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_memory_lint_rejects_usage_errors() {
|
fn parse_memory_lint_rejects_usage_errors() {
|
||||||
let err = parse_args_from(["memory", "lint", "--workspace"]).unwrap_err();
|
let err = parse_args_from(["memory", "lint", "--workspace"]).unwrap_err();
|
||||||
|
|
|
||||||
1528
crates/yoi/src/plugin_cli.rs
Normal file
1528
crates/yoi/src/plugin_cli.rs
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user