feat(acp): add opencode_plugins module with types and parse_plugin_spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,3 +8,4 @@ pub mod preflight;
|
||||
pub mod registry;
|
||||
pub mod terminal_runtime;
|
||||
pub mod types;
|
||||
pub mod opencode_plugins;
|
||||
|
||||
86
src-tauri/src/acp/opencode_plugins.rs
Normal file
86
src-tauri/src/acp/opencode_plugins.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PluginStatus {
|
||||
Installed,
|
||||
Missing,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PluginInfo {
|
||||
pub name: String,
|
||||
pub declared_spec: String,
|
||||
pub installed_version: Option<String>,
|
||||
pub status: PluginStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PluginCheckSummary {
|
||||
pub config_path: PathBuf,
|
||||
pub cache_dir: PathBuf,
|
||||
pub plugins: Vec<PluginInfo>,
|
||||
pub has_project_config_hint: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PluginInstallEventKind {
|
||||
Started,
|
||||
Log,
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PluginInstallEvent {
|
||||
pub task_id: String,
|
||||
pub kind: PluginInstallEventKind,
|
||||
pub payload: String,
|
||||
}
|
||||
|
||||
/// Parse a plugin spec string from opencode.json `plugin[]` into (package_name, full_spec).
|
||||
///
|
||||
/// Examples:
|
||||
/// - `"foo"` → `Some(("foo", "foo"))`
|
||||
/// - `"foo@latest"` → `Some(("foo", "foo@latest"))`
|
||||
/// - `"foo@1.2.3"` → `Some(("foo", "foo@1.2.3"))`
|
||||
/// - `"@scope/name"` → `Some(("@scope/name", "@scope/name"))`
|
||||
/// - `"@scope/name@1.2.3"` → `Some(("@scope/name", "@scope/name@1.2.3"))`
|
||||
/// - `""` → `None`
|
||||
pub fn parse_plugin_spec(spec: &str) -> Option<(String, String)> {
|
||||
let spec = spec.trim();
|
||||
if spec.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let full_spec = spec.to_string();
|
||||
|
||||
if spec.starts_with('@') {
|
||||
// Scoped package: @scope/name or @scope/name@version
|
||||
let without_at = &spec[1..]; // strip leading @
|
||||
let slash_pos = without_at.find('/')?;
|
||||
let after_slash = &without_at[slash_pos + 1..];
|
||||
// Look for @ that separates name from version
|
||||
if let Some(version_at) = after_slash.find('@') {
|
||||
let name = &spec[..1 + slash_pos + 1 + version_at]; // @scope/name
|
||||
Some((name.to_string(), full_spec))
|
||||
} else {
|
||||
// No version part
|
||||
Some((spec.to_string(), full_spec))
|
||||
}
|
||||
} else {
|
||||
// Unscoped: name or name@version
|
||||
if let Some(at_pos) = spec.find('@') {
|
||||
let name = &spec[..at_pos];
|
||||
if name.is_empty() {
|
||||
return None; // bare "@" is invalid
|
||||
}
|
||||
Some((name.to_string(), full_spec))
|
||||
} else {
|
||||
Some((spec.to_string(), full_spec))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user