use std::path::{Path, PathBuf};
use serde_json::json;
use standard_plugin_manifest::manifest::is_identifier;
use standard_plugin::sources::examples;
const UI_TEMPLATE: &str = examples::CLOCK_CARD;
const DAEMON_TEMPLATE: &str = examples::HEARTBEAT_DAEMON;
const COMPANION_UI_TEMPLATE: &str = examples::TOGETHER_UI;
const COMPANION_TEMPLATE: &str = examples::TOGETHER_COMPANION;
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
pub enum NewKind {
Ui,
Daemon,
Companion,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SdkSource {
Path(PathBuf),
Version(String),
}
impl SdkSource {
pub fn default_for_this_build() -> Self {
let beside = Path::new(env!("CARGO_MANIFEST_DIR")).join("../standard-plugin-sdk");
match beside.canonicalize() {
Ok(path) if path.join("Cargo.toml").is_file() => Self::Path(path),
_ => Self::Version(release_line(env!("CARGO_PKG_VERSION"))),
}
}
fn dependency(&self) -> String {
match self {
Self::Path(path) => format!("{{ path = {:?} }}", path.display().to_string()),
Self::Version(version) => format!("{version:?}"),
}
}
}
fn release_line(version: &str) -> String {
version.splitn(3, '.').take(2).collect::<Vec<_>>().join(".")
}
#[derive(Debug, thiserror::Error)]
pub enum ScaffoldError {
#[error("{0:?} is not a plugin id: use lowercase letters, digits and dashes")]
Name(String),
#[error("{0} already exists")]
Exists(PathBuf),
#[error("{0}")]
Io(#[from] std::io::Error),
}
const PROFILES: &str = r#"
# Small components: plugins ship as bytes to every viewer.
[profile.release]
opt-level = "s"
lto = true
codegen-units = 1
panic = "abort"
strip = true
[profile.dev]
panic = "abort"
"#;
const GITIGNORE: &str = "target/\nbundle/*.wasm\nbundle/browser/\n";
const AGENTS_MD: &str = "# Notes for coding agents
This is a Standard Code plugin, written in Rust with `standard-plugin-sdk`.
Read the SDK's guides before you change it. They come from the SDK version
in this project's `Cargo.lock`:
- `standard-plugin guide` lists them, and `standard-plugin guide <name>`
prints one. Start with `standard-plugin guide agent-guide`.
- The same files are the `docs/` directory of the `standard-plugin-sdk`
crate that `cargo metadata` names.
- `cargo doc --open -p standard-plugin-sdk` shows the API, with the guides
under `standard_plugin::guide`.
";
const CLAUDE_MD: &str = "@AGENTS.md\n";
fn module_name(crate_name: &str) -> String {
format!("{}.wasm", crate_name.replace('-', "_"))
}
fn cargo_toml(crate_name: &str, sdk: &SdkSource, standalone: bool) -> String {
let mut toml = format!(
r#"[package]
name = "{crate_name}"
version = "0.1.0"
edition = "2024"
publish = false
[lib]
# cdylib: the component; rlib: the plugin's own `cargo test` links it.
crate-type = ["cdylib", "rlib"]
[dependencies]
serde = {{ version = "1", default-features = false, features = ["derive"] }}
standard-plugin-sdk = {}
"#,
sdk.dependency()
);
if standalone {
toml.push_str("\n# Its own workspace, even inside another project.\n[workspace]\n");
toml.push_str(PROFILES);
}
toml
}
fn manifest(
id: &str,
kind: &str,
module: &str,
surfaces: serde_json::Value,
grants: serde_json::Value,
) -> String {
let mut manifest = json!({
"apiVersion": 2,
"kind": kind,
"id": id,
"version": "0.1.0",
"module": module,
});
if !surfaces.is_null() {
manifest["surfaces"] = surfaces;
}
manifest["grants"] = grants;
let mut text = serde_json::to_string_pretty(&manifest).expect("a JSON value serializes");
text.push('\n');
text
}
fn testable_ui(source: &str, id: &str, plugin: &str, surface: &str) -> String {
let source = source.replacen("#![no_std]", "#![cfg_attr(not(test), no_std)]", 1);
format!(
r#"{source}
#[cfg(test)]
mod tests {{
use super::*;
use standard_plugin::Power;
use standard_plugin::surface::Model;
use standard_plugin::testing::{{Harness, MockHost}};
#[test]
fn it_paints_once_the_viewer_lays_it_out() {{
let host = MockHost::new("{id}").surface("{surface}", Model::Cells);
let mut harness = Harness::<{plugin}>::activate(host, "{{}}");
harness.resize("{surface}", Geometry::cells(24, 2, (0, 0)));
harness.show("{surface}", true);
harness.frame(1_000, 16, Power::Mains);
assert!(!harness.commits("{surface}").is_empty());
}}
}}
"#
)
}
fn personalise(
template: &str,
example_id: &str,
id: &str,
example_module: &str,
module: &str,
) -> String {
template
.replace(&format!("\"{example_id}.",), &format!("\"{id}."))
.replace(&format!("`{example_id}."), &format!("`{id}."))
.replace(
&format!("\"id\": \"{example_id}\""),
&format!("\"id\": \"{id}\""),
)
.replace(&format!("call:{example_id}"), &format!("call:{id}"))
.replace(example_module, module)
}
fn write_crate(
dir: &Path,
crate_name: &str,
sdk: &SdkSource,
standalone: bool,
source: &str,
manifest: &str,
) -> Result<(), ScaffoldError> {
std::fs::create_dir_all(dir.join("src"))?;
std::fs::create_dir_all(dir.join("bundle"))?;
std::fs::write(
dir.join("Cargo.toml"),
cargo_toml(crate_name, sdk, standalone),
)?;
std::fs::write(dir.join("src/lib.rs"), source)?;
std::fs::write(dir.join("bundle/standard-plugin.json"), manifest)?;
if standalone {
std::fs::write(dir.join(".gitignore"), GITIGNORE)?;
}
Ok(())
}
pub fn scaffold(
parent: &Path,
name: &str,
kind: NewKind,
sdk: &SdkSource,
) -> Result<PathBuf, ScaffoldError> {
if !is_identifier(name) {
return Err(ScaffoldError::Name(name.into()));
}
let root = parent.join(name);
if root.exists() {
return Err(ScaffoldError::Exists(root));
}
match kind {
NewKind::Ui => {
let module = module_name(name);
let source = testable_ui(
&personalise(UI_TEMPLATE, "clock-card", name, "clock_card.wasm", &module),
name,
"ClockCard",
"card",
);
let manifest = manifest(
name,
"ui",
&module,
json!([{ "id": "card", "anchor": "sidebar.card", "model": "cells", "height": 2 }]),
json!({}),
);
write_crate(&root, name, sdk, true, &source, &manifest)?;
}
NewKind::Daemon => {
let module = module_name(name);
let source = personalise(
DAEMON_TEMPLATE,
"heartbeat",
name,
"heartbeat_daemon.wasm",
&module,
);
let manifest = manifest(name, "daemon", &module, serde_json::Value::Null, json!({}));
write_crate(&root, name, sdk, true, &source, &manifest)?;
}
NewKind::Companion => {
std::fs::create_dir_all(&root)?;
std::fs::write(
root.join("Cargo.toml"),
format!(
"# A UI plugin and its companion daemon: one plugin id, two components.\n\
[workspace]\nmembers = [\"ui\", \"companion\"]\nresolver = \"2\"\n{PROFILES}"
),
)?;
std::fs::write(
root.join(".gitignore"),
GITIGNORE.replace("bundle", "*/bundle"),
)?;
let ui_crate = format!("{name}-ui");
let ui_module = module_name(&ui_crate);
let ui_source = testable_ui(
&personalise(
COMPANION_UI_TEMPLATE,
"together",
name,
"together_ui.wasm",
&ui_module,
),
name,
"Together",
"card",
);
let ui_manifest = manifest(
name,
"ui",
&ui_module,
json!([{ "id": "card", "anchor": "sidebar.card", "model": "cells", "height": 1 }]),
json!({
format!("call:{name}"): "Asks the companion on your machine to start a build"
}),
);
write_crate(
&root.join("ui"),
&ui_crate,
sdk,
false,
&ui_source,
&ui_manifest,
)?;
let companion_crate = format!("{name}-companion");
let companion_module = module_name(&companion_crate);
let companion_source = personalise(
COMPANION_TEMPLATE,
"together",
name,
"together_companion.wasm",
&companion_module,
);
let companion_manifest = manifest(
name,
"companion",
&companion_module,
serde_json::Value::Null,
json!({
"process.exec:make": "Runs make in your project when you ask for a build"
}),
);
write_crate(
&root.join("companion"),
&companion_crate,
sdk,
false,
&companion_source,
&companion_manifest,
)?;
}
}
std::fs::write(root.join("AGENTS.md"), AGENTS_MD)?;
std::fs::write(root.join("CLAUDE.md"), CLAUDE_MD)?;
Ok(root)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scaffolds_pass_check_and_carry_the_new_id() {
let temp = tempfile::tempdir().unwrap();
let sdk = SdkSource::Version("0.1.0".into());
let ui = scaffold(temp.path(), "my-card", NewKind::Ui, &sdk).unwrap();
let pair = scaffold(temp.path(), "builds", NewKind::Companion, &sdk).unwrap();
let daemon = scaffold(temp.path(), "beat", NewKind::Daemon, &sdk).unwrap();
for root in [&ui, &pair, &daemon] {
assert!(root.join("AGENTS.md").is_file() && root.join("CLAUDE.md").is_file());
}
for dir in [ui, pair.join("ui"), pair.join("companion"), daemon] {
let manifest = std::fs::read(dir.join("bundle/standard-plugin.json")).unwrap();
let report = crate::check::check(&manifest, None);
assert!(report.is_ok(), "{dir:?}: {report}");
assert!(report.warnings.is_empty(), "{dir:?}: {report}");
}
let source = std::fs::read_to_string(pair.join("ui/src/lib.rs")).unwrap();
assert!(source.contains("\"builds.status\""));
assert!(source.contains("#![cfg_attr(not(test), no_std)]"));
let toml = std::fs::read_to_string(pair.join("ui/Cargo.toml")).unwrap();
assert!(toml.contains(r#"crate-type = ["cdylib", "rlib"]"#));
assert!(!source.contains("\"together."));
assert!(matches!(
scaffold(temp.path(), "Bad Name", NewKind::Ui, &sdk),
Err(ScaffoldError::Name(_))
));
assert!(matches!(
scaffold(temp.path(), "my-card", NewKind::Ui, &sdk),
Err(ScaffoldError::Exists(_))
));
}
}