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
41 changes: 41 additions & 0 deletions .github/workflows/rust-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Rust Tests

on:
push:
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: rust-tests-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: fmt, clippy and tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install stable Rust toolchain
uses: dtolnay/rust-toolchain@18f0487477426dce244608110aa2660c2893deb7 # stable
with:
toolchain: stable
components: rustfmt, clippy

- name: Check formatting
run: cargo fmt --all --check

- name: Run clippy
run: cargo clippy --workspace --all-targets -- -D warnings

- name: Run tests
run: cargo test --workspace

- name: Build examples
run: cargo build --workspace --examples
4 changes: 2 additions & 2 deletions .github/workflows/security-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install stable Rust toolchain
uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable
uses: dtolnay/rust-toolchain@18f0487477426dce244608110aa2660c2893deb7 # stable
with:
toolchain: stable

Expand Down
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
# Changelog

## 2026-11-03 — v0.3.0

### Added

- `xfetch-api-common` 0.3.0: shared JSON protocol constants, frame shape,
timeout helper and stdin/stdout JSON helpers. It carries no per-kind
request/response types; plugins, effects and extensions keep their own
contracts.
- `xfetch-extension-api`: `io` module, standard entrypoints
(`read_config_provider_request`, `read_config_provider_args_or_default`,
`write_config_provider_response`, `write_config`), `ExtensionApiError`,
`ConfigProviderRequest::validate()`, `kind_enum()`, `parse_args()` and
`parse_args_or_default()`.
- Effect and extension examples: `crates/effect-api/examples/simple-effect.rs`
and `crates/extension-api/examples/simple-extension.rs`.
- `rust-tests.yml`: fmt, clippy `-D warnings`, workspace tests and example
builds on push and pull request.

### Changed

- All SDK crates bumped to `0.3.0`.
- `with_timeout` and JSON IO now delegate to `xfetch-api-common`; the existing
public paths and error variants are preserved.
- `LogoAnimationRequest` and `EffectRequest` tolerate a missing optional
`args` field through `serde(default)`.
- `PluginApiError` and `EffectApiError` implement `From<serde_json::Error>`
and `From<xfetch-api-common::IoError>`.
- `PROTOCOL_VERSION` and the kind constants now reference the shared
protocol module; values are unchanged (`1`, `logo_animation`,
`info_provider`, `effect`, `config_provider`).
- `xfetch-guest-api` uses `base64` 0.23.
- `security-audit.yml` uses `actions/checkout` 7.0.1 and the current stable
`dtolnay/rust-toolchain` pin.
- `AnimationFrame` and `EffectFrame` gained `From`/`Into` conversions to the
shared `Frame`; the existing types and field layout are unchanged.

### Compatibility

- JSON protocol remains version `1`.
- WIT remains `xfetch:runtime@0.1.0`.
- Existing public items, crate names and request/response shapes are
preserved; the release is additive.

## 2026-10-24 — Dependency security automation

- Added `.github/workflows/security-audit.yml`: runs `cargo audit` 0.22.2 with `--deny warnings` on push, pull request and a weekly schedule. The workflow generates a temporary lockfile before auditing because the workspace does not version one.
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"crates/common",
"crates/plugin-api",
"crates/extension-api",
"crates/effect-api",
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@
</p>

<p>
Today this repository contains the public plugin SDK used by the core,
official plugins, and third-party plugin authors.
The current release line is <strong>0.3.0</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>

<h2>Current Scope</h2>

<ul>
<li><code>crates/common</code> (<code>xfetch-api-common</code>): shared protocol constants, frame shape, timeout and JSON IO helpers.</li>
<li><code>crates/plugin-api</code>: shared protocol crate for xfetch plugins.</li>
<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>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>
</ul>

<h2>Main Crate</h2>
Expand Down Expand Up @@ -60,6 +63,8 @@
<li><a href="./docs/getting-started.md">Getting Started</a></li>
<li><a href="./docs/plugin-sdk.md">Plugin SDK</a></li>
<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/examples.md">Examples</a></li>
</ul>

Expand All @@ -69,4 +74,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>
</ul>
11 changes: 11 additions & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "xfetch-api-common"
version = "0.3.0"
edition = "2024"
license = "MIT"
description = "Shared protocol, IO, frame and timeout primitives for the xfetch addon SDKs"
repository = "https://github.com/xfetch-cli/api"

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

Shared primitives for the xfetch addon SDK crates:

- JSON wire protocol version and kind constants;
- the common `Frame { delay_ms, lines }` shape;
- stdin/stdout JSON helpers returning `IoError`;
- `with_timeout` for native addons, with the host-enforced wasm behavior.

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`.

Published first, before the per-kind crates.
34 changes: 34 additions & 0 deletions crates/common/src/frame.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! Shared frame shape for logo animations and effects.
//!
//! The per-kind SDKs keep their public `AnimationFrame`/`EffectFrame` names;
//! this type is the common representation used by shared tooling and
//! conversions.

use serde::{Deserialize, Serialize};

/// One rendered frame: how long to hold it and the lines to print.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Frame {
pub delay_ms: u64,
pub lines: Vec<String>,
}

impl Frame {
pub fn new(delay_ms: u64, lines: Vec<String>) -> Self {
Self { delay_ms, lines }
}
}

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

#[test]
fn frame_roundtrips_as_json() {
let frame = Frame::new(16, vec!["one".to_string(), "two".to_string()]);
let encoded = serde_json::to_string(&frame).expect("serialize");
let decoded: Frame = serde_json::from_str(&encoded).expect("deserialize");

assert_eq!(decoded, frame);
}
}
113 changes: 113 additions & 0 deletions crates/common/src/io.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//! JSON stdin/stdout helpers shared by the addon SDKs.
//!
//! The per-kind crates keep their public `io` modules and error types; they
//! delegate the actual parsing and IO here and map [`IoError`] into their own
//! error enums.

use std::fmt;
use std::io::{self, Read, Write};

use serde::Serialize;
use serde::de::DeserializeOwned;

/// Failure while reading, parsing or writing a JSON protocol message.
#[derive(Debug)]
pub enum IoError {
Io(io::Error),
Serialize(serde_json::Error),
Deserialize(serde_json::Error),
}

impl fmt::Display for IoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(err) => write!(f, "I/O error: {}", err),
Self::Serialize(err) => write!(f, "Failed to serialize JSON: {}", err),
Self::Deserialize(err) => write!(f, "Failed to parse JSON: {}", err),
}
}
}

impl std::error::Error for IoError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(err) => Some(err),
Self::Serialize(err) | Self::Deserialize(err) => Some(err),
}
}
}

impl From<io::Error> for IoError {
fn from(value: io::Error) -> Self {
Self::Io(value)
}
}

impl From<serde_json::Error> for IoError {
fn from(value: serde_json::Error) -> Self {
Self::Deserialize(value)
}
}

pub fn parse_json_str<T: DeserializeOwned>(input: &str) -> Result<T, IoError> {
serde_json::from_str(input).map_err(IoError::Deserialize)
}

pub fn parse_json_slice<T: DeserializeOwned>(input: &[u8]) -> Result<T, IoError> {
serde_json::from_slice(input).map_err(IoError::Deserialize)
}

pub fn to_json_vec<T: Serialize>(value: &T) -> Result<Vec<u8>, IoError> {
serde_json::to_vec(value).map_err(IoError::Serialize)
}

pub fn read_json_from_stdin<T: DeserializeOwned>() -> Result<T, IoError> {
let mut input = String::new();
io::stdin().read_to_string(&mut input)?;
parse_json_str(&input)
}

pub fn write_json_to_stdout<T: Serialize>(value: &T) -> Result<(), IoError> {
let body = to_json_vec(value)?;
let mut stdout = io::stdout();
stdout.write_all(&body)?;
stdout.flush()?;
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
struct Payload {
value: u32,
}

#[test]
fn parses_from_str_and_slice() {
assert_eq!(
parse_json_str::<Payload>(r#"{"value":7}"#).expect("str"),
Payload { value: 7 }
);
assert_eq!(
parse_json_slice::<Payload>(br#"{"value":7}"#).expect("slice"),
Payload { value: 7 }
);
}

#[test]
fn serializes_to_vec() {
let encoded = to_json_vec(&Payload { value: 7 }).expect("serialize");
assert_eq!(encoded, br#"{"value":7}"#);
}

#[test]
fn invalid_json_is_a_deserialize_error() {
assert!(matches!(
parse_json_str::<Payload>("{"),
Err(IoError::Deserialize(_))
));
}
}
15 changes: 15 additions & 0 deletions crates/common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! Shared primitives for the xfetch addon SDK crates.
//!
//! This crate deliberately contains no per-kind protocol types. Plugins,
//! effects and extensions keep their own request/response contracts; only the
//! wire constants, JSON IO, frame shape and timeout helper are shared.

pub mod frame;
pub mod io;
pub mod protocol;
pub mod timeout;

pub use frame::Frame;
pub use io::IoError;
pub use protocol::{JSON_PROTOCOL_VERSION, is_current_version};
pub use timeout::{TimedOut, with_timeout};
34 changes: 34 additions & 0 deletions crates/common/src/protocol.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! Wire-level constants shared by every xfetch addon kind.

/// JSON protocol version used by native addons and WebAssembly core modules.
pub const JSON_PROTOCOL_VERSION: u32 = 1;

/// Plugin kind: animated logo frames.
pub const KIND_LOGO_ANIMATION: &str = "logo_animation";
/// Plugin kind: additional information lines.
pub const KIND_INFO_PROVIDER: &str = "info_provider";
/// Effect kind: intro animation over the rendered lines.
pub const KIND_EFFECT: &str = "effect";
/// Extension kind: config-provider hook.
pub const KIND_CONFIG_PROVIDER: &str = "config_provider";

/// Returns true when `version` is the current JSON protocol version.
pub const fn is_current_version(version: u32) -> bool {
version == JSON_PROTOCOL_VERSION
}

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

#[test]
fn current_version_is_accepted() {
assert!(is_current_version(JSON_PROTOCOL_VERSION));
}

#[test]
fn other_versions_are_rejected() {
assert!(!is_current_version(JSON_PROTOCOL_VERSION + 1));
assert!(!is_current_version(0));
}
}
Loading
Loading