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
435 lines (382 loc) • 15.4 kB
JavaScript
/**
* Status Contribution Tool
*
* Shows contribution status and next steps for AIWG contributor workflow.
* Can list all contributions or show detailed status for a specific feature.
*
* Usage:
* aiwg -contribute-status # List all contributions
* aiwg -contribute-status cursor-integration # Show feature details
*/
import { execSync } from 'child_process';
import {
listWorkspaces,
loadWorkspaceData,
getWorkspacePath,
workspaceExists
} from './lib/workspace-manager.mjs';
import { getPRStatus, checkGhAuth } from './lib/github-client.mjs';
// ANSI color codes
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m'
};
// Status icons
const icons = {
success: '✓',
warning: '⚠',
error: '✗',
info: 'ℹ',
inProgress: '⏳',
ready: '✓'
};
/**
* Format timestamp as human-readable relative time
* @param {string} isoDate - ISO date string
* @returns {string} Human-readable time (e.g., "2 hours ago")
*/
function formatRelativeTime(isoDate) {
const now = new Date();
const date = new Date(isoDate);
const diffMs = now - date;
const diffSecs = Math.floor(diffMs / 1000);
const diffMins = Math.floor(diffSecs / 60);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffSecs < 60) {
return 'just now';
} else if (diffMins < 60) {
return `${diffMins} ${diffMins === 1 ? 'minute' : 'minutes'} ago`;
} else if (diffHours < 24) {
return `${diffHours} ${diffHours === 1 ? 'hour' : 'hours'} ago`;
} else if (diffDays < 30) {
return `${diffDays} ${diffDays === 1 ? 'day' : 'days'} ago`;
} else {
const diffMonths = Math.floor(diffDays / 30);
return `${diffMonths} ${diffMonths === 1 ? 'month' : 'months'} ago`;
}
}
/**
* Get git status for current branch
* @param {string} projectRoot - Project root directory
* @returns {Object} Git status information
*/
function getGitStatus(projectRoot = process.cwd()) {
try {
// Get current branch
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
cwd: projectRoot,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim();
// Get uncommitted changes
const statusOutput = execSync('git status --porcelain', {
cwd: projectRoot,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim();
const uncommittedCount = statusOutput ? statusOutput.split('\n').length : 0;
// Get commits ahead/behind origin
let aheadOrigin = 0;
let behindOrigin = 0;
try {
const aheadBehindOrigin = execSync(`git rev-list --left-right --count origin/${branch}...HEAD 2>/dev/null`, {
cwd: projectRoot,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim().split('\t');
behindOrigin = parseInt(aheadBehindOrigin[0], 10) || 0;
aheadOrigin = parseInt(aheadBehindOrigin[1], 10) || 0;
} catch (err) {
// Branch may not have remote tracking yet
}
// Get commits ahead/behind upstream
let aheadUpstream = 0;
let behindUpstream = 0;
try {
const aheadBehindUpstream = execSync(`git rev-list --left-right --count upstream/main...HEAD 2>/dev/null`, {
cwd: projectRoot,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim().split('\t');
behindUpstream = parseInt(aheadBehindUpstream[0], 10) || 0;
aheadUpstream = parseInt(aheadBehindUpstream[1], 10) || 0;
} catch (err) {
// Upstream may not be configured
}
return {
branch,
uncommittedCount,
aheadOrigin,
behindOrigin,
aheadUpstream,
behindUpstream
};
} catch (err) {
return {
error: `Failed to get git status: ${err.message}`
};
}
}
/**
* Get quality status color
* @param {number} score - Quality score (0-100)
* @returns {string} Color code
*/
function getQualityColor(score) {
if (score >= 90) return colors.green;
if (score >= 80) return colors.yellow;
return colors.red;
}
/**
* Get status label with icon
* @param {string} status - Status value
* @returns {string} Formatted status
*/
function getStatusLabel(status) {
const statusMap = {
'initialized': { icon: icons.info, color: colors.blue, label: 'Initialized' },
'intake-complete': { icon: icons.success, color: colors.green, label: 'Intake Complete' },
'development': { icon: icons.inProgress, color: colors.yellow, label: 'In Progress' },
'testing': { icon: icons.inProgress, color: colors.yellow, label: 'Testing' },
'pr-created': { icon: icons.ready, color: colors.cyan, label: 'PR Created' },
'pr-updated': { icon: icons.ready, color: colors.cyan, label: 'PR Updated' },
'merged': { icon: icons.success, color: colors.green, label: 'Merged' },
'aborted': { icon: icons.error, color: colors.red, label: 'Aborted' }
};
const { icon, color, label } = statusMap[status] || { icon: '?', color: colors.dim, label: status };
return `${color}${icon} ${label}${colors.reset}`;
}
/**
* List all active contributions
* @param {string} projectRoot - Project root directory
*/
function listAllContributions(projectRoot = process.cwd()) {
console.log(`${colors.bright}Active Contributions${colors.reset}\n`);
const result = listWorkspaces(projectRoot);
if (!result.success) {
console.error(`${colors.red}${icons.error} Error: ${result.error}${colors.reset}`);
process.exit(1);
}
if (result.workspaces.length === 0) {
console.log(`${colors.dim}No active contributions found.${colors.reset}`);
console.log(`\nStart a new contribution: ${colors.cyan}aiwg -contribute-start <feature-name>${colors.reset}`);
return;
}
// Sort by updated date (most recent first)
const workspaces = result.workspaces.sort((a, b) => {
return new Date(b.updated) - new Date(a.updated);
});
workspaces.forEach((workspace, index) => {
console.log(`${colors.bright}${index + 1}. ${workspace.feature}${colors.reset}`);
console.log(` Branch: ${colors.cyan}${workspace.feature}${colors.reset}`);
console.log(` Status: ${getStatusLabel(workspace.status)}`);
// Show quality score if available
const qualityData = loadWorkspaceData(workspace.feature, 'quality.json', projectRoot);
if (qualityData.success && qualityData.data.score !== undefined) {
const scoreColor = getQualityColor(qualityData.data.score);
console.log(` Quality: ${scoreColor}${qualityData.data.score}/100${colors.reset}`);
}
// Show PR info if available
if (workspace.pr) {
console.log(` PR: ${colors.blue}#${workspace.pr.number}${colors.reset} (${workspace.pr.state})`);
} else {
console.log(` PR: ${colors.dim}Not created${colors.reset}`);
}
console.log(` Last updated: ${colors.dim}${formatRelativeTime(workspace.updated)}${colors.reset}`);
console.log('');
});
console.log(`${colors.dim}Use: ${colors.cyan}aiwg -contribute-status <feature>${colors.dim} for details${colors.reset}`);
}
/**
* Show detailed status for a specific feature
* @param {string} feature - Feature name
* @param {string} projectRoot - Project root directory
*/
async function showFeatureStatus(feature, projectRoot = process.cwd()) {
// Check if workspace exists
if (!workspaceExists(feature, projectRoot)) {
console.error(`${colors.red}${icons.error} Error: Contribution workspace not found for '${feature}'${colors.reset}`);
console.log(`\nAvailable contributions:`);
listAllContributions(projectRoot);
process.exit(1);
}
// Load workspace data
const statusData = loadWorkspaceData(feature, 'status.json', projectRoot);
if (!statusData.success) {
console.error(`${colors.red}${icons.error} Error: ${statusData.error}${colors.reset}`);
process.exit(1);
}
const status = statusData.data;
const workspacePath = getWorkspacePath(feature, projectRoot);
// Header
console.log(`${colors.bright}Feature: ${feature}${colors.reset}`);
console.log(`Branch: ${colors.cyan}${status.branch}${colors.reset}`);
console.log(`Workspace: ${colors.dim}${workspacePath}${colors.reset}\n`);
// Git Status
console.log(`${colors.bright}Git Status:${colors.reset}`);
const gitStatus = getGitStatus(projectRoot);
if (gitStatus.error) {
console.log(`${colors.red}${icons.error} ${gitStatus.error}${colors.reset}\n`);
} else {
// Uncommitted changes
if (gitStatus.uncommittedCount > 0) {
console.log(`${colors.yellow}${icons.warning} Uncommitted: ${gitStatus.uncommittedCount} files modified${colors.reset}`);
} else {
console.log(`${colors.green}${icons.success} Working directory clean${colors.reset}`);
}
// Ahead/behind origin
if (gitStatus.aheadOrigin > 0) {
console.log(`${colors.blue}${icons.info} Ahead of origin: ${gitStatus.aheadOrigin} commits${colors.reset}`);
}
if (gitStatus.behindOrigin > 0) {
console.log(`${colors.yellow}${icons.warning} Behind origin: ${gitStatus.behindOrigin} commits (push recommended)${colors.reset}`);
}
// Ahead/behind upstream
if (gitStatus.behindUpstream > 0) {
console.log(`${colors.yellow}${icons.warning} Behind upstream: ${gitStatus.behindUpstream} commits (rebase recommended)${colors.reset}`);
}
console.log('');
}
// Quality Status
const qualityData = loadWorkspaceData(feature, 'quality.json', projectRoot);
if (qualityData.success) {
console.log(`${colors.bright}Quality Status:${colors.reset}`);
const quality = qualityData.data;
const scoreColor = getQualityColor(quality.score);
console.log(`Score: ${scoreColor}${quality.score}/100${colors.reset} ${quality.score >= 80 ? '(READY for PR)' : '(needs improvement)'}`);
console.log(`Last validated: ${colors.dim}${formatRelativeTime(quality.updated || status.updated)}${colors.reset}\n`);
} else {
console.log(`${colors.bright}Quality Status:${colors.reset}`);
console.log(`${colors.dim}Not validated yet${colors.reset}\n`);
}
// PR Status
console.log(`${colors.bright}PR Status:${colors.reset}`);
if (status.pr) {
const prNumber = status.pr.number;
console.log(`${colors.blue}#${prNumber}${colors.reset}: ${status.pr.title || 'Pull Request'}`);
// Try to fetch live PR status
const authCheck = checkGhAuth();
if (authCheck.authenticated) {
const prStatus = getPRStatus(prNumber);
if (prStatus.success) {
const state = prStatus.status.state;
const stateColor = state === 'OPEN' ? colors.green : state === 'MERGED' ? colors.magenta : colors.red;
console.log(`Status: ${stateColor}${state}${colors.reset}`);
// Show reviews
if (prStatus.status.reviews && prStatus.status.reviews.length > 0) {
const approvals = prStatus.status.reviews.filter(r => r.state === 'APPROVED').length;
const changesRequested = prStatus.status.reviews.filter(r => r.state === 'CHANGES_REQUESTED').length;
if (changesRequested > 0) {
console.log(`Reviews: ${colors.red}${changesRequested} changes requested${colors.reset}`);
} else if (approvals > 0) {
console.log(`Reviews: ${colors.green}${approvals} approved${colors.reset}`);
} else {
console.log(`Reviews: ${colors.dim}${prStatus.status.reviews.length} pending${colors.reset}`);
}
}
// Show CI status
if (prStatus.status.statusCheckRollup && prStatus.status.statusCheckRollup.length > 0) {
const checks = prStatus.status.statusCheckRollup;
const passing = checks.filter(c => c.conclusion === 'SUCCESS').length;
const failing = checks.filter(c => c.conclusion === 'FAILURE').length;
const pending = checks.filter(c => !c.conclusion).length;
if (failing > 0) {
console.log(`CI: ${colors.red}${failing} failing${colors.reset}`);
} else if (pending > 0) {
console.log(`CI: ${colors.yellow}${pending} in progress${colors.reset}`);
} else if (passing > 0) {
console.log(`CI: ${colors.green}Passing${colors.reset}`);
}
}
}
}
console.log('');
} else {
console.log(`${colors.dim}Not created${colors.reset}\n`);
}
// Next Steps
console.log(`${colors.bright}Next steps:${colors.reset}`);
const recommendations = getRecommendations(status, gitStatus, qualityData.data);
recommendations.forEach((rec, index) => {
console.log(`${index + 1}. ${rec}`);
});
}
/**
* Get smart recommendations based on current status
* @param {Object} status - Workspace status
* @param {Object} gitStatus - Git status
* @param {Object} quality - Quality data
* @returns {string[]} List of recommendations
*/
function getRecommendations(status, gitStatus, quality) {
const recommendations = [];
// Check uncommitted changes
if (!gitStatus.error && gitStatus.uncommittedCount > 0) {
recommendations.push(`${colors.yellow}Commit changes before creating PR${colors.reset}`);
}
// Check behind upstream
if (!gitStatus.error && gitStatus.behindUpstream > 0) {
recommendations.push(`${colors.cyan}Sync with upstream: aiwg -contribute-sync ${status.feature}${colors.reset}`);
}
// Check quality score
if (quality && quality.score < 80) {
recommendations.push(`${colors.yellow}Run aiwg -contribute-test to improve quality (current: ${quality.score}/100)${colors.reset}`);
}
// Check PR status
if (status.pr) {
// PR exists
const prNumber = status.pr.number;
const authCheck = checkGhAuth();
if (authCheck.authenticated) {
const prStatus = getPRStatus(prNumber);
if (prStatus.success && prStatus.status.reviews) {
const changesRequested = prStatus.status.reviews.filter(r => r.state === 'CHANGES_REQUESTED').length;
if (changesRequested > 0) {
recommendations.push(`${colors.red}Address PR feedback: aiwg -contribute-respond ${status.feature}${colors.reset}`);
}
}
}
// Check if behind origin (changes need to be pushed)
if (!gitStatus.error && gitStatus.aheadOrigin > 0) {
recommendations.push(`${colors.blue}Push updates to PR: git push origin ${status.branch}${colors.reset}`);
}
} else {
// No PR yet
if (quality && quality.score >= 80 && !gitStatus.error && gitStatus.uncommittedCount === 0) {
recommendations.push(`${colors.green}Ready to create PR: aiwg -contribute-pr ${status.feature}${colors.reset}`);
} else if (!quality) {
recommendations.push(`${colors.cyan}Validate quality: aiwg -contribute-test ${status.feature}${colors.reset}`);
}
}
// Default if no specific recommendations
if (recommendations.length === 0) {
recommendations.push(`${colors.green}Continue development or run aiwg -contribute-status for updates${colors.reset}`);
}
return recommendations;
}
/**
* Main function
*/
function main() {
const args = process.argv.slice(2);
const feature = args[0];
const projectRoot = process.cwd();
if (!feature) {
// List all contributions
listAllContributions(projectRoot);
} else {
// Show feature details
showFeatureStatus(feature, projectRoot);
}
}
// Run main
main();