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
795 lines (694 loc) • 32 kB
JavaScript
/**
* Deploy Agents and Commands - Orchestrator
*
* Deploy agents/commands from this repository to a target project.
* Delegates to provider-specific modules for platform-specific deployment.
*
* Usage:
* node tools/agents/deploy-agents.mjs [options]
*
* Options:
* --source <path> Source directory (defaults to repo root)
* --target <path> Target directory (defaults to cwd)
* --mode <type> Deployment mode: general, sdlc, marketing (alias: mmk), media-curator, research, both, or all (default)
* --deploy-commands Deploy commands in addition to agents
* --deploy-skills Deploy skills in addition to agents
* --deploy-rules Deploy rules in addition to agents
* --commands-only Deploy only commands (skip agents)
* --skills-only Deploy only skills (skip agents)
* --rules-only Deploy only rules (skip agents)
* --dry-run Show what would be deployed without writing
* --force Overwrite existing files
* --provider <name> Target provider: claude (default), openai, codex, cursor, opencode, copilot, factory, warp, windsurf, hermes, or openclaw
* --model <name> Override model for all tiers (blanket)
* --reasoning-model <name> Override model for reasoning tasks
* --coding-model <name> Override model for coding tasks
* --efficiency-model <name> Override model for efficiency tasks
* --as-agents-md Aggregate to single AGENTS.md (OpenAI/Codex)
* --create-agents-md Create/update AGENTS.md template
* --skip-commands-migration Skip deleting the commands directory (warns about duplicate TUI entries) (Factory/Codex/OpenCode/Cursor)
*
* Modes:
* general - Deploy only writing-quality addon agents and commands (alias: writing)
* writing - Deploy only writing-quality addon agents (alias for general)
* sdlc - Deploy only SDLC Complete framework agents and commands
* marketing - Deploy only Media/Marketing Kit framework agents and commands (alias: mmk)
* media-curator - Deploy only Media Curator framework agents and commands
* research - Deploy only Research Complete framework agents and commands
* both - Deploy writing + SDLC (legacy compatibility)
* all - Deploy all frameworks + addons (default)
*
* Providers:
* claude - Claude Code (default) - .claude/agents/, .claude/commands/, .claude/skills/, .claude/rules/
* factory - Factory AI - .factory/droids/, .factory/commands/, .factory/skills/, .factory/rules/
* codex - OpenAI Codex - .codex/agents/, .codex/commands/, .codex/skills/, .codex/rules/
* openai - Alias for codex
* opencode - OpenCode - .opencode/skill/, .opencode/rule/ (agents+commands not file-based)
* copilot - GitHub Copilot - .github/agents/, .github/commands/, .github/skills/, .github/copilot-rules/
* cursor - Cursor IDE - .cursor/agents/, .cursor/commands/, .cursor/skills/, .cursor/rules/
* warp - Warp Terminal - .warp/agents/, .warp/commands/, .warp/skills/, .warp/rules/ + WARP.md
* windsurf - Windsurf - .windsurf/agents/, .windsurf/workflows/, .windsurf/skills/, .windsurf/rules/
* openclaw - OpenClaw - ~/.openclaw/agents/, ~/.openclaw/commands/, ~/.openclaw/skills/, ~/.openclaw/rules/, ~/.openclaw/behaviors/
*
* Defaults:
* --source resolves relative to this script's repo root (../..)
* --target is process.cwd()
* --mode is 'all'
*/
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 readline from 'readline';
import { fileURLToPath } from 'url';
import {
collectBehaviorDirs,
collectFrameworkArtifacts,
deployEmulatedBehaviors,
getAddonSkillDirs,
listSkillDirs,
loadModelConfig,
migrateCommandsDirectory,
parseFrontmatter,
} from './providers/base.mjs';
/**
* Read version from package.json at the source root.
* Falls back to 'unknown' if unavailable.
*/
function getDeployVersion(srcRoot) {
try {
const pkg = JSON.parse(fs.readFileSync(path.join(srcRoot, 'package.json'), 'utf8'));
return pkg.version || 'unknown';
} catch {
return 'unknown';
}
}
// ============================================================================
// Provider Registry
// ============================================================================
const PROVIDER_ALIASES = {
'openai': 'codex'
};
const AVAILABLE_PROVIDERS = ['claude', 'factory', 'codex', 'opencode', 'copilot', 'cursor', 'warp', 'windsurf', 'hermes', 'openclaw'];
const MIRRORED_STANDARD_COMMAND_SKILLS = new Set([
'aiwg-setup-project',
'aiwg-update-claude',
'aiwg-update-agents-md',
'sdlc-accelerate',
'project-status',
'intake-wizard',
'intake-from-codebase',
'intake-start',
]);
const MIRRORED_KERNEL_COMMAND_SKILLS = new Set([
'aiwg-refresh',
'aiwg-doctor',
'aiwg-status',
'aiwg-help',
'aiwg-regenerate',
'aiwg-regenerate-claude',
'aiwg-regenerate-codex',
'aiwg-regenerate-opencode',
'aiwg-regenerate-agents',
'aiwg-issue',
'aiwg-pr',
'use',
'steward',
]);
function providerUsesSkillsNatively(providerName) {
return ['claude', 'cursor', 'hermes'].includes(providerName);
}
function shouldMirrorStandardCommandSkill(skillName) {
return (
skillName.startsWith('flow-') ||
MIRRORED_STANDARD_COMMAND_SKILLS.has(skillName)
);
}
function shouldMirrorCommandSkill(providerName, skillName) {
if (MIRRORED_KERNEL_COMMAND_SKILLS.has(skillName)) {
return !providerUsesSkillsNatively(providerName);
}
return shouldMirrorStandardCommandSkill(skillName);
}
function uniquePaths(paths) {
return [...new Set(paths.filter(Boolean))];
}
function collectMirrorSkillDirs(srcRoot, mode) {
const dirs = [];
const directSkillsDir = path.join(srcRoot, 'skills');
dirs.push(...listSkillDirs(directSkillsDir));
// If --source itself is a skills directory, support that shape too.
dirs.push(...listSkillDirs(srcRoot));
const repoFrameworksRoot = path.join(srcRoot, 'agentic', 'code', 'frameworks');
if (fs.existsSync(repoFrameworksRoot)) {
const artifacts = collectFrameworkArtifacts(srcRoot, mode, {
includeAgents: false,
includeCommands: false,
includeSkills: true,
includeRules: false,
});
dirs.push(...artifacts.skills);
if (mode === 'all') {
dirs.push(...getAddonSkillDirs(srcRoot));
}
}
return uniquePaths(dirs);
}
function resolveCommandMirrorDir(provider, target) {
const commandsPath = provider.paths?.commands;
if (commandsPath) {
return path.isAbsolute(commandsPath) ? commandsPath : path.join(target, commandsPath);
}
// Closest conventional location for providers whose primary command
// surface is MCP/aggregation rather than a documented command directory.
if (provider.name === 'hermes') {
return path.join(os.homedir(), '.hermes', 'commands');
}
return null;
}
function commandFileExtensionForProvider(provider) {
if (provider.name === 'copilot') return '.prompt.md';
return '.md';
}
function skillToCommandContent(skillDir) {
const skillName = path.basename(skillDir);
const skillPath = path.join(skillDir, 'SKILL.md');
const raw = fs.readFileSync(skillPath, 'utf8');
const { metadata, body } = parseFrontmatter(raw);
const description = metadata.description || `Run AIWG skill ${skillName}`;
return `---\nname: ${skillName}\ndescription: ${description}\n---\n\n${body.trim()}\n`;
}
function mirrorSkillsAsCommands(provider, target, srcRoot, opts) {
if (!opts.deployCommands && !opts.commandsOnly) return 0;
const targetDir = resolveCommandMirrorDir(provider, target);
if (!targetDir) return 0;
const skillDirs = collectMirrorSkillDirs(srcRoot, opts.mode)
.filter((dir) => shouldMirrorCommandSkill(provider.name, path.basename(dir)));
if (skillDirs.length === 0) return 0;
if (!opts.dryRun) fs.mkdirSync(targetDir, { recursive: true });
const ext = commandFileExtensionForProvider(provider);
let count = 0;
for (const skillDir of skillDirs) {
const skillName = path.basename(skillDir);
let content = skillToCommandContent(skillDir);
if (typeof provider.transformCommand === 'function') {
content = provider.transformCommand(path.join(skillDir, `${skillName}.md`), content, opts);
}
const dest = path.join(targetDir, `${skillName}${ext}`);
if (opts.dryRun) {
if (opts.verbose) console.log(`[dry-run] mirror skill command ${skillName} -> ${dest}`);
} else {
fs.writeFileSync(dest, content, 'utf8');
}
count++;
}
return count;
}
// ============================================================================
// Argument Parsing
// ============================================================================
function parseArgs() {
const args = process.argv.slice(2);
const cfg = {
source: null,
target: process.cwd(),
mode: 'all', // 'general', 'sdlc', 'marketing', 'media-curator', 'research', 'both' (legacy), or 'all'
dryRun: false,
force: false,
provider: 'claude',
model: null, // Blanket override for all tiers
reasoningModel: null,
codingModel: null,
efficiencyModel: null,
asAgentsMd: false,
createAgentsMd: false,
deployCommands: false,
deploySkills: false,
deployRules: false,
deployBehaviors: false, // Deploy behaviors (native on openclaw, emulated on others)
commandsOnly: false,
skillsOnly: false,
rulesOnly: false,
filter: null, // Glob pattern for agent names
filterRole: null, // Filter by role: reasoning|coding|efficiency
save: false, // Save model config to project models.json
saveUser: false, // Save model config to ~/.config/aiwg/models.json
verbose: false, // Show per-file deployment details
quiet: false, // Suppress all non-error output (for embedding in use.ts)
asPlugin: false, // Generate .factory-plugin/ bundle (Factory provider only)
deployBehaviors: false, // Deploy behaviors in addition to agents
skipCommandsMigration: false // Skip commands → skills migration (warns about duplicates)
};
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 === '--provider' || a === '--platform') && args[i + 1]) cfg.provider = String(args[++i]).toLowerCase();
else if (a === '--model' && args[i + 1]) cfg.model = args[++i];
else if ((a === '--reasoning-model' || a === '--reasoning') && args[i + 1]) cfg.reasoningModel = args[++i];
else if ((a === '--coding-model' || a === '--coding') && args[i + 1]) cfg.codingModel = args[++i];
else if ((a === '--efficiency-model' || a === '--efficiency') && args[i + 1]) cfg.efficiencyModel = args[++i];
else if (a === '--as-agents-md') cfg.asAgentsMd = true;
else if (a === '--create-agents-md') cfg.createAgentsMd = true;
else if (a === '--deploy-commands') cfg.deployCommands = true;
else if (a === '--deploy-skills') cfg.deploySkills = true;
else if (a === '--deploy-rules') cfg.deployRules = true;
else if (a === '--deploy-behaviors') cfg.deployBehaviors = true;
else if (a === '--commands-only') cfg.commandsOnly = true;
else if (a === '--skills-only') cfg.skillsOnly = true;
else if (a === '--rules-only') cfg.rulesOnly = true;
else if (a === '--deploy-behaviors') cfg.deployBehaviors = true;
else if (a === '--filter' && args[i + 1]) cfg.filter = args[++i];
else if (a === '--filter-role' && args[i + 1]) cfg.filterRole = args[++i];
else if (a === '--save') cfg.save = true;
else if (a === '--save-user') cfg.saveUser = true;
else if (a === '--verbose' || a === '-v') cfg.verbose = true;
else if (a === '--quiet' || a === '-q') cfg.quiet = true;
else if (a === '--as-plugin') cfg.asPlugin = true;
else if (a === '--skip-commands-migration') cfg.skipCommandsMigration = true;
else if (a === '--copy-all' || a === '--copy-standard-skills') cfg.copyStandardSkills = true;
else if (a === '--help' || a === '-h') {
printHelp();
process.exit(0);
}
}
return cfg;
}
function printHelp() {
console.log(`
Deploy Agents and Commands
Usage:
node tools/agents/deploy-agents.mjs [options]
aiwg -deploy-agents [options]
Options:
--source <path> Source directory (defaults to repo root)
--target <path> Target directory (defaults to cwd)
--mode <type> Deployment mode: general, sdlc, marketing (alias: mmk), media-curator, research, both, or all (default)
--deploy-commands Deploy commands in addition to agents
--deploy-skills Deploy skills in addition to agents
--deploy-rules Deploy rules in addition to agents
--commands-only Deploy only commands (skip agents)
--skills-only Deploy only skills (skip agents)
--rules-only Deploy only rules (skip agents)
--dry-run Show what would be deployed without writing
--force Overwrite existing files
--provider <name> Target provider (see below)
--model <name> Override model for all tiers (blanket)
--reasoning-model <name> Override model for reasoning tasks (alias: --reasoning)
--coding-model <name> Override model for coding tasks (alias: --coding)
--efficiency-model <name> Override model for efficiency tasks (alias: --efficiency)
--filter <pattern> Only deploy agents matching pattern (glob)
--filter-role <role> Only deploy agents of role: reasoning|coding|efficiency
--save Save model config to project models.json
--save-user Save model config to ~/.config/aiwg/models.json
--as-agents-md Aggregate to single AGENTS.md (Codex)
--create-agents-md Create/update AGENTS.md template
--skip-commands-migration Skip deleting the commands directory before skills deployment
--copy-all Copy ALL skills per-project (legacy mirror at <provider>/.aiwg/skills/).
Default is kernel-only + index-driven discovery for the rest (#1217).
Use this for sandboxed runtimes / air-gapped corpora where
$AIWG_ROOT isn't readable from the agent's working dir.
Alias: --copy-standard-skills — rc.29 era).
(warns about duplicate entries in the provider command palette)
Model Override Examples:
# Use sonnet for everything
aiwg use sdlc --model sonnet
# Override individual tiers
aiwg use sdlc --reasoning opus --coding sonnet --efficiency haiku
# Use a specific model ID for coding tier
aiwg use sdlc --provider factory --coding-model gpt-5.3-codex
# Blanket with per-tier override (reasoning uses opus, others use sonnet)
aiwg use sdlc --model sonnet --reasoning opus
# Save overrides to project models.json for future deployments
aiwg use sdlc --model sonnet --save
Model Precedence:
CLI flags > project models.json > user ~/.config/aiwg/models.json > AIWG defaults
Shorthand Values:
opus, sonnet, haiku, inherit — resolved per provider to full model IDs
Providers (all deploy agents, commands, skills, and rules):
claude - Claude Code (default)
Paths: .claude/agents/, .claude/commands/, .claude/skills/, .claude/rules/
factory - Factory AI
Paths: .factory/droids/, .factory/commands/, .factory/skills/, .factory/rules/
codex - OpenAI Codex (alias: openai)
Paths: .codex/agents/, .codex/commands/, .codex/skills/, .codex/rules/
opencode - OpenCode
Paths: .opencode/skill/, .opencode/rule/ (agents/commands not file-based in OpenCode)
copilot - GitHub Copilot
Paths: .github/agents/, .github/commands/, .github/skills/, .github/copilot-rules/
cursor - Cursor IDE
Paths: .cursor/agents/, .cursor/commands/, .cursor/skills/, .cursor/rules/
warp - Warp Terminal
Paths: .warp/agents/, .warp/commands/, .warp/skills/, .warp/rules/ + WARP.md
windsurf - Windsurf
Paths: .windsurf/agents/, .windsurf/workflows/, .windsurf/skills/, .windsurf/rules/
hermes - Hermes Agent (MCP-based integration)
Skills: ~/.hermes/skills/ (user-global) | Agents: AGENTS.md (lean routing guide)
Commands/Rules: served via MCP, not file-deployed
Modes:
general - Writing-quality addon agents and commands (alias: writing)
sdlc - SDLC Complete framework agents and commands
marketing - Media/Marketing Kit framework agents and commands (alias: mmk)
media-curator - Media Curator framework agents and commands
research - Research Complete framework agents and commands
both - writing + SDLC (legacy compatibility)
all - All frameworks + addons (default)
Examples:
# Deploy SDLC framework to Claude Code
aiwg -deploy-agents --mode sdlc
# Deploy to Factory with commands and AGENTS.md
aiwg -deploy-agents --provider factory --mode sdlc --deploy-commands --create-agents-md
# Deploy to GitHub Copilot
aiwg -deploy-agents --provider copilot --mode sdlc
# Override model for all tiers
aiwg -deploy-agents --mode sdlc --model sonnet
# Override individual tiers
aiwg -deploy-agents --mode sdlc --reasoning opus --coding sonnet --efficiency haiku
# Dry run to preview deployment
aiwg -deploy-agents --provider cursor --mode sdlc --dry-run
`);
}
// ============================================================================
// Provider Loading
// ============================================================================
/**
* Resolve provider name (handle aliases)
*/
function resolveProvider(name) {
const resolved = PROVIDER_ALIASES[name] || name;
if (!AVAILABLE_PROVIDERS.includes(resolved)) {
console.error(`Unknown provider: ${name}`);
console.error(`Available providers: ${AVAILABLE_PROVIDERS.join(', ')}`);
console.error(`Aliases: ${Object.entries(PROVIDER_ALIASES).map(([a, p]) => `${a} -> ${p}`).join(', ')}`);
process.exit(1);
}
return resolved;
}
/**
* Dynamically load provider module
*/
async function loadProvider(providerName) {
const resolved = resolveProvider(providerName);
const providerPath = `./providers/${resolved}.mjs`;
try {
const provider = await import(providerPath);
return provider.default || provider;
} catch (err) {
console.error(`Failed to load provider '${resolved}':`, err.message);
process.exit(1);
}
}
// ============================================================================
// Model Shorthand Resolution
// ============================================================================
/**
* Resolve a shorthand model value to a full model ID
*
* If the value is a known shorthand (opus, sonnet, haiku, inherit), resolve it
* through the provider's shorthand map. Otherwise treat it as a literal model ID.
*
* @param {string|null} value - CLI flag value (e.g., "opus" or "claude-opus-4-6")
* @param {object} shorthandMap - Provider shorthand map (e.g., { opus: "claude-opus-4-6" })
* @param {object} modelsConfig - Full models config from loadModelConfig()
* @param {string} provider - Provider name
* @param {string} tier - Tier name: "reasoning", "coding", or "efficiency"
* @returns {string|null} Resolved model ID or null if input was null
*/
function resolveShorthand(value, shorthandMap, modelsConfig, provider, tier) {
if (!value) return null;
const clean = value.toLowerCase().replace(/['"]/g, '');
// Check shorthand map first
if (shorthandMap[clean]) return shorthandMap[clean];
// Check provider-specific tier config (e.g., modelsConfig.factory.reasoning.model)
const providerConfig = modelsConfig?.[provider];
if (providerConfig?.[tier]?.model && clean === tier) {
return providerConfig[tier].model;
}
// Not a shorthand — return as literal model ID
return value;
}
// ============================================================================
// Model Configuration Persistence
// ============================================================================
/**
* Save model configuration to project or user config file
* @param {object} cfg - Configuration with model overrides
* @param {string} providerName - Provider name for provider-specific config
*/
async function saveModelConfig(cfg, providerName) {
// Build config object with only the provided overrides
const modelConfig = {};
// Provider-specific tier configuration
if (cfg.reasoningModel || cfg.codingModel || cfg.efficiencyModel) {
modelConfig[providerName] = {};
if (cfg.reasoningModel) {
modelConfig[providerName].reasoning = { model: cfg.reasoningModel };
}
if (cfg.codingModel) {
modelConfig[providerName].coding = { model: cfg.codingModel };
}
if (cfg.efficiencyModel) {
modelConfig[providerName].efficiency = { model: cfg.efficiencyModel };
}
}
// Shorthand mappings
if (cfg.reasoningModel || cfg.codingModel || cfg.efficiencyModel) {
modelConfig.shorthand = {};
if (cfg.reasoningModel) modelConfig.shorthand.opus = cfg.reasoningModel;
if (cfg.codingModel) modelConfig.shorthand.sonnet = cfg.codingModel;
if (cfg.efficiencyModel) modelConfig.shorthand.haiku = cfg.efficiencyModel;
}
// Determine target path
let targetPath;
if (cfg.saveUser) {
const configDir = path.join(os.homedir(), '.config', 'aiwg');
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
targetPath = path.join(configDir, 'models.json');
} else {
targetPath = path.join(cfg.target, 'models.json');
}
// Merge with existing config if it exists
let existingConfig = {};
if (fs.existsSync(targetPath)) {
try {
existingConfig = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
} catch {
// Ignore parse errors, start fresh
}
}
// Deep merge: existing config + new overrides
const mergedConfig = deepMerge(existingConfig, modelConfig);
// Write the config
fs.writeFileSync(targetPath, JSON.stringify(mergedConfig, null, 2) + '\n', 'utf8');
console.log(`\nModel configuration saved to: ${targetPath}`);
}
/**
* Deep merge two objects
*/
function deepMerge(target, source) {
const result = { ...target };
for (const key of Object.keys(source)) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = deepMerge(result[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
// ============================================================================
// Commands → Skills Migration
// ============================================================================
/**
* Ask the user whether to delete the provider's commands directory before
* deploying skills. Skipped automatically when not running in a TTY (CI/pipe)
* or when --quiet is set.
*
* @param {object} cfg - Parsed CLI config
* @param {object} provider - Loaded provider module
* @param {string} targetDir - Resolved target directory
* @returns {Promise<boolean>} true = proceed with migration, false = skip
*/
async function promptCommandsMigration(cfg, provider, targetDir) {
// Home-directory providers share commands across projects — do not delete.
if (provider.capabilities?.homeDirectoryDeploy) return false;
const commandsRelPath = provider.paths?.commands;
if (!commandsRelPath) return false;
const commandsDir = path.join(targetDir, commandsRelPath);
if (!fs.existsSync(commandsDir)) return false;
const entries = fs.readdirSync(commandsDir).filter(e => e.endsWith('.md'));
if (entries.length === 0) return false;
// Non-interactive context: migrate silently.
if (cfg.quiet || !process.stdout.isTTY) return true;
const rel = path.relative(process.cwd(), commandsDir);
console.log(`\n⚠ Commands → Skills Migration`);
console.log(` ${rel} contains ${entries.length} legacy command file(s).`);
console.log(` AIWG now serves these as skills (.claude/skills/).`);
console.log(` Keeping both causes duplicate entries in the Claude Code command palette.`);
console.log('');
const answer = await new Promise(resolve => {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
rl.question(' Remove commands directory and migrate? [Y/n] ', ans => {
rl.close();
resolve(ans.trim().toLowerCase());
});
});
if (answer === 'n' || answer === 'no') {
console.log('');
console.log(' Skipping migration. Run `aiwg use` again without --skip-commands-migration');
console.log(` to clean up, or delete ${rel} manually.`);
console.log('');
return false;
}
return true;
}
// ============================================================================
// Main Entry Point
// ============================================================================
(async () => {
const cfg = parseArgs();
// Resolve source directory (default to repo root relative to this script)
const __filename = fileURLToPath(import.meta.url);
const scriptDir = path.dirname(__filename);
const srcRoot = cfg.source || path.resolve(scriptDir, '..', '..');
// Validate source directory
if (!fs.existsSync(srcRoot)) {
console.error(`Source directory not found: ${srcRoot}`);
process.exit(1);
}
// Validate target directory
if (!fs.existsSync(cfg.target)) {
console.log(`Creating target directory: ${cfg.target}`);
fs.mkdirSync(cfg.target, { recursive: true });
}
// Normalize mode aliases
if (cfg.mode === 'writing') cfg.mode = 'general';
if (cfg.mode === 'mmk') cfg.mode = 'marketing';
// Apply --model blanket override: sets all three tiers unless individually overridden
if (cfg.model) {
if (!cfg.reasoningModel) cfg.reasoningModel = cfg.model;
if (!cfg.codingModel) cfg.codingModel = cfg.model;
if (!cfg.efficiencyModel) cfg.efficiencyModel = cfg.model;
}
// Load model configuration (project > user > AIWG defaults)
const modelsConfig = loadModelConfig(srcRoot);
// Resolve shorthand values in CLI flags using provider-specific config
if (cfg.reasoningModel || cfg.codingModel || cfg.efficiencyModel) {
const resolvedProvider = resolveProvider(cfg.provider);
const providerShorthand = modelsConfig?.[`${resolvedProvider}_shorthand`] || modelsConfig?.shorthand || {};
cfg.reasoningModel = resolveShorthand(cfg.reasoningModel, providerShorthand, modelsConfig, resolvedProvider, 'reasoning');
cfg.codingModel = resolveShorthand(cfg.codingModel, providerShorthand, modelsConfig, resolvedProvider, 'coding');
cfg.efficiencyModel = resolveShorthand(cfg.efficiencyModel, providerShorthand, modelsConfig, resolvedProvider, 'efficiency');
}
if (!cfg.quiet) {
console.log(`\n=== AIWG Agent Deployment ===`);
console.log(`Provider: ${cfg.provider}`);
console.log(`Source: ${srcRoot}`);
console.log(`Target: ${cfg.target}`);
console.log(`Mode: ${cfg.mode}`);
if (cfg.dryRun) console.log(`Dry run: enabled`);
if (cfg.filter) console.log(`Filter: ${cfg.filter}`);
if (cfg.filterRole) console.log(`Filter role: ${cfg.filterRole}`);
if (cfg.model) console.log(`Model (all tiers): ${cfg.model}`);
if (cfg.reasoningModel) console.log(`Reasoning model: ${cfg.reasoningModel}`);
if (cfg.codingModel) console.log(`Coding model: ${cfg.codingModel}`);
if (cfg.efficiencyModel) console.log(`Efficiency model: ${cfg.efficiencyModel}`);
if (cfg.save) console.log(`Save to project: enabled`);
if (cfg.saveUser) console.log(`Save to user config: enabled`);
}
// Load provider module
const provider = await loadProvider(cfg.provider);
if (!cfg.quiet) console.log(`\nLoaded provider: ${provider.name}`);
// Build options for provider
const opts = {
srcRoot,
target: cfg.target,
mode: cfg.mode,
provider: cfg.provider,
dryRun: cfg.dryRun,
force: cfg.force,
reasoningModel: cfg.reasoningModel,
codingModel: cfg.codingModel,
efficiencyModel: cfg.efficiencyModel,
modelsConfig,
asAgentsMd: cfg.asAgentsMd,
createAgentsMd: cfg.createAgentsMd,
deployCommands: cfg.deployCommands,
deploySkills: cfg.deploySkills,
deployRules: cfg.deployRules,
deployBehaviors: cfg.deployBehaviors,
commandsOnly: cfg.commandsOnly,
skillsOnly: cfg.skillsOnly,
rulesOnly: cfg.rulesOnly,
filter: cfg.filter,
filterRole: cfg.filterRole,
save: cfg.save,
saveUser: cfg.saveUser,
verbose: cfg.verbose,
asPlugin: cfg.asPlugin,
deployBehaviors: cfg.deployBehaviors,
skipCommandsMigration: cfg.skipCommandsMigration,
// #1217 / #1219: --copy-all flag forces legacy per-project mirror
// for the standard tier. Default is no-copy + index-driven discovery.
// Replaces the legacy AIWG_COPY_STANDARD_SKILLS env var (removed rc.30).
// Default (#1217) is no-copy + index-driven discovery.
copyStandardSkills: cfg.copyStandardSkills === true,
deployVersion: getDeployVersion(srcRoot),
deploySource: 'bundled',
};
// Commands → Skills migration: prompt then delete the commands directory
// so stale command files don't create duplicates in the provider TUI.
if (!cfg.dryRun && !cfg.skipCommandsMigration) {
const doMigrate = await promptCommandsMigration(cfg, provider, cfg.target);
if (doMigrate) {
const commandsRelPath = provider.paths?.commands;
if (commandsRelPath) {
migrateCommandsDirectory(path.join(cfg.target, commandsRelPath), opts);
}
} else {
// User said no — flip the flag so the provider knows to emit the duplicate warning
opts.skipCommandsMigration = true;
}
} else if (cfg.skipCommandsMigration) {
// Flag was passed explicitly — emit the duplicate warning now via a dry migration call
const commandsRelPath = provider.paths?.commands;
if (commandsRelPath && !provider.capabilities?.homeDirectoryDeploy) {
migrateCommandsDirectory(path.join(cfg.target, commandsRelPath), opts);
}
}
// Delegate to provider
try {
await provider.deploy(opts);
const mirroredCommandCount = mirrorSkillsAsCommands(provider, cfg.target, srcRoot, opts);
if (mirroredCommandCount > 0 && !cfg.quiet) {
console.log(` Mirrored: ${mirroredCommandCount} skill command wrapper${mirroredCommandCount === 1 ? '' : 's'}`);
}
if (!opts.commandsOnly && !opts.skillsOnly && !opts.rulesOnly) {
const behaviorDirs = collectBehaviorDirs(srcRoot);
const behaviorCount = deployEmulatedBehaviors(behaviorDirs, provider.name, cfg.target, opts);
if (behaviorCount > 0 && !cfg.quiet) {
console.log(` Deployed: ${behaviorCount} behavior emulation${behaviorCount === 1 ? '' : 's'}`);
}
}
// Save model configuration if requested
if ((cfg.save || cfg.saveUser) && !cfg.dryRun) {
await saveModelConfig(cfg, provider.name);
}
if (!cfg.quiet) console.log(`\n=== Deployment complete ===\n`);
} catch (err) {
console.error(`\nDeployment failed:`, err.message);
if (cfg.dryRun) {
console.error('(dry-run mode - no files were modified)');
}
process.exit(1);
}
})();