aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
260 lines • 8.08 kB
JavaScript
/**
* Command generation and deployment
*/
import { mkdir, writeFile, readFile, access } from 'fs/promises';
import { join, dirname } from 'path';
import { constants } from 'fs';
import { generateCommandContent } from './templates.js';
import { getCommandsDirectory, getFileExtension } from '../platform-paths.js';
/**
* Validate command name (kebab-case)
*/
function validateCommandName(name) {
if (!name || name.trim() === '') {
return 'Command name cannot be empty';
}
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
return 'Command name must be kebab-case (lowercase letters, numbers, hyphens only, must start with letter)';
}
if (name.startsWith('-') || name.endsWith('-')) {
return 'Command name cannot start or end with a hyphen';
}
if (name.includes('--')) {
return 'Command name cannot contain consecutive hyphens';
}
return null;
}
/**
* Validate command options
*/
export function validateCommand(command) {
const issues = [];
// Validate name
const nameError = validateCommandName(command.name);
if (nameError) {
issues.push({
type: 'error',
field: 'name',
message: nameError,
suggestion: 'Use lowercase letters, numbers, and hyphens only (e.g., my-command)',
});
}
// Validate content
if (!command.content || command.content.trim() === '') {
issues.push({
type: 'error',
field: 'content',
message: 'Command content cannot be empty',
});
}
// Validate platform
const validPlatforms = ['claude', 'codex', 'copilot', 'cursor', 'factory', 'generic', 'windsurf'];
if (!validPlatforms.includes(command.platform)) {
issues.push({
type: 'error',
field: 'platform',
message: `Invalid platform: ${command.platform}`,
suggestion: `Must be one of: ${validPlatforms.join(', ')}`,
});
}
// Warn if content doesn't include standard options
if (!command.content.includes('--interactive') && !command.content.includes('--guidance')) {
issues.push({
type: 'warning',
field: 'content',
message: 'Command should support --interactive and --guidance options',
suggestion: 'Add these standard options to command arguments',
});
}
return {
valid: issues.filter(i => i.type === 'error').length === 0,
issues,
command,
};
}
/**
* Transform command content for Cursor platform (JSON format)
*/
function transformForCursor(name, description, options) {
const spec = {
name,
description,
arguments: (options.args || []).map(arg => ({
name: arg.name,
description: arg.description,
required: arg.required,
type: 'string',
default: arg.default,
})),
options: [
...(options.options || []).map(opt => ({
name: opt.name,
description: opt.description,
type: opt.type,
default: opt.default,
})),
{
name: 'interactive',
description: 'Enable interactive mode with guided questions',
type: 'boolean',
default: false,
},
{
name: 'guidance',
description: 'Provide strategic guidance for execution',
type: 'string',
},
],
execution: {
steps: [
'Validate inputs',
'Process request',
'Generate output',
'Report status',
],
},
};
return JSON.stringify(spec, null, 2);
}
/**
* Generate a command from options
*/
export async function generateCommand(options) {
// Validate command name
const nameError = validateCommandName(options.name);
if (nameError) {
throw new Error(nameError);
}
// Default template
const template = options.template || 'utility';
// Determine target directory
const targetDir = options.targetDir || getCommandsDirectory(options.platform, options.projectPath);
// Get file extension
const ext = getFileExtension(options.platform);
// Build file path
const filePath = join(targetDir, `${options.name}${ext}`);
// Generate content based on platform
let content;
if (options.platform === 'cursor') {
// Cursor uses JSON format
content = transformForCursor(options.name, options.description, options);
}
else {
// Other platforms use markdown
content = generateCommandContent(template, options.name, options.description, options.args, options.options, options.guidance);
}
return {
name: options.name,
path: filePath,
content,
platform: options.platform,
template,
};
}
/**
* Check if file exists
*/
async function fileExists(path) {
try {
await access(path, constants.F_OK);
return true;
}
catch {
return false;
}
}
/**
* Backup existing file
*/
async function backupFile(path) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
const backupPath = `${path}.backup-${timestamp}`;
const content = await readFile(path, 'utf-8');
await writeFile(backupPath, content, 'utf-8');
return backupPath;
}
/**
* Deploy a generated command to the filesystem
*/
export async function deployCommand(command, options = {}) {
try {
// Validate command first
const validation = validateCommand(command);
if (!validation.valid) {
const errors = validation.issues.filter(i => i.type === 'error').map(i => i.message);
return {
command,
success: false,
error: `Validation failed: ${errors.join('; ')}`,
};
}
// Check if file exists
const exists = await fileExists(command.path);
// Handle existing file
if (exists && !options.force) {
return {
command,
success: false,
error: `File already exists: ${command.path}. Use force option to overwrite.`,
};
}
// Backup if requested and file exists
let backed_up = false;
if (exists && options.backup) {
await backupFile(command.path);
backed_up = true;
}
// Ensure directory exists
const dir = dirname(command.path);
await mkdir(dir, { recursive: true });
// Write command file
await writeFile(command.path, command.content, 'utf-8');
return {
command,
success: true,
backed_up,
};
}
catch (error) {
return {
command,
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* Generate and deploy a command in one step
*/
export async function generateAndDeploy(options, deployOptions = {}) {
const command = await generateCommand(options);
// If dry run, don't deploy
if (options.dryRun) {
return {
command,
success: true,
error: undefined,
};
}
return deployCommand(command, deployOptions);
}
/**
* List existing commands in a project
*/
export async function listCommands(platform, projectPath) {
const commandsDir = getCommandsDirectory(platform, projectPath);
const ext = getFileExtension(platform);
try {
const { readdir } = await import('fs/promises');
const files = await readdir(commandsDir);
return files
.filter(f => f.endsWith(ext))
.map(f => f.replace(ext, ''))
.sort();
}
catch {
// Directory doesn't exist or is empty
return [];
}
}
//# sourceMappingURL=generator.js.map