Allow plugins to report their own version and store it in the registry (#12883)

# Description

This allows plugins to report their version (and potentially other
metadata in the future). The version is shown in `plugin list` and in
`version`.

The metadata is stored in the registry file, and reflects whatever was
retrieved on `plugin add`, not necessarily the running binary. This can
help you to diagnose if there's some kind of mismatch with what you
expect. We could potentially use this functionality to show a warning or
error if a plugin being run does not have the same version as what was
in the cache file, suggesting `plugin add` be run again, but I haven't
done that at this point.

It is optional, and it requires the plugin author to make some code
changes if they want to provide it, since I can't automatically
determine the version of the calling crate or anything tricky like that
to do it.

Example:

```
> plugin list | select name version is_running pid
╭───┬────────────────┬─────────┬────────────┬─────╮
│ # │      name      │ version │ is_running │ pid │
├───┼────────────────┼─────────┼────────────┼─────┤
│ 0 │ example        │ 0.93.1  │ false      │     │
│ 1 │ gstat          │ 0.93.1  │ false      │     │
│ 2 │ inc            │ 0.93.1  │ false      │     │
│ 3 │ python_example │ 0.1.0   │ false      │     │
╰───┴────────────────┴─────────┴────────────┴─────╯
```

cc @maxim-uvarov (he asked for it)

# User-Facing Changes

- `plugin list` gets a `version` column
- `version` shows plugin versions when available
- plugin authors *should* add `fn metadata()` to their `impl Plugin`,
but don't have to

# Tests + Formatting

Tested the low level stuff and also the `plugin list` column.

# After Submitting
- [ ] update plugin guide docs
- [ ] update plugin protocol docs (`Metadata` call & response)
- [ ] update plugin template (`fn metadata()` should be easy)
- [ ] release notes
This commit is contained in:
Devyn Cairns 2024-06-21 04:27:09 -07:00 committed by GitHub
parent dd8f8861ed
commit 91d44f15c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 360 additions and 46 deletions

View File

@ -344,7 +344,10 @@ pub fn migrate_old_plugin_file(engine_state: &EngineState, storage_path: &str) -
name: identity.name().to_owned(), name: identity.name().to_owned(),
filename: identity.filename().to_owned(), filename: identity.filename().to_owned(),
shell: identity.shell().map(|p| p.to_owned()), shell: identity.shell().map(|p| p.to_owned()),
data: PluginRegistryItemData::Valid { commands }, data: PluginRegistryItemData::Valid {
metadata: Default::default(),
commands,
},
}); });
} }

View File

@ -116,11 +116,18 @@ pub fn version(engine_state: &EngineState, span: Span) -> Result<PipelineData, S
Value::string(features_enabled().join(", "), span), Value::string(features_enabled().join(", "), span),
); );
// Get a list of plugin names // Get a list of plugin names and versions if present
let installed_plugins = engine_state let installed_plugins = engine_state
.plugins() .plugins()
.iter() .iter()
.map(|x| x.identity().name()) .map(|x| {
let name = x.identity().name();
if let Some(version) = x.metadata().and_then(|m| m.version) {
format!("{name} {version}")
} else {
name.into()
}
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
record.push( record.push(

View File

@ -118,11 +118,12 @@ apparent the next time `nu` is next launched with that plugin registry file.
}, },
)); ));
let interface = plugin.clone().get_plugin(Some((engine_state, stack)))?; let interface = plugin.clone().get_plugin(Some((engine_state, stack)))?;
let metadata = interface.get_metadata()?;
let commands = interface.get_signature()?; let commands = interface.get_signature()?;
modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| { modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| {
// Update the file with the received signatures // Update the file with the received metadata and signatures
let item = PluginRegistryItem::new(plugin.identity(), commands); let item = PluginRegistryItem::new(plugin.identity(), metadata, commands);
contents.upsert_plugin(item); contents.upsert_plugin(item);
Ok(()) Ok(())
})?; })?;

View File

@ -16,6 +16,7 @@ impl Command for PluginList {
Type::Table( Type::Table(
[ [
("name".into(), Type::String), ("name".into(), Type::String),
("version".into(), Type::String),
("is_running".into(), Type::Bool), ("is_running".into(), Type::Bool),
("pid".into(), Type::Int), ("pid".into(), Type::Int),
("filename".into(), Type::String), ("filename".into(), Type::String),
@ -43,6 +44,7 @@ impl Command for PluginList {
description: "List installed plugins.", description: "List installed plugins.",
result: Some(Value::test_list(vec![Value::test_record(record! { result: Some(Value::test_list(vec![Value::test_record(record! {
"name" => Value::test_string("inc"), "name" => Value::test_string("inc"),
"version" => Value::test_string(env!("CARGO_PKG_VERSION")),
"is_running" => Value::test_bool(true), "is_running" => Value::test_bool(true),
"pid" => Value::test_int(106480), "pid" => Value::test_int(106480),
"filename" => if cfg!(windows) { "filename" => if cfg!(windows) {
@ -98,8 +100,15 @@ impl Command for PluginList {
.map(|s| Value::string(s.to_string_lossy(), head)) .map(|s| Value::string(s.to_string_lossy(), head))
.unwrap_or(Value::nothing(head)); .unwrap_or(Value::nothing(head));
let metadata = plugin.metadata();
let version = metadata
.and_then(|m| m.version)
.map(|s| Value::string(s, head))
.unwrap_or(Value::nothing(head));
let record = record! { let record = record! {
"name" => Value::string(plugin.identity().name(), head), "name" => Value::string(plugin.identity().name(), head),
"version" => version,
"is_running" => Value::bool(plugin.is_running(), head), "is_running" => Value::bool(plugin.is_running(), head),
"pid" => pid, "pid" => pid,
"filename" => Value::string(plugin.identity().filename().to_string_lossy(), head), "filename" => Value::string(plugin.identity().filename().to_string_lossy(), head),

View File

@ -3740,28 +3740,37 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm
) )
})?; })?;
let signatures = plugin let metadata_and_signatures = plugin
.clone() .clone()
.get(get_envs) .get(get_envs)
.and_then(|p| p.get_signature()) .and_then(|p| {
let meta = p.get_metadata()?;
let sigs = p.get_signature()?;
Ok((meta, sigs))
})
.map_err(|err| { .map_err(|err| {
log::warn!("Error getting signatures: {err:?}"); log::warn!("Error getting metadata and signatures: {err:?}");
ParseError::LabeledError( ParseError::LabeledError(
"Error getting signatures".into(), "Error getting metadata and signatures".into(),
err.to_string(), err.to_string(),
spans[0], spans[0],
) )
}); });
if let Ok(ref signatures) = signatures { match metadata_and_signatures {
// Add the loaded plugin to the delta Ok((meta, sigs)) => {
working_set.update_plugin_registry(PluginRegistryItem::new( // Set the metadata on the plugin
&identity, plugin.set_metadata(Some(meta.clone()));
signatures.clone(), // Add the loaded plugin to the delta
)); working_set.update_plugin_registry(PluginRegistryItem::new(
&identity,
meta,
sigs.clone(),
));
Ok(sigs)
}
Err(err) => Err(err),
} }
signatures
}, },
|sig| sig.map(|sig| vec![sig]), |sig| sig.map(|sig| vec![sig]),
)?; )?;

View File

@ -252,7 +252,7 @@ pub fn load_plugin_registry_item(
})?; })?;
match &plugin.data { match &plugin.data {
PluginRegistryItemData::Valid { commands } => { PluginRegistryItemData::Valid { metadata, commands } => {
let plugin = add_plugin_to_working_set(working_set, &identity)?; let plugin = add_plugin_to_working_set(working_set, &identity)?;
// Ensure that the plugin is reset. We're going to load new signatures, so we want to // Ensure that the plugin is reset. We're going to load new signatures, so we want to
@ -260,6 +260,9 @@ pub fn load_plugin_registry_item(
// doesn't. // doesn't.
plugin.reset()?; plugin.reset()?;
// Set the plugin metadata from the file
plugin.set_metadata(Some(metadata.clone()));
// Create the declarations from the commands // Create the declarations from the commands
for signature in commands { for signature in commands {
let decl = PluginDeclaration::new(plugin.clone(), signature.clone()); let decl = PluginDeclaration::new(plugin.clone(), signature.clone());

View File

@ -11,8 +11,8 @@ use nu_plugin_protocol::{
PluginOutput, ProtocolInfo, StreamId, StreamMessage, PluginOutput, ProtocolInfo, StreamId, StreamMessage,
}; };
use nu_protocol::{ use nu_protocol::{
ast::Operator, CustomValue, IntoSpanned, PipelineData, PluginSignature, ShellError, Span, ast::Operator, CustomValue, IntoSpanned, PipelineData, PluginMetadata, PluginSignature,
Spanned, Value, ShellError, Span, Spanned, Value,
}; };
use std::{ use std::{
collections::{btree_map, BTreeMap}, collections::{btree_map, BTreeMap},
@ -716,6 +716,7 @@ impl PluginInterface {
// Convert the call into one with a header and handle the stream, if necessary // Convert the call into one with a header and handle the stream, if necessary
let (call, writer) = match call { let (call, writer) = match call {
PluginCall::Metadata => (PluginCall::Metadata, Default::default()),
PluginCall::Signature => (PluginCall::Signature, Default::default()), PluginCall::Signature => (PluginCall::Signature, Default::default()),
PluginCall::CustomValueOp(value, op) => { PluginCall::CustomValueOp(value, op) => {
(PluginCall::CustomValueOp(value, op), Default::default()) (PluginCall::CustomValueOp(value, op), Default::default())
@ -913,6 +914,17 @@ impl PluginInterface {
self.receive_plugin_call_response(result.receiver, context, result.state) self.receive_plugin_call_response(result.receiver, context, result.state)
} }
/// Get the metadata from the plugin.
pub fn get_metadata(&self) -> Result<PluginMetadata, ShellError> {
match self.plugin_call(PluginCall::Metadata, None)? {
PluginCallResponse::Metadata(meta) => Ok(meta),
PluginCallResponse::Error(err) => Err(err.into()),
_ => Err(ShellError::PluginFailedToDecode {
msg: "Received unexpected response to plugin Metadata call".into(),
}),
}
}
/// Get the command signatures from the plugin. /// Get the command signatures from the plugin.
pub fn get_signature(&self) -> Result<Vec<PluginSignature>, ShellError> { pub fn get_signature(&self) -> Result<Vec<PluginSignature>, ShellError> {
match self.plugin_call(PluginCall::Signature, None)? { match self.plugin_call(PluginCall::Signature, None)? {
@ -1206,6 +1218,7 @@ impl CurrentCallState {
source: &PluginSource, source: &PluginSource,
) -> Result<(), ShellError> { ) -> Result<(), ShellError> {
match call { match call {
PluginCall::Metadata => Ok(()),
PluginCall::Signature => Ok(()), PluginCall::Signature => Ok(()),
PluginCall::Run(CallInfo { call, .. }) => self.prepare_call_args(call, source), PluginCall::Run(CallInfo { call, .. }) => self.prepare_call_args(call, source),
PluginCall::CustomValueOp(_, op) => { PluginCall::CustomValueOp(_, op) => {

View File

@ -18,7 +18,7 @@ use nu_protocol::{
ast::{Math, Operator}, ast::{Math, Operator},
engine::Closure, engine::Closure,
ByteStreamType, CustomValue, IntoInterruptiblePipelineData, IntoSpanned, PipelineData, ByteStreamType, CustomValue, IntoInterruptiblePipelineData, IntoSpanned, PipelineData,
PluginSignature, ShellError, Span, Spanned, Value, PluginMetadata, PluginSignature, ShellError, Span, Spanned, Value,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
@ -1019,6 +1019,25 @@ fn start_fake_plugin_call_responder(
.expect("failed to spawn thread"); .expect("failed to spawn thread");
} }
#[test]
fn interface_get_metadata() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.plugin("test");
let interface = manager.get_interface();
start_fake_plugin_call_responder(manager, 1, |_| {
vec![ReceivedPluginCallMessage::Response(
PluginCallResponse::Metadata(PluginMetadata::new().with_version("test")),
)]
});
let metadata = interface.get_metadata()?;
assert_eq!(Some("test"), metadata.version.as_deref());
assert!(test.has_unconsumed_write());
Ok(())
}
#[test] #[test]
fn interface_get_signature() -> Result<(), ShellError> { fn interface_get_signature() -> Result<(), ShellError> {
let test = TestCase::new(); let test = TestCase::new();

View File

@ -7,7 +7,7 @@ use super::{PluginInterface, PluginSource};
use nu_plugin_core::CommunicationMode; use nu_plugin_core::CommunicationMode;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError, PluginGcConfig, PluginIdentity, PluginMetadata, RegisteredPlugin, ShellError,
}; };
use std::{ use std::{
collections::HashMap, collections::HashMap,
@ -31,6 +31,8 @@ pub struct PersistentPlugin {
struct MutableState { struct MutableState {
/// Reference to the plugin if running /// Reference to the plugin if running
running: Option<RunningPlugin>, running: Option<RunningPlugin>,
/// Metadata for the plugin, e.g. version.
metadata: Option<PluginMetadata>,
/// Plugin's preferred communication mode (if known) /// Plugin's preferred communication mode (if known)
preferred_mode: Option<PreferredCommunicationMode>, preferred_mode: Option<PreferredCommunicationMode>,
/// Garbage collector config /// Garbage collector config
@ -59,6 +61,7 @@ impl PersistentPlugin {
identity, identity,
mutable: Mutex::new(MutableState { mutable: Mutex::new(MutableState {
running: None, running: None,
metadata: None,
preferred_mode: None, preferred_mode: None,
gc_config, gc_config,
}), }),
@ -268,6 +271,16 @@ impl RegisteredPlugin for PersistentPlugin {
self.stop_internal(true) self.stop_internal(true)
} }
fn metadata(&self) -> Option<PluginMetadata> {
self.mutable.lock().ok().and_then(|m| m.metadata.clone())
}
fn set_metadata(&self, metadata: Option<PluginMetadata>) {
if let Ok(mut mutable) = self.mutable.lock() {
mutable.metadata = metadata;
}
}
fn set_gc_config(&self, gc_config: &PluginGcConfig) { fn set_gc_config(&self, gc_config: &PluginGcConfig) {
if let Ok(mut mutable) = self.mutable.lock() { if let Ok(mut mutable) = self.mutable.lock() {
// Save the new config for future calls // Save the new config for future calls

View File

@ -23,7 +23,7 @@ pub mod test_util;
use nu_protocol::{ use nu_protocol::{
ast::Operator, engine::Closure, ByteStreamType, Config, LabeledError, PipelineData, ast::Operator, engine::Closure, ByteStreamType, Config, LabeledError, PipelineData,
PluginSignature, ShellError, Span, Spanned, Value, PluginMetadata, PluginSignature, ShellError, Span, Spanned, Value,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@ -119,6 +119,7 @@ pub struct ByteStreamInfo {
/// Calls that a plugin can execute. The type parameter determines the input type. /// Calls that a plugin can execute. The type parameter determines the input type.
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub enum PluginCall<D> { pub enum PluginCall<D> {
Metadata,
Signature, Signature,
Run(CallInfo<D>), Run(CallInfo<D>),
CustomValueOp(Spanned<PluginCustomValue>, CustomValueOp), CustomValueOp(Spanned<PluginCustomValue>, CustomValueOp),
@ -132,6 +133,7 @@ impl<D> PluginCall<D> {
f: impl FnOnce(D) -> Result<T, ShellError>, f: impl FnOnce(D) -> Result<T, ShellError>,
) -> Result<PluginCall<T>, ShellError> { ) -> Result<PluginCall<T>, ShellError> {
Ok(match self { Ok(match self {
PluginCall::Metadata => PluginCall::Metadata,
PluginCall::Signature => PluginCall::Signature, PluginCall::Signature => PluginCall::Signature,
PluginCall::Run(call) => PluginCall::Run(call.map_data(f)?), PluginCall::Run(call) => PluginCall::Run(call.map_data(f)?),
PluginCall::CustomValueOp(custom_value, op) => { PluginCall::CustomValueOp(custom_value, op) => {
@ -143,6 +145,7 @@ impl<D> PluginCall<D> {
/// The span associated with the call. /// The span associated with the call.
pub fn span(&self) -> Option<Span> { pub fn span(&self) -> Option<Span> {
match self { match self {
PluginCall::Metadata => None,
PluginCall::Signature => None, PluginCall::Signature => None,
PluginCall::Run(CallInfo { call, .. }) => Some(call.head), PluginCall::Run(CallInfo { call, .. }) => Some(call.head),
PluginCall::CustomValueOp(val, _) => Some(val.span), PluginCall::CustomValueOp(val, _) => Some(val.span),
@ -309,6 +312,7 @@ pub enum StreamMessage {
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub enum PluginCallResponse<D> { pub enum PluginCallResponse<D> {
Error(LabeledError), Error(LabeledError),
Metadata(PluginMetadata),
Signature(Vec<PluginSignature>), Signature(Vec<PluginSignature>),
Ordering(Option<Ordering>), Ordering(Option<Ordering>),
PipelineData(D), PipelineData(D),
@ -323,6 +327,7 @@ impl<D> PluginCallResponse<D> {
) -> Result<PluginCallResponse<T>, ShellError> { ) -> Result<PluginCallResponse<T>, ShellError> {
Ok(match self { Ok(match self {
PluginCallResponse::Error(err) => PluginCallResponse::Error(err), PluginCallResponse::Error(err) => PluginCallResponse::Error(err),
PluginCallResponse::Metadata(meta) => PluginCallResponse::Metadata(meta),
PluginCallResponse::Signature(sigs) => PluginCallResponse::Signature(sigs), PluginCallResponse::Signature(sigs) => PluginCallResponse::Signature(sigs),
PluginCallResponse::Ordering(ordering) => PluginCallResponse::Ordering(ordering), PluginCallResponse::Ordering(ordering) => PluginCallResponse::Ordering(ordering),
PluginCallResponse::PipelineData(input) => PluginCallResponse::PipelineData(f(input)?), PluginCallResponse::PipelineData(input) => PluginCallResponse::PipelineData(f(input)?),

View File

@ -6,7 +6,7 @@ use std::{
use nu_plugin_engine::{GetPlugin, PluginInterface}; use nu_plugin_engine::{GetPlugin, PluginInterface};
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError, PluginGcConfig, PluginIdentity, PluginMetadata, RegisteredPlugin, ShellError,
}; };
pub struct FakePersistentPlugin { pub struct FakePersistentPlugin {
@ -42,6 +42,12 @@ impl RegisteredPlugin for FakePersistentPlugin {
None None
} }
fn metadata(&self) -> Option<PluginMetadata> {
None
}
fn set_metadata(&self, _metadata: Option<PluginMetadata>) {}
fn set_gc_config(&self, _gc_config: &PluginGcConfig) { fn set_gc_config(&self, _gc_config: &PluginGcConfig) {
// We don't have a GC // We don't have a GC
} }

View File

@ -66,6 +66,10 @@
//! } //! }
//! //!
//! impl Plugin for LowercasePlugin { //! impl Plugin for LowercasePlugin {
//! fn version(&self) -> String {
//! env!("CARGO_PKG_VERSION").into()
//! }
//!
//! fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> { //! fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
//! vec![Box::new(Lowercase)] //! vec![Box::new(Lowercase)]
//! } //! }

View File

@ -53,6 +53,10 @@ struct IntoU32;
struct IntoIntFromU32; struct IntoIntFromU32;
impl Plugin for CustomU32Plugin { impl Plugin for CustomU32Plugin {
fn version(&self) -> String {
"0.0.0".into()
}
fn commands(&self) -> Vec<Box<dyn nu_plugin::PluginCommand<Plugin = Self>>> { fn commands(&self) -> Vec<Box<dyn nu_plugin::PluginCommand<Plugin = Self>>> {
vec![Box::new(IntoU32), Box::new(IntoIntFromU32)] vec![Box::new(IntoU32), Box::new(IntoIntFromU32)]
} }

View File

@ -8,6 +8,10 @@ struct HelloPlugin;
struct Hello; struct Hello;
impl Plugin for HelloPlugin { impl Plugin for HelloPlugin {
fn version(&self) -> String {
"0.0.0".into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![Box::new(Hello)] vec![Box::new(Hello)]
} }

View File

@ -59,6 +59,10 @@ impl PluginCommand for Lowercase {
} }
impl Plugin for LowercasePlugin { impl Plugin for LowercasePlugin {
fn version(&self) -> String {
"0.0.0".into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![Box::new(Lowercase)] vec![Box::new(Lowercase)]
} }

View File

@ -24,6 +24,10 @@
//! struct MyCommand; //! struct MyCommand;
//! //!
//! impl Plugin for MyPlugin { //! impl Plugin for MyPlugin {
//! fn version(&self) -> String {
//! env!("CARGO_PKG_VERSION").into()
//! }
//!
//! fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { //! fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
//! vec![Box::new(MyCommand)] //! vec![Box::new(MyCommand)]
//! } //! }

View File

@ -60,6 +60,9 @@ use crate::{EngineInterface, EvaluatedCall, Plugin};
/// } /// }
/// ///
/// # impl Plugin for LowercasePlugin { /// # impl Plugin for LowercasePlugin {
/// # fn version(&self) -> String {
/// # "0.0.0".into()
/// # }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> { /// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
/// # vec![Box::new(Lowercase)] /// # vec![Box::new(Lowercase)]
/// # } /// # }
@ -195,6 +198,9 @@ pub trait PluginCommand: Sync {
/// } /// }
/// ///
/// # impl Plugin for HelloPlugin { /// # impl Plugin for HelloPlugin {
/// # fn version(&self) -> String {
/// # "0.0.0".into()
/// # }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> { /// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
/// # vec![Box::new(Hello)] /// # vec![Box::new(Hello)]
/// # } /// # }

View File

@ -11,8 +11,8 @@ use nu_plugin_protocol::{
ProtocolInfo, ProtocolInfo,
}; };
use nu_protocol::{ use nu_protocol::{
engine::Closure, Config, LabeledError, PipelineData, PluginSignature, ShellError, Span, engine::Closure, Config, LabeledError, PipelineData, PluginMetadata, PluginSignature,
Spanned, Value, ShellError, Span, Spanned, Value,
}; };
use std::{ use std::{
collections::{btree_map, BTreeMap, HashMap}, collections::{btree_map, BTreeMap, HashMap},
@ -29,6 +29,9 @@ use std::{
#[derive(Debug)] #[derive(Debug)]
#[doc(hidden)] #[doc(hidden)]
pub enum ReceivedPluginCall { pub enum ReceivedPluginCall {
Metadata {
engine: EngineInterface,
},
Signature { Signature {
engine: EngineInterface, engine: EngineInterface,
}, },
@ -280,8 +283,11 @@ impl InterfaceManager for EngineInterfaceManager {
} }
}; };
match call { match call {
// We just let the receiver handle it rather than trying to store signature here // Ask the plugin for metadata
// or something PluginCall::Metadata => {
self.send_plugin_call(ReceivedPluginCall::Metadata { engine: interface })
}
// Ask the plugin for signatures
PluginCall::Signature => { PluginCall::Signature => {
self.send_plugin_call(ReceivedPluginCall::Signature { engine: interface }) self.send_plugin_call(ReceivedPluginCall::Signature { engine: interface })
} }
@ -416,6 +422,13 @@ impl EngineInterface {
} }
} }
/// Write a call response of plugin metadata.
pub(crate) fn write_metadata(&self, metadata: PluginMetadata) -> Result<(), ShellError> {
let response = PluginCallResponse::Metadata(metadata);
self.write(PluginOutput::CallResponse(self.context()?, response))?;
self.flush()
}
/// Write a call response of plugin signatures. /// Write a call response of plugin signatures.
/// ///
/// Any custom values in the examples will be rendered using `to_base_value()`. /// Any custom values in the examples will be rendered using `to_base_value()`.

View File

@ -322,6 +322,26 @@ fn manager_consume_goodbye_closes_plugin_call_channel() -> Result<(), ShellError
Ok(()) Ok(())
} }
#[test]
fn manager_consume_call_metadata_forwards_to_receiver_with_context() -> Result<(), ShellError> {
let mut manager = TestCase::new().engine();
set_default_protocol_info(&mut manager)?;
let rx = manager
.take_plugin_call_receiver()
.expect("couldn't take receiver");
manager.consume(PluginInput::Call(0, PluginCall::Metadata))?;
match rx.try_recv().expect("call was not forwarded to receiver") {
ReceivedPluginCall::Metadata { engine } => {
assert_eq!(Some(0), engine.context);
Ok(())
}
call => panic!("wrong call type: {call:?}"),
}
}
#[test] #[test]
fn manager_consume_call_signature_forwards_to_receiver_with_context() -> Result<(), ShellError> { fn manager_consume_call_signature_forwards_to_receiver_with_context() -> Result<(), ShellError> {
let mut manager = TestCase::new().engine(); let mut manager = TestCase::new().engine();

View File

@ -16,7 +16,8 @@ use nu_plugin_core::{
}; };
use nu_plugin_protocol::{CallInfo, CustomValueOp, PluginCustomValue, PluginInput, PluginOutput}; use nu_plugin_protocol::{CallInfo, CustomValueOp, PluginCustomValue, PluginInput, PluginOutput};
use nu_protocol::{ use nu_protocol::{
ast::Operator, CustomValue, IntoSpanned, LabeledError, PipelineData, ShellError, Spanned, Value, ast::Operator, CustomValue, IntoSpanned, LabeledError, PipelineData, PluginMetadata,
ShellError, Spanned, Value,
}; };
use thiserror::Error; use thiserror::Error;
@ -52,6 +53,10 @@ pub(crate) const OUTPUT_BUFFER_SIZE: usize = 16384;
/// struct Hello; /// struct Hello;
/// ///
/// impl Plugin for HelloPlugin { /// impl Plugin for HelloPlugin {
/// fn version(&self) -> String {
/// env!("CARGO_PKG_VERSION").into()
/// }
///
/// fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> { /// fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
/// vec![Box::new(Hello)] /// vec![Box::new(Hello)]
/// } /// }
@ -89,6 +94,23 @@ pub(crate) const OUTPUT_BUFFER_SIZE: usize = 16384;
/// # } /// # }
/// ``` /// ```
pub trait Plugin: Sync { pub trait Plugin: Sync {
/// The version of the plugin.
///
/// The recommended implementation, which will use the version from your crate's `Cargo.toml`
/// file:
///
/// ```no_run
/// # use nu_plugin::{Plugin, PluginCommand};
/// # struct MyPlugin;
/// # impl Plugin for MyPlugin {
/// fn version(&self) -> String {
/// env!("CARGO_PKG_VERSION").into()
/// }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { vec![] }
/// # }
/// ```
fn version(&self) -> String;
/// The commands supported by the plugin /// The commands supported by the plugin
/// ///
/// Each [`PluginCommand`] contains both the signature of the command and the functionality it /// Each [`PluginCommand`] contains both the signature of the command and the functionality it
@ -216,6 +238,7 @@ pub trait Plugin: Sync {
/// # struct MyPlugin; /// # struct MyPlugin;
/// # impl MyPlugin { fn new() -> Self { Self }} /// # impl MyPlugin { fn new() -> Self { Self }}
/// # impl Plugin for MyPlugin { /// # impl Plugin for MyPlugin {
/// # fn version(&self) -> String { "0.0.0".into() }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {todo!();} /// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {todo!();}
/// # } /// # }
/// fn main() { /// fn main() {
@ -504,6 +527,12 @@ where
} }
match plugin_call { match plugin_call {
// Send metadata back to nushell so it can be stored with the plugin signatures
ReceivedPluginCall::Metadata { engine } => {
engine
.write_metadata(PluginMetadata::new().with_version(plugin.version()))
.try_to_report(&engine)?;
}
// Sending the signature back to nushell to create the declaration definition // Sending the signature back to nushell to create the declaration definition
ReceivedPluginCall::Signature { engine } => { ReceivedPluginCall::Signature { engine } => {
let sigs = commands let sigs = commands

View File

@ -0,0 +1,38 @@
use serde::{Deserialize, Serialize};
/// Metadata about the installed plugin. This is cached in the registry file along with the
/// signatures. None of the metadata fields are required, and more may be added in the future.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[non_exhaustive]
pub struct PluginMetadata {
/// The version of the plugin itself, as self-reported.
pub version: Option<String>,
}
impl PluginMetadata {
/// Create empty metadata.
pub const fn new() -> PluginMetadata {
PluginMetadata { version: None }
}
/// Set the version of the plugin on the metadata. A suggested way to construct this is:
///
/// ```no_run
/// # use nu_protocol::PluginMetadata;
/// # fn example() -> PluginMetadata {
/// PluginMetadata::new().with_version(env!("CARGO_PKG_VERSION"))
/// # }
/// ```
///
/// which will use the version of your plugin's crate from its `Cargo.toml` file.
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
}
impl Default for PluginMetadata {
fn default() -> Self {
Self::new()
}
}

View File

@ -1,9 +1,11 @@
mod identity; mod identity;
mod metadata;
mod registered; mod registered;
mod registry_file; mod registry_file;
mod signature; mod signature;
pub use identity::*; pub use identity::*;
pub use metadata::*;
pub use registered::*; pub use registered::*;
pub use registry_file::*; pub use registry_file::*;
pub use signature::*; pub use signature::*;

View File

@ -1,6 +1,6 @@
use std::{any::Any, sync::Arc}; use std::{any::Any, sync::Arc};
use crate::{PluginGcConfig, PluginIdentity, ShellError}; use crate::{PluginGcConfig, PluginIdentity, PluginMetadata, ShellError};
/// Trait for plugins registered in the [`EngineState`](crate::engine::EngineState). /// Trait for plugins registered in the [`EngineState`](crate::engine::EngineState).
pub trait RegisteredPlugin: Send + Sync { pub trait RegisteredPlugin: Send + Sync {
@ -13,6 +13,12 @@ pub trait RegisteredPlugin: Send + Sync {
/// Process ID of the plugin executable, if running. /// Process ID of the plugin executable, if running.
fn pid(&self) -> Option<u32>; fn pid(&self) -> Option<u32>;
/// Get metadata for the plugin, if set.
fn metadata(&self) -> Option<PluginMetadata>;
/// Set metadata for the plugin.
fn set_metadata(&self, metadata: Option<PluginMetadata>);
/// Set garbage collection config for the plugin. /// Set garbage collection config for the plugin.
fn set_gc_config(&self, gc_config: &PluginGcConfig); fn set_gc_config(&self, gc_config: &PluginGcConfig);

View File

@ -5,7 +5,7 @@ use std::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{PluginIdentity, PluginSignature, ShellError, Span}; use crate::{PluginIdentity, PluginMetadata, PluginSignature, ShellError, Span};
// This has a big impact on performance // This has a big impact on performance
const BUFFER_SIZE: usize = 65536; const BUFFER_SIZE: usize = 65536;
@ -121,9 +121,10 @@ pub struct PluginRegistryItem {
} }
impl PluginRegistryItem { impl PluginRegistryItem {
/// Create a [`PluginRegistryItem`] from an identity and signatures. /// Create a [`PluginRegistryItem`] from an identity, metadata, and signatures.
pub fn new( pub fn new(
identity: &PluginIdentity, identity: &PluginIdentity,
metadata: PluginMetadata,
mut commands: Vec<PluginSignature>, mut commands: Vec<PluginSignature>,
) -> PluginRegistryItem { ) -> PluginRegistryItem {
// Sort the commands for consistency // Sort the commands for consistency
@ -133,7 +134,7 @@ impl PluginRegistryItem {
name: identity.name().to_owned(), name: identity.name().to_owned(),
filename: identity.filename().to_owned(), filename: identity.filename().to_owned(),
shell: identity.shell().map(|p| p.to_owned()), shell: identity.shell().map(|p| p.to_owned()),
data: PluginRegistryItemData::Valid { commands }, data: PluginRegistryItemData::Valid { metadata, commands },
} }
} }
} }
@ -144,6 +145,9 @@ impl PluginRegistryItem {
#[serde(untagged)] #[serde(untagged)]
pub enum PluginRegistryItemData { pub enum PluginRegistryItemData {
Valid { Valid {
/// Metadata for the plugin, including its version.
#[serde(default)]
metadata: PluginMetadata,
/// Signatures and examples for each command provided by the plugin. /// Signatures and examples for each command provided by the plugin.
commands: Vec<PluginSignature>, commands: Vec<PluginSignature>,
}, },

View File

@ -1,6 +1,7 @@
use super::{PluginRegistryFile, PluginRegistryItem, PluginRegistryItemData}; use super::{PluginRegistryFile, PluginRegistryItem, PluginRegistryItemData};
use crate::{ use crate::{
Category, PluginExample, PluginSignature, ShellError, Signature, SyntaxShape, Type, Value, Category, PluginExample, PluginMetadata, PluginSignature, ShellError, Signature, SyntaxShape,
Type, Value,
}; };
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use std::io::Cursor; use std::io::Cursor;
@ -11,6 +12,9 @@ fn foo_plugin() -> PluginRegistryItem {
filename: "/path/to/nu_plugin_foo".into(), filename: "/path/to/nu_plugin_foo".into(),
shell: None, shell: None,
data: PluginRegistryItemData::Valid { data: PluginRegistryItemData::Valid {
metadata: PluginMetadata {
version: Some("0.1.0".into()),
},
commands: vec![PluginSignature { commands: vec![PluginSignature {
sig: Signature::new("foo") sig: Signature::new("foo")
.input_output_type(Type::Int, Type::List(Box::new(Type::Int))) .input_output_type(Type::Int, Type::List(Box::new(Type::Int)))
@ -36,6 +40,9 @@ fn bar_plugin() -> PluginRegistryItem {
filename: "/path/to/nu_plugin_bar".into(), filename: "/path/to/nu_plugin_bar".into(),
shell: None, shell: None,
data: PluginRegistryItemData::Valid { data: PluginRegistryItemData::Valid {
metadata: PluginMetadata {
version: Some("0.2.0".into()),
},
commands: vec![PluginSignature { commands: vec![PluginSignature {
sig: Signature::new("bar") sig: Signature::new("bar")
.usage("overwrites files with random data") .usage("overwrites files with random data")

View File

@ -42,6 +42,10 @@ impl CustomValuePlugin {
} }
impl Plugin for CustomValuePlugin { impl Plugin for CustomValuePlugin {
fn version(&self) -> String {
env!("CARGO_PKG_VERSION").into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![ vec![
Box::new(Generate), Box::new(Generate),

View File

@ -7,6 +7,10 @@ pub use commands::*;
pub use example::ExamplePlugin; pub use example::ExamplePlugin;
impl Plugin for ExamplePlugin { impl Plugin for ExamplePlugin {
fn version(&self) -> String {
env!("CARGO_PKG_VERSION").into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
// This is a list of all of the commands you would like Nu to register when your plugin is // This is a list of all of the commands you would like Nu to register when your plugin is
// loaded. // loaded.

View File

@ -10,6 +10,10 @@ pub use from::vcf::FromVcf;
pub struct FromCmds; pub struct FromCmds;
impl Plugin for FromCmds { impl Plugin for FromCmds {
fn version(&self) -> String {
env!("CARGO_PKG_VERSION").into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![ vec![
Box::new(FromEml), Box::new(FromEml),

View File

@ -5,6 +5,10 @@ use nu_protocol::{Category, LabeledError, Signature, Spanned, SyntaxShape, Value
pub struct GStatPlugin; pub struct GStatPlugin;
impl Plugin for GStatPlugin { impl Plugin for GStatPlugin {
fn version(&self) -> String {
env!("CARGO_PKG_VERSION").into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![Box::new(GStat)] vec![Box::new(GStat)]
} }

View File

@ -5,6 +5,10 @@ use nu_protocol::{ast::CellPath, LabeledError, Signature, SyntaxShape, Value};
pub struct IncPlugin; pub struct IncPlugin;
impl Plugin for IncPlugin { impl Plugin for IncPlugin {
fn version(&self) -> String {
env!("CARGO_PKG_VERSION").into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![Box::new(Inc::new())] vec![Box::new(Inc::new())]
} }

View File

@ -7,6 +7,7 @@
# language without adding any extra dependencies to our tests. # language without adding any extra dependencies to our tests.
const NUSHELL_VERSION = "0.94.3" const NUSHELL_VERSION = "0.94.3"
const PLUGIN_VERSION = "0.1.0" # bump if you change commands!
def main [--stdio] { def main [--stdio] {
if ($stdio) { if ($stdio) {
@ -229,6 +230,13 @@ def handle_input []: any -> nothing {
} }
{ Call: [$id, $plugin_call] } => { { Call: [$id, $plugin_call] } => {
match $plugin_call { match $plugin_call {
"Metadata" => {
write_response $id {
Metadata: {
version: $PLUGIN_VERSION
}
}
}
"Signature" => { "Signature" => {
write_response $id { Signature: $SIGNATURES } write_response $id { Signature: $SIGNATURES }
} }

View File

@ -99,6 +99,10 @@ pub struct PolarsPlugin {
} }
impl Plugin for PolarsPlugin { impl Plugin for PolarsPlugin {
fn version(&self) -> String {
env!("CARGO_PKG_VERSION").into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
let mut commands: Vec<Box<dyn PluginCommand<Plugin = Self>>> = vec![Box::new(PolarsCmd)]; let mut commands: Vec<Box<dyn PluginCommand<Plugin = Self>>> = vec![Box::new(PolarsCmd)];
commands.append(&mut eager_commands()); commands.append(&mut eager_commands());

View File

@ -28,6 +28,7 @@ import json
NUSHELL_VERSION = "0.94.3" NUSHELL_VERSION = "0.94.3"
PLUGIN_VERSION = "0.1.0" # bump if you change commands!
def signatures(): def signatures():
@ -228,7 +229,13 @@ def handle_input(input):
exit(0) exit(0)
elif "Call" in input: elif "Call" in input:
[id, plugin_call] = input["Call"] [id, plugin_call] = input["Call"]
if plugin_call == "Signature": if plugin_call == "Metadata":
write_response(id, {
"Metadata": {
"version": PLUGIN_VERSION,
}
})
elif plugin_call == "Signature":
write_response(id, signatures()) write_response(id, signatures())
elif "Run" in plugin_call: elif "Run" in plugin_call:
process_call(id, plugin_call["Run"]) process_call(id, plugin_call["Run"])

View File

@ -16,6 +16,10 @@ impl Query {
} }
impl Plugin for Query { impl Plugin for Query {
fn version(&self) -> String {
env!("CARGO_PKG_VERSION").into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![ vec![
Box::new(QueryCommand), Box::new(QueryCommand),

View File

@ -136,7 +136,21 @@ fn handle_message(
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
if let Some(plugin_call) = message.get("Call") { if let Some(plugin_call) = message.get("Call") {
let (id, plugin_call) = (&plugin_call[0], &plugin_call[1]); let (id, plugin_call) = (&plugin_call[0], &plugin_call[1]);
if plugin_call.as_str() == Some("Signature") { if plugin_call.as_str() == Some("Metadata") {
write(
output,
&json!({
"CallResponse": [
id,
{
"Metadata": {
"version": env!("CARGO_PKG_VERSION"),
}
}
]
}),
)
} else if plugin_call.as_str() == Some("Signature") {
write( write(
output, output,
&json!({ &json!({

View File

@ -400,7 +400,7 @@ fn main() -> Result<()> {
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
if let Some(plugins) = &parsed_nu_cli_args.plugins { if let Some(plugins) = &parsed_nu_cli_args.plugins {
use nu_plugin_engine::{GetPlugin, PluginDeclaration}; use nu_plugin_engine::{GetPlugin, PluginDeclaration};
use nu_protocol::{engine::StateWorkingSet, ErrSpan, PluginIdentity}; use nu_protocol::{engine::StateWorkingSet, ErrSpan, PluginIdentity, RegisteredPlugin};
// Load any plugins specified with --plugins // Load any plugins specified with --plugins
start_time = std::time::Instant::now(); start_time = std::time::Instant::now();
@ -419,8 +419,14 @@ fn main() -> Result<()> {
// Create the plugin and add it to the working set // Create the plugin and add it to the working set
let plugin = nu_plugin_engine::add_plugin_to_working_set(&mut working_set, &identity)?; let plugin = nu_plugin_engine::add_plugin_to_working_set(&mut working_set, &identity)?;
// Spawn the plugin to get its signatures, and then add the commands to the working set // Spawn the plugin to get the metadata and signatures
for signature in plugin.clone().get_plugin(None)?.get_signature()? { let interface = plugin.clone().get_plugin(None)?;
// Set its metadata
plugin.set_metadata(Some(interface.get_metadata()?));
// Add the commands from the signature to the working set
for signature in interface.get_signature()? {
let decl = PluginDeclaration::new(plugin.clone(), signature); let decl = PluginDeclaration::new(plugin.clone(), signature);
working_set.add_decl(Box::new(decl)); working_set.add_decl(Box::new(decl));
} }

View File

@ -16,6 +16,17 @@ fn plugin_list_shows_installed_plugins() {
assert!(out.status.success()); assert!(out.status.success());
} }
#[test]
fn plugin_list_shows_installed_plugin_version() {
let out = nu_with_plugins!(
cwd: ".",
plugin: ("nu_plugin_inc"),
r#"(plugin list).version.0"#
);
assert_eq!(env!("CARGO_PKG_VERSION"), out.out);
assert!(out.status.success());
}
#[test] #[test]
fn plugin_keeps_running_after_calling_it() { fn plugin_keeps_running_after_calling_it() {
let out = nu_with_plugins!( let out = nu_with_plugins!(

View File

@ -18,6 +18,13 @@ fn example_plugin_path() -> PathBuf {
.expect("nu_plugin_example not found") .expect("nu_plugin_example not found")
} }
fn valid_plugin_item_data() -> PluginRegistryItemData {
PluginRegistryItemData::Valid {
metadata: Default::default(),
commands: vec![],
}
}
#[test] #[test]
fn plugin_add_then_restart_nu() { fn plugin_add_then_restart_nu() {
let result = nu_with_plugins!( let result = nu_with_plugins!(
@ -149,7 +156,7 @@ fn plugin_rm_then_restart_nu() {
name: "example".into(), name: "example".into(),
filename: example_plugin_path, filename: example_plugin_path,
shell: None, shell: None,
data: PluginRegistryItemData::Valid { commands: vec![] }, data: valid_plugin_item_data(),
}); });
contents.upsert_plugin(PluginRegistryItem { contents.upsert_plugin(PluginRegistryItem {
@ -157,7 +164,7 @@ fn plugin_rm_then_restart_nu() {
// this doesn't exist, but it should be ok // this doesn't exist, but it should be ok
filename: dirs.test().join("nu_plugin_foo"), filename: dirs.test().join("nu_plugin_foo"),
shell: None, shell: None,
data: PluginRegistryItemData::Valid { commands: vec![] }, data: valid_plugin_item_data(),
}); });
contents contents
@ -225,7 +232,7 @@ fn plugin_rm_from_custom_path() {
name: "example".into(), name: "example".into(),
filename: example_plugin_path, filename: example_plugin_path,
shell: None, shell: None,
data: PluginRegistryItemData::Valid { commands: vec![] }, data: valid_plugin_item_data(),
}); });
contents.upsert_plugin(PluginRegistryItem { contents.upsert_plugin(PluginRegistryItem {
@ -233,7 +240,7 @@ fn plugin_rm_from_custom_path() {
// this doesn't exist, but it should be ok // this doesn't exist, but it should be ok
filename: dirs.test().join("nu_plugin_foo"), filename: dirs.test().join("nu_plugin_foo"),
shell: None, shell: None,
data: PluginRegistryItemData::Valid { commands: vec![] }, data: valid_plugin_item_data(),
}); });
contents contents
@ -273,7 +280,7 @@ fn plugin_rm_using_filename() {
name: "example".into(), name: "example".into(),
filename: example_plugin_path.clone(), filename: example_plugin_path.clone(),
shell: None, shell: None,
data: PluginRegistryItemData::Valid { commands: vec![] }, data: valid_plugin_item_data(),
}); });
contents.upsert_plugin(PluginRegistryItem { contents.upsert_plugin(PluginRegistryItem {
@ -281,7 +288,7 @@ fn plugin_rm_using_filename() {
// this doesn't exist, but it should be ok // this doesn't exist, but it should be ok
filename: dirs.test().join("nu_plugin_foo"), filename: dirs.test().join("nu_plugin_foo"),
shell: None, shell: None,
data: PluginRegistryItemData::Valid { commands: vec![] }, data: valid_plugin_item_data(),
}); });
contents contents
@ -331,7 +338,7 @@ fn warning_on_invalid_plugin_item() {
name: "example".into(), name: "example".into(),
filename: example_plugin_path, filename: example_plugin_path,
shell: None, shell: None,
data: PluginRegistryItemData::Valid { commands: vec![] }, data: valid_plugin_item_data(),
}); });
contents.upsert_plugin(PluginRegistryItem { contents.upsert_plugin(PluginRegistryItem {