UNPKG

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

337 lines (286 loc) 10.8 kB
#!/usr/bin/env node /** * Deploy Commands as Prompts to Codex * * Transforms AIWG slash commands to Codex prompt format and deploys to ~/.codex/prompts/ * * Codex Prompt Format: * - Location: ~/.codex/prompts/<name>.md * - YAML frontmatter (optional): description, argument-hint * - Placeholders: $1-$9, $ARGUMENTS, $NAMED * * Usage: * node tools/commands/deploy-prompts-codex.mjs [options] * * Options: * --source <path> Source directory (defaults to repo root) * --target <path> Target directory (defaults to ~/.codex/prompts) * --mode <type> Deployment mode: general, sdlc, marketing (alias: mmk), media-curator, research, or all (default) * --dry-run Show what would be deployed without writing * --force Overwrite existing files * --prefix <str> Prefix for prompt names (default: 'aiwg') */ import realFs from 'fs'; import { createRequire } from 'module'; const _require = createRequire(import.meta.url); let fs; try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs; } catch { fs = realFs; } import path from 'path'; import os from 'os'; import { getFrameworksForMode, normalizeDeploymentMode, getAddonSkillDirs, listSkillDirs, collectFrameworkArtifacts } from '../agents/providers/base.mjs'; const CODEX_PROMPTS_DIR = path.join(os.homedir(), '.codex', 'prompts'); function parseArgs() { const args = process.argv.slice(2); const cfg = { source: null, target: CODEX_PROMPTS_DIR, mode: 'all', dryRun: false, force: false, prefix: 'aiwg' }; for (let i = 0; i < args.length; i++) { const a = args[i]; if (a === '--source' && args[i + 1]) cfg.source = path.resolve(args[++i]); else if (a === '--target' && args[i + 1]) cfg.target = path.resolve(args[++i]); else if (a === '--mode' && args[i + 1]) cfg.mode = String(args[++i]).toLowerCase(); else if (a === '--dry-run') cfg.dryRun = true; else if (a === '--force') cfg.force = true; else if (a === '--prefix' && args[i + 1]) cfg.prefix = args[++i]; } cfg.mode = normalizeDeploymentMode(cfg.mode); return cfg; } function ensureDir(d) { if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); } /** * List .md command files in a directory */ function listCommandFiles(dir) { if (!fs.existsSync(dir)) return []; const excluded = ['README.md', 'manifest.md', 'DEVELOPMENT_GUIDE.md']; return fs.readdirSync(dir, { withFileTypes: true }) .filter(e => e.isFile() && e.name.endsWith('.md') && !excluded.includes(e.name)) .map(e => path.join(dir, e.name)); } /** * Parse AIWG command frontmatter and body */ function parseCommandFile(filePath) { const content = fs.readFileSync(filePath, 'utf8'); // Check for YAML frontmatter const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (fmMatch) { const [, frontmatter, body] = fmMatch; const metadata = {}; for (const line of frontmatter.split('\n')) { const colonIdx = line.indexOf(':'); if (colonIdx > 0) { const key = line.slice(0, colonIdx).trim(); const value = line.slice(colonIdx + 1).trim(); metadata[key] = value; } } return { metadata, body: body.trim(), hasMetadata: true }; } // No frontmatter, entire content is body return { metadata: {}, body: content.trim(), hasMetadata: false }; } /** * Transform AIWG command to Codex prompt format */ function transformToCodexPrompt(commandPath, prefix) { const parsed = parseCommandFile(commandPath); const commandName = path.basename(commandPath, '.md'); // Extract description from metadata or first paragraph let description = ''; if (parsed.metadata.description) { description = parsed.metadata.description; } else { // Try to extract from first line after any heading const lines = parsed.body.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('-') && !trimmed.startsWith('```')) { description = trimmed.slice(0, 200); break; } } } // Extract argument hints from command body let argumentHint = ''; const argPatterns = parsed.body.match(/\$ARGUMENTS|\$\d+|\$[A-Z_]+/g); if (argPatterns) { const unique = [...new Set(argPatterns)]; argumentHint = unique.join(' '); } // Convert AIWG placeholders to Codex format // AIWG: $ARGUMENTS, $1, etc. -> Same in Codex let body = parsed.body; // Convert {{variable}} to $VARIABLE format body = body.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, name) => `$${name.toUpperCase()}`); // Build Codex prompt const promptName = prefix ? `${prefix}-${commandName}` : commandName; let promptContent = ''; // Add frontmatter if we have description or argument-hint if (description || argumentHint) { promptContent = `---\n`; if (description) promptContent += `description: ${description}\n`; if (argumentHint) promptContent += `argument-hint: ${argumentHint}\n`; promptContent += `---\n\n`; } promptContent += body; return { name: promptName, description, argumentHint, content: promptContent, sourcePath: commandPath }; } /** * Deploy prompt to Codex prompts directory */ function deployPrompt(prompt, targetDir, opts) { const { force = false, dryRun = false } = opts; const destPath = path.join(targetDir, `${prompt.name}.md`); // Check if prompt already exists if (fs.existsSync(destPath)) { const existingContent = fs.readFileSync(destPath, 'utf8'); if (existingContent === prompt.content && !force) { console.log(` skip (unchanged): ${prompt.name}`); return { action: 'skip', reason: 'unchanged' }; } } if (dryRun) { console.log(` [dry-run] deploy: ${prompt.name}`); return { action: 'deploy', reason: 'dry-run' }; } // Write prompt file fs.writeFileSync(destPath, prompt.content, 'utf8'); console.log(` deployed: ${prompt.name}.md`); return { action: 'deploy', reason: 'success' }; } /** * Load addon manifest if it exists */ function loadAddonManifest(addonPath) { const manifestPath = path.join(addonPath, 'manifest.json'); if (fs.existsSync(manifestPath)) { try { return JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch (e) { console.warn(` Warning: Could not parse manifest at ${manifestPath}`); } } return {}; } /** * Get command directories based on mode * Returns objects with: dir, label, isCore (whether to deploy all commands) */ function getCommandDirectories(srcRoot, mode) { const dirs = []; // General commands (not core - apply priority filter) if (mode === 'general' || mode === 'all') { const generalCommandsDir = path.join(srcRoot, 'commands'); if (fs.existsSync(generalCommandsDir)) { dirs.push({ dir: generalCommandsDir, label: 'general', isCore: false }); } } // Addon commands (dynamically discovered) // Check manifest for core/autoInstall flags if (mode === 'general' || mode === 'sdlc' || mode === 'both' || mode === 'all') { const addonsRoot = path.join(srcRoot, 'agentic', 'code', 'addons'); if (fs.existsSync(addonsRoot)) { const addonEntries = fs.readdirSync(addonsRoot, { withFileTypes: true }) .filter(e => e.isDirectory()); for (const entry of addonEntries) { const addonPath = path.join(addonsRoot, entry.name); const addonCommandsDir = path.join(addonPath, 'commands'); if (fs.existsSync(addonCommandsDir)) { const manifest = loadAddonManifest(addonPath); // Core addons (core: true or autoInstall: true) deploy ALL commands const isCore = manifest.core === true || manifest.autoInstall === true; dirs.push({ dir: addonCommandsDir, label: entry.name, isCore }); } } } } // Framework commands are discovered from framework manifests/directory structure. const frameworks = getFrameworksForMode(srcRoot, mode); for (const framework of frameworks) { if (framework.components.commands.exists) { dirs.push({ dir: framework.components.commands.path, label: framework.id, isCore: false }); } } return dirs; } (async function main() { const cfg = parseArgs(); const { source, target, mode, dryRun, force, prefix } = cfg; // Resolve source directory const scriptDir = path.dirname(new URL(import.meta.url).pathname); const repoRoot = path.resolve(scriptDir, '..', '..'); const srcRoot = source || repoRoot; console.log(`Deploying commands as Codex prompts (~/.codex/prompts/)`); console.log(` Source: ${srcRoot}`); console.log(` Target: ${target}`); console.log(` Mode: ${mode}`); console.log(` Prefix: ${prefix}`); if (dryRun) console.log(` [DRY RUN]`); console.log(); // Create target directory if (!dryRun) { ensureDir(target); } // Collect skill names to detect command/skill collisions const skillNames = new Set(); const addonSkills = getAddonSkillDirs(srcRoot); for (const d of addonSkills) skillNames.add(path.basename(d)); const frameworkSkills = collectFrameworkArtifacts(srcRoot, mode, { includeAgents: false, includeCommands: false, includeSkills: true, includeRules: false }); for (const d of frameworkSkills.skills) skillNames.add(path.basename(d)); // Get command directories based on mode const commandDirs = getCommandDirectories(srcRoot, mode); let totalDeployed = 0; let totalSkipped = 0; for (const { dir, label } of commandDirs) { const commandFiles = listCommandFiles(dir); if (commandFiles.length === 0) continue; console.log(`\n${label} (${commandFiles.length} prompts):`); for (const commandFile of commandFiles) { // Skip commands that collide with skills (skills take precedence) const commandName = path.basename(commandFile, '.md'); if (skillNames.has(commandName)) { console.log(` skip (skill precedence): command "${commandName}" — skill with same name takes precedence`); totalSkipped++; continue; } try { const prompt = transformToCodexPrompt(commandFile, prefix); const result = deployPrompt(prompt, target, { force, dryRun }); if (result.action === 'deploy') totalDeployed++; else totalSkipped++; } catch (err) { console.log(` error: ${path.basename(commandFile)} - ${err.message}`); totalSkipped++; } } } console.log(`\nSummary: ${totalDeployed} deployed, ${totalSkipped} skipped`); if (!dryRun && totalDeployed > 0) { console.log(`\nRestart Codex to load new prompts.`); console.log(`Use prompts with: /prompts:${prefix}-<command-name>`); } })();