foundry_cli/introspect/document.rs
1//! Serializable types describing a Foundry binary's command surface.
2//!
3//! See [`docs/agents/spec.md`](../../../../docs/agents/spec.md) for the
4//! contract these types implement.
5
6use serde::{Deserialize, Serialize};
7use std::borrow::Cow;
8
9/// Top-level introspection document.
10#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
11pub struct IntrospectDocument {
12 /// Stable logical schema id, e.g. `foundry:introspect@v1`.
13 pub schema_id: String,
14 /// Schema version for the introspect document.
15 pub schema_version: u32,
16 /// Information about the binary being introspected.
17 pub binary: BinaryInfo,
18 /// Tree of commands exposed by the binary.
19 pub commands: Vec<CommandInfo>,
20}
21
22/// Information about the binary itself.
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
24pub struct BinaryInfo {
25 /// Binary name (`forge`, `cast`, `anvil`, `chisel`).
26 pub name: String,
27 /// Short version string.
28 pub version: String,
29 /// Long version string with build metadata.
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub long_version: Option<String>,
32 /// Description of the binary.
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub description: Option<String>,
35 /// Args accepted by every command (clap `global = true`).
36 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 pub global_args: Vec<ArgInfo>,
38}
39
40/// Information about a single command (or group) in the CLI tree.
41#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
42pub struct CommandInfo {
43 /// Stable machine identifier (e.g. `forge.build`).
44 pub command_id: String,
45 /// Whether `command_id` is pinned in the per-binary registry (frozen) or
46 /// derived from the clap path (provisional and may shift on CLI renames).
47 pub command_id_stable: bool,
48 /// Clap path components (e.g. `["forge", "build"]`).
49 pub path: Vec<String>,
50 /// Visible aliases for this command.
51 pub aliases: Vec<String>,
52 /// Short, single-line summary.
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub summary: Option<String>,
55 /// Long description (multi-line allowed).
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub description: Option<String>,
58 /// Arguments declared directly on this command.
59 pub args: Vec<ArgInfo>,
60 /// Subcommands of this command.
61 pub subcommands: Vec<Self>,
62 /// Capabilities reported for agent consumers.
63 pub capabilities: Capabilities,
64 /// Whether `capabilities` was authored in the registry. When `false`,
65 /// every capability field is a non-authoritative default and consumers
66 /// MUST treat side-effects, project requirement, etc. as unknown.
67 pub capabilities_declared: bool,
68 /// Command-specific exit codes (in addition to the global table).
69 pub exit_codes: Vec<ExitCodeInfo>,
70 /// Whether this command is hidden in the human-facing help.
71 #[serde(default, skip_serializing_if = "is_false")]
72 pub hidden: bool,
73}
74
75/// Capability flags exposed for agent consumers.
76#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
77pub struct Capabilities {
78 /// What the command emits when run in machine mode.
79 pub output_mode: OutputMode,
80 /// Stable schema id for the envelope payload.
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub result_schema_ref: Option<Cow<'static, str>>,
83 /// Stable schema id for stream event records.
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub event_schema_ref: Option<Cow<'static, str>>,
86 /// Stable schema id for session-record startup/state.
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub session_schema_ref: Option<Cow<'static, str>>,
89 /// Whether the command can take input on stdin via `--input -`.
90 pub reads_stdin: bool,
91 /// Whether the command supports `--output PATH`.
92 pub supports_output_path: bool,
93 /// Whether the command requires a Foundry project to run.
94 pub requires_project: bool,
95 /// Coarse classification of the command's side effects.
96 pub side_effects: SideEffects,
97 /// Whether the command can stream output for an extended period.
98 pub long_running: bool,
99 /// Whether the command opens a session that persists beyond a single call.
100 pub stateful: bool,
101}
102
103impl Capabilities {
104 /// Const-constructible default suitable for use in `static` registries.
105 pub const NONE: Self = Self {
106 output_mode: OutputMode::None,
107 result_schema_ref: None,
108 event_schema_ref: None,
109 session_schema_ref: None,
110 reads_stdin: false,
111 supports_output_path: false,
112 requires_project: false,
113 side_effects: SideEffects::None,
114 long_running: false,
115 stateful: false,
116 };
117}
118
119impl Default for Capabilities {
120 fn default() -> Self {
121 Self::NONE
122 }
123}
124
125/// Output mode under machine mode.
126#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum OutputMode {
129 /// No machine-mode contract yet; output is human-only.
130 None,
131 /// Pre-existing `--json` shape predating this contract.
132 LegacyJson,
133 /// Single terminal `JsonEnvelope<T>` on stdout.
134 Envelope,
135 /// Newline-delimited JSON event records on stdout.
136 Stream,
137 /// Long-running session (e.g. `anvil`); emits a `session_start` record.
138 Session,
139}
140
141/// Coarse classification of a command's side effects.
142///
143/// Reports only the highest-impact effect (e.g. a chain-writing command that
144/// also writes files reports `ChainWrite`); it is not an exhaustive set.
145#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(rename_all = "snake_case")]
147pub enum SideEffects {
148 /// Pure: reads only (e.g. `cast tx`).
149 None,
150 /// Writes files on the local filesystem.
151 FsWrite,
152 /// Performs network reads (RPC, HTTP).
153 Network,
154 /// Submits transactions or otherwise mutates chain state.
155 ChainWrite,
156 /// Spawns a long-running server (e.g. `anvil`).
157 SpawnServer,
158}
159
160/// Information about a single command argument.
161#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
162pub struct ArgInfo {
163 /// Argument identifier (clap arg id).
164 pub name: String,
165 /// Argument kind.
166 pub kind: ArgKind,
167 /// Best-effort classification of the value type.
168 pub value_type: ValueType,
169 /// Help text, when present.
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub help: Option<String>,
172 /// Long form (`--foo`), if any.
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub long: Option<String>,
175 /// Short form (`-f`), if any.
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub short: Option<char>,
178 /// All visible aliases.
179 pub aliases: Vec<String>,
180 /// Bound environment variable, if any.
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub env: Option<String>,
183 /// Default value, if any.
184 #[serde(skip_serializing_if = "Option::is_none")]
185 pub default: Option<String>,
186 /// Permitted values for value-enum arguments.
187 pub possible_values: Vec<String>,
188 /// Whether the argument is required.
189 pub required: bool,
190 /// Whether the argument can be supplied multiple times.
191 pub repeatable: bool,
192 /// Other arguments this argument conflicts with.
193 pub conflicts_with: Vec<String>,
194 /// Help heading the argument is grouped under.
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub help_heading: Option<String>,
197 /// Whether the argument is hidden in human help.
198 #[serde(default, skip_serializing_if = "is_false")]
199 pub hidden: bool,
200}
201
202/// Argument shape.
203#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
204#[serde(rename_all = "snake_case")]
205pub enum ArgKind {
206 /// Boolean flag (`--quiet`).
207 Flag,
208 /// Option that takes a value (`--rpc-url URL`).
209 Option,
210 /// Positional argument.
211 Positional,
212}
213
214/// Best-effort classification of an argument's value type, for agent UI hints.
215#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(rename_all = "snake_case")]
217pub enum ValueType {
218 Bool,
219 String,
220 Integer,
221 Path,
222 Url,
223 Address,
224 Selector,
225 Hex,
226 Json,
227 Other,
228}
229
230/// Documented exit code for a command, beyond the global table.
231#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
232pub struct ExitCodeInfo {
233 /// Numeric process exit code.
234 pub code: i32,
235 /// Stable name (e.g. `TestFailure`).
236 pub name: Cow<'static, str>,
237 /// Description of when this code is emitted.
238 pub description: Cow<'static, str>,
239}
240
241#[allow(clippy::trivially_copy_pass_by_ref)]
242const fn is_false(b: &bool) -> bool {
243 !*b
244}