LogoCyanPrint

What Are Plugins?

Understanding plugins and their role in CyanPrint

What Are Plugins?

Plugins are post-processing components that run after all file generation is complete. They enable actions that processors cannot perform, such as running shell commands and executing system operations.

The Big Picture

Plugins are the final step in the generation pipeline. After processors have transformed and written all files, plugins can:

  • Run commands on the generated files
  • Modify the filesystem
  • Execute build steps
  • Initialize version control

What Plugins Can Do

CapabilityExampleWhy It's a Plugin
Initialize gitgit initRequires shell access
Install dependenciesnpm installRequires package manager
Run formattersprettier --write .Requires npm packages
Create symlinksln -sRequires filesystem access
Build stepsnpm run buildRequires toolchain
Set up hookshusky installRequires git + npm

Plugins are the only component with shell access. Processors are isolated and cannot execute commands.

How Plugins Work

Input

Plugins receive a CyanPluginInput object:

Prop

Type

Execution

Output

Plugins return a PluginOutput object:

Prop

Type

When to Use Plugins

Use a Plugin When You Need To:

  • Initialize a git repository
  • Install npm/bun dependencies
  • Run code formatters or linters
  • Execute build commands
  • Create symlinks or special files
  • Set up development tooling

Don't Use a Plugin When You Need To:

  • Transform file content (use a processor)
  • Substitute variables (use a processor)
  • Process individual files (use a processor)

Plugin vs Processor

AspectProcessorPlugin
PurposeTransform file contentRun operations
AccessFile content onlyFull filesystem + shell
WhenDuring file generationAfter all files written
StateStatelessCan have side effects
ExampleReplace var__name__Run git init

Read more in Plugins vs Processors.

Common Plugin Patterns

Setup Plugin

Initialize the project environment:

plugin.ts
import { $ } from 'bun';
import { StartPluginWithLambda } from '@atomicloud/cyan-sdk';
import type { CyanPluginInput, PluginOutput } from '@atomicloud/cyan-sdk';
StartPluginWithLambda(async (input: CyanPluginInput): Promise<PluginOutput> => {
const { directory } = input;
await $`git -C ${directory} init`.quiet();
Bun's shell template literal for running commands
await $`cd ${directory} && npm install`.quiet();
return { directory };
});
plugin.py
import subprocess
from cyan_sdk import start_plugin_with_fn
from cyan_sdk.types import CyanPluginInput, PluginOutput
@start_plugin_with_fn
async def setup_plugin(input: CyanPluginInput) -> PluginOutput:
# Extract directory from input
directory = input.directory
# Run shell commands
subprocess.run(["git", "-C", directory, "init"], capture_output=True)
subprocess.run(["npm", "install"], cwd=directory, capture_output=True)
# Always return the directory for pipeline continuity
return PluginOutput(directory=directory)
Plugin.cs
using CyanSdk;
using CyanSdk.Types;
[PluginMain]
public static class SetupPlugin
{
public static async Task<PluginOutput> Run(CyanPluginInput input)
{
// Extract directory from input
var directory = input.Directory;
// Run shell commands
await Shell.Exec($"git -C {directory} init");
await Shell.Exec($"cd {directory} && npm install");
// Always return the directory for pipeline continuity
return new PluginOutput(directory);
}
}

Formatter Plugin

Run code formatters:

import { $ } from 'bun';
import { StartPluginWithLambda } from '@atomicloud/cyan-sdk';
import type { CyanPluginInput, PluginOutput } from '@atomicloud/cyan-sdk';
StartPluginWithLambda(async (input: CyanPluginInput): Promise<PluginOutput> => {
const { directory, config } = input;
const cfg = config as { formatter?: string };
if (cfg.formatter === 'prettier') {
await $`cd ${directory} && npx prettier --write .`.quiet();
}
return { directory };
});
import subprocess
from cyan_sdk import start_plugin_with_fn
from cyan_sdk.types import CyanPluginInput, PluginOutput
from typing import TypedDict
class PluginConfig(TypedDict, total=False):
formatter: str
@start_plugin_with_fn
async def formatter_plugin(input: CyanPluginInput) -> PluginOutput:
directory = input.directory
config: PluginConfig = input.config
if config.get("formatter") == "prettier":
subprocess.run(
["npx", "prettier", "--write", "."],
cwd=directory,
capture_output=True
)
return PluginOutput(directory=directory)
using CyanSdk;
using CyanSdk.Types;
[PluginMain]
public static class FormatterPlugin
{
public static async Task<PluginOutput> Run(CyanPluginInput input)
{
var directory = input.Directory;
var config = input.Config as dynamic;
if (config?.formatter == "prettier")
{
await Shell.Exec($"cd {directory} && npx prettier --write .");
}
return new PluginOutput(directory);
}
}

Build Plugin

Run build steps:

import { $ } from 'bun';
import { StartPluginWithLambda } from '@atomicloud/cyan-sdk';
import type { CyanPluginInput, PluginOutput } from '@atomicloud/cyan-sdk';
StartPluginWithLambda(async (input: CyanPluginInput): Promise<PluginOutput> => {
const { directory, config } = input;
const cfg = config as { build?: boolean };
if (cfg.build) {
await $`cd ${directory} && npm run build`.quiet();
}
return { directory };
});
import subprocess
from cyan_sdk import start_plugin_with_fn
from cyan_sdk.types import CyanPluginInput, PluginOutput
from typing import TypedDict
class BuildConfig(TypedDict, total=False):
build: bool
@start_plugin_with_fn
async def build_plugin(input: CyanPluginInput) -> PluginOutput:
directory = input.directory
config: BuildConfig = input.config
if config.get("build", False):
subprocess.run(
["npm", "run", "build"],
cwd=directory,
capture_output=True
)
return PluginOutput(directory=directory)
using CyanSdk;
using CyanSdk.Types;
[PluginMain]
public static class BuildPlugin
{
public static async Task<PluginOutput> Run(CyanPluginInput input)
{
var directory = input.Directory;
var config = input.Config as dynamic;
if (config?.build == true)
{
await Shell.Exec($"cd {directory} && npm run build");
}
return new PluginOutput(directory);
}
}

Design Philosophy

Plugins are designed to be:

  1. Simple - Minimal API, just receive directory and return it
  2. Flexible - Full filesystem and shell access
  3. Optional - Templates work without plugins
  4. Composable - Multiple plugins can be chained