LogoCyanPrint

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 git
await $`git -C ${directory} init`;
// Add all files
await $`git -C ${directory} add .`;
// Create initial commit
await $`git -C ${directory} commit -m "Initial commit"`;
return { directory };
});
import subprocess
from cyan_sdk import PluginOutput, StartPluginWithLambda
@StartPluginWithLambda
async def main(input_data) -> PluginOutput:
directory = input_data.directory
# Initialize git
subprocess.run(['git', '-C', directory, 'init'], check=True)
# Add all files
subprocess.run(['git', '-C', directory, 'add', '.'], check=True)
# Create initial commit
subprocess.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 git
await RunCommand("git", $"-C {directory} init");
// Add all files
await RunCommand("git", $"-C {directory} add .");
// Create initial commit
await 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

FeatureExampleDescription
Interpolation$`cd ${dir}`Variables are escaped and safe
Quiet mode$`cmd`.quiet()Suppress stdout/stderr
Output captureconst 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 output
const result = await $`git -C ${directory} rev-parse HEAD`;
const commitHash = result.stdout.toString().trim();
console.log('Current commit:', commitHash);
return { directory };
});
import subprocess
from cyan_sdk import PluginOutput, StartPluginWithLambda
@StartPluginWithLambda
async def main(input_data) -> PluginOutput:
directory = input_data.directory
# Capture command output
result = 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 output
var 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 error
console.log('Git status failed:', error);
}
return { directory };
});
import subprocess
from cyan_sdk import PluginOutput, StartPluginWithLambda
@StartPluginWithLambda
async def main(input_data) -> PluginOutput:
directory = input_data.directory
try:
subprocess.run(['git', '-C', directory, 'status'], check=True)
except subprocess.CalledProcessError as error:
# Git not initialized or other error
print('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 error
Console.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 command
const { 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 separately
import { execa } from 'execa';
import { PluginOutput, StartPluginWithLambda } from '@atomicloud/cyan-sdk';
StartPluginWithLambda(async (input): Promise<PluginOutput> => {
const { directory } = input;
// execa provides better DX
await 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 subprocess
from cyan_sdk import PluginOutput, StartPluginWithLambda
@StartPluginWithLambda
async def main(input_data) -> PluginOutput:
directory = input_data.directory
config = 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;
// Initialize
await $`git -C ${directory} init`;
// Configure user (from env or config)
await $`git -C ${directory} config user.email "[email protected]"`;
await $`git -C ${directory} config user.name "CyanPrint Bot"`;
// Add and commit
await $`git -C ${directory} add .`;
await $`git -C ${directory} commit -m "Initial commit from CyanPrint"`;
return { directory };
});
import subprocess
from cyan_sdk import PluginOutput, StartPluginWithLambda
@StartPluginWithLambda
async def main(input_data) -> PluginOutput:
directory = input_data.directory
# Initialize
subprocess.run(['git', '-C', directory, 'init'], check=True)
# Configure user (from env or config)
subprocess.run(['git', '-C', directory, 'config', 'user.email', '[email protected]'], check=True)
subprocess.run(['git', '-C', directory, 'config', 'user.name', 'CyanPrint Bot'], check=True)
# Add and commit
subprocess.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;
// Initialize
await RunGitCommand(directory, "init");
// Configure user (from env or config)
await RunGitCommand(directory, "config user.email \"[email protected]\"");
await RunGitCommand(directory, "config user.name \"CyanPrint Bot\"");
// Add and commit
await 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');
}
// Build
await $`cd ${directory} && npm run build --if-present`;
return { directory };
});
import subprocess
from cyan_sdk import PluginOutput, StartPluginWithLambda
@StartPluginWithLambda
async 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')
# Build
subprocess.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");
}
// Build
await 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.