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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## 2026-11-06 — v0.3.1

### Added

- New `command` world in `wit/xfetch-runtime.wit` for CLI command addons. The
request is JSON and the response is plain text lines.
- New `xfetch-command-api` crate with the command request type, stdin/stdout
helpers and line formatting utilities.
- `docs/command-sdk.md` documents the command protocol, Rust core-module
example, component world and native command conventions.

### Changed

- All API crates bumped to `0.3.1`.
- `xfetch-guest-api` no longer declares an unused direct `serde` dependency.
- Removed the unused `parse_json` helper from `xfetch-command-api`.
- Updated the protocol reference, WebAssembly guest guide, README, ROADMAP and
getting-started guide for the command kind and 0.3.1.

## 2026-11-03 — v0.3.0

### Added
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ members = [
"crates/extension-api",
"crates/effect-api",
"crates/guest-api",
"crates/command-api",
]
resolver = "2"
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</p>

<p>
The current release line is <strong>0.3.0</strong>. It keeps the JSON
The current release line is <strong>0.3.1</strong>. It keeps the JSON
protocol at version <code>1</code> and the WIT at
<code>xfetch:runtime@0.1.0</code>; the changes are additive.
</p>
Expand All @@ -21,9 +21,10 @@
<li><code>crates/extension-api</code>: shared protocol crate for xfetch extensions (config providers).</li>
<li><code>crates/effect-api</code>: shared protocol crate for xfetch effects (intro animations over the info lines).</li>
<li><code>crates/guest-api</code>: host-call helpers for WebAssembly guests (<code>http_request</code>, <code>exec</code>, <code>log</code>, <code>protocol_version</code>).</li>
<li><code>wit/xfetch-runtime.wit</code>: component-model contract (<code>plugin</code>, <code>effect</code> and <code>extension</code> worlds).</li>
<li><code>crates/command-api</code>: shared protocol crate for CLI command addons.</li>
<li><code>wit/xfetch-runtime.wit</code>: component-model contract (<code>plugin</code>, <code>effect</code>, <code>extension</code> and <code>command</code> worlds).</li>
<li><code>docs/</code>: focused documentation for setup, protocol, SDK usage, wasm guests, and examples.</li>
<li><code>examples/</code>: minimal plugin, effect and extension binaries that compile against the public crates.</li>
<li><code>crates/*/examples/</code>: minimal plugin, effect and extension binaries that compile against the public crates.</li>
</ul>

<h2>Main Crate</h2>
Expand Down Expand Up @@ -56,6 +57,16 @@
<li>Effects are opt-in: a missing binary or invalid response simply skips the intro (no behavior change).</li>
</ul>

<h2>Command API</h2>

<p>
<code>crates/command-api</code> (<code>xfetch-command-api</code>) is the
contract for CLI command addons. Commands receive arguments and return plain
text lines, so a command owns its own output format. Components use the
<code>command</code> world in the WIT package; core modules use the JSON
request on stdin and plain text on stdout.
</p>

<h2>Documentation</h2>

<ul>
Expand All @@ -65,6 +76,7 @@
<li><a href="./docs/protocol.md">Protocol Reference</a></li>
<li><a href="./docs/extension-sdk.md">Extension SDK</a></li>
<li><a href="./docs/effect-sdk.md">Effect SDK</a></li>
<li><a href="./docs/command-sdk.md">Command SDK</a></li>
<li><a href="./docs/examples.md">Examples</a></li>
</ul>

Expand All @@ -74,5 +86,5 @@
<li>This repository owns shared contracts, not end-user config files.</li>
<li>Official plugins are expected to use the same public SDK as external plugins.</li>
<li>Protocol changes should land here first and then be adopted by the core and plugin repos.</li>
<li>Publishing order: <code>xfetch-api-common</code> first, then <code>xfetch-plugin-api</code>, <code>xfetch-extension-api</code> and <code>xfetch-effect-api</code>; <code>xfetch-guest-api</code> is independent.</li>
<li>Publishing order: <code>xfetch-api-common</code> first, then <code>xfetch-plugin-api</code>, <code>xfetch-extension-api</code>, <code>xfetch-effect-api</code> and <code>xfetch-command-api</code>; <code>xfetch-guest-api</code> is independent.</li>
</ul>
4 changes: 3 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ only when the core or the ecosystem needs them.
- [x] `xfetch-plugin-api` — info providers and logo animations
- [x] `xfetch-extension-api` — config providers
- [x] `xfetch-effect-api` — intro effects
- [x] `xfetch-command-api` — CLI command addons
- [x] Versioned protocol constants (`PROTOCOL_VERSION`, kind constants)
- [x] Timeout contract: `with_timeout`/`TimedOut` as the single enforcement point
(see `docs/timeouts.md`)
- [x] SDK documentation: `docs/getting-started.md`, `docs/protocol.md`,
`docs/plugin-sdk.md`, `docs/extension-sdk.md`, `docs/effect-sdk.md`, `docs/examples.md`
`docs/plugin-sdk.md`, `docs/extension-sdk.md`, `docs/effect-sdk.md`,
`docs/command-sdk.md`, `docs/examples.md`
- [x] Local CI (fmt, clippy, tests + Windows cross-target) via `scripts/`
12 changes: 12 additions & 0 deletions crates/command-api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "xfetch-command-api"
version = "0.3.1"
edition = "2024"
license = "MIT"
description = "Shared command protocol types and IO helpers for xfetch"
repository = "https://github.com/xfetch-cli/api"

[dependencies]
xfetch-api-common = { version = "0.3.1", path = "../common" }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.148"
20 changes: 20 additions & 0 deletions crates/command-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# xfetch-command-api

Shared types and IO helpers for xfetch command addons.

A command addon receives a JSON request on stdin and writes plain text to
stdout. The response is intentionally line-oriented rather than JSON so a
command can render its own key/value output.

```rust
use xfetch_command_api::{read_command_args_or_default, write_command_lines};

fn main() {
let args = read_command_args_or_default().unwrap_or_default();
let repo = args.first().map(String::as_str).unwrap_or("owner/repo");
write_command_lines(&[format!("repo: {repo}")]).unwrap();
}
```

Component guests use the `command` world in `wit/xfetch-runtime.wit` and return
the same plain text response from `run`.
138 changes: 138 additions & 0 deletions crates/command-api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//! Shared command protocol types and IO helpers for xfetch command addons.
//!
//! Command addons receive a JSON request and return plain text. Native
//! commands may also use process arguments; wasm commands read the JSON
//! request from stdin (or from the component `run` request string).

use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::io::{Read, Write};

pub use xfetch_api_common::with_timeout;

/// Current JSON command protocol version.
pub const COMMAND_PROTOCOL_VERSION: u32 = 1;

/// Request kind used by command addons.
pub const KIND_COMMAND: &str = "command";

/// One command invocation sent by the core.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandRequest {
#[serde(default = "default_version")]
pub version: u32,
#[serde(default = "default_kind")]
pub kind: String,
#[serde(default)]
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub cwd: Option<String>,
#[serde(default)]
pub env: BTreeMap<String, String>,
}

impl CommandRequest {
pub fn is_current(&self) -> bool {
self.version == COMMAND_PROTOCOL_VERSION && self.kind == KIND_COMMAND
}
}

fn default_version() -> u32 {
COMMAND_PROTOCOL_VERSION
}

fn default_kind() -> String {
KIND_COMMAND.to_string()
}

/// Reads one command request from stdin.
pub fn read_command_request() -> Result<CommandRequest, String> {
let mut input = String::new();
std::io::stdin()
.read_to_string(&mut input)
.map_err(|err| format!("failed to read command request: {err}"))?;
parse_command_request(&input)
}

/// Parses one command request from a string.
pub fn parse_command_request(input: &str) -> Result<CommandRequest, String> {
let request: CommandRequest =
serde_json::from_str(input).map_err(|err| format!("invalid command request: {err}"))?;
if !request.is_current() {
return Err(format!(
"unsupported command request version/kind: {}/{}",
request.version, request.kind
));
}
Ok(request)
}

/// Reads the request and returns its arguments, or an empty list when stdin is
/// empty. This is convenient for commands that only need positional arguments.
pub fn read_command_args_or_default() -> Result<Vec<String>, String> {
let mut input = String::new();
std::io::stdin()
.read_to_string(&mut input)
.map_err(|err| format!("failed to read command request: {err}"))?;
if input.trim().is_empty() {
return Ok(Vec::new());
}
Ok(parse_command_request(&input)?.args)
}

/// Writes plain text to stdout. The caller is responsible for adding newlines.
pub fn write_command_text(text: &str) -> Result<(), String> {
let mut stdout = std::io::stdout();
stdout
.write_all(text.as_bytes())
.map_err(|err| format!("failed to write command output: {err}"))?;
stdout
.flush()
.map_err(|err| format!("failed to flush command output: {err}"))
}

/// Formats one line per item with a trailing newline.
pub fn format_lines(lines: &[String]) -> String {
let mut text = lines.join("\n");
if !text.is_empty() {
text.push('\n');
}
text
}

/// Writes one line per item, terminated by a newline.
pub fn write_command_lines(lines: &[String]) -> Result<(), String> {
write_command_text(&format_lines(lines))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parses_a_command_request() {
let request = parse_command_request(
r#"{"version":1,"kind":"command","command":"github","args":["owner/repo"]}"#,
)
.expect("request");
assert_eq!(request.command, "github");
assert_eq!(request.args, vec!["owner/repo"]);
}

#[test]
fn rejects_unknown_kinds() {
let error = parse_command_request(r#"{"version":1,"kind":"effect"}"#).unwrap_err();
assert!(error.contains("unsupported"));
}

#[test]
fn formats_lines_with_a_trailing_newline() {
assert_eq!(
format_lines(&["repo: x".to_string(), "author: y".to_string()]),
"repo: x\nauthor: y\n"
);
assert_eq!(format_lines(&[]), "");
}
}
2 changes: 1 addition & 1 deletion crates/common/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "xfetch-api-common"
version = "0.3.0"
version = "0.3.1"
edition = "2024"
license = "MIT"
description = "Shared protocol, IO, frame and timeout primitives for the xfetch addon SDKs"
Expand Down
3 changes: 2 additions & 1 deletion crates/common/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Shared primitives for the xfetch addon SDK crates:

This crate intentionally contains no per-kind request/response types.
Plugins, effects and extensions keep their own contracts in
`xfetch-plugin-api`, `xfetch-effect-api` and `xfetch-extension-api`.
`xfetch-plugin-api`, `xfetch-effect-api`, `xfetch-extension-api` and
`xfetch-command-api`.

Published first, before the per-kind crates.
4 changes: 2 additions & 2 deletions crates/effect-api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[package]
name = "xfetch-effect-api"
version = "0.3.0"
version = "0.3.1"
edition = "2024"
license = "MIT"
description = "Shared effect protocol types and IO helpers for xfetch"
repository = "https://github.com/xfetch-cli/api"

[dependencies]
xfetch-api-common = { version = "0.3.0", path = "../common" }
xfetch-api-common = { version = "0.3.1", path = "../common" }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.148"
4 changes: 2 additions & 2 deletions crates/extension-api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[package]
name = "xfetch-extension-api"
version = "0.3.0"
version = "0.3.1"
edition = "2024"
license = "MIT"
description = "Shared extension protocol types and IO helpers for xfetch"
repository = "https://github.com/xfetch-cli/api"

[dependencies]
xfetch-api-common = { version = "0.3.0", path = "../common" }
xfetch-api-common = { version = "0.3.1", path = "../common" }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.148"
3 changes: 1 addition & 2 deletions crates/guest-api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
[package]
name = "xfetch-guest-api"
version = "0.3.0"
version = "0.3.1"
edition = "2024"
license = "MIT"
description = "Host-call helpers for xfetch WebAssembly guests"
repository = "https://github.com/xfetch-cli/api"

[dependencies]
base64 = "0.23"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
4 changes: 2 additions & 2 deletions crates/plugin-api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[package]
name = "xfetch-plugin-api"
version = "0.3.0"
version = "0.3.1"
edition = "2024"
license = "MIT"
description = "Shared plugin protocol types and IO helpers for xfetch"
repository = "https://github.com/xfetch-cli/api"

[dependencies]
xfetch-api-common = { version = "0.3.0", path = "../common" }
xfetch-api-common = { version = "0.3.1", path = "../common" }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.148"
8 changes: 5 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<li><a href="./plugin-sdk.md">Plugin SDK</a>: public types, entrypoints, and error handling.</li>
<li><a href="./extension-sdk.md">Extension SDK</a>: public types and the config-provider protocol.</li>
<li><a href="./effect-sdk.md">Effect SDK</a>: public types, entrypoints, and error handling.</li>
<li><a href="./command-sdk.md">Command SDK</a>: CLI command addons and the line-oriented output contract.</li>
<li><a href="./protocol.md">Protocol Reference</a>: wire formats, kinds, versioning, and JSON examples.</li>
<li><a href="./timeouts.md">Timeouts</a>: the <code>with_timeout</code> helper and runtime budgets.</li>
<li><a href="./wasm-guests.md">WebAssembly Guests</a>: building core modules and components in Rust, Python, Go and C.</li>
Expand All @@ -20,11 +21,12 @@
<h2>Current Focus</h2>

<p>
The public surface is centered on five crates:
The public surface is centered on six crates:
<code>xfetch-api-common</code> (shared protocol/IO/frame/timeout primitives),
<code>xfetch-plugin-api</code>, <code>xfetch-extension-api</code>,
<code>xfetch-effect-api</code>, and <code>xfetch-guest-api</code> (host calls
for WebAssembly guests). The current release line is <strong>0.3.0</strong>.
<code>xfetch-effect-api</code>, <code>xfetch-command-api</code>, and
<code>xfetch-guest-api</code> (host calls for WebAssembly guests). The current
release line is <strong>0.3.1</strong>.
</p>

<p>
Expand Down
Loading
Loading