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
641 lines (550 loc) • 18.7 kB
JavaScript
/**
* Start Contribution - Fork and Initialize Workflow
*
* Implements the complete fork and initialization workflow for AIWG contributions.
*
* Usage:
* aiwg -contribute-start <feature-name>
*
* Workflow:
* 1. Check prerequisites (gh CLI, git, authentication)
* 2. Fork jmagly/ai-writing-guide if not exists
* 3. Add remotes (origin=fork, upstream=main)
* 4. Fetch both remotes
* 5. Create feature branch: contrib/{username}/{feature-name}
* 6. Initialize workspace: .aiwg/contrib/{feature-name}/
* 7. Create intake template
* 8. Optionally deploy SDLC agents
* 9. Print next steps
*/
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import {
checkGhAuth,
getUsername,
forkRepo
} from './lib/github-client.mjs';
import {
initWorkspace,
updateWorkspaceStatus
} from './lib/workspace-manager.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Execute command with error handling
* @param {string} command - Command to execute
* @param {Object} options - Execution options
* @returns {Object} { success: boolean, stdout: string, stderr: string }
*/
function exec(command, options = {}) {
try {
const stdout = execSync(command, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
...options
});
return { success: true, stdout: stdout.trim(), stderr: '' };
} catch (err) {
return {
success: false,
stdout: err.stdout?.toString() || '',
stderr: err.stderr?.toString() || err.message
};
}
}
/**
* Check if git is installed and configured
* @returns {Object} { installed: boolean, configured: boolean, error: string|null }
*/
function checkGit() {
const versionCheck = exec('git --version');
if (!versionCheck.success) {
return {
installed: false,
configured: false,
error: 'Git not found. Install git and try again.'
};
}
const nameCheck = exec('git config --global user.name');
const emailCheck = exec('git config --global user.email');
if (!nameCheck.success || !emailCheck.success) {
return {
installed: true,
configured: false,
error: 'Git not configured. Run:\n git config --global user.name "Your Name"\n git config --global user.email "your.email@example.com"'
};
}
return {
installed: true,
configured: true,
error: null
};
}
/**
* Check if in AIWG installation directory
* @returns {Object} { inAIWG: boolean, path: string, error: string|null }
*/
function checkInAIWG() {
const cwd = process.cwd();
const expectedPath = path.join(process.env.HOME, '.local/share/ai-writing-guide');
// Check if current directory is AIWG installation
if (cwd === expectedPath) {
return {
inAIWG: true,
path: cwd,
error: null
};
}
// Check if inside AIWG installation
if (cwd.startsWith(expectedPath)) {
return {
inAIWG: true,
path: expectedPath,
error: null
};
}
// Check if .git exists and remote is AIWG
const gitConfigPath = path.join(cwd, '.git/config');
if (fs.existsSync(gitConfigPath)) {
const config = fs.readFileSync(gitConfigPath, 'utf8');
if (config.includes('ai-writing-guide')) {
return {
inAIWG: true,
path: cwd,
error: null
};
}
}
return {
inAIWG: false,
path: cwd,
error: `Not in AIWG installation directory.\nExpected: ${expectedPath}\nCurrent: ${cwd}\n\nRun: cd ${expectedPath}`
};
}
/**
* Check if git repository is initialized
* @param {string} dir - Directory to check
* @returns {boolean} True if git repo exists
*/
function isGitRepo(dir) {
return fs.existsSync(path.join(dir, '.git'));
}
/**
* Add git remotes if not present
* @param {string} fork - Fork repository (owner/repo)
* @param {string} upstream - Upstream repository (owner/repo)
* @param {string} cwd - Working directory
* @returns {Object} { success: boolean, error: string|null }
*/
function addRemotes(fork, upstream, cwd) {
// Check existing remotes
const remotesResult = exec('git remote -v', { cwd });
const remotes = remotesResult.stdout;
// Add origin (fork) if not present
if (!remotes.includes('origin')) {
const addOrigin = exec(`git remote add origin https://github.com/${fork}.git`, { cwd });
if (!addOrigin.success) {
return {
success: false,
error: `Failed to add origin remote: ${addOrigin.stderr}`
};
}
console.log('✓ Added origin remote (fork)');
} else {
console.log('✓ Origin remote already configured');
}
// Add upstream if not present
if (!remotes.includes('upstream')) {
const addUpstream = exec(`git remote add upstream https://github.com/${upstream}.git`, { cwd });
if (!addUpstream.success) {
return {
success: false,
error: `Failed to add upstream remote: ${addUpstream.stderr}`
};
}
console.log('✓ Added upstream remote (main repo)');
} else {
console.log('✓ Upstream remote already configured');
}
// Fetch both remotes
console.log('Fetching remotes...');
const fetchOrigin = exec('git fetch origin', { cwd });
if (!fetchOrigin.success) {
console.warn('⚠ Warning: Failed to fetch origin');
}
const fetchUpstream = exec('git fetch upstream', { cwd });
if (!fetchUpstream.success) {
console.warn('⚠ Warning: Failed to fetch upstream');
}
return {
success: true,
error: null
};
}
/**
* Create and checkout feature branch
* @param {string} branchName - Branch name to create
* @param {string} cwd - Working directory
* @returns {Object} { success: boolean, error: string|null }
*/
function createBranch(branchName, cwd) {
// Check if branch already exists
const branchCheck = exec(`git rev-parse --verify ${branchName}`, { cwd });
if (branchCheck.success) {
// Branch exists, just checkout
const checkout = exec(`git checkout ${branchName}`, { cwd });
if (!checkout.success) {
return {
success: false,
error: `Failed to checkout existing branch: ${checkout.stderr}`
};
}
console.log(`✓ Checked out existing branch: ${branchName}`);
return { success: true, error: null };
}
// Create new branch from main (or master)
const mainCheck = exec('git rev-parse --verify main', { cwd });
const baseBranch = mainCheck.success ? 'main' : 'master';
const createBranch = exec(`git checkout -b ${branchName} ${baseBranch}`, { cwd });
if (!createBranch.success) {
return {
success: false,
error: `Failed to create branch: ${createBranch.stderr}`
};
}
console.log(`✓ Created branch: ${branchName}`);
return { success: true, error: null };
}
/**
* Create intake template for feature
* @param {string} feature - Feature name
* @param {string} workspacePath - Workspace directory path
* @returns {Object} { success: boolean, error: string|null }
*/
function createIntakeTemplate(feature, workspacePath) {
const intakePath = path.join(workspacePath, 'intake.md');
const template = `# Feature Intake: ${feature}
**Feature ID:** ${feature}
**Created:** ${new Date().toISOString().split('T')[0]}
**Status:** Planning
## Feature Overview
**Feature Name:** ${feature}
**Priority:** Medium | High | Low
<!-- Select priority based on business value and urgency -->
**Complexity:** Low | Medium | High
<!-- Estimate implementation complexity -->
**Timeline:** 1 week | 2 weeks | 1 month
<!-- Estimated time to complete -->
## Description
<!-- Provide a clear 2-3 sentence description of what this feature does -->
**Example:**
Add native Cursor Editor support with single-file .cursor/rules integration. This enables AIWG users working in Cursor to deploy agents and commands with a single command.
## Deliverables
<!-- List specific files, commands, or features you'll create -->
**Example:**
- Setup command: aiwg -setup-cursor
- Agent deployment script: tools/cursor/setup-cursor.mjs
- Quick-start documentation: docs/integrations/cursor-quickstart.md
- Update install.sh for --platform cursor routing
## Dependencies
<!-- List tools, libraries, or other features this depends on -->
**Example:**
- Cursor Editor 0.40+
- Node.js 18.20.8+
- Existing agent deployment infrastructure
## Success Criteria
<!-- How will you know the feature is complete and working? -->
**Example:**
- Users can deploy agents with one command: aiwg -setup-cursor
- Documentation is clear and complete (95/100 quality score)
- Works on macOS, Linux, Windows
- Integration test passes
## Notes
<!-- Any additional context, constraints, or considerations -->
---
## Checklist
Before creating PR:
- [ ] Intake complete and reviewed
- [ ] Implementation complete
- [ ] Documentation written (README, quickstart, integration guide)
- [ ] Quality validation passed (aiwg -contribute-test)
- [ ] All lint errors fixed
- [ ] Manifests synced
`;
try {
fs.writeFileSync(intakePath, template, 'utf8');
console.log(`✓ Created intake template: ${path.relative(process.cwd(), intakePath)}`);
return { success: true, error: null };
} catch (err) {
return {
success: false,
error: `Failed to create intake template: ${err.message}`
};
}
}
/**
* Check if SDLC agents are deployed
* @param {string} cwd - Working directory
* @returns {boolean} True if agents deployed
*/
function agentsDeployed(cwd) {
const agentsDir = path.join(cwd, '.claude/agents');
return fs.existsSync(agentsDir) && fs.readdirSync(agentsDir).length > 0;
}
/**
* Prompt user for SDLC agent deployment
* @param {string} cwd - Working directory
* @returns {Promise<boolean>} True if user wants to deploy
*/
async function promptDeployAgents(cwd) {
// Check if already deployed
if (agentsDeployed(cwd)) {
console.log('✓ SDLC agents already deployed');
return false;
}
// In non-interactive mode, skip deployment
if (!process.stdin.isTTY) {
console.log('⚠ SDLC agents not deployed (non-interactive mode)');
console.log(' Deploy later with: aiwg -deploy-agents --mode sdlc');
return false;
}
const readline = await import('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
rl.question('\nDeploy SDLC agents to fork? [y/n]: ', (answer) => {
rl.close();
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
});
});
}
/**
* Deploy SDLC agents to fork
* @param {string} cwd - Working directory
* @returns {Object} { success: boolean, error: string|null }
*/
function deployAgents(cwd) {
// Find deploy-agents.mjs
const deployScript = path.join(__dirname, '../agents/deploy-agents.mjs');
if (!fs.existsSync(deployScript)) {
return {
success: false,
error: `Deploy script not found: ${deployScript}`
};
}
const result = exec(`node ${deployScript} --target ${cwd} --mode sdlc`, { cwd });
if (!result.success) {
return {
success: false,
error: `Failed to deploy agents: ${result.stderr}`
};
}
console.log('✓ Deployed SDLC agents to fork');
return { success: true, error: null };
}
/**
* Print next steps
* @param {string} feature - Feature name
* @param {string} username - GitHub username
* @param {string} cwd - Working directory
*/
function printNextSteps(feature, username, cwd) {
console.log('\n' + '='.repeat(60));
console.log('Next steps:');
console.log('='.repeat(60));
console.log('');
console.log('1. Complete intake (using Claude Code or Warp):');
console.log(' Natural language: "Complete intake for ' + feature + ' feature"');
console.log(' Or manually edit: ' + path.join('.aiwg/contrib', feature, 'intake.md'));
console.log('');
console.log('2. Start development:');
console.log(' Natural language: "Start Inception phase for ' + feature + '"');
console.log('');
console.log('3. Validate quality:');
console.log(' Command: aiwg -contribute-test ' + feature);
console.log('');
console.log('4. Create PR:');
console.log(' Command: aiwg -contribute-pr ' + feature);
console.log('');
console.log('Quick reference:');
console.log(' - Status: aiwg -contribute-status ' + feature);
console.log(' - Monitor PR: aiwg -contribute-monitor ' + feature);
console.log(' - Sync fork: aiwg -contribute-sync ' + feature);
console.log(' - Abort: aiwg -contribute-abort ' + feature);
console.log('');
console.log('Documentation:');
console.log(' - Contributor Guide: docs/contributing/contributor-quickstart.md');
console.log(' - Using AIWG: docs/contributing/using-aiwg-for-contributions.md');
console.log('');
}
/**
* Main function
*/
async function main() {
const args = process.argv.slice(2);
// Parse arguments
if (args.length === 0 || args[0] === '-h' || args[0] === '--help') {
console.log('Usage: aiwg -contribute-start <feature-name>');
console.log('');
console.log('Initialize contribution workspace with fork and feature branch.');
console.log('');
console.log('Example:');
console.log(' aiwg -contribute-start cursor-integration');
process.exit(0);
}
const feature = args[0];
console.log('Starting contribution workflow...');
console.log('Feature:', feature);
console.log('');
// Step 1: Check prerequisites
console.log('[1/8] Checking prerequisites...');
// Check gh CLI
const ghCheck = checkGhAuth();
if (!ghCheck.authenticated) {
console.error('✗ Error:', ghCheck.error);
process.exit(1);
}
console.log('✓ GitHub CLI authenticated');
// Check git
const gitCheck = checkGit();
if (!gitCheck.configured) {
console.error('✗ Error:', gitCheck.error);
process.exit(1);
}
console.log('✓ Git installed and configured');
// Check if in AIWG installation
const aiwgCheck = checkInAIWG();
if (!aiwgCheck.inAIWG) {
console.error('✗ Error:', aiwgCheck.error);
process.exit(1);
}
console.log('✓ In AIWG installation directory');
const cwd = aiwgCheck.path;
// Step 2: Get username
console.log('');
console.log('[2/8] Getting GitHub username...');
const usernameResult = getUsername();
if (!usernameResult.success) {
console.error('✗ Error:', usernameResult.error);
process.exit(1);
}
const username = usernameResult.username;
console.log('✓ GitHub username:', username);
// Step 3: Fork repository
console.log('');
console.log('[3/8] Forking repository...');
const upstream = 'jmagly/ai-writing-guide';
const forkResult = forkRepo(upstream);
if (!forkResult.success) {
console.error('✗ Error:', forkResult.error);
process.exit(1);
}
if (forkResult.alreadyExists) {
console.log('✓ Fork already exists:', forkResult.fork);
} else {
console.log('✓ Forked repository:', forkResult.fork);
}
const fork = forkResult.fork;
// Step 4: Add remotes
console.log('');
console.log('[4/8] Configuring git remotes...');
if (!isGitRepo(cwd)) {
// Clone the fork if git repo doesn't exist
console.log('Cloning fork...');
const cloneResult = exec(`git clone https://github.com/${fork}.git ${cwd}`);
if (!cloneResult.success) {
console.error('✗ Error: Failed to clone fork:', cloneResult.stderr);
process.exit(1);
}
console.log('✓ Cloned fork');
// Add upstream remote
const addUpstream = exec(`git remote add upstream https://github.com/${upstream}.git`, { cwd });
if (!addUpstream.success) {
console.error('✗ Error: Failed to add upstream remote:', addUpstream.stderr);
process.exit(1);
}
console.log('✓ Added upstream remote');
} else {
const remotesResult = addRemotes(fork, upstream, cwd);
if (!remotesResult.success) {
console.error('✗ Error:', remotesResult.error);
process.exit(1);
}
}
// Step 5: Create feature branch
console.log('');
console.log('[5/8] Creating feature branch...');
const branchName = path.join('contrib', username, feature);
const branchResult = createBranch(branchName, cwd);
if (!branchResult.success) {
console.error('✗ Error:', branchResult.error);
process.exit(1);
}
// Step 6: Initialize workspace
console.log('');
console.log('[6/8] Initializing workspace...');
const workspaceResult = initWorkspace(feature, {
branch: branchName,
upstream,
projectRoot: cwd
});
if (!workspaceResult.success) {
// Workspace might already exist from previous run
if (workspaceResult.error.includes('already exists')) {
console.log('✓ Workspace already initialized:', path.relative(cwd, workspaceResult.error.split(':')[1].trim()));
} else {
console.error('✗ Error:', workspaceResult.error);
process.exit(1);
}
} else {
console.log('✓ Created workspace:', path.relative(cwd, workspaceResult.path));
}
// Step 7: Create intake template
console.log('');
console.log('[7/8] Creating intake template...');
const intakeResult = createIntakeTemplate(feature, workspaceResult.path || path.join(cwd, '.aiwg/contrib', feature));
if (!intakeResult.success) {
console.error('✗ Error:', intakeResult.error);
process.exit(1);
}
// Step 8: Deploy SDLC agents (optional)
console.log('');
console.log('[8/8] SDLC agent deployment...');
const shouldDeploy = await promptDeployAgents(cwd);
if (shouldDeploy) {
const deployResult = deployAgents(cwd);
if (!deployResult.success) {
console.warn('⚠ Warning:', deployResult.error);
console.log(' Deploy later with: aiwg -deploy-agents --mode sdlc');
}
} else if (!agentsDeployed(cwd)) {
console.log('⚠ SDLC agents not deployed');
console.log(' Deploy later with: aiwg -deploy-agents --mode sdlc');
}
// Update workspace status
updateWorkspaceStatus(feature, 'initialized', {}, cwd);
// Print success summary
console.log('');
console.log('='.repeat(60));
console.log('✅ Contribution workspace initialized successfully!');
console.log('='.repeat(60));
console.log('');
console.log('Summary:');
console.log(' Fork:', fork);
console.log(' Branch:', branchName);
console.log(' Workspace:', path.join('.aiwg/contrib', feature));
console.log(' Intake:', path.join('.aiwg/contrib', feature, 'intake.md'));
// Print next steps
printNextSteps(feature, username, cwd);
process.exit(0);
}
// Run main function
main().catch(err => {
console.error('Fatal error:', err.message);
process.exit(1);
});