Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 8 additions & 169 deletions crates/wright-cli/src/bin/wright-serve.rs
Original file line number Diff line number Diff line change
@@ -1,176 +1,15 @@
//! Exposes the same operations as [`wright_driver::service::ToolService`] over two transports.
#[path = "../serve.rs"]
mod serve;

use std::io::{BufRead, Write};
use clap::Parser;
use std::process::ExitCode;

use serde_json::{Value, json};
use wright_driver::config::{InputSpec, SessionConfig, SourceKind};
use wright_driver::service::{ToolRequest, ToolService};

fn usage() -> &'static str {
"usage: wright-serve --transport stdio|jsonrpc [--kind opy|ostw|workshop|protocol] [--locale LOC] [--profile off|compat|aggressive] [INPUT]\n\
\n\
Serves the Wright tool service over stdin/stdout. With no INPUT, reads a\n\
protocol payload or Workshop text from stdin (auto-detected)."
#[derive(Debug, Parser)]
struct Cli {
#[command(flatten)]
serve: serve::ServeArgs,
}

fn main() -> ExitCode {
let mut args = std::env::args().skip(1);
let mut transport = "stdio".to_string();
let mut config = SessionConfig::default();
let mut positional: Option<std::path::PathBuf> = None;
while let Some(arg) = args.next() {
match arg.as_str() {
"--transport" => transport = args.next().unwrap_or_else(|| "stdio".to_string()),
"--kind" => {
config.kind =
SourceKind::parse(&args.next().unwrap_or_default()).unwrap_or(SourceKind::Auto)
}
"--locale" => config.locale = args.next(),
"--profile" => {
config.profile = wright_driver::Profile::parse(&args.next().unwrap_or_default())
.unwrap_or_default()
}
"--help" | "-h" => {
println!("{}", usage());
return ExitCode::SUCCESS;
}
other if other.starts_with('-') => {
eprintln!("wright-serve: unknown argument '{other}'\n{}", usage());
return ExitCode::from(2);
}
other => positional = Some(std::path::PathBuf::from(other)),
}
}

config.input = positional.map(InputSpec::Path).unwrap_or(InputSpec::Stdin);
let mut session = match wright_driver::CompilerSession::new(config) {
Ok(s) => s,
Err(d) => {
eprintln!("wright-serve: {}", d.message);
return ExitCode::from(1);
}
};
let mut service = match ToolService::new(&mut session) {
Ok(s) => s,
Err(d) => {
eprintln!("wright-serve: {}", d.message);
return ExitCode::from(1);
}
};

match transport.as_str() {
"stdio" => serve_stdio(&mut service),
"jsonrpc" => serve_jsonrpc(&mut service),
other => {
eprintln!("wright-serve: unknown transport '{other}'\n{}", usage());
ExitCode::from(2)
}
}
}

fn serve_stdio(service: &mut ToolService<'_>) -> ExitCode {
serve_lines(|line| Some(dispatch(service, line)))
}

fn serve_jsonrpc(service: &mut ToolService<'_>) -> ExitCode {
serve_lines(|line| {
match serde_json::from_str(line) {
Ok(value) => jsonrpc_dispatch(service, value),
Err(_) => Some(jsonrpc_error(Value::Null, -32700, "Parse error")),
}
.map(|response| response.to_string())
})
}

fn serve_lines(mut dispatch: impl FnMut(&str) -> Option<String>) -> ExitCode {
let stdout = std::io::stdout();
let mut out = stdout.lock();
for line in std::io::stdin().lock().lines().map_while(Result::ok) {
if line.trim().is_empty() {
continue;
}
if dispatch(&line).is_some_and(|response| writeln!(out, "{response}").is_err()) {
break;
}
}
ExitCode::SUCCESS
}

fn jsonrpc_dispatch(service: &mut ToolService<'_>, value: Value) -> Option<Value> {
if let Value::Array(batch) = value {
if batch.is_empty() {
return Some(jsonrpc_error(Value::Null, -32600, "Invalid Request"));
}
let responses: Vec<Value> = batch
.into_iter()
.filter_map(|v| jsonrpc_dispatch_request(service, v))
.collect();
return (!responses.is_empty()).then_some(Value::Array(responses));
}
jsonrpc_dispatch_request(service, value)
}

fn jsonrpc_dispatch_request(service: &mut ToolService<'_>, value: Value) -> Option<Value> {
let Some(object) = value.as_object() else {
return Some(jsonrpc_error(Value::Null, -32600, "Invalid Request"));
};
if object.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
return Some(jsonrpc_error(Value::Null, -32600, "Invalid Request"));
}
let method = match object.get("method").and_then(Value::as_str) {
Some(m) => m,
None => return Some(jsonrpc_error(Value::Null, -32600, "Invalid Request")),
};
let has_id = object.contains_key("id");
let id = object.get("id").cloned().unwrap_or(Value::Null);
if has_id && !matches!(id, Value::Null | Value::String(_) | Value::Number(_)) {
return Some(jsonrpc_error(Value::Null, -32600, "Invalid Request"));
}

if matches!(method, "compile" | "check" | "analyze" | "inspect")
&& object.contains_key("params")
{
return has_id.then(|| jsonrpc_error(id, -32602, "Invalid params"));
}

let response = match method {
"request" => {
let params = match object.get("params") {
Some(Value::Object(p)) if p.get("op").is_some() => Value::Object(p.clone()),
_ => return has_id.then(|| jsonrpc_error(id, -32602, "Invalid params")),
};
let Ok(req) = serde_json::from_value::<ToolRequest>(params) else {
return has_id.then(|| jsonrpc_error(id, -32602, "Invalid params"));
};
match service.handle(&req) {
wright_driver::service::ToolResponse::Ok { result } => result,
wright_driver::service::ToolResponse::Error { error } => json!({ "error": error }),
}
}
"compile" => serde_json::to_value(service.compile()).expect("serializes"),
"check" => serde_json::to_value(service.check()).expect("serializes"),
"analyze" => serde_json::to_value(service.analyze()).expect("serializes"),
"inspect" => serde_json::to_value(service.inspect()).expect("serializes"),
other => {
return has_id.then(|| jsonrpc_error(id, -32601, format!("Method not found: {other}")));
}
};

has_id.then(|| json!({ "jsonrpc": "2.0", "id": id, "result": response }))
}

fn jsonrpc_error(id: Value, code: i64, message: impl Into<String>) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message.into() } })
}

fn dispatch(service: &mut ToolService<'_>, line: &str) -> String {
let req: ToolRequest = match serde_json::from_str(line) {
Ok(r) => r,
Err(e) => {
return json!({ "error": { "code": "malformed-request", "message": e.to_string() } })
.to_string();
}
};
serde_json::to_string(&service.handle(&req)).expect("serializes")
serve::run(Cli::parse().serve)
}
6 changes: 5 additions & 1 deletion crates/wright-cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::path::PathBuf;

use crate::serve::ServeArgs;
use clap::{Args, Parser, Subcommand, ValueEnum};

/// The top-level command model used by parsing, help, and completion.
Expand All @@ -25,7 +26,8 @@ pub(crate) const LONG_ABOUT: &str = "Wright compiler and Workshop tooling CLI.
Commands check correctness, summarize semantic hotspots, lint, inspect exhaustive
facts, compile, or reconstruct source through the typed wright-driver result envelope. `compile` and `convert`
keep their source artifact stdout contracts; JSON mode prints only one
wright-result/v1 envelope to stdout.
wright-result/v1 envelope to stdout. `serve` exposes the versioned
wright-agent/v1 session contract over stdio or JSON-RPC 2.0.

EXIT CODES:
0 success
Expand Down Expand Up @@ -78,6 +80,8 @@ pub(crate) enum Command {
Update(UpdateArgs),
/// Manage first-party language providers.
Provider(ProviderArgs),
/// Serve the versioned agent contract over stdio or JSON-RPC 2.0.
Serve(ServeArgs),
/// Show the top-level help.
Help,
/// Show version and result-contract metadata.
Expand Down
3 changes: 3 additions & 0 deletions crates/wright-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod cli;
mod completion;
mod present;
mod provider;
mod serve;
mod update;

use std::io::Read;
Expand Down Expand Up @@ -62,6 +63,7 @@ fn main() -> ExitCode {
ExitCode::SUCCESS
}
Some(Command::SemanticCompare(args)) => run_semantic_compare(args),
Some(Command::Serve(args)) => serve::run(args),
Some(Command::Completion(args)) => match args.subcommand {
Some(cli::CompletionSubcommand::Install(install_args)) => {
match completion::run_install(&install_args) {
Expand Down Expand Up @@ -194,6 +196,7 @@ fn run_workflow(command: Command) -> ExitCode {
Command::Completion(_)
| Command::Update(_)
| Command::Provider(_)
| Command::Serve(_)
| Command::Help
| Command::Version
| Command::SemanticCompare(_) => {
Expand Down
Loading
Loading