osp_cli/native.rs
1//! In-process native command surface.
2//!
3//! This module exists so `osp` can expose built-in commands through the same
4//! catalog, policy, and dispatch-adjacent shapes that plugin commands use,
5//! without spawning a subprocess.
6//!
7//! High-level flow:
8//!
9//! - register native command implementations in a [`NativeCommandRegistry`]
10//! - describe them through clap-derived metadata
11//! - project that metadata into completion, help, and policy surfaces
12//! - execute them in-process with a small runtime context
13//!
14//! Contract:
15//!
16//! - native commands are the in-process counterpart to plugin commands
17//! - catalog and policy projection should stay aligned with the plugin-facing
18//! protocol types in `crate::core::plugin`
19//!
20//! Public API shape:
21//!
22//! - [`NativeCommandRegistry`] is the canonical registration surface
23//! - catalog/context structs stay plain describe-time or execute-time payloads
24//! - commands should expose behavior through the registry rather than by
25//! leaking host-internal runtime state
26
27use std::collections::BTreeMap;
28use std::sync::Arc;
29
30use anyhow::Result;
31use clap::Command;
32
33use crate::completion::CommandSpec;
34use crate::config::ResolvedConfig;
35use crate::core::command_policy::CommandPolicyRegistry;
36use crate::core::plugin::{DescribeCommandAuthV1, DescribeCommandV1, ResponseV1};
37use crate::core::runtime::RuntimeHints;
38
39/// Public metadata snapshot for one registered native command.
40///
41/// This is the describe-time surface projected into help, completion, and
42/// policy code. It is not an execution handle; callers should fetch the command
43/// from [`NativeCommandRegistry`] when they need to run it.
44#[derive(Debug, Clone)]
45pub struct NativeCommandCatalogEntry {
46 /// Canonical command path root exposed to CLI and REPL users.
47 pub name: String,
48 /// Short human-facing summary used in listings and overviews.
49 pub about: String,
50 /// Optional auth/visibility metadata projected into policy surfaces.
51 pub auth: Option<DescribeCommandAuthV1>,
52 /// Direct child names available immediately below this command.
53 pub subcommands: Vec<String>,
54 /// Completion tree rooted at this command's describe-time shape.
55 pub completion: CommandSpec,
56}
57
58/// Runtime context passed to native command implementations.
59///
60/// This keeps the command surface small and stable: commands receive the
61/// resolved config snapshot and runtime hints they need to behave like the host
62/// would, without exposing the whole app runtime for ad hoc coupling.
63pub struct NativeCommandContext<'a> {
64 /// Current resolved config snapshot for this execution.
65 pub config: &'a ResolvedConfig,
66 /// Runtime hints that should be propagated to child processes and adapters.
67 pub runtime_hints: RuntimeHints,
68}
69
70impl<'a> NativeCommandContext<'a> {
71 /// Creates the runtime context passed to one native-command execution.
72 pub fn new(config: &'a ResolvedConfig, runtime_hints: RuntimeHints) -> Self {
73 Self {
74 config,
75 runtime_hints,
76 }
77 }
78}
79
80/// Result of executing a native command.
81pub enum NativeCommandOutcome {
82 /// Return rendered help text directly.
83 Help(String),
84 /// Return a protocol response payload.
85 Response(Box<ResponseV1>),
86 /// Exit immediately with the given status code.
87 Exit(i32),
88}
89
90/// Trait implemented by in-process commands registered alongside plugins.
91pub trait NativeCommand: Send + Sync {
92 /// Returns the clap command definition for this command.
93 fn command(&self) -> Command;
94
95 /// Returns optional auth/visibility metadata for the command.
96 fn auth(&self) -> Option<DescribeCommandAuthV1> {
97 None
98 }
99
100 /// Builds the plugin-protocol style description for this command.
101 fn describe(&self) -> DescribeCommandV1 {
102 let mut describe = DescribeCommandV1::from_clap(self.command());
103 describe.auth = self.auth();
104 describe
105 }
106
107 /// Executes the command using already-parsed argument tokens.
108 fn execute(
109 &self,
110 args: &[String],
111 context: &NativeCommandContext<'_>,
112 ) -> Result<NativeCommandOutcome>;
113}
114
115/// Registry of in-process native commands exposed alongside plugin commands.
116#[derive(Clone, Default)]
117pub struct NativeCommandRegistry {
118 commands: Arc<BTreeMap<String, Arc<dyn NativeCommand>>>,
119}
120
121impl NativeCommandRegistry {
122 /// Creates an empty native command registry.
123 pub fn new() -> Self {
124 Self::default()
125 }
126
127 /// Returns a registry with one additional registered command.
128 pub fn with_command(mut self, command: impl NativeCommand + 'static) -> Self {
129 self.register(command);
130 self
131 }
132
133 /// Registers or replaces a native command by normalized command name.
134 pub fn register(&mut self, command: impl NativeCommand + 'static) {
135 let mut next = (*self.commands).clone();
136 let command = Arc::new(command) as Arc<dyn NativeCommand>;
137 let name = normalize_name(&command.describe().name);
138 next.insert(name, command);
139 self.commands = Arc::new(next);
140 }
141
142 /// Returns `true` when no native commands are registered.
143 pub fn is_empty(&self) -> bool {
144 self.commands.is_empty()
145 }
146
147 /// Returns a registered command by normalized name.
148 ///
149 /// Lookup is case- and surrounding-whitespace-insensitive so callers can
150 /// reuse human-typed names without normalizing them first.
151 pub fn command(&self, name: &str) -> Option<&Arc<dyn NativeCommand>> {
152 self.commands.get(&normalize_name(name))
153 }
154
155 /// Returns catalog metadata for all registered native commands.
156 pub fn catalog(&self) -> Vec<NativeCommandCatalogEntry> {
157 self.commands
158 .values()
159 .map(|command| {
160 let describe = command.describe();
161 let completion = crate::plugin::conversion::to_command_spec(&describe);
162 NativeCommandCatalogEntry {
163 name: describe.name.clone(),
164 about: describe.about.clone(),
165 auth: describe.auth.clone(),
166 subcommands: crate::plugin::conversion::direct_subcommand_names(&completion),
167 completion,
168 }
169 })
170 .collect()
171 }
172
173 /// Builds a command-policy registry derived from command descriptions.
174 pub fn command_policy_registry(&self) -> CommandPolicyRegistry {
175 let mut registry = CommandPolicyRegistry::new();
176 for command in self.commands.values() {
177 let describe = command.describe();
178 register_describe_command_policies(&mut registry, &describe, &[]);
179 }
180 registry
181 }
182}
183
184fn register_describe_command_policies(
185 registry: &mut CommandPolicyRegistry,
186 command: &DescribeCommandV1,
187 parent: &[String],
188) {
189 let mut segments = parent.to_vec();
190 segments.push(command.name.clone());
191 if let Some(policy) = command.command_policy(crate::core::command_policy::CommandPath::new(
192 segments.clone(),
193 )) {
194 registry.register(policy);
195 }
196 for subcommand in &command.subcommands {
197 register_describe_command_policies(registry, subcommand, &segments);
198 }
199}
200
201fn normalize_name(value: &str) -> String {
202 value.trim().to_ascii_lowercase()
203}
204
205#[cfg(test)]
206mod tests;