@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
1,124 lines (1,111 loc) โข 45.5 kB
JavaScript
import { randomUUID } from 'crypto';
import { exec } from 'child_process';
import { promisify } from 'util';
import { promises as fs } from 'fs';
import path from 'path';
import { createTool, createSuccessResult, createErrorResult } from '../../core/tool-framework.js';
const execAsync = promisify(exec);
/**
* Start a new feature with TDD workflow
*/
const startFeatureTool = createTool({
name: 'start_feature',
description: 'Start a new feature with TDD workflow and branch management',
category: 'developer-workflow',
inputSchema: {
type: 'object',
properties: {
featureName: {
type: 'string',
description: 'Name of the feature (will be used for branch name)',
minLength: 1,
maxLength: 100,
pattern: '^[a-zA-Z0-9-_]+$'
},
branchFrom: {
type: 'string',
default: 'main',
description: 'Base branch to create feature branch from',
maxLength: 100
},
description: {
type: 'string',
description: 'Optional description of the feature',
maxLength: 500
}
},
required: ['featureName'],
additionalProperties: false
},
async execute(input, context) {
try {
const featureId = randomUUID();
const now = Date.now();
const branchName = `feature/${input.featureName}`;
// Check for uncommitted changes
const { stdout: status } = await execAsync('git status --porcelain');
if (status.trim()) {
return createErrorResult({
code: 'UNCOMMITTED_CHANGES',
message: 'You have uncommitted changes. Please commit or stash them before starting a new feature.',
category: 'validation'
});
}
// Create and checkout new branch
try {
await execAsync(`git checkout -b ${branchName} ${input.branchFrom || 'main'}`);
}
catch (error) {
return createErrorResult({
code: 'GIT_ERROR',
message: `Failed to create feature branch: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
// Store feature in database
const result = await context.db.run(`INSERT INTO development_features
(id, project_id, name, description, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
featureId,
context.projectId || 'default',
input.featureName,
input.description || '',
'planning',
now,
now
]);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to save feature to database',
details: { error: result.error },
category: 'system'
});
}
// Get TDD configuration
const tddConfig = await context.db.get('SELECT * FROM development_tdd_config WHERE project_id = ?', [context.projectId || 'default']);
return createSuccessResult({
feature: {
id: featureId,
name: input.featureName,
description: input.description,
branch: branchName,
status: 'planning',
tddRequired: tddConfig.data?.enforce_test_first !== false
},
workflow: {
currentStep: 'write_tests',
nextSteps: [
'Write failing tests for your feature',
'Run tests to verify they fail (RED phase)',
'Implement minimal code to make tests pass (GREEN phase)',
'Refactor while keeping tests green (REFACTOR phase)'
]
},
message: `Started feature: ${input.featureName}`,
suggestions: [
'Use "write_test" to create your first test',
'Use "run_tdd_cycle" to guide through TDD phases',
'Use "check_workflow_status" to see current progress'
]
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to start feature: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Check current development workflow status
*/
const checkWorkflowStatusTool = createTool({
name: 'check_workflow_status',
description: 'Check current development workflow status including TDD phase and git state',
category: 'developer-workflow',
readOnly: true,
inputSchema: {
type: 'object',
properties: {
includeTests: {
type: 'boolean',
default: true,
description: 'Include test status in the response'
},
includeGitStatus: {
type: 'boolean',
default: true,
description: 'Include git status in the response'
}
},
additionalProperties: false
},
async execute(input, context) {
try {
const status = {
project: context.projectId || 'default',
timestamp: new Date().toISOString()
};
// Get current git status
if (input.includeGitStatus !== false) {
try {
const { stdout: branch } = await execAsync('git branch --show-current');
const { stdout: gitStatus } = await execAsync('git status --porcelain');
const { stdout: ahead } = await execAsync('git rev-list --count @{u}..HEAD 2>/dev/null || echo "0"');
const { stdout: behind } = await execAsync('git rev-list --count HEAD..@{u} 2>/dev/null || echo "0"');
status.git = {
currentBranch: branch.trim(),
uncommittedChanges: gitStatus.split('\n').filter(line => line.trim()).length,
changedFiles: gitStatus.split('\n').filter(line => line.trim()).map(line => line.substring(3)),
aheadOfRemote: parseInt(ahead.trim()),
behindRemote: parseInt(behind.trim())
};
}
catch (error) {
status.git = {
error: 'Failed to get git status',
details: error instanceof Error ? error.message : 'Unknown error'
};
}
}
// Get active feature
const activeFeature = await context.db.get(`SELECT * FROM development_features
WHERE project_id = ? AND status != 'completed'
ORDER BY created_at DESC LIMIT 1`, [context.projectId || 'default']);
if (activeFeature.data) {
status.activeFeature = {
id: activeFeature.data.id,
name: activeFeature.data.name,
status: activeFeature.data.status,
testCoverage: activeFeature.data.test_coverage || 0
};
// Get current TDD session
const tddSession = await context.db.get(`SELECT * FROM development_tdd_sessions
WHERE feature_id = ? AND completed_at IS NULL
ORDER BY started_at DESC LIMIT 1`, [activeFeature.data.id]);
if (tddSession.data) {
status.tddSession = {
id: tddSession.data.id,
currentPhase: tddSession.data.current_phase,
startedAt: new Date(tddSession.data.started_at).toISOString()
};
}
// Get test cases for this feature
if (input.includeTests !== false) {
const testCases = await context.db.all(`SELECT * FROM development_test_cases
WHERE feature_id = ? ORDER BY created_at`, [activeFeature.data.id]);
status.tests = {
total: testCases.data?.length || 0,
passing: testCases.data?.filter(t => t.status === 'passing').length || 0,
failing: testCases.data?.filter(t => t.status === 'failing').length || 0,
pending: testCases.data?.filter(t => t.status === 'pending').length || 0
};
}
}
// Get TDD configuration
const tddConfig = await context.db.get('SELECT * FROM development_tdd_config WHERE project_id = ?', [context.projectId || 'default']);
status.tddConfig = {
enforceTestFirst: tddConfig.data?.enforce_test_first !== false,
minimumCoverage: tddConfig.data?.minimum_test_coverage || 80,
requireTestsBeforeImplementation: tddConfig.data?.require_tests_before_implementation !== false
};
// Calculate workflow recommendations
const recommendations = [];
if (!status.activeFeature) {
recommendations.push('Start a new feature with "start_feature"');
}
else if (status.tests?.total === 0) {
recommendations.push('Write your first test with "write_test"');
}
else if (status.tests?.failing > 0) {
recommendations.push('Fix failing tests or implement features to make them pass');
}
else if (status.tests?.passing > 0 && status.git?.uncommittedChanges > 0) {
recommendations.push('Commit your progress');
}
status.recommendations = recommendations;
return createSuccessResult({
status,
message: 'Workflow status retrieved successfully'
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to check workflow status: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Get AI suggestions for next development steps
*/
const suggestNextActionTool = createTool({
name: 'suggest_next_action',
description: 'Get AI-powered suggestions for next development steps based on current workflow state',
category: 'developer-workflow',
readOnly: true,
inputSchema: {
type: 'object',
properties: {
currentContext: {
type: 'string',
description: 'Additional context about what you are working on',
maxLength: 500
}
},
additionalProperties: false
},
async execute(input, context) {
try {
// Get current workflow status
const statusResult = await checkWorkflowStatusTool.execute({}, context);
if (!statusResult.success) {
return statusResult;
}
const status = statusResult.data.status;
const suggestions = [];
// Analyze current state and provide suggestions
if (!status.activeFeature) {
suggestions.push({
action: 'start_feature',
priority: 'high',
reason: 'No active feature detected',
description: 'Begin by starting a new feature to establish TDD workflow',
nextSteps: ['Define feature requirements', 'Create feature branch', 'Write first test']
});
}
else {
const feature = status.activeFeature;
const tests = status.tests || {};
const git = status.git || {};
// TDD Phase analysis
if (tests.total === 0) {
suggestions.push({
action: 'write_test',
priority: 'critical',
reason: 'No tests written for active feature',
description: 'Start TDD cycle by writing a failing test',
nextSteps: ['Define expected behavior', 'Write failing test', 'Verify test fails']
});
}
else if (tests.failing > 0) {
suggestions.push({
action: 'implement_feature',
priority: 'high',
reason: `${tests.failing} failing test(s) detected`,
description: 'Implement minimal code to make failing tests pass (GREEN phase)',
nextSteps: ['Implement feature logic', 'Run tests', 'Verify tests pass']
});
}
else if (tests.passing > 0 && git.uncommittedChanges > 0) {
suggestions.push({
action: 'commit_changes',
priority: 'medium',
reason: 'Tests passing with uncommitted changes',
description: 'Commit your progress to save working state',
nextSteps: ['Review changes', 'Commit with descriptive message', 'Consider refactoring']
});
}
else if (tests.passing > 0) {
suggestions.push({
action: 'refactor_or_extend',
priority: 'medium',
reason: 'Tests passing - good time to refactor or add features',
description: 'REFACTOR phase: Improve code quality while keeping tests green',
nextSteps: ['Identify code smells', 'Refactor safely', 'Add edge case tests']
});
}
// Git workflow suggestions
if (git.aheadOfRemote > 0) {
suggestions.push({
action: 'push_changes',
priority: 'low',
reason: `${git.aheadOfRemote} commits ahead of remote`,
description: 'Push local commits to remote repository',
nextSteps: ['Review commits', 'Push to remote', 'Consider creating PR']
});
}
// Coverage suggestions
if (feature.testCoverage < (status.tddConfig?.minimumCoverage || 80)) {
suggestions.push({
action: 'improve_coverage',
priority: 'medium',
reason: `Test coverage (${feature.testCoverage}%) below minimum`,
description: 'Add more comprehensive tests to meet coverage requirements',
nextSteps: ['Identify uncovered code', 'Write additional tests', 'Run coverage report']
});
}
}
// Add context-specific suggestions
if (input.currentContext) {
suggestions.push({
action: 'context_analysis',
priority: 'info',
reason: 'Based on provided context',
description: `Consider: ${input.currentContext}`,
nextSteps: ['Analyze current context', 'Plan implementation approach', 'Write tests first']
});
}
return createSuccessResult({
suggestions: suggestions.slice(0, 5), // Limit to top 5 suggestions
workflowState: {
phase: status.tddSession?.currentPhase || 'planning',
activeFeature: status.activeFeature?.name || null,
testsSummary: status.tests
},
message: 'Next action suggestions generated successfully'
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to generate suggestions: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Run TDD cycle with guidance
*/
const runTDDCycleTool = createTool({
name: 'run_tdd_cycle',
description: 'Guide through Test-Driven Development red-green-refactor cycle',
category: 'developer-workflow',
inputSchema: {
type: 'object',
properties: {
featureId: {
type: 'string',
description: 'Feature ID to run TDD cycle for (optional, uses active feature if not provided)'
},
testCommand: {
type: 'string',
default: 'npm test',
description: 'Command to run tests',
maxLength: 200
},
testFile: {
type: 'string',
description: 'Specific test file to run',
maxLength: 500
},
phase: {
type: 'string',
enum: ['red', 'green', 'refactor'],
description: 'Specific TDD phase to execute'
}
},
additionalProperties: false
},
async execute(input, context) {
try {
// Get or determine feature
let featureId = input.featureId;
if (!featureId) {
const activeFeature = await context.db.get(`SELECT id FROM development_features
WHERE project_id = ? AND status != 'completed'
ORDER BY created_at DESC LIMIT 1`, [context.projectId || 'default']);
featureId = activeFeature.data?.id;
}
if (!featureId) {
return createErrorResult({
code: 'NO_ACTIVE_FEATURE',
message: 'No active feature found. Start a feature first.',
category: 'validation'
});
}
const sessionId = randomUUID();
const now = Date.now();
// Create or get TDD session
let session = await context.db.get(`SELECT * FROM development_tdd_sessions
WHERE feature_id = ? AND completed_at IS NULL
ORDER BY started_at DESC LIMIT 1`, [featureId]);
if (!session.data) {
// Create new session
const result = await context.db.run(`INSERT INTO development_tdd_sessions
(id, feature_id, project_id, current_phase, started_at, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`, [sessionId, featureId, context.projectId || 'default', 'red', now, now, now]);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create TDD session',
category: 'system'
});
}
session = { success: true, data: { id: sessionId, current_phase: 'red' } };
}
const currentPhase = input.phase || session.data.current_phase;
const testCommand = input.testFile ?
`${input.testCommand || 'npm test'} ${input.testFile}` :
(input.testCommand || 'npm test');
let phaseResult = {};
let nextPhase = currentPhase;
// Execute TDD phase
switch (currentPhase) {
case 'red':
phaseResult = await executeRedPhase(testCommand, featureId, context);
nextPhase = phaseResult.testsFailingAsExpected ? 'green' : 'red';
break;
case 'green':
phaseResult = await executeGreenPhase(testCommand, featureId, context);
nextPhase = phaseResult.testsPassingNow ? 'refactor' : 'green';
break;
case 'refactor':
phaseResult = await executeRefactorPhase(testCommand, featureId, context);
nextPhase = 'red'; // Ready for next feature/test
break;
}
// Update session phase
await context.db.run('UPDATE development_tdd_sessions SET current_phase = ?, updated_at = ? WHERE id = ?', [nextPhase, Date.now(), session.data.id]);
// Record phase history
await context.db.run(`INSERT INTO development_tdd_phase_history
(id, session_id, project_id, phase, started_at, completed_at, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
randomUUID(),
session.data.id,
context.projectId || 'default',
currentPhase,
now,
Date.now(),
JSON.stringify(phaseResult)
]);
return createSuccessResult({
tddCycle: {
currentPhase,
nextPhase,
sessionId: session.data.id,
featureId,
result: phaseResult
},
guidance: getTDDPhaseGuidance(currentPhase, nextPhase, phaseResult),
message: `TDD ${currentPhase.toUpperCase()} phase completed`
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to run TDD cycle: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
// Helper functions for TDD cycle execution
async function executeRedPhase(testCommand, featureId, context) {
try {
const { stdout, stderr } = await execAsync(testCommand);
return {
testsFailingAsExpected: false,
output: stdout,
error: stderr,
message: 'Tests are passing - you need to write a failing test first!'
};
}
catch (error) {
return {
testsFailingAsExpected: true,
output: '',
error: error instanceof Error ? error.message : 'Tests failed as expected',
message: 'Good! Tests are failing as expected. Now implement the feature.'
};
}
}
async function executeGreenPhase(testCommand, featureId, context) {
try {
const { stdout, stderr } = await execAsync(testCommand);
return {
testsPassingNow: true,
output: stdout,
error: '',
message: 'Excellent! Tests are now passing. Ready for refactoring.'
};
}
catch (error) {
return {
testsPassingNow: false,
output: '',
error: error instanceof Error ? error.message : 'Tests still failing',
message: 'Tests still failing. Continue implementing the feature.'
};
}
}
async function executeRefactorPhase(testCommand, featureId, context) {
try {
const { stdout, stderr } = await execAsync(testCommand);
return {
testsStillPassing: true,
output: stdout,
error: '',
message: 'Tests still passing after refactoring. Great job!'
};
}
catch (error) {
return {
testsStillPassing: false,
output: '',
error: error instanceof Error ? error.message : 'Tests broken during refactoring',
message: 'Tests broken during refactoring. Revert and try again.'
};
}
}
function getTDDPhaseGuidance(currentPhase, nextPhase, result) {
const guidance = {
currentPhase: currentPhase.toUpperCase(),
nextPhase: nextPhase.toUpperCase()
};
switch (currentPhase) {
case 'red':
guidance.description = 'RED phase: Write a failing test';
guidance.success = result.testsFailingAsExpected;
guidance.nextSteps = result.testsFailingAsExpected ?
['Implement minimal code to make the test pass'] :
['Write a failing test first - this is crucial for TDD'];
break;
case 'green':
guidance.description = 'GREEN phase: Make the test pass';
guidance.success = result.testsPassingNow;
guidance.nextSteps = result.testsPassingNow ?
['Refactor the code while keeping tests green'] :
['Continue implementing until tests pass'];
break;
case 'refactor':
guidance.description = 'REFACTOR phase: Improve code quality';
guidance.success = result.testsStillPassing;
guidance.nextSteps = result.testsStillPassing ?
['Write next test or complete feature'] :
['Revert refactoring that broke tests'];
break;
}
return guidance;
}
/**
* Setup git commit hooks for TDD enforcement
*/
const setupCommitHooksTool = createTool({
name: 'setup_commit_hooks',
description: 'Setup git hooks to enforce TDD practices and code quality',
category: 'developer-workflow',
inputSchema: {
type: 'object',
properties: {
preCommit: {
type: 'boolean',
default: true,
description: 'Run tests before every commit'
},
prePush: {
type: 'boolean',
default: true,
description: 'Run full test suite before push'
},
commitMsg: {
type: 'boolean',
default: false,
description: 'Validate commit message format'
},
preRebase: {
type: 'boolean',
default: false,
description: 'Run tests before rebase'
}
},
additionalProperties: false
},
async execute(input, context) {
try {
const hooksDir = path.join('.git', 'hooks');
const setupHooks = [];
// Create hooks directory if it doesn't exist
try {
await fs.mkdir(hooksDir, { recursive: true });
}
catch (error) {
return createErrorResult({
code: 'FILESYSTEM_ERROR',
message: 'Failed to create git hooks directory',
category: 'system'
});
}
// Pre-commit hook
if (input.preCommit !== false) {
const preCommitHook = `#!/bin/sh
# Atlas TDD Pre-commit Hook
# Ensures tests pass before allowing commits
echo "๐งช Running tests before commit..."
npm test -- --passWithNoTests
if [ $? -ne 0 ]; then
echo "โ Tests failed! Fix them before committing."
echo "TDD requires all tests to pass before committing changes."
exit 1
fi
echo "โ
All tests pass! Proceeding with commit..."
`;
await fs.writeFile(path.join(hooksDir, 'pre-commit'), preCommitHook, { mode: 0o755 });
setupHooks.push('pre-commit');
}
// Pre-push hook
if (input.prePush !== false) {
const prePushHook = `#!/bin/sh
# Atlas TDD Pre-push Hook
# Runs comprehensive checks before pushing
echo "๐ Running pre-push checks..."
# Run full test suite
echo "Running full test suite..."
npm test
if [ $? -ne 0 ]; then
echo "โ Tests failed! Cannot push."
exit 1
fi
# Run linter if available
if command -v npm run lint >/dev/null 2>&1; then
echo "Running linter..."
npm run lint
if [ $? -ne 0 ]; then
echo "โ ๏ธ Linting issues found. Please fix them."
exit 1
fi
fi
echo "โ
All checks passed! Pushing to remote..."
`;
await fs.writeFile(path.join(hooksDir, 'pre-push'), prePushHook, { mode: 0o755 });
setupHooks.push('pre-push');
}
// Commit message hook
if (input.commitMsg) {
const commitMsgHook = `#!/bin/sh
# Atlas Commit Message Hook
# Validates commit message format
commit_regex='^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,50}'
if ! grep -qE "$commit_regex" "$1"; then
echo "โ Invalid commit message format!"
echo "Format: type(scope): description"
echo "Types: feat, fix, docs, style, refactor, test, chore"
echo "Example: feat(auth): add user login functionality"
exit 1
fi
`;
await fs.writeFile(path.join(hooksDir, 'commit-msg'), commitMsgHook, { mode: 0o755 });
setupHooks.push('commit-msg');
}
// Pre-rebase hook
if (input.preRebase) {
const preRebaseHook = `#!/bin/sh
# Atlas Pre-rebase Hook
# Ensures tests pass before rebasing
echo "๐งช Running tests before rebase..."
npm test -- --passWithNoTests
if [ $? -ne 0 ]; then
echo "โ Tests failed! Fix them before rebasing."
exit 1
fi
echo "โ
Tests pass! Proceeding with rebase..."
`;
await fs.writeFile(path.join(hooksDir, 'pre-rebase'), preRebaseHook, { mode: 0o755 });
setupHooks.push('pre-rebase');
}
return createSuccessResult({
hooks: setupHooks,
message: `Git hooks configured successfully`,
benefits: [
'TDD practices enforced automatically',
'Code quality maintained',
'Prevents broken code from being committed',
'Ensures test coverage before sharing code'
],
usage: 'Hooks will run automatically during git operations'
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to setup commit hooks: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Write a test case
*/
const writeTestTool = createTool({
name: 'write_test',
description: 'Write a test case for TDD development',
category: 'developer-workflow',
inputSchema: {
type: 'object',
properties: {
featureId: {
type: 'string',
description: 'Feature ID to write test for (optional, uses active feature if not provided)'
},
testName: {
type: 'string',
description: 'Name of the test case',
minLength: 1,
maxLength: 200
},
testType: {
type: 'string',
enum: ['unit', 'integration', 'e2e'],
default: 'unit',
description: 'Type of test'
},
description: {
type: 'string',
description: 'Description of what the test does',
maxLength: 500
},
expectedBehavior: {
type: 'string',
description: 'Expected behavior when test passes',
minLength: 1,
maxLength: 1000
}
},
required: ['testName', 'expectedBehavior'],
additionalProperties: false
},
async execute(input, context) {
try {
// Get or determine feature
let featureId = input.featureId;
if (!featureId) {
const activeFeature = await context.db.get(`SELECT id FROM development_features
WHERE project_id = ? AND status != 'completed'
ORDER BY created_at DESC LIMIT 1`, [context.projectId || 'default']);
featureId = activeFeature.data?.id;
}
if (!featureId) {
return createErrorResult({
code: 'NO_ACTIVE_FEATURE',
message: 'No active feature found. Start a feature first.',
category: 'validation'
});
}
const testId = randomUUID();
const now = Date.now();
// Store test case
const result = await context.db.run(`INSERT INTO development_test_cases
(id, feature_id, project_id, name, description, type, status, expected_behavior, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
testId,
featureId,
context.projectId || 'default',
input.testName,
input.description || '',
input.testType || 'unit',
'pending',
input.expectedBehavior,
now,
now
]);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to save test case',
category: 'system'
});
}
// Update feature status to testing if still in planning
await context.db.run(`UPDATE development_features
SET status = 'testing', updated_at = ?
WHERE id = ? AND status = 'planning'`, [now, featureId]);
return createSuccessResult({
test: {
id: testId,
name: input.testName,
type: input.testType || 'unit',
description: input.description,
expectedBehavior: input.expectedBehavior,
status: 'pending'
},
nextSteps: [
'Run the test to see it fail (RED phase)',
'Implement minimal code to make it pass (GREEN phase)',
'Refactor while keeping tests green (REFACTOR phase)'
],
tddGuidance: {
phase: 'red',
description: 'You should now run this test and verify it fails',
command: 'Use "run_tdd_cycle" to guide through the TDD process'
},
message: 'Test case created successfully'
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to write test: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Run tests with options
*/
const runTestsTool = createTool({
name: 'run_tests',
description: 'Run tests with various options and track results',
category: 'developer-workflow',
inputSchema: {
type: 'object',
properties: {
featureId: {
type: 'string',
description: 'Feature ID to run tests for (optional)'
},
testFile: {
type: 'string',
description: 'Specific test file to run',
maxLength: 500
},
watch: {
type: 'boolean',
default: false,
description: 'Run tests in watch mode'
},
coverage: {
type: 'boolean',
default: false,
description: 'Generate coverage report'
}
},
additionalProperties: false
},
async execute(input, context) {
try {
let command = 'npm test';
if (input.coverage) {
command += ' -- --coverage';
}
if (input.testFile) {
command += ` ${input.testFile}`;
}
if (input.watch) {
command += ' -- --watch';
return createSuccessResult({
command,
message: 'Started tests in watch mode',
instructions: 'Tests will run automatically when files change. Press Ctrl+C to stop.'
});
}
// Run tests
let testResult;
try {
const { stdout, stderr } = await execAsync(command);
testResult = {
success: true,
output: stdout,
error: stderr || ''
};
}
catch (error) {
testResult = {
success: false,
output: error.stdout || '',
error: error.stderr || error.message || 'Test execution failed'
};
}
// Update test case statuses if feature specified
if (input.featureId) {
// Parse test results (basic implementation)
const passingTests = (testResult.output.match(/โ/g) || []).length;
const failingTests = (testResult.output.match(/โ|ร/g) || []).length;
await context.db.run(`UPDATE development_test_cases
SET status = ?, updated_at = ?
WHERE feature_id = ?`, [testResult.success ? 'passing' : 'failing', Date.now(), input.featureId]);
// Update feature test coverage
const coverage = input.coverage ? parseCoverage(testResult.output) : null;
if (coverage) {
await context.db.run('UPDATE development_features SET test_coverage = ?, updated_at = ? WHERE id = ?', [coverage, Date.now(), input.featureId]);
}
}
return createSuccessResult({
testResult,
summary: {
success: testResult.success,
hasOutput: testResult.output.length > 0,
hasErrors: testResult.error.length > 0
},
nextSteps: testResult.success ?
['Tests passing! Consider refactoring or adding more tests'] :
['Fix failing tests before proceeding'],
message: testResult.success ? 'Tests completed successfully' : 'Some tests failed'
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to run tests: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
// Helper function for parsing test coverage
function parseCoverage(output) {
// Basic coverage parsing - would need to be enhanced for specific test runners
const coverageMatch = output.match(/All files\s+\|\s+(\d+\.?\d*)/);
return coverageMatch ? parseFloat(coverageMatch[1]) : null;
}
/**
* Complete a feature
*/
const completeFeatureTool = createTool({
name: 'complete_feature',
description: 'Complete a feature and optionally create a pull request',
category: 'developer-workflow',
inputSchema: {
type: 'object',
properties: {
featureId: {
type: 'string',
description: 'Feature ID to complete',
minLength: 1
},
createPR: {
type: 'boolean',
default: false,
description: 'Create a pull request after completing feature'
},
prTitle: {
type: 'string',
description: 'Pull request title',
maxLength: 200
},
prDescription: {
type: 'string',
description: 'Pull request description',
maxLength: 2000
}
},
required: ['featureId'],
additionalProperties: false
},
async execute(input, context) {
try {
const now = Date.now();
// Get feature details
const feature = await context.db.get('SELECT * FROM development_features WHERE id = ?', [input.featureId]);
if (!feature.data) {
return createErrorResult({
code: 'FEATURE_NOT_FOUND',
message: 'Feature not found',
category: 'validation'
});
}
// Check if tests are passing
const testSummary = await context.db.get(`SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'passing' THEN 1 ELSE 0 END) as passing,
SUM(CASE WHEN status = 'failing' THEN 1 ELSE 0 END) as failing
FROM development_test_cases
WHERE feature_id = ?`, [input.featureId]);
const tests = testSummary.data;
if (tests && tests.failing > 0) {
return createErrorResult({
code: 'TESTS_FAILING',
message: `Cannot complete feature with ${tests.failing} failing tests`,
category: 'validation'
});
}
// Complete TDD sessions
await context.db.run('UPDATE development_tdd_sessions SET completed_at = ? WHERE feature_id = ? AND completed_at IS NULL', [now, input.featureId]);
// Update feature status
const result = await context.db.run('UPDATE development_features SET status = ?, updated_at = ? WHERE id = ?', ['completed', now, input.featureId]);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to update feature status',
category: 'system'
});
}
let prInfo = null;
if (input.createPR) {
try {
const branchName = `feature/${feature.data.name}`;
const title = input.prTitle || `feat: ${feature.data.name}`;
const description = input.prDescription ||
`## Summary\n\n${feature.data.description || 'Feature implementation'}\n\n## Tests\n\n- โ
${tests?.passing || 0} tests passing\n- ๐งช Test coverage: ${feature.data.test_coverage || 0}%`;
// Create PR using gh CLI if available
try {
const { stdout } = await execAsync(`gh pr create --title "${title}" --body "${description}"`);
prInfo = {
created: true,
url: stdout.trim(),
title,
description
};
}
catch (error) {
prInfo = {
created: false,
error: 'gh CLI not available or authentication failed',
suggestion: `Manually create PR from ${branchName} to main`
};
}
}
catch (error) {
prInfo = {
created: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
return createSuccessResult({
feature: {
id: input.featureId,
name: feature.data.name,
status: 'completed',
completedAt: new Date(now).toISOString()
},
testSummary: {
total: tests?.total || 0,
passing: tests?.passing || 0,
coverage: feature.data.test_coverage || 0
},
pullRequest: prInfo,
nextSteps: [
'Review the completed feature',
'Merge pull request if created',
'Start next feature or epic',
'Update project documentation'
],
message: `Feature '${feature.data.name}' completed successfully`
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to complete feature: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Setup developer workflow tools
*/
export async function setupDeveloperWorkflowTools() {
return {
module: 'developer-workflow',
tools: [
startFeatureTool,
checkWorkflowStatusTool,
suggestNextActionTool,
runTDDCycleTool,
setupCommitHooksTool,
writeTestTool,
runTestsTool,
completeFeatureTool
]
};
}
//# sourceMappingURL=tools.js.map