Skip to main content

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;