Your First Plugin
Create a plugin that runs post-processing commands
Your First Plugin
Create a plugin that runs post-processing commands after file generation. This tutorial builds a plugin that initializes git and optionally installs dependencies.
Prerequisites
- Docker installed and running
- One of the following runtimes:
Node.js 18+ or Bun installed
Python 3.11+ and pip
.NET 8 SDK
Create Project
Initialize Project
mkdir my-plugincd my-pluginbun init -ybun add @atomicloud/cyan-sdk
mkdir my-plugincd my-pluginpython -m venv .venvsource .venv/bin/activate # On Windows: .venv\Scripts\activatepip install cyanprintsdk
mkdir my-plugincd my-plugindotnet new consoledotnet add package AtomiCloud.CyanPrint
Create Plugin Logic
Create index.ts:
import { StartPluginWithLambda, PluginOutput } from '@atomicloud/cyan-sdk';import { $ } from 'bun';StartPluginWithLambda(async (input): Promise<PluginOutput> => {Entry point - starts the plugin server on port 5552const { directory, config } = input;The input contains the generated directory and plugin configconst cfg = config as {Cast config to expected type since it comes as unknowngitInit?: boolean;installDeps?: boolean;packageManager?: 'npm' | 'bun' | 'yarn' | 'pnpm';};// Initialize git if requestedif (cfg.gitInit !== false) {await $`git -C ${directory} init`.quiet();await $`git -C ${directory} add .`.quiet();}// Install dependencies if requestedif (cfg.installDeps) {const pm = cfg.packageManager || 'npm';if (pm === 'bun') {await $`cd ${directory} && bun install`.quiet();} else if (pm === 'yarn') {await $`cd ${directory} && yarn install`.quiet();} else if (pm === 'pnpm') {await $`cd ${directory} && pnpm install`.quiet();} else {await $`cd ${directory} && npm install`.quiet();}}// Always return the directoryreturn { directory };});
Create index.py:
import subprocessfrom cyanprintsdk import start_plugin_with_fnfrom cyanprintsdk.domain.plugin.input import PluginInputfrom cyanprintsdk.domain.plugin.output import PluginOutputasync def my_plugin(input: PluginInput) -> PluginOutput:directory = input.directoryconfig = input.config or {}git_init = config.get("gitInit", True)Access config with defaultsinstall_deps = config.get("installDeps", False)package_manager = config.get("packageManager", "npm")# Initialize git if requestedif git_init:subprocess.run(["git", "-C", directory, "init"], check=True, capture_output=True)subprocess.run(["git", "-C", directory, "add", "."], check=True, capture_output=True)# Install dependencies if requestedif install_deps:if package_manager == "bun":subprocess.run(["bun", "install"], cwd=directory, check=True, capture_output=True)elif package_manager == "yarn":subprocess.run(["yarn", "install"], cwd=directory, check=True, capture_output=True)elif package_manager == "pnpm":subprocess.run(["pnpm", "install"], cwd=directory, check=True, capture_output=True)else:subprocess.run(["npm", "install"], cwd=directory, check=True, capture_output=True)# Always return the directoryreturn PluginOutput(directory=directory)start_plugin_with_fn(my_plugin)
Create requirements.txt:
cyanprintsdk>=1.0.0
Create Program.cs:
using sulfone_helium;using sulfone_helium.Domain.Plugin;CyanEngine.StartPlugin(args, async (input) =>Entry point - starts the plugin server on port 5552{var directory = input.Directory;var cfg = input.Config != null? System.Text.Json.JsonSerializer.Deserialize<PluginConfig>(input.Config.ToString()!): new PluginConfig();// Initialize git if requestedif (cfg?.GitInit != false){await RunCommand($"git -C {directory} init");await RunCommand($"git -C {directory} add .");}// Install dependencies if requestedif (cfg?.InstallDeps == true){var pm = cfg.PackageManager ?? "npm";await RunCommand($"cd {directory} && {pm} install");}// Always return the directoryreturn new PluginOutput { Directory = directory };});static async Task RunCommand(string command){var parts = command.Split(' ');var proc = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo{FileName = parts[0],Arguments = string.Join(" ", parts.Skip(1)),RedirectStandardOutput = true,RedirectStandardError = true});await proc.WaitForExitAsync();}class PluginConfig{public bool? GitInit { get; set; }public bool? InstallDeps { get; set; }public string? PackageManager { get; set; }}
Create Dockerfile
Create Dockerfile:
FROM oven/bun:1.1.31WORKDIR /app# Mark as CyanPrint pluginLABEL cyanprint.dev=true# Install dependenciesCOPY package.json bun.lockb* ./RUN bun install# Copy plugin codeCOPY . .# Run pluginCMD ["bun", "run", "index.ts"]
Create Dockerfile:
FROM python:3.11-slimWORKDIR /app# Mark as CyanPrint pluginLABEL cyanprint.dev=true# Install git (needed for git init commands)RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*# Install dependenciesCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt# Copy plugin codeCOPY . .# Run pluginCMD ["python", "index.py"]
Create Dockerfile:
FROM mcr.microsoft.com/dotnet/sdk:8.0WORKDIR /app# Mark as CyanPrint pluginLABEL cyanprint.dev=true# Install git (needed for git init commands)RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*# Install dependenciesCOPY *.csproj .RUN dotnet restore# Copy plugin codeCOPY . .# Run pluginCMD ["dotnet", "run"]
Understanding the Code
Entry Point
StartPluginWithLambda receives one argument:
Prop
Type
start_plugin_with_fn receives one argument:
Prop
Type
StartPlugin receives one argument:
Prop
Type
Unlike processors, plugins don't receive a file helper—they work directly with the filesystem.
PluginInput
Prop
Type
PluginOutput
Prop
Type
Test with Try Command
Build Docker Image
docker build -t my-plugin:dev .
Create a Test Template
Create a template that uses your plugin:
import { StartTemplateWithLambda, GlobType } from '@atomicloud/cyan-sdk';StartTemplateWithLambda(async (i, d) => {const name = await i.text('Project name?', 'test/name');return {processors: [{name: 'cyan/default',files: [{ root: 'templates', glob: '**/*', exclude: [], type: GlobType.Copy }],config: { vars: { name } }}],plugins: [{name: 'my-plugin:dev',config: {gitInit: true,installDeps: true,packageManager: 'bun'}}]};});
from cyanprintsdk import start_template_with_fn, GlobTypefrom cyanprintsdk.domain.core.cyan import Cyan, CyanProcessor, CyanGlob, CyanPluginasync def my_template(i, d):name = await i.text("Project name?", "test/name")return Cyan(processors=[CyanProcessor(name="cyan/default",files=[CyanGlob(root="templates",glob="**/*",exclude=[],type=GlobType.Copy)],config={"vars": {"name": name}})],plugins=[CyanPlugin(name="my-plugin:dev",config={"gitInit": True,"installDeps": True,"packageManager": "bun"})])start_template_with_fn(my_template)
using sulfone_helium;using sulfone_helium.Domain.Core;CyanEngine.StartTemplate(args, async (i, d) =>{var name = await i.Text("Project name?", "test/name");return new Cyan{Processors =[new CyanProcessor{Name = "cyan/default",Files =[new CyanGlob{Root = "templates",Glob = "**/*",Exclude = [],Type = GlobType.Copy}],Config = new { vars = new { name } }}],Plugins =[new CyanPlugin{Name = "my-plugin:dev",Config = new{gitInit = true,installDeps = true,packageManager = "bun"}}]};});
Run Try Command
cd ../test-templatecyanprint try template . ./output
Check Output
cd ./outputgit status # Should show initialized git repols node_modules # If installDeps was true
The try command builds and runs your plugin automatically. After file generation completes, the plugin initializes git and installs dependencies.
Automated Testing
Set up snapshot tests to verify your plugin produces consistent output:
cyanprint test plugin .
Create a test.cyan.yaml with input fixtures and expected snapshots. See Automated Testing for the full guide.
What You Learned
- How to create a plugin project
- Using the plugin entry point (port 5552)
- Running shell commands
- Accessing plugin configuration
- Returning the directory path
- Testing plugins with the
trycommand - Setting up automated snapshot tests
Next Steps
Learn more about plugin capabilities: