PluginsHow-To Guides
Run Shell Commands
Execute shell commands in the generated project directory
Run Shell Commands
Execute shell commands in the generated project directory. This is one of the most common plugin use cases.
Using Bun Shell
Bun provides a convenient shell API using template literals:
Bun Shell is specific to the Bun runtime. If you're using Node.js, skip to the Node.js child_process section.
import { PluginOutput, StartPluginWithLambda } from '@atomicloud/cyan-sdk';import { $ } from 'bun';StartPluginWithLambda(async (input): Promise<PluginOutput> => {const { directory } = input;// Initialize gitawait $`git -C ${directory} init`;// Add all filesawait $`git -C ${directory} add .`;// Create initial commitawait $`git -C ${directory} commit -m "Initial commit"`;return { directory };});
import subprocessfrom cyan_sdk import PluginOutput, StartPluginWithLambda@StartPluginWithLambdaasync def main(input_data) -> PluginOutput:directory = input_data.directory# Initialize gitsubprocess.run(['git', '-C', directory, 'init'], check=True)# Add all filessubprocess.run(['git', '-C', directory, 'add', '.'], check=True)# Create initial commitsubprocess.run(['git', '-C', directory, 'commit', '-m', 'Initial commit'], check=True)return PluginOutput(directory=directory)
using System.Diagnostics;using CyanSdk;public class Plugin{public static async Task<PluginOutput> Run(PluginInput input){var directory = input.Directory;// Initialize gitawait RunCommand("git", $"-C {directory} init");// Add all filesawait RunCommand("git", $"-C {directory} add .");// Create initial commitawait RunCommand("git", $"-C {directory} commit -m \"Initial commit\"");return new PluginOutput { Directory = directory };}private static Task RunCommand(string command, string args){var tcs = new TaskCompletionSource<bool>();var process = new Process{StartInfo = new ProcessStartInfo{FileName = command,Arguments = args,RedirectStandardOutput = true,RedirectStandardError = true}};process.Exited += (s, e) => tcs.SetResult(true);process.Start();return tcs.Task;}}
Bun Shell Features
| Feature | Example | Description |
|---|---|---|
| Interpolation | $`cd ${dir}` | Variables are escaped and safe |
| Quiet mode | $`cmd`.quiet() | Suppress stdout/stderr |
| Output capture | const p = await $`cmd` | Access p.stdout, p.stderr |
| Pipes | $`cmd1 | cmd2` | Shell pipes work naturally |
| Redirects | $`cmd > file.txt` | Output redirection |
Capturing Output
import { PluginOutput, StartPluginWithLambda } from '@atomicloud/cyan-sdk';import { $ } from 'bun';StartPluginWithLambda(async (input): Promise<PluginOutput> => {const { directory } = input;// Capture command outputconst result = await $`git -C ${directory} rev-parse HEAD`;const commitHash = result.stdout.toString().trim();console.log('Current commit:', commitHash);return { directory };});
import subprocessfrom cyan_sdk import PluginOutput, StartPluginWithLambda@StartPluginWithLambdaasync def main(input_data) -> PluginOutput:directory = input_data.directory# Capture command outputresult = subprocess.run(['git', '-C', directory, 'rev-parse', 'HEAD'],capture_output=True,text=True,check=True)commit_hash = result.stdout.strip()print('Current commit:', commit_hash)return PluginOutput(directory=directory)
using System.Diagnostics;using CyanSdk;public class Plugin{public static async Task<PluginOutput> Run(PluginInput input){var directory = input.Directory;// Capture command outputvar process = new Process{StartInfo = new ProcessStartInfo{FileName = "git",Arguments = $"-C {directory} rev-parse HEAD",RedirectStandardOutput = true,RedirectStandardError = true,UseShellExecute = false}};process.Start();var commitHash = (await process.StandardOutput.ReadToEndAsync()).Trim();await process.WaitForExitAsync();Console.WriteLine($"Current commit: {commitHash}");return new PluginOutput { Directory = directory };}}
Handling Errors
import { PluginOutput, StartPluginWithLambda } from '@atomicloud/cyan-sdk';import { $ } from 'bun';StartPluginWithLambda(async (input): Promise<PluginOutput> => {const { directory } = input;try {await $`git -C ${directory} status`;} catch (error) {// Git not initialized or other errorconsole.log('Git status failed:', error);}return { directory };});
import subprocessfrom cyan_sdk import PluginOutput, StartPluginWithLambda@StartPluginWithLambdaasync def main(input_data) -> PluginOutput:directory = input_data.directorytry:subprocess.run(['git', '-C', directory, 'status'], check=True)except subprocess.CalledProcessError as error:# Git not initialized or other errorprint('Git status failed:', error)return PluginOutput(directory=directory)
using System.Diagnostics;using CyanSdk;public class Plugin{public static async Task<PluginOutput> Run(PluginInput input){var directory = input.Directory;try{var process = new Process{StartInfo = new ProcessStartInfo{FileName = "git",Arguments = $"-C {directory} status",RedirectStandardOutput = true,RedirectStandardError = true,UseShellExecute = false}};process.Start();await process.WaitForExitAsync();if (process.ExitCode != 0){throw new Exception($"Command failed with exit code {process.ExitCode}");}}catch (Exception error){// Git not initialized or other errorConsole.WriteLine($"Git status failed: {error}");}return new PluginOutput { Directory = directory };}}
Using Node.js child_process
For more control or compatibility, use Node.js APIs:
import { exec } from 'child_process';import { promisify } from 'util';import { PluginOutput, StartPluginWithLambda } from '@atomicloud/cyan-sdk';const execAsync = promisify(exec);StartPluginWithLambda(async (input): Promise<PluginOutput> => {const { directory } = input;// Execute commandconst { stdout, stderr } = await execAsync('npm install', {cwd: directory,maxBuffer: 1024 * 1024 * 10 // 10MB buffer});console.log('Output:', stdout);if (stderr) console.error('Errors:', stderr);return { directory };});
import { spawn } from 'child_process';import { PluginOutput, StartPluginWithLambda } from '@atomicloud/cyan-sdk';StartPluginWithLambda(async (input): Promise<PluginOutput> => {const { directory } = input;return new Promise((resolve) => {const proc = spawn('npm', ['install'], {cwd: directory,stdio: 'inherit' // Pipe to parent});proc.on('close', (code) => {if (code === 0) {resolve({ directory });} else {throw new Error(`npm install failed with code ${code}`);}});});});
// Install: bun add execa// Note: execa is not included in the SDK and must be installed separatelyimport { execa } from 'execa';import { PluginOutput, StartPluginWithLambda } from '@atomicloud/cyan-sdk';StartPluginWithLambda(async (input): Promise<PluginOutput> => {const { directory } = input;// execa provides better DXawait execa('npm', ['install'], {cwd: directory,stdio: 'inherit'});return { directory };});
Common Command Patterns
Package Manager Commands
import { PluginOutput, StartPluginWithLambda } from '@atomicloud/cyan-sdk';import { $ } from 'bun';StartPluginWithLambda(async (input): Promise<PluginOutput> => {const { directory, config } = input;const cfg = config as { packageManager?: string };const pm = cfg.packageManager || 'npm';const commands: Record<string, string> = {npm: 'npm install',bun: 'bun install',yarn: 'yarn install',pnpm: 'pnpm install'};await $`cd ${directory} && ${commands[pm]}`;return { directory };});
import subprocessfrom cyan_sdk import PluginOutput, StartPluginWithLambda@StartPluginWithLambdaasync def main(input_data) -> PluginOutput:directory = input_data.directoryconfig = input_data.config or {}pm = config.get('packageManager', 'npm')commands = {'npm': ['npm', 'install'],'bun': ['bun', 'install'],'yarn': ['yarn', 'install'],'pnpm': ['pnpm', 'install']}subprocess.run(commands[pm], cwd=directory, check=True)return PluginOutput(directory=directory)
using System.Diagnostics;using CyanSdk;public class Plugin{public static async Task<PluginOutput> Run(PluginInput input){var directory = input.Directory;var config = input.Config ?? new Dictionary<string, object>();var pm = config.TryGetValue("packageManager", out var pmObj) ? pmObj.ToString() : "npm";var commands = new Dictionary<string, (string, string)>{["npm"] = ("npm", "install"),["bun"] = ("bun", "install"),["yarn"] = ("yarn", "install"),["pnpm"] = ("pnpm", "install")};var (cmd, args) = commands[pm];var process = new Process{StartInfo = new ProcessStartInfo{FileName = cmd,Arguments = args,WorkingDirectory = directory,UseShellExecute = false}};process.Start();await process.WaitForExitAsync();return new PluginOutput { Directory = directory };}}
Git Operations
import { PluginOutput, StartPluginWithLambda } from '@atomicloud/cyan-sdk';import { $ } from 'bun';StartPluginWithLambda(async (input): Promise<PluginOutput> => {const { directory } = input;// Initializeawait $`git -C ${directory} init`;// Configure user (from env or config)await $`git -C ${directory} config user.name "CyanPrint Bot"`;// Add and commitawait $`git -C ${directory} add .`;await $`git -C ${directory} commit -m "Initial commit from CyanPrint"`;return { directory };});
import subprocessfrom cyan_sdk import PluginOutput, StartPluginWithLambda@StartPluginWithLambdaasync def main(input_data) -> PluginOutput:directory = input_data.directory# Initializesubprocess.run(['git', '-C', directory, 'init'], check=True)# Configure user (from env or config)subprocess.run(['git', '-C', directory, 'config', 'user.name', 'CyanPrint Bot'], check=True)# Add and commitsubprocess.run(['git', '-C', directory, 'add', '.'], check=True)subprocess.run(['git', '-C', directory, 'commit', '-m', 'Initial commit from CyanPrint'], check=True)return PluginOutput(directory=directory)
using System.Diagnostics;using CyanSdk;public class Plugin{public static async Task<PluginOutput> Run(PluginInput input){var directory = input.Directory;// Initializeawait RunGitCommand(directory, "init");// Configure user (from env or config)await RunGitCommand(directory, "config user.name \"CyanPrint Bot\"");// Add and commitawait RunGitCommand(directory, "add .");await RunGitCommand(directory, "commit -m \"Initial commit from CyanPrint\"");return new PluginOutput { Directory = directory };}private static Task RunGitCommand(string directory, string args){var tcs = new TaskCompletionSource<bool>();var process = new Process{StartInfo = new ProcessStartInfo{FileName = "git",Arguments = $"-C {directory} {args}",UseShellExecute = false}};process.Exited += (s, e) => tcs.SetResult(true);process.EnableRaisingEvents = true;process.Start();return tcs.Task;}}
Running Build Tools
import { PluginOutput, StartPluginWithLambda } from '@atomicloud/cyan-sdk';import { $ } from 'bun';StartPluginWithLambda(async (input): Promise<PluginOutput> => {const { directory } = input;// Run linter (npm run --if-present requires npm 7+)await $`cd ${directory} && npm run lint --if-present`;// Run tests (optional)try {await $`cd ${directory} && npm test --if-present`.quiet();} catch {console.log('Tests skipped or failed');}// Buildawait $`cd ${directory} && npm run build --if-present`;return { directory };});
import subprocessfrom cyan_sdk import PluginOutput, StartPluginWithLambda@StartPluginWithLambdaasync def main(input_data) -> PluginOutput:directory = input_data.directory# Run linter (npm run --if-present requires npm 7+)subprocess.run(['npm', 'run', 'lint', '--if-present'], cwd=directory, check=True)# Run tests (optional)try:subprocess.run(['npm', 'test', '--if-present'],cwd=directory,check=True,capture_output=True)except subprocess.CalledProcessError:print('Tests skipped or failed')# Buildsubprocess.run(['npm', 'run', 'build', '--if-present'], cwd=directory, check=True)return PluginOutput(directory=directory)
using System.Diagnostics;using CyanSdk;public class Plugin{public static async Task<PluginOutput> Run(PluginInput input){var directory = input.Directory;// Run linter (npm run --if-present requires npm 7+)await RunNpmCommand(directory, "run lint --if-present");// Run tests (optional)try{await RunNpmCommand(directory, "test --if-present");}catch{Console.WriteLine("Tests skipped or failed");}// Buildawait RunNpmCommand(directory, "run build --if-present");return new PluginOutput { Directory = directory };}private static Task RunNpmCommand(string directory, string args){var tcs = new TaskCompletionSource<bool>();var process = new Process{StartInfo = new ProcessStartInfo{FileName = "npm",Arguments = args,WorkingDirectory = directory,UseShellExecute = false}};process.Exited += (s, e) => tcs.SetResult(true);process.EnableRaisingEvents = true;process.Start();return tcs.Task;}}
The --if-present flag for npm scripts requires npm version 7 or later. For older npm versions, you'll need to check if the script exists before running it.
Be careful with long-running commands. The plugin timeout is configurable but has limits. For heavy operations, consider making them optional.