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
674 lines (593 loc) • 22.6 kB
JavaScript
/**
* Cursor IDE Provider
*
* Deploys agents, commands, skills, and rules for Cursor IDE.
* Rules use native .cursor/rules/ support with MDC format.
* Other artifacts use conventional .cursor/ subdirectories.
*
* Deployment paths:
* - Agents: .cursor/agents/
* - Commands: .cursor/commands/
* - Skills: .cursor/skills/
* - Rules: .cursor/rules/
*
* Special features:
* - MDC format (.mdc extension) for rules
* - Glob pattern attachment for rules
* - $ARGUMENTS -> [arguments] conversion for rules
* - Delegates rules deployment to deploy-rules-cursor.mjs
*/
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 { spawn } from 'child_process';
import {
ensureDir,
listMdFiles,
listMdFilesRecursive,
listSkillDirs,
deployFiles,
deploySkillDir,
deploySkillsWithKernelRouting,
isKernelSkill,
pruneStaleAiwgSkills,
computeAllKernelNames,
filterAgentFiles,
getAddonAgentFiles,
getAddonCommandFiles,
getAddonSkillDirs,
getAddonRuleFiles,
createAgentsMdFromTemplate,
initializeFrameworkWorkspace,
normalizeDeploymentMode,
collectFrameworkArtifacts,
cleanupOldRuleFiles,
filterCommandsAgainstSkills,
deploySoulCompanions
} from './base.mjs';
// ============================================================================
// Provider Configuration
// ============================================================================
export const name = 'cursor';
export const aliases = [];
export const paths = {
agents: '.cursor/agents/',
commands: '.cursor/commands/',
// Skills sequestered under .cursor/.aiwg/skills/ — index-driven discovery (#1212).
skills: '.cursor/.aiwg/skills/',
rules: '.cursor/rules/'
};
// Kernel skills (always-loaded) deploy to the platform-native dir.
export const kernelSkillsPath = '.cursor/skills/';
export const support = {
agents: 'conventional',
commands: 'conventional',
skills: 'conventional',
rules: 'native'
};
export const capabilities = {
agents: true,
commands: true,
skills: true,
rules: true, // Rules-focused provider with native support
aggregatedOutput: false,
yamlFormat: false
};
// ============================================================================
// Content Transformation
// ============================================================================
/**
* Transform agent content for Cursor
* Cursor uses conventional deployment - minimal transformation needed
*/
export function transformAgent(srcPath, content, opts) {
return content;
}
/**
* Transform command content for Cursor
* Cursor uses conventional deployment - minimal transformation needed
*/
export function transformCommand(srcPath, content, opts) {
return content;
}
/**
* Transform rule content for Cursor — inject MDC frontmatter with the
* activation mode mapping per ADR-2 (rule activation mode schema).
*
* Reads the source rule's `activation:` field (or defaults to alwaysApply
* per ADR-2 §2 default-preservation) and emits the corresponding MDC
* frontmatter fields:
* - alwaysApply (true): activation === 'alwaysApply' (default)
* - alwaysApply (false), no globs: activation === 'auto'
* - globs: activation === 'glob' + globs: '<pattern>'
* - alwaysApply (false), description: activation === 'manual'
*
* Per ADR-2 §5: until the live-Cursor smoke test gate is in CI, all rules
* deploy with alwaysApply: true regardless of source `activation` value
* (with a deploy-time warning when the source declared a non-alwaysApply
* mode). The deploy-time warning is emitted by the deployer, not this
* transform.
*/
export function transformRule(srcPath, content, opts) {
// Detect proper YAML frontmatter — must close within the first 30 lines.
// Files with `---` horizontal rules deeper in markdown content (like
// RULES-INDEX.md) can have multiple `---` lines without being frontmatter;
// the line-budget keeps us from misidentifying horizontal rules as frontmatter
// delimiters.
const lines = content.split('\n');
let fmEnd = -1;
if (lines[0]?.trim() === '---') {
for (let i = 1; i < Math.min(lines.length, 30); i++) {
if (lines[i].trim() === '---') {
fmEnd = i;
break;
}
// YAML frontmatter is key:value pairs. If we see a markdown heading or
// bold/italic markers in what would be frontmatter, this is not YAML.
if (/^(#{1,6}\s|\*\*|^\* )/.test(lines[i])) {
fmEnd = -1;
break;
}
}
}
if (fmEnd > 0) {
const existingFm = lines.slice(1, fmEnd).join('\n');
const body = lines.slice(fmEnd + 1).join('\n');
// If the operator already set alwaysApply, leave it alone.
if (/^\s*alwaysApply\s*:\s*\w+/m.test(existingFm)) {
return content;
}
// PUW-021 (#1122): if source frontmatter declares `globs:` (or
// `applyTo:` for Copilot-style rules), emit Cursor MDC `globs:` plus
// `alwaysApply: false`. The activation mode is implicitly `glob`.
const globsMatch = /^\s*globs?\s*:\s*(.+)$/m.exec(existingFm);
const applyToMatch = /^\s*applyTo\s*:\s*(.+)$/m.exec(existingFm);
if (globsMatch || applyToMatch) {
const globValue = (globsMatch?.[1] || applyToMatch?.[1] || '').trim().replace(/^['"]|['"]$/g, '');
let mergedFm = existingFm.trimEnd();
// Add globs field if absent (operator already set globs would have matched).
if (!globsMatch) {
mergedFm += `\nglobs: '${globValue}'`;
}
mergedFm += '\nalwaysApply: false';
return `---\n${mergedFm}\n---\n${body}`;
}
const updatedFm = existingFm.trimEnd() + '\nalwaysApply: true';
return `---\n${updatedFm}\n---\n${body}`;
}
// No (proper) frontmatter — prepend a minimal MDC block.
return `---\nalwaysApply: true\n---\n${content}`;
}
// ============================================================================
// Model Mapping (not applicable for Cursor)
// ============================================================================
export function mapModel(shorthand, modelCfg, modelsConfig) {
return shorthand;
}
// ============================================================================
// Deployment Functions
// ============================================================================
/**
* Deploy agents to .cursor/agents/
*/
export function deployAgents(agentFiles, targetDir, opts) {
const destDir = path.join(targetDir, paths.agents);
ensureDir(destDir, opts.dryRun);
return deployFiles(agentFiles, destDir, { ...opts, injectPlatform: true }, transformAgent);
}
/**
* Deploy commands to .cursor/commands/
*/
export function deployCommands(commandFiles, targetDir, opts) {
const destDir = path.join(targetDir, paths.commands);
ensureDir(destDir, opts.dryRun);
return deployFiles(commandFiles, destDir, opts, transformCommand);
}
/**
* Deploy skills with kernel-vs-standard routing (#1212/#1216).
* - kernel skills → .cursor/skills/ (platform-native, always-loaded)
* - standard → .cursor/.aiwg/skills/ (index-discoverable)
*/
export function deploySkills(skillDirs, targetDir, opts) {
const standardDestDir = path.join(targetDir, paths.skills);
const kernelDestDir = path.join(targetDir, kernelSkillsPath);
deploySkillsWithKernelRouting(skillDirs, standardDestDir, kernelDestDir, opts);
}
/**
* Deploy rules via external script (native MDC support)
* Falls back to inline deployment if script not found
*/
export async function deployRulesViaScript(targetDir, srcRoot, opts) {
const scriptPath = path.join(srcRoot, 'tools', 'rules', 'deploy-rules-cursor.mjs');
if (!fs.existsSync(scriptPath)) {
console.warn(`Cursor rules deployment script not found at ${scriptPath}`);
return false;
}
console.log('Delegating rules deployment to deploy-rules-cursor.mjs...');
return new Promise((resolve, reject) => {
const args = ['--target', targetDir, '--source', srcRoot];
if (opts.dryRun) args.push('--dry-run');
if (opts.force) args.push('--force');
if (opts.mode) args.push('--mode', opts.mode);
const child = spawn('node', [scriptPath, ...args], {
stdio: 'inherit',
cwd: srcRoot
});
child.on('close', (code) => {
if (code === 0) resolve(true);
else reject(new Error(`deploy-rules-cursor.mjs exited with code ${code}`));
});
child.on('error', reject);
});
}
/**
* Deploy rules to .cursor/rules/ (inline deployment)
* Used as fallback when external script is not available
*/
export function deployRulesInline(ruleFiles, targetDir, opts) {
const destDir = path.join(targetDir, paths.rules);
ensureDir(destDir, opts.dryRun);
// #1143: skip cleanup when this deploy has 0 rules.
cleanupOldRuleFiles(destDir, { ...opts, incomingFiles: ruleFiles });
// Use transformRule (PUW-011 #1112) so deployed rules carry MDC
// alwaysApply: true frontmatter — the safe default per ADR-2 §2.
return deployFiles(ruleFiles, destDir, opts, transformRule);
}
/**
* Deploy rules - tries external script first, falls back to inline
*/
export async function deployRules(ruleFilesOrTarget, targetDirOrSrcRoot, optsOrUndefined) {
// Handle both call signatures:
// 1. deployRules(targetDir, srcRoot, opts) - from old code
// 2. deployRules(ruleFiles, targetDir, opts) - from new code
// Check if first arg is array (new signature) or string (old signature)
if (Array.isArray(ruleFilesOrTarget)) {
// New signature: deployRules(ruleFiles, targetDir, opts)
return deployRulesInline(ruleFilesOrTarget, targetDirOrSrcRoot, optsOrUndefined);
} else {
// Old signature: deployRules(targetDir, srcRoot, opts)
// Try external script first
try {
const success = await deployRulesViaScript(ruleFilesOrTarget, targetDirOrSrcRoot, optsOrUndefined);
if (success) return;
} catch (err) {
console.warn('External rules script failed, using inline deployment');
}
// Fallback to inline - need to collect rule files
console.log('Using inline rules deployment...');
const ruleFiles = [];
const srcRoot = targetDirOrSrcRoot;
const opts = optsOrUndefined;
// Use consolidated index if available
const indexPath = getRulesIndexPath(srcRoot);
if (indexPath) {
ruleFiles.push(indexPath);
} else {
const sdlcRulesDir = path.join(srcRoot, 'agentic', 'code', 'frameworks', 'sdlc-complete', 'rules');
if (fs.existsSync(sdlcRulesDir)) {
ruleFiles.push(...listMdFiles(sdlcRulesDir));
}
}
deployRulesInline(ruleFiles, ruleFilesOrTarget, opts);
}
}
// ============================================================================
// AGENTS.md
// ============================================================================
export function createAgentsMd(target, srcRoot, dryRun) {
createAgentsMdFromTemplate(target, srcRoot, 'cursor/AGENTS.md.aiwg-template', dryRun);
}
// ============================================================================
// Cloud agent + worktree configuration (PUW-019 #1120)
// ============================================================================
/**
* Deploy Cursor cloud-agent + worktree config templates.
*
* Per PUW-019: AIWG ships templates at
* agentic/code/frameworks/sdlc-complete/templates/cursor/environment.json.aiwg-template
* agentic/code/frameworks/sdlc-complete/templates/cursor/worktrees.json.aiwg-template
* but they were never wired into the deployer. This emits them at
* .cursor/environment.json
* .cursor/worktrees.json
* only when the operator-provided file is absent (always-deploy invariant
* §0.6 — we don't overwrite operator-authored config). When the file
* exists, deploy is skipped with a verbose-only note.
*/
export function deployCursorConfigTemplates(targetDir, srcRoot, opts) {
const cursorDir = path.join(targetDir, '.cursor');
ensureDir(cursorDir, opts?.dryRun);
const templates = [
{ src: 'cursor/environment.json.aiwg-template', dest: 'environment.json' },
{ src: 'cursor/worktrees.json.aiwg-template', dest: 'worktrees.json' },
];
for (const t of templates) {
const srcPath = path.join(
srcRoot,
'agentic',
'code',
'frameworks',
'sdlc-complete',
'templates',
t.src,
);
const destPath = path.join(cursorDir, t.dest);
if (!fs.existsSync(srcPath)) {
if (opts?.verbose) {
console.log(` cursor template not found, skipping: ${t.src}`);
}
continue;
}
if (fs.existsSync(destPath)) {
if (opts?.verbose) {
console.log(` ${t.dest} exists, preserving operator content`);
}
continue;
}
if (opts?.dryRun) {
console.log(`[dry-run] Would deploy cursor template ${t.src} -> .cursor/${t.dest}`);
continue;
}
fs.copyFileSync(srcPath, destPath);
if (opts?.verbose) {
console.log(`deployed cursor template: .cursor/${t.dest}`);
}
}
}
// ============================================================================
// Post-Deployment
// ============================================================================
export async function postDeploy(targetDir, opts) {
initializeFrameworkWorkspace(targetDir, opts.mode, opts.dryRun, opts.srcRoot);
if (opts.createAgentsMd) {
createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
}
// PUW-019 (#1120): wire environment.json + worktrees.json templates.
try {
deployCursorConfigTemplates(targetDir, opts.srcRoot, opts);
} catch (err) {
console.warn(
`Warning: cursor template deploy failed: ${err && err.message ? err.message : err}`,
);
}
}
// ============================================================================
// File Extension
// ============================================================================
export function getFileExtension(type) {
// Rules use .mdc for native Cursor support, everything else uses .md
if (type === 'rule' || type === 'rules') {
return '.mdc';
}
return '.md';
}
// ============================================================================
// Main Deploy Function
// ============================================================================
/**
* Main deployment function for Cursor provider
* Orchestrates deployment of agents, commands, skills, and rules
*/
export async function deploy(opts) {
const {
srcRoot,
target,
mode,
deployCommands: shouldDeployCommands,
deploySkills: shouldDeploySkills,
deployRules: shouldDeployRules,
commandsOnly,
skillsOnly,
rulesOnly,
dryRun,
createAgentsMd: shouldCreateAgentsMd
} = opts;
console.log(`\n=== Cursor IDE Provider ===`);
console.log(`Target: ${target}`);
console.log(`Mode: ${mode}`);
// Collect source files based on mode
const agentFiles = [];
const commandFiles = [];
const skillDirs = [];
const ruleFiles = [];
// Check for addon-style directory structure (direct agents/, commands/, skills/ subdirs)
// This handles deployment when --source points to an addon directory
const isAddonSource = fs.existsSync(path.join(srcRoot, 'agents')) ||
fs.existsSync(path.join(srcRoot, 'commands')) ||
fs.existsSync(path.join(srcRoot, 'skills'));
if (isAddonSource) {
// Deploy from addon-style directory structure
const addonAgentsDir = path.join(srcRoot, 'agents');
if (fs.existsSync(addonAgentsDir)) {
agentFiles.push(...listMdFiles(addonAgentsDir));
}
if (shouldDeployCommands || commandsOnly) {
const addonCommandsDir = path.join(srcRoot, 'commands');
if (fs.existsSync(addonCommandsDir)) {
commandFiles.push(...listMdFiles(addonCommandsDir));
}
}
if (shouldDeploySkills || skillsOnly) {
const addonSkillsDir = path.join(srcRoot, 'skills');
if (fs.existsSync(addonSkillsDir)) {
skillDirs.push(...listSkillDirs(addonSkillsDir));
// Holistic post-deploy cleanup of stale AIWG-managed kernel
// skills (renamed/removed sources). Uses the global kernel set
// (computeAllKernelNames walks all source frameworks/addons),
// not just this-call's skillDirs, because aiwg use invokes
// deploy-agents.mjs multiple times.
{
const _kernelDestDir = path.isAbsolute(kernelSkillsPath)
? kernelSkillsPath
: path.join(target, kernelSkillsPath);
pruneStaleAiwgSkills(_kernelDestDir, computeAllKernelNames(srcRoot), opts);
}
}
}
if (shouldDeployRules || rulesOnly) {
const addonRulesDir = path.join(srcRoot, 'rules');
if (fs.existsSync(addonRulesDir)) {
ruleFiles.push(...listMdFiles(addonRulesDir));
}
}
}
// All addons (dynamically discovered)
const normalizedMode = normalizeDeploymentMode(mode);
if (normalizedMode === 'general' || normalizedMode === 'sdlc' || normalizedMode === 'both' || normalizedMode === 'all') {
agentFiles.push(...getAddonAgentFiles(srcRoot));
if (shouldDeployCommands || commandsOnly) {
commandFiles.push(...getAddonCommandFiles(srcRoot));
}
if (shouldDeploySkills || skillsOnly) {
skillDirs.push(...getAddonSkillDirs(srcRoot));
}
if (shouldDeployRules || rulesOnly) {
ruleFiles.push(...getAddonRuleFiles(srcRoot));
}
}
const frameworkArtifacts = collectFrameworkArtifacts(srcRoot, normalizedMode, {
includeAgents: true,
includeCommands: shouldDeployCommands || commandsOnly,
includeSkills: shouldDeploySkills || skillsOnly,
includeRules: shouldDeployRules || rulesOnly,
recursiveCommands: true,
consolidatedSdlcRules: true
});
agentFiles.push(...frameworkArtifacts.agents);
const soulFiles = [...(frameworkArtifacts.souls || [])];
commandFiles.push(...frameworkArtifacts.commands);
skillDirs.push(...frameworkArtifacts.skills);
ruleFiles.push(...frameworkArtifacts.rules);
// Deploy based on flags
if (!commandsOnly && !skillsOnly && !rulesOnly) {
// Apply filters if specified
const filteredAgents = filterAgentFiles(agentFiles, opts);
if (opts.filter || opts.filterRole) {
console.log(`\nFiltered from ${agentFiles.length} to ${filteredAgents.length} agents`);
}
console.log(`\nDeploying ${filteredAgents.length} agents...`);
deployAgents(filteredAgents, target, opts);
// Deploy soul companion files alongside agents
if (soulFiles.length > 0) {
const destDir = path.join(target, paths.agents);
console.log(`\nDeploying ${soulFiles.length} soul files...`);
deploySoulCompanions(soulFiles, destDir, opts);
}
}
// Filter commands that collide with skills (skills take precedence)
const filteredCommands = (shouldDeploySkills || skillsOnly)
? filterCommandsAgainstSkills(commandFiles, skillDirs)
: commandFiles;
if (shouldDeployCommands || commandsOnly) {
console.log(`\nDeploying ${filteredCommands.length} commands...`);
deployCommands(filteredCommands, target, opts);
}
if (shouldDeploySkills || skillsOnly) {
console.log(`\nDeploying ${skillDirs.length} skills...`);
deploySkills(skillDirs, target, opts);
}
if (shouldDeployRules || rulesOnly) {
console.log(`\nDeploying ${ruleFiles.length} rules...`);
// Use inline deployment (external script relied on commands/ dirs which are now skills)
deployRulesInline(ruleFiles, target, opts);
}
// Post-deployment
await postDeploy(target, { ...opts, createAgentsMd: shouldCreateAgentsMd });
console.log('\n=== Cursor deployment complete ===\n');
}
// ============================================================================
// Default Export
// ============================================================================
// ============================================================================
// Plugin Bundle Generation (Cursor)
// ============================================================================
/**
* Generate a `.cursor-plugin/plugin.json` manifest for distributing AIWG as a
* Cursor plugin. Cursor's cursor.com/marketplace is centralized and partner-
* oriented; this manifest enables manual/local installation and future
* submission if the marketplace opens third-party submissions.
*
* Layout produced:
* <targetDir>/.cursor-plugin/plugin.json — the plugin manifest
*
* @param {string} targetDir - Plugin bundle root (typically plugins/<name>/)
* @param {{ dryRun?: boolean, srcRoot?: string, name?: string, version?: string, description?: string, contents?: object }} opts
*/
export function generatePluginBundle(targetDir, opts = {}) {
const {
dryRun = false,
srcRoot = process.cwd(),
name: pluginName = 'aiwg-plugin',
version: overrideVersion,
description = 'AIWG plugin for Cursor',
contents = {
agents: 'agents/',
commands: 'commands/',
skills: 'skills/',
rules: 'rules/',
},
} = opts;
// Resolve version: opts.version > package.json > 'unknown'
let version = overrideVersion;
if (!version) {
try {
const pkgPath = path.join(srcRoot, 'package.json');
if (fs.existsSync(pkgPath)) {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
version = pkg.version || 'unknown';
} else {
version = 'unknown';
}
} catch {
version = 'unknown';
}
}
const pluginDir = path.join(targetDir, '.cursor-plugin');
const manifest = {
name: pluginName,
version,
displayName: pluginName.replace(/^aiwg-/, 'AIWG ').replace(/-/g, ' '),
description,
publisher: 'aiwg',
homepage: 'https://aiwg.io',
repository: 'https://github.com/jmagly/aiwg',
license: 'MIT',
contents,
};
if (dryRun) {
console.log(`[dry-run] Would create ${pluginDir}/plugin.json`);
return { pluginDir, manifest };
}
if (!fs.existsSync(pluginDir)) {
fs.mkdirSync(pluginDir, { recursive: true });
}
fs.writeFileSync(
path.join(pluginDir, 'plugin.json'),
JSON.stringify(manifest, null, 2) + '\n',
'utf-8'
);
console.log(`Generated Cursor plugin manifest: ${pluginDir}/plugin.json`);
return { pluginDir, manifest };
}
export default {
name,
aliases,
paths,
kernelSkillsPath,
support,
capabilities,
transformAgent,
transformCommand,
mapModel,
deployAgents,
deployCommands,
deploySkills,
deployRules,
createAgentsMd,
postDeploy,
getFileExtension,
generatePluginBundle,
deploy
};