LogoCyanPrint
PluginsTutorials

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-plugin
cd my-plugin
bun init -y
bun add @atomicloud/cyan-sdk
mkdir my-plugin
cd my-plugin
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install cyanprintsdk
mkdir my-plugin
cd my-plugin
dotnet new console
dotnet add package AtomiCloud.CyanPrint

Create Plugin Logic

Create index.ts:

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 5552
const { directory, config } = input;
The input contains the generated directory and plugin config
const cfg = config as {
Cast config to expected type since it comes as unknown
gitInit?: boolean;
installDeps?: boolean;
packageManager?: 'npm' | 'bun' | 'yarn' | 'pnpm';
};
// Initialize git if requested
if (cfg.gitInit !== false) {
await $`git -C ${directory} init`.quiet();
await $`git -C ${directory} add .`.quiet();
}
// Install dependencies if requested
if (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 directory
return { directory };
});

Create index.py:

index.py
import subprocess
from cyanprintsdk import start_plugin_with_fn
from cyanprintsdk.domain.plugin.input import PluginInput
from cyanprintsdk.domain.plugin.output import PluginOutput
async def my_plugin(input: PluginInput) -> PluginOutput:
directory = input.directory
config = input.config or {}
git_init = config.get("gitInit", True)
Access config with defaults
install_deps = config.get("installDeps", False)
package_manager = config.get("packageManager", "npm")
# Initialize git if requested
if 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 requested
if 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 directory
return PluginOutput(directory=directory)
start_plugin_with_fn(my_plugin)

Create requirements.txt:

requirements.txt
cyanprintsdk>=1.0.0

Create Program.cs:

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 requested
if (cfg?.GitInit != false)
{
await RunCommand($"git -C {directory} init");
await RunCommand($"git -C {directory} add .");
}
// Install dependencies if requested
if (cfg?.InstallDeps == true)
{
var pm = cfg.PackageManager ?? "npm";
await RunCommand($"cd {directory} && {pm} install");
}
// Always return the directory
return 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:

Dockerfile
FROM oven/bun:1.1.31
WORKDIR /app
# Mark as CyanPrint plugin
LABEL cyanprint.dev=true
# Install dependencies
COPY package.json bun.lockb* ./
RUN bun install
# Copy plugin code
COPY . .
# Run plugin
CMD ["bun", "run", "index.ts"]

Create Dockerfile:

Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Mark as CyanPrint plugin
LABEL 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 dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy plugin code
COPY . .
# Run plugin
CMD ["python", "index.py"]

Create Dockerfile:

Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0
WORKDIR /app
# Mark as CyanPrint plugin
LABEL 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 dependencies
COPY *.csproj .
RUN dotnet restore
# Copy plugin code
COPY . .
# Run plugin
CMD ["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:

../test-template/cyan/index.ts
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'
}
}]
};
});
../test-template/cyan/index.py
from cyanprintsdk import start_template_with_fn, GlobType
from cyanprintsdk.domain.core.cyan import Cyan, CyanProcessor, CyanGlob, CyanPlugin
async 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)
../test-template/cyan/Program.cs
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-template
cyanprint try template . ./output

Check Output

cd ./output
git status # Should show initialized git repo
ls 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 try command
  • Setting up automated snapshot tests

Next Steps

Learn more about plugin capabilities: