software-engineer 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -0
- package/dist/claude.d.ts +10 -0
- package/dist/claude.js +68 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +35 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +51 -0
- package/dist/logger.d.ts +14 -0
- package/dist/logger.js +72 -0
- package/dist/pipeline.d.ts +2 -0
- package/dist/pipeline.js +81 -0
- package/dist/prompts.d.ts +8 -0
- package/dist/prompts.js +36 -0
- package/dist/steps/branchManagement.d.ts +9 -0
- package/dist/steps/branchManagement.js +56 -0
- package/dist/steps/changelog.d.ts +2 -0
- package/dist/steps/changelog.js +26 -0
- package/dist/steps/commit.d.ts +2 -0
- package/dist/steps/commit.js +36 -0
- package/dist/steps/implement.d.ts +2 -0
- package/dist/steps/implement.js +15 -0
- package/dist/steps/index.d.ts +8 -0
- package/dist/steps/index.js +8 -0
- package/dist/steps/review.d.ts +6 -0
- package/dist/steps/review.js +37 -0
- package/dist/steps/simplify.d.ts +2 -0
- package/dist/steps/simplify.js +47 -0
- package/dist/steps/solid.d.ts +2 -0
- package/dist/steps/solid.js +53 -0
- package/dist/steps/test.d.ts +2 -0
- package/dist/steps/test.js +23 -0
- package/dist/utils/branchAnalyzer.d.ts +10 -0
- package/dist/utils/branchAnalyzer.js +105 -0
- package/dist/utils/git.d.ts +19 -0
- package/dist/utils/git.js +86 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# software-engineer
|
|
2
|
+
|
|
3
|
+
A CLI tool that automates the software development workflow using Claude AI. It runs a 7-step pipeline to implement features, simplify code, review, ensure quality, test, commit, and update changelogs.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g software-engineer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
- Node.js >= 18.0.0
|
|
14
|
+
- [Claude CLI](https://github.com/anthropics/claude-code) installed and configured
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
sf "<requirement>"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Examples
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Basic usage
|
|
26
|
+
sf "add user authentication with JWT"
|
|
27
|
+
|
|
28
|
+
# Custom review iterations
|
|
29
|
+
sf --reviews 3 "refactor database layer"
|
|
30
|
+
|
|
31
|
+
# Dry run to preview commands
|
|
32
|
+
sf --dry-run "add dark mode toggle"
|
|
33
|
+
|
|
34
|
+
# Skip tests and push
|
|
35
|
+
sf --skip-tests --skip-push "update README"
|
|
36
|
+
|
|
37
|
+
# Log output to file
|
|
38
|
+
sf --log pipeline.log "implement caching layer"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Options
|
|
42
|
+
|
|
43
|
+
| Option | Description |
|
|
44
|
+
|--------|-------------|
|
|
45
|
+
| `-d, --dry-run` | Print commands without executing |
|
|
46
|
+
| `-r, --reviews <n>` | Number of review iterations (default: 2) |
|
|
47
|
+
| `--skip-tests` | Skip the testing step |
|
|
48
|
+
| `--skip-push` | Commit but don't push to remote |
|
|
49
|
+
| `--log <file>` | Log output to file |
|
|
50
|
+
| `--dangerously-skip-permissions` | Skip Claude permission prompts |
|
|
51
|
+
| `-h, --help` | Display help |
|
|
52
|
+
| `-V, --version` | Display version |
|
|
53
|
+
|
|
54
|
+
## Environment Variables
|
|
55
|
+
|
|
56
|
+
All options can be set via environment variables:
|
|
57
|
+
|
|
58
|
+
| Variable | Description |
|
|
59
|
+
|----------|-------------|
|
|
60
|
+
| `SF_REVIEW_ITERATIONS` | Number of review iterations |
|
|
61
|
+
| `SF_DRY_RUN` | `true`/`false` for dry run |
|
|
62
|
+
| `SF_LOG_FILE` | Path to log file |
|
|
63
|
+
| `SF_SKIP_TESTS` | `true`/`false` to skip tests |
|
|
64
|
+
| `SF_SKIP_PUSH` | `true`/`false` to skip push |
|
|
65
|
+
| `SF_DANGEROUSLY_SKIP_PERMISSIONS` | `true`/`false` to skip Claude permissions |
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
```bash
|
|
69
|
+
SF_REVIEW_ITERATIONS=3 sf "add feature X"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Pipeline Steps
|
|
73
|
+
|
|
74
|
+
### 1. Implement
|
|
75
|
+
Takes your requirement and asks Claude to implement it following project conventions.
|
|
76
|
+
|
|
77
|
+
### 2. Code Simplification
|
|
78
|
+
Refines the implementation for clarity, consistency, and maintainability while preserving functionality:
|
|
79
|
+
- Follows project standards (ES modules, explicit types)
|
|
80
|
+
- Enhances clarity by avoiding nested ternaries
|
|
81
|
+
- Removes redundant abstractions
|
|
82
|
+
|
|
83
|
+
### 3. Code Review (configurable iterations)
|
|
84
|
+
Reviews the implementation for:
|
|
85
|
+
- Bugs (logic errors, null refs, race conditions)
|
|
86
|
+
- Security issues (injection, auth problems)
|
|
87
|
+
- Performance (N+1 queries, memory leaks)
|
|
88
|
+
- Maintainability (clarity, naming, complexity)
|
|
89
|
+
|
|
90
|
+
### 4. SOLID & Clean Code
|
|
91
|
+
Ensures compliance with:
|
|
92
|
+
- SOLID principles (SRP, OCP, LSP, ISP, DIP)
|
|
93
|
+
- Clean code practices (naming, small functions, no magic numbers)
|
|
94
|
+
|
|
95
|
+
### 5. Testing
|
|
96
|
+
- Runs existing tests
|
|
97
|
+
- Adds new tests for changed code
|
|
98
|
+
- Verifies coverage
|
|
99
|
+
|
|
100
|
+
### 6. Commit
|
|
101
|
+
Creates a well-formatted commit with:
|
|
102
|
+
- Conventional commit format
|
|
103
|
+
- Clear subject line
|
|
104
|
+
- Detailed body explaining why and what
|
|
105
|
+
|
|
106
|
+
### 7. Changelog
|
|
107
|
+
Updates CHANGELOG.md following Keep a Changelog format.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
package/dist/claude.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Config } from './config.js';
|
|
2
|
+
export interface ClaudeOptions {
|
|
3
|
+
prompt: string;
|
|
4
|
+
continueConversation?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface ClaudeResult {
|
|
7
|
+
success: boolean;
|
|
8
|
+
output: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function runClaude(options: ClaudeOptions, config: Config): Promise<ClaudeResult>;
|
package/dist/claude.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { logInfo, logSuccess, logError, logDryRun } from './logger.js';
|
|
3
|
+
// Exit codes
|
|
4
|
+
const EXIT_SUCCESS = 0;
|
|
5
|
+
const EXIT_INTERRUPTED = 130;
|
|
6
|
+
export async function runClaude(options, config) {
|
|
7
|
+
// Build the full prompt with exit instruction (like Python version)
|
|
8
|
+
const fullPrompt = `${options.prompt}. Once the work is completed, exit.`;
|
|
9
|
+
const args = [];
|
|
10
|
+
if (config.dangerouslySkipPermissions) {
|
|
11
|
+
args.push('--dangerously-skip-permissions');
|
|
12
|
+
}
|
|
13
|
+
if (options.continueConversation) {
|
|
14
|
+
args.push('-c');
|
|
15
|
+
}
|
|
16
|
+
// Pass prompt directly like Python version (not with -p flag)
|
|
17
|
+
args.push(fullPrompt);
|
|
18
|
+
if (config.dryRun) {
|
|
19
|
+
logDryRun(`claude ${args.join(' ')}`);
|
|
20
|
+
return { success: true, output: '' };
|
|
21
|
+
}
|
|
22
|
+
logInfo('Calling Claude...');
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
let capturedOutput = '';
|
|
25
|
+
// Spawn Claude using child_process
|
|
26
|
+
// Use 'inherit' for stdin to allow interactive input
|
|
27
|
+
// Use 'pipe' for stdout/stderr to capture output while passing through
|
|
28
|
+
const child = spawn('claude', args, {
|
|
29
|
+
cwd: process.cwd(),
|
|
30
|
+
env: process.env,
|
|
31
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
32
|
+
});
|
|
33
|
+
// Capture and pass through stdout
|
|
34
|
+
if (child.stdout) {
|
|
35
|
+
child.stdout.on('data', (data) => {
|
|
36
|
+
const text = data.toString();
|
|
37
|
+
process.stdout.write(text);
|
|
38
|
+
capturedOutput += text;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// Capture and pass through stderr
|
|
42
|
+
if (child.stderr) {
|
|
43
|
+
child.stderr.on('data', (data) => {
|
|
44
|
+
process.stderr.write(data.toString());
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// Handle process exit
|
|
48
|
+
child.on('close', (exitCode) => {
|
|
49
|
+
if (exitCode === EXIT_INTERRUPTED || exitCode === 2) {
|
|
50
|
+
logError('Claude interrupted');
|
|
51
|
+
process.exit(EXIT_INTERRUPTED);
|
|
52
|
+
}
|
|
53
|
+
else if (exitCode === EXIT_SUCCESS) {
|
|
54
|
+
logSuccess('Claude completed');
|
|
55
|
+
resolve({ success: true, output: capturedOutput });
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
logError(`Claude exited with code ${exitCode}`);
|
|
59
|
+
resolve({ success: false, output: capturedOutput });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
// Handle spawn errors
|
|
63
|
+
child.on('error', (err) => {
|
|
64
|
+
logError(`Failed to start Claude: ${err.message}`);
|
|
65
|
+
resolve({ success: false, output: '' });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
reviewIterations: number;
|
|
3
|
+
dryRun: boolean;
|
|
4
|
+
logFile?: string;
|
|
5
|
+
skipTests: boolean;
|
|
6
|
+
skipPush: boolean;
|
|
7
|
+
skipBranchManagement: boolean;
|
|
8
|
+
dangerouslySkipPermissions: boolean;
|
|
9
|
+
requirement: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function loadConfigFromEnv(): Partial<Config>;
|
|
12
|
+
export declare function mergeConfig(envConfig: Partial<Config>, cliConfig: Partial<Config>): Config;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const DEFAULT_REVIEW_ITERATIONS = 2;
|
|
2
|
+
function parseBoolEnv(value, defaultValue) {
|
|
3
|
+
if (value === undefined)
|
|
4
|
+
return defaultValue;
|
|
5
|
+
return value.toLowerCase() === 'true';
|
|
6
|
+
}
|
|
7
|
+
function parseIntEnv(value, defaultValue) {
|
|
8
|
+
if (value === undefined)
|
|
9
|
+
return defaultValue;
|
|
10
|
+
const parsed = parseInt(value, 10);
|
|
11
|
+
return isNaN(parsed) ? defaultValue : parsed;
|
|
12
|
+
}
|
|
13
|
+
export function loadConfigFromEnv() {
|
|
14
|
+
return {
|
|
15
|
+
reviewIterations: parseIntEnv(process.env.SF_REVIEW_ITERATIONS, DEFAULT_REVIEW_ITERATIONS),
|
|
16
|
+
dryRun: parseBoolEnv(process.env.SF_DRY_RUN, false),
|
|
17
|
+
logFile: process.env.SF_LOG_FILE || undefined,
|
|
18
|
+
skipTests: parseBoolEnv(process.env.SF_SKIP_TESTS, false),
|
|
19
|
+
skipPush: parseBoolEnv(process.env.SF_SKIP_PUSH, false),
|
|
20
|
+
skipBranchManagement: parseBoolEnv(process.env.SF_SKIP_BRANCH_MANAGEMENT, false),
|
|
21
|
+
dangerouslySkipPermissions: parseBoolEnv(process.env.SF_DANGEROUSLY_SKIP_PERMISSIONS, false),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function mergeConfig(envConfig, cliConfig) {
|
|
25
|
+
return {
|
|
26
|
+
reviewIterations: cliConfig.reviewIterations ?? envConfig.reviewIterations ?? DEFAULT_REVIEW_ITERATIONS,
|
|
27
|
+
dryRun: cliConfig.dryRun ?? envConfig.dryRun ?? false,
|
|
28
|
+
logFile: cliConfig.logFile ?? envConfig.logFile,
|
|
29
|
+
skipTests: cliConfig.skipTests ?? envConfig.skipTests ?? false,
|
|
30
|
+
skipPush: cliConfig.skipPush ?? envConfig.skipPush ?? false,
|
|
31
|
+
skipBranchManagement: cliConfig.skipBranchManagement ?? envConfig.skipBranchManagement ?? false,
|
|
32
|
+
dangerouslySkipPermissions: cliConfig.dangerouslySkipPermissions ?? envConfig.dangerouslySkipPermissions ?? false,
|
|
33
|
+
requirement: cliConfig.requirement ?? '',
|
|
34
|
+
};
|
|
35
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
import { loadConfigFromEnv, mergeConfig } from './config.js';
|
|
8
|
+
import { runPipeline } from './pipeline.js';
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
12
|
+
const program = new Command();
|
|
13
|
+
program
|
|
14
|
+
.name('sf')
|
|
15
|
+
.description('Software Factory Pipeline - Automate development workflow with Claude AI')
|
|
16
|
+
.version(pkg.version)
|
|
17
|
+
.argument('<requirement>', 'The requirement or task to implement')
|
|
18
|
+
.option('-d, --dry-run', 'Print commands without executing')
|
|
19
|
+
.option('-r, --reviews <n>', 'Number of review iterations', '2')
|
|
20
|
+
.option('--skip-tests', 'Skip the testing step')
|
|
21
|
+
.option('--skip-push', 'Commit but do not push')
|
|
22
|
+
.option('--skip-branch-management', 'Skip smart branch management')
|
|
23
|
+
.option('--log <file>', 'Log output to file')
|
|
24
|
+
.option('--dangerously-skip-permissions', 'Pass flag to claude to skip permission prompts')
|
|
25
|
+
.action(async (requirement, options) => {
|
|
26
|
+
const envConfig = loadConfigFromEnv();
|
|
27
|
+
const cliConfig = {
|
|
28
|
+
requirement,
|
|
29
|
+
dryRun: options.dryRun ?? undefined,
|
|
30
|
+
reviewIterations: options.reviews ? parseInt(options.reviews, 10) : undefined,
|
|
31
|
+
skipTests: options.skipTests ?? undefined,
|
|
32
|
+
skipPush: options.skipPush ?? undefined,
|
|
33
|
+
skipBranchManagement: options.skipBranchManagement ?? undefined,
|
|
34
|
+
logFile: options.log ?? undefined,
|
|
35
|
+
dangerouslySkipPermissions: options.dangerouslySkipPermissions ?? undefined,
|
|
36
|
+
};
|
|
37
|
+
const config = mergeConfig(envConfig, cliConfig);
|
|
38
|
+
if (!config.requirement) {
|
|
39
|
+
console.error(chalk.red('Error:') + ' No requirement provided');
|
|
40
|
+
program.help();
|
|
41
|
+
return; // Ensure we don't continue if help() doesn't exit
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
await runPipeline(config);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error(chalk.red('Pipeline failed:'), error);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
program.parse();
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function setLogFile(path: string | undefined): void;
|
|
2
|
+
export declare function log(message: string): void;
|
|
3
|
+
export declare function logRaw(message: string): void;
|
|
4
|
+
export declare function logStep(stepNum: string, title: string): void;
|
|
5
|
+
export declare function logHeader(config: {
|
|
6
|
+
reviewIterations: number;
|
|
7
|
+
dryRun: boolean;
|
|
8
|
+
dangerouslySkipPermissions: boolean;
|
|
9
|
+
}): void;
|
|
10
|
+
export declare function logSuccess(message: string): void;
|
|
11
|
+
export declare function logError(message: string): void;
|
|
12
|
+
export declare function logWarning(message: string): void;
|
|
13
|
+
export declare function logInfo(message: string): void;
|
|
14
|
+
export declare function logDryRun(command: string): void;
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { appendFileSync } from 'fs';
|
|
3
|
+
const BOX_WIDTH = 62;
|
|
4
|
+
let logFilePath;
|
|
5
|
+
export function setLogFile(path) {
|
|
6
|
+
logFilePath = path;
|
|
7
|
+
}
|
|
8
|
+
function stripAnsi(str) {
|
|
9
|
+
// eslint-disable-next-line no-control-regex
|
|
10
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
11
|
+
}
|
|
12
|
+
function getTimestamp() {
|
|
13
|
+
return new Date().toISOString().replace('T', ' ').substring(0, 19);
|
|
14
|
+
}
|
|
15
|
+
export function log(message) {
|
|
16
|
+
const timestamped = `[${getTimestamp()}] ${message}`;
|
|
17
|
+
console.log(timestamped);
|
|
18
|
+
if (logFilePath) {
|
|
19
|
+
appendFileSync(logFilePath, stripAnsi(timestamped) + '\n');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function logRaw(message) {
|
|
23
|
+
console.log(message);
|
|
24
|
+
if (logFilePath) {
|
|
25
|
+
appendFileSync(logFilePath, stripAnsi(message) + '\n');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function logStep(stepNum, title) {
|
|
29
|
+
const line = '─'.repeat(BOX_WIDTH);
|
|
30
|
+
console.log();
|
|
31
|
+
console.log(chalk.cyan(`┌${line}┐`));
|
|
32
|
+
console.log(chalk.cyan('│') + ' ' + chalk.green(`STEP ${stepNum}:`) + ' ' + title);
|
|
33
|
+
console.log(chalk.cyan(`└${line}┘`));
|
|
34
|
+
console.log();
|
|
35
|
+
if (logFilePath) {
|
|
36
|
+
appendFileSync(logFilePath, `\n┌${line}┐\n│ STEP ${stepNum}: ${title}\n└${line}┘\n\n`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function logHeader(config) {
|
|
40
|
+
const line = '═'.repeat(BOX_WIDTH);
|
|
41
|
+
console.log();
|
|
42
|
+
console.log(chalk.green(`╔${line}╗`));
|
|
43
|
+
console.log(chalk.green('║') + ' SOFTWARE FACTORY PIPELINE v2.0 ' + chalk.green('║'));
|
|
44
|
+
console.log(chalk.green(`╠${line}╣`));
|
|
45
|
+
const configLine = ` Reviews: ${config.reviewIterations} | Dry-run: ${config.dryRun}`;
|
|
46
|
+
console.log(chalk.green('║') + configLine.padEnd(BOX_WIDTH) + chalk.green('║'));
|
|
47
|
+
if (config.dangerouslySkipPermissions) {
|
|
48
|
+
const permLine = ' ' + chalk.yellow('⚠ Skip permissions: enabled');
|
|
49
|
+
console.log(chalk.green('║') + permLine + ' '.repeat(BOX_WIDTH - stripAnsi(permLine).length) + chalk.green('║'));
|
|
50
|
+
}
|
|
51
|
+
console.log(chalk.green(`╚${line}╝`));
|
|
52
|
+
console.log();
|
|
53
|
+
}
|
|
54
|
+
export function logSuccess(message) {
|
|
55
|
+
log(chalk.green(`✓ ${message}`));
|
|
56
|
+
}
|
|
57
|
+
export function logError(message) {
|
|
58
|
+
log(chalk.red(`✗ ${message}`));
|
|
59
|
+
}
|
|
60
|
+
export function logWarning(message) {
|
|
61
|
+
log(chalk.yellow(`⚠ ${message}`));
|
|
62
|
+
}
|
|
63
|
+
export function logInfo(message) {
|
|
64
|
+
log(chalk.blue(`▶ ${message}`));
|
|
65
|
+
}
|
|
66
|
+
export function logDryRun(command) {
|
|
67
|
+
const message = '[DRY-RUN] ' + command;
|
|
68
|
+
console.log(chalk.yellow('[DRY-RUN]') + ' ' + command);
|
|
69
|
+
if (logFilePath) {
|
|
70
|
+
appendFileSync(logFilePath, message + '\n');
|
|
71
|
+
}
|
|
72
|
+
}
|
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { log, logHeader, setLogFile } from './logger.js';
|
|
3
|
+
import { stepBranchManagement, stepImplement, stepSimplify, stepReview, stepSolidCleanCode, stepTest, stepCommit, stepChangelog, } from './steps/index.js';
|
|
4
|
+
let signalHandlersRegistered = false;
|
|
5
|
+
export async function runPipeline(config) {
|
|
6
|
+
// Setup logging
|
|
7
|
+
if (config.logFile) {
|
|
8
|
+
setLogFile(config.logFile);
|
|
9
|
+
}
|
|
10
|
+
// Setup interrupt handler (only once to prevent duplicate registrations)
|
|
11
|
+
if (!signalHandlersRegistered) {
|
|
12
|
+
process.on('SIGINT', () => {
|
|
13
|
+
console.log(chalk.red('\n\nPipeline interrupted'));
|
|
14
|
+
process.exit(130);
|
|
15
|
+
});
|
|
16
|
+
process.on('SIGTERM', () => {
|
|
17
|
+
console.log(chalk.red('\n\nPipeline terminated'));
|
|
18
|
+
process.exit(130);
|
|
19
|
+
});
|
|
20
|
+
signalHandlersRegistered = true;
|
|
21
|
+
}
|
|
22
|
+
// Display header
|
|
23
|
+
logHeader(config);
|
|
24
|
+
log(`Starting pipeline for: ${config.requirement}`);
|
|
25
|
+
// Step 1: Smart Branch Management
|
|
26
|
+
const branchResult = await stepBranchManagement(config);
|
|
27
|
+
if (!branchResult.success) {
|
|
28
|
+
console.log(chalk.red('\nBranch management step failed. Exiting.'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
// Step 2: Implement
|
|
32
|
+
const implSuccess = await stepImplement(config);
|
|
33
|
+
if (!implSuccess) {
|
|
34
|
+
console.log(chalk.red('\nImplementation step failed. Exiting.'));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
// Step 3: Simplify
|
|
38
|
+
const simplifySuccess = await stepSimplify(config);
|
|
39
|
+
if (!simplifySuccess) {
|
|
40
|
+
console.log(chalk.red('\nSimplification step failed. Exiting.'));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
// Step 4: Review loop
|
|
44
|
+
for (let i = 1; i <= config.reviewIterations; i++) {
|
|
45
|
+
const reviewResult = await stepReview(i, config);
|
|
46
|
+
if (!reviewResult.success) {
|
|
47
|
+
console.log(chalk.red('\nReview step failed. Exiting.'));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
// Skip remaining reviews if no issues were found
|
|
51
|
+
if (reviewResult.noIssuesFound) {
|
|
52
|
+
console.log(chalk.green('\n✓ Code review passed - no issues found, skipping remaining reviews'));
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Step 5: SOLID & Clean Code
|
|
57
|
+
const solidSuccess = await stepSolidCleanCode(config);
|
|
58
|
+
if (!solidSuccess) {
|
|
59
|
+
console.log(chalk.red('\nSOLID review step failed. Exiting.'));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
// Step 6: Test
|
|
63
|
+
const testSuccess = await stepTest(config);
|
|
64
|
+
if (!testSuccess) {
|
|
65
|
+
console.log(chalk.red('\nTest step failed. Exiting.'));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
// Step 7: Commit
|
|
69
|
+
const commitSuccess = await stepCommit(config);
|
|
70
|
+
if (!commitSuccess) {
|
|
71
|
+
console.log(chalk.red('\nCommit step failed. Exiting.'));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
// Step 8: Changelog
|
|
75
|
+
const changelogSuccess = await stepChangelog(config);
|
|
76
|
+
if (!changelogSuccess) {
|
|
77
|
+
console.log(chalk.red('\nChangelog step failed. Exiting.'));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
console.log(chalk.green('\n✓ Pipeline completed successfully\n'));
|
|
81
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Config } from './config.js';
|
|
2
|
+
export declare enum ConfirmResult {
|
|
3
|
+
Continue = "continue",
|
|
4
|
+
Skip = "skip",
|
|
5
|
+
Quit = "quit"
|
|
6
|
+
}
|
|
7
|
+
export declare function confirm(config: Config): Promise<ConfirmResult>;
|
|
8
|
+
export declare function handleConfirm(config: Config): Promise<boolean>;
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
export var ConfirmResult;
|
|
3
|
+
(function (ConfirmResult) {
|
|
4
|
+
ConfirmResult["Continue"] = "continue";
|
|
5
|
+
ConfirmResult["Skip"] = "skip";
|
|
6
|
+
ConfirmResult["Quit"] = "quit";
|
|
7
|
+
})(ConfirmResult || (ConfirmResult = {}));
|
|
8
|
+
export async function confirm(config) {
|
|
9
|
+
if (config.autoMode) {
|
|
10
|
+
return ConfirmResult.Continue;
|
|
11
|
+
}
|
|
12
|
+
const { action } = await inquirer.prompt([
|
|
13
|
+
{
|
|
14
|
+
type: 'expand',
|
|
15
|
+
name: 'action',
|
|
16
|
+
message: 'Continue?',
|
|
17
|
+
default: 'y',
|
|
18
|
+
choices: [
|
|
19
|
+
{ key: 'y', name: 'Yes - continue', value: 'continue' },
|
|
20
|
+
{ key: 'n', name: 'No - exit', value: 'quit' },
|
|
21
|
+
{ key: 's', name: 'Skip - skip this step', value: 'skip' },
|
|
22
|
+
{ key: 'q', name: 'Quit - exit pipeline', value: 'quit' },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
]);
|
|
26
|
+
return action;
|
|
27
|
+
}
|
|
28
|
+
export async function handleConfirm(config) {
|
|
29
|
+
const result = await confirm(config);
|
|
30
|
+
if (result === ConfirmResult.Quit) {
|
|
31
|
+
console.log('\nPipeline cancelled by user.');
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
// Continue or Skip both mean proceed
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type BranchAnalysis } from '../utils/branchAnalyzer.js';
|
|
2
|
+
import type { Config } from '../config.js';
|
|
3
|
+
export interface BranchResult {
|
|
4
|
+
success: boolean;
|
|
5
|
+
branchCreated: boolean;
|
|
6
|
+
branchName: string;
|
|
7
|
+
analysis: BranchAnalysis | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function stepBranchManagement(config: Config): Promise<BranchResult>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { logStep, logInfo, logSuccess, logWarning, logDryRun } from '../logger.js';
|
|
2
|
+
import { getGitState, branchExists, createBranch, findSimilarBranches } from '../utils/git.js';
|
|
3
|
+
import { analyzeRequirement } from '../utils/branchAnalyzer.js';
|
|
4
|
+
function createResult(branchName, analysis = null, branchCreated = false) {
|
|
5
|
+
return { success: true, branchCreated, branchName, analysis };
|
|
6
|
+
}
|
|
7
|
+
export async function stepBranchManagement(config) {
|
|
8
|
+
logStep('1/8', 'SMART BRANCH MANAGEMENT');
|
|
9
|
+
if (config.skipBranchManagement) {
|
|
10
|
+
logInfo('Branch management skipped (--skip-branch-management)');
|
|
11
|
+
return createResult('');
|
|
12
|
+
}
|
|
13
|
+
const gitState = getGitState();
|
|
14
|
+
logInfo(`Current branch: ${gitState.currentBranch}`);
|
|
15
|
+
if (!gitState.isMainBranch) {
|
|
16
|
+
logInfo(`Already on branch '${gitState.currentBranch}' - skipping branch creation`);
|
|
17
|
+
return createResult(gitState.currentBranch);
|
|
18
|
+
}
|
|
19
|
+
logInfo('On main branch - analyzing requirement...');
|
|
20
|
+
// Analyze requirement to determine change type
|
|
21
|
+
const analysis = await analyzeRequirement(config.requirement, config);
|
|
22
|
+
logInfo(`Change type: ${analysis.changeType}`);
|
|
23
|
+
logInfo(`Suggested branch: ${analysis.suggestedBranchName}`);
|
|
24
|
+
if (analysis.isTrivial) {
|
|
25
|
+
logInfo('Trivial change detected - staying on current branch');
|
|
26
|
+
return createResult(gitState.currentBranch, analysis);
|
|
27
|
+
}
|
|
28
|
+
// Check for similar/conflicting branches
|
|
29
|
+
const conflicts = findSimilarBranches(analysis.shortDescription, gitState.remoteBranches);
|
|
30
|
+
if (conflicts.length > 0) {
|
|
31
|
+
logWarning('Potential conflicting branches detected:');
|
|
32
|
+
for (const conflict of conflicts) {
|
|
33
|
+
logWarning(` - ${conflict.branchName} (${conflict.similarity})`);
|
|
34
|
+
}
|
|
35
|
+
logWarning('Consider checking these branches before proceeding');
|
|
36
|
+
}
|
|
37
|
+
// Generate unique branch name
|
|
38
|
+
let branchName = analysis.suggestedBranchName;
|
|
39
|
+
let counter = 1;
|
|
40
|
+
while (branchExists(branchName)) {
|
|
41
|
+
branchName = `${analysis.branchPrefix}/${analysis.shortDescription}-${counter}`;
|
|
42
|
+
counter++;
|
|
43
|
+
}
|
|
44
|
+
if (config.dryRun) {
|
|
45
|
+
logDryRun(`git checkout -b ${branchName}`);
|
|
46
|
+
return createResult(branchName, analysis);
|
|
47
|
+
}
|
|
48
|
+
logInfo(`Creating branch: ${branchName}`);
|
|
49
|
+
const created = createBranch(branchName);
|
|
50
|
+
if (created) {
|
|
51
|
+
logSuccess(`Switched to new branch '${branchName}'`);
|
|
52
|
+
return createResult(branchName, analysis, true);
|
|
53
|
+
}
|
|
54
|
+
logWarning(`Failed to create branch '${branchName}' - continuing on current branch`);
|
|
55
|
+
return createResult(gitState.currentBranch, analysis);
|
|
56
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { logStep } from '../logger.js';
|
|
2
|
+
import { runClaude } from '../claude.js';
|
|
3
|
+
export async function stepChangelog(config) {
|
|
4
|
+
logStep('8/8', 'UPDATE CHANGELOG');
|
|
5
|
+
const prompt = `Update CHANGELOG.md:
|
|
6
|
+
|
|
7
|
+
## Format (Keep a Changelog):
|
|
8
|
+
\`\`\`markdown
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- New features
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Changes in existing functionality
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Bug fixes
|
|
19
|
+
\`\`\`
|
|
20
|
+
|
|
21
|
+
1. Add entry for this change under appropriate category
|
|
22
|
+
2. Be concise but descriptive
|
|
23
|
+
3. Commit the CHANGELOG update`;
|
|
24
|
+
const result = await runClaude({ prompt, continueConversation: true }, config);
|
|
25
|
+
return result.success;
|
|
26
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { logStep } from '../logger.js';
|
|
2
|
+
import { runClaude } from '../claude.js';
|
|
3
|
+
export async function stepCommit(config) {
|
|
4
|
+
logStep('7/8', 'COMMIT CHANGES');
|
|
5
|
+
const pushInstruction = config.skipPush ? '' : 'Then push to remote.';
|
|
6
|
+
const prompt = `Commit the changes:
|
|
7
|
+
|
|
8
|
+
## Commit Message Requirements (Target: 10/10):
|
|
9
|
+
|
|
10
|
+
**Subject Line (max 3 pts):**
|
|
11
|
+
- Imperative mood ('Add' not 'Added')
|
|
12
|
+
- ≤50 characters, no trailing period
|
|
13
|
+
- Format: type(scope): description
|
|
14
|
+
|
|
15
|
+
**Body - WHY (max 3 pts):**
|
|
16
|
+
- What problem does this solve?
|
|
17
|
+
- Why was this approach chosen?
|
|
18
|
+
|
|
19
|
+
**Body - WHAT (max 2 pts):**
|
|
20
|
+
- Key technical changes
|
|
21
|
+
- Files/components affected
|
|
22
|
+
|
|
23
|
+
**Structure (max 2 pts):**
|
|
24
|
+
- Blank line after subject
|
|
25
|
+
- Body wrapped at 72 chars
|
|
26
|
+
|
|
27
|
+
## Tasks:
|
|
28
|
+
1. Stage relevant files (git add)
|
|
29
|
+
2. Commit with excellent message
|
|
30
|
+
|
|
31
|
+
Do not add any attribution or co author by.
|
|
32
|
+
|
|
33
|
+
${pushInstruction}`;
|
|
34
|
+
const result = await runClaude({ prompt, continueConversation: true }, config);
|
|
35
|
+
return result.success;
|
|
36
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { logStep } from '../logger.js';
|
|
2
|
+
import { runClaude } from '../claude.js';
|
|
3
|
+
export async function stepImplement(config) {
|
|
4
|
+
logStep('2/8', 'IMPLEMENT REQUIREMENT');
|
|
5
|
+
const prompt = `${config.requirement}
|
|
6
|
+
|
|
7
|
+
## Implementation Guidelines:
|
|
8
|
+
- Understand the existing codebase structure first
|
|
9
|
+
- Write clean, idiomatic code following project conventions
|
|
10
|
+
- Handle edge cases and errors appropriately
|
|
11
|
+
- Add necessary comments for complex logic
|
|
12
|
+
- Keep changes focused and minimal`;
|
|
13
|
+
const result = await runClaude({ prompt }, config);
|
|
14
|
+
return result.success;
|
|
15
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { stepBranchManagement, type BranchResult } from './branchManagement.js';
|
|
2
|
+
export { stepImplement } from './implement.js';
|
|
3
|
+
export { stepSimplify } from './simplify.js';
|
|
4
|
+
export { stepReview, type ReviewResult } from './review.js';
|
|
5
|
+
export { stepSolidCleanCode } from './solid.js';
|
|
6
|
+
export { stepTest } from './test.js';
|
|
7
|
+
export { stepCommit } from './commit.js';
|
|
8
|
+
export { stepChangelog } from './changelog.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { stepBranchManagement } from './branchManagement.js';
|
|
2
|
+
export { stepImplement } from './implement.js';
|
|
3
|
+
export { stepSimplify } from './simplify.js';
|
|
4
|
+
export { stepReview } from './review.js';
|
|
5
|
+
export { stepSolidCleanCode } from './solid.js';
|
|
6
|
+
export { stepTest } from './test.js';
|
|
7
|
+
export { stepCommit } from './commit.js';
|
|
8
|
+
export { stepChangelog } from './changelog.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { logStep } from '../logger.js';
|
|
2
|
+
import { runClaude } from '../claude.js';
|
|
3
|
+
// Patterns that indicate no issues were found during review
|
|
4
|
+
const NO_ISSUES_PATTERNS = [
|
|
5
|
+
/no\s+issues?\s+(found|detected|identified)/i,
|
|
6
|
+
/code\s+(is\s+)?clean/i,
|
|
7
|
+
/lgtm/i,
|
|
8
|
+
/looks\s+good(\s+to\s+me)?/i,
|
|
9
|
+
/no\s+(changes|modifications)\s+(needed|required|necessary)/i,
|
|
10
|
+
/nothing\s+to\s+(fix|change|improve)/i,
|
|
11
|
+
/all\s+(checks\s+)?pass(ed)?/i,
|
|
12
|
+
/code\s+quality\s+(is\s+)?(good|excellent|solid)/i,
|
|
13
|
+
];
|
|
14
|
+
function detectNoIssues(output) {
|
|
15
|
+
return NO_ISSUES_PATTERNS.some((pattern) => pattern.test(output));
|
|
16
|
+
}
|
|
17
|
+
export async function stepReview(iteration, config) {
|
|
18
|
+
logStep('4/8', `CODE REVIEW (Round ${iteration}/${config.reviewIterations})`);
|
|
19
|
+
const prompt = `Review and improve the changes:
|
|
20
|
+
|
|
21
|
+
## Critical Review Points:
|
|
22
|
+
1. **Bugs**: Logic errors, off-by-one, null refs, race conditions
|
|
23
|
+
2. **Security**: Input validation, injection, auth issues
|
|
24
|
+
3. **Performance**: N+1 queries, unnecessary loops, memory leaks
|
|
25
|
+
4. **Maintainability**: Code clarity, naming, complexity
|
|
26
|
+
|
|
27
|
+
## Action Required:
|
|
28
|
+
- FIX any issues found immediately
|
|
29
|
+
- Be specific about what you changed and why
|
|
30
|
+
- If code is clean, say "No issues found" or "LGTM"`;
|
|
31
|
+
const result = await runClaude({ prompt, continueConversation: true }, config);
|
|
32
|
+
const noIssuesFound = result.success && detectNoIssues(result.output);
|
|
33
|
+
return {
|
|
34
|
+
success: result.success,
|
|
35
|
+
noIssuesFound,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { logStep } from '../logger.js';
|
|
2
|
+
import { runClaude } from '../claude.js';
|
|
3
|
+
export async function stepSimplify(config) {
|
|
4
|
+
logStep('3/8', 'CODE SIMPLIFICATION');
|
|
5
|
+
const prompt = `Refine the recently modified code for clarity, consistency, and maintainability while preserving all functionality.
|
|
6
|
+
|
|
7
|
+
## Core Principles:
|
|
8
|
+
|
|
9
|
+
### 1. Functionality Preservation
|
|
10
|
+
- Never change what the code does - only how it does it
|
|
11
|
+
- All original features, outputs, and behaviors must remain intact
|
|
12
|
+
|
|
13
|
+
### 2. Project Standards Alignment
|
|
14
|
+
- Follow ES modules and project conventions
|
|
15
|
+
- Prefer \`function\` declarations over arrow functions for top-level functions
|
|
16
|
+
- Use explicit return type annotations
|
|
17
|
+
- Maintain established naming conventions
|
|
18
|
+
|
|
19
|
+
### 3. Clarity Enhancement
|
|
20
|
+
- Reducing unnecessary complexity and nesting
|
|
21
|
+
- Eliminating redundant code and abstractions
|
|
22
|
+
- Improving readability through clear variable and function names
|
|
23
|
+
- Consolidating related logic
|
|
24
|
+
- Removing unnecessary comments that describe obvious code
|
|
25
|
+
- IMPORTANT: Avoid nested ternary operators - prefer switch statements or if/else chains for multiple conditions
|
|
26
|
+
- Choose clarity over brevity - explicit code is often better than overly compact code
|
|
27
|
+
|
|
28
|
+
### 4. Balanced Approach
|
|
29
|
+
- Avoid over-simplification that could reduce maintainability
|
|
30
|
+
- Don't create overly clever solutions
|
|
31
|
+
- Don't combine too many concerns into single functions
|
|
32
|
+
|
|
33
|
+
### 5. Scoped Focus
|
|
34
|
+
- Target the recently modified code sections
|
|
35
|
+
- Don't refactor unrelated code unless explicitly requested
|
|
36
|
+
|
|
37
|
+
## Actions:
|
|
38
|
+
1. IDENTIFY opportunities for simplification in recently modified code
|
|
39
|
+
2. APPLY refinements that improve readability without changing behavior
|
|
40
|
+
3. VERIFY all functionality is preserved
|
|
41
|
+
4. EXPLAIN significant changes made
|
|
42
|
+
|
|
43
|
+
You operate autonomously and proactively, refining code immediately after it's written or modified without requiring explicit requests. Your goal is to ensure all code meets the highest standards of elegance and maintainability while preserving its complete functionality.
|
|
44
|
+
`;
|
|
45
|
+
const result = await runClaude({ prompt, continueConversation: true }, config);
|
|
46
|
+
return result.success;
|
|
47
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { logStep } from '../logger.js';
|
|
2
|
+
import { runClaude } from '../claude.js';
|
|
3
|
+
export async function stepSolidCleanCode(config) {
|
|
4
|
+
logStep('5/8', 'SOLID PRINCIPLES & CLEAN CODE');
|
|
5
|
+
const prompt = `Review the code for SOLID principles and Clean Code compliance:
|
|
6
|
+
|
|
7
|
+
## SOLID Principles:
|
|
8
|
+
|
|
9
|
+
### S - Single Responsibility
|
|
10
|
+
- Each class/function has ONE reason to change
|
|
11
|
+
- Split classes doing multiple things
|
|
12
|
+
|
|
13
|
+
### O - Open/Closed
|
|
14
|
+
- Open for extension, closed for modification
|
|
15
|
+
- Replace switch/if-else chains with polymorphism
|
|
16
|
+
|
|
17
|
+
### L - Liskov Substitution
|
|
18
|
+
- Subtypes must be substitutable for base types
|
|
19
|
+
- Prefer composition over inheritance when appropriate
|
|
20
|
+
|
|
21
|
+
### I - Interface Segregation
|
|
22
|
+
- Small, focused interfaces
|
|
23
|
+
- Clients should not depend on unused methods
|
|
24
|
+
|
|
25
|
+
### D - Dependency Inversion
|
|
26
|
+
- Depend on abstractions, not concretions
|
|
27
|
+
- Inject dependencies, dont hard-code them
|
|
28
|
+
|
|
29
|
+
## Clean Code Checklist:
|
|
30
|
+
|
|
31
|
+
**Naming:**
|
|
32
|
+
- [ ] Intention-revealing names
|
|
33
|
+
- [ ] Pronounceable, searchable names
|
|
34
|
+
- [ ] Class=nouns, Methods=verbs, Booleans=questions
|
|
35
|
+
|
|
36
|
+
**Functions:**
|
|
37
|
+
- [ ] Small (<20 lines), single purpose
|
|
38
|
+
- [ ] Max 3 params (wrap in object if more)
|
|
39
|
+
- [ ] No side effects, no null returns
|
|
40
|
+
|
|
41
|
+
**Code Smells to Fix:**
|
|
42
|
+
- [ ] Magic numbers → named constants
|
|
43
|
+
- [ ] Deep nesting → early returns/extraction
|
|
44
|
+
- [ ] Duplicate code → DRY refactor
|
|
45
|
+
- [ ] Long methods → extract smaller functions
|
|
46
|
+
|
|
47
|
+
## Actions:
|
|
48
|
+
1. IDENTIFY all violations
|
|
49
|
+
2. REFACTOR to fix each issue
|
|
50
|
+
3. EXPLAIN your changes`;
|
|
51
|
+
const result = await runClaude({ prompt, continueConversation: true }, config);
|
|
52
|
+
return result.success;
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { logStep } from '../logger.js';
|
|
3
|
+
import { runClaude } from '../claude.js';
|
|
4
|
+
export async function stepTest(config) {
|
|
5
|
+
logStep('6/8', 'TESTING');
|
|
6
|
+
if (config.skipTests) {
|
|
7
|
+
console.log(chalk.yellow('[SKIPPED]') + ' Testing step skipped via configuration');
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
const prompt = `Testing phase:
|
|
11
|
+
|
|
12
|
+
1. **Run existing tests** - Fix any failures
|
|
13
|
+
2. **Add new tests** for changed code:
|
|
14
|
+
- Happy path tests
|
|
15
|
+
- Edge cases
|
|
16
|
+
- Error conditions
|
|
17
|
+
3. **Verify coverage** - Ensure critical paths are tested
|
|
18
|
+
4. **Final test run** - All tests must pass
|
|
19
|
+
|
|
20
|
+
Report: tests run, passed, failed, new tests added`;
|
|
21
|
+
const result = await runClaude({ prompt, continueConversation: true }, config);
|
|
22
|
+
return result.success;
|
|
23
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Config } from '../config.js';
|
|
2
|
+
export type ChangeType = 'feature' | 'fix' | 'refactor' | 'docs' | 'chore' | 'trivial';
|
|
3
|
+
export interface BranchAnalysis {
|
|
4
|
+
changeType: ChangeType;
|
|
5
|
+
isTrivial: boolean;
|
|
6
|
+
suggestedBranchName: string;
|
|
7
|
+
branchPrefix: string;
|
|
8
|
+
shortDescription: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function analyzeRequirement(requirement: string, config: Config): Promise<BranchAnalysis>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
const MAX_DESCRIPTION_LENGTH = 40;
|
|
3
|
+
function sanitizeDescription(input) {
|
|
4
|
+
return input
|
|
5
|
+
.trim()
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
8
|
+
.replace(/\s+/g, '-')
|
|
9
|
+
.substring(0, MAX_DESCRIPTION_LENGTH);
|
|
10
|
+
}
|
|
11
|
+
const ANALYSIS_PROMPT = `Analyze the following requirement and determine the type of change it represents.
|
|
12
|
+
|
|
13
|
+
Requirement: "{REQUIREMENT}"
|
|
14
|
+
|
|
15
|
+
Respond in EXACTLY this format (no markdown, no extra text):
|
|
16
|
+
CHANGE_TYPE: <one of: feature, fix, refactor, docs, chore, trivial>
|
|
17
|
+
IS_TRIVIAL: <true or false>
|
|
18
|
+
SHORT_DESC: <2-4 word kebab-case description for branch name>
|
|
19
|
+
|
|
20
|
+
Guidelines:
|
|
21
|
+
- feature = new functionality being added
|
|
22
|
+
- fix = bug fix or error correction
|
|
23
|
+
- refactor = code restructuring without changing behavior
|
|
24
|
+
- docs = documentation changes only
|
|
25
|
+
- chore = maintenance tasks (deps, config, build)
|
|
26
|
+
- trivial = tiny changes like typo fixes, single-line changes
|
|
27
|
+
|
|
28
|
+
IS_TRIVIAL should be true only for very minor changes that don't need a branch.
|
|
29
|
+
SHORT_DESC should be suitable for a branch name like "feature/SHORT_DESC"`;
|
|
30
|
+
const DEFAULT_ANALYSIS = {
|
|
31
|
+
changeType: 'feature',
|
|
32
|
+
isTrivial: false,
|
|
33
|
+
suggestedBranchName: 'feature/changes',
|
|
34
|
+
branchPrefix: 'feature',
|
|
35
|
+
shortDescription: 'changes',
|
|
36
|
+
};
|
|
37
|
+
function parseAnalysisResponse(output) {
|
|
38
|
+
const lines = output.split('\n').filter((line) => line.trim());
|
|
39
|
+
let changeType = 'feature';
|
|
40
|
+
let isTrivial = false;
|
|
41
|
+
let shortDescription = 'changes';
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
const changeMatch = line.match(/CHANGE_TYPE:\s*(feature|fix|refactor|docs|chore|trivial)/i);
|
|
44
|
+
if (changeMatch) {
|
|
45
|
+
changeType = changeMatch[1].toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
const trivialMatch = line.match(/IS_TRIVIAL:\s*(true|false)/i);
|
|
48
|
+
if (trivialMatch) {
|
|
49
|
+
isTrivial = trivialMatch[1].toLowerCase() === 'true';
|
|
50
|
+
}
|
|
51
|
+
const descMatch = line.match(/SHORT_DESC:\s*(.+)/i);
|
|
52
|
+
if (descMatch) {
|
|
53
|
+
shortDescription = sanitizeDescription(descMatch[1]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const branchPrefix = changeType === 'trivial' ? 'chore' : changeType;
|
|
57
|
+
return {
|
|
58
|
+
changeType,
|
|
59
|
+
isTrivial,
|
|
60
|
+
suggestedBranchName: `${branchPrefix}/${shortDescription}`,
|
|
61
|
+
branchPrefix,
|
|
62
|
+
shortDescription,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export async function analyzeRequirement(requirement, config) {
|
|
66
|
+
const prompt = ANALYSIS_PROMPT.replace('{REQUIREMENT}', requirement);
|
|
67
|
+
const args = ['--print'];
|
|
68
|
+
if (config.dangerouslySkipPermissions) {
|
|
69
|
+
args.push('--dangerously-skip-permissions');
|
|
70
|
+
}
|
|
71
|
+
args.push(prompt);
|
|
72
|
+
if (config.dryRun) {
|
|
73
|
+
return {
|
|
74
|
+
...DEFAULT_ANALYSIS,
|
|
75
|
+
suggestedBranchName: 'feature/dry-run-changes',
|
|
76
|
+
shortDescription: 'dry-run-changes',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
let output = '';
|
|
81
|
+
const child = spawn('claude', args, {
|
|
82
|
+
cwd: process.cwd(),
|
|
83
|
+
env: process.env,
|
|
84
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
85
|
+
});
|
|
86
|
+
if (child.stdout) {
|
|
87
|
+
child.stdout.on('data', (data) => {
|
|
88
|
+
output += data.toString();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
child.on('close', (exitCode) => {
|
|
92
|
+
if (exitCode === 0) {
|
|
93
|
+
const analysis = parseAnalysisResponse(output);
|
|
94
|
+
if (analysis) {
|
|
95
|
+
resolve(analysis);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
resolve(DEFAULT_ANALYSIS);
|
|
100
|
+
});
|
|
101
|
+
child.on('error', () => {
|
|
102
|
+
resolve(DEFAULT_ANALYSIS);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface GitState {
|
|
2
|
+
currentBranch: string;
|
|
3
|
+
isMainBranch: boolean;
|
|
4
|
+
hasUncommittedChanges: boolean;
|
|
5
|
+
remoteBranches: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface BranchConflict {
|
|
8
|
+
branchName: string;
|
|
9
|
+
similarity: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function getCurrentBranch(): string;
|
|
12
|
+
export declare function isMainBranch(branchName: string): boolean;
|
|
13
|
+
export declare function hasUncommittedChanges(): boolean;
|
|
14
|
+
export declare function getRemoteBranches(): string[];
|
|
15
|
+
export declare function getGitState(): GitState;
|
|
16
|
+
export declare function branchExists(branchName: string): boolean;
|
|
17
|
+
export declare function createBranch(branchName: string): boolean;
|
|
18
|
+
export declare function findSimilarBranches(keyword: string, remoteBranches: string[]): BranchConflict[];
|
|
19
|
+
export declare function generateBranchName(prefix: string, description: string): string;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
const MAX_BRANCH_NAME_LENGTH = 40;
|
|
3
|
+
const MAIN_BRANCH_NAMES = ['main', 'master', 'develop', 'dev'];
|
|
4
|
+
function execGit(command) {
|
|
5
|
+
try {
|
|
6
|
+
return execSync(`git ${command}`, { encoding: 'utf-8' }).trim();
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function getCurrentBranch() {
|
|
13
|
+
return execGit('symbolic-ref --short HEAD') || execGit('rev-parse --short HEAD');
|
|
14
|
+
}
|
|
15
|
+
export function isMainBranch(branchName) {
|
|
16
|
+
return MAIN_BRANCH_NAMES.includes(branchName.toLowerCase());
|
|
17
|
+
}
|
|
18
|
+
export function hasUncommittedChanges() {
|
|
19
|
+
const status = execGit('status --porcelain');
|
|
20
|
+
return status.length > 0;
|
|
21
|
+
}
|
|
22
|
+
export function getRemoteBranches() {
|
|
23
|
+
const output = execGit('branch -r');
|
|
24
|
+
if (!output)
|
|
25
|
+
return [];
|
|
26
|
+
return output
|
|
27
|
+
.split('\n')
|
|
28
|
+
.map((b) => b.trim().replace(/^origin\//, ''))
|
|
29
|
+
.filter((b) => b && !b.includes('HEAD'));
|
|
30
|
+
}
|
|
31
|
+
export function getGitState() {
|
|
32
|
+
const currentBranch = getCurrentBranch();
|
|
33
|
+
return {
|
|
34
|
+
currentBranch,
|
|
35
|
+
isMainBranch: isMainBranch(currentBranch),
|
|
36
|
+
hasUncommittedChanges: hasUncommittedChanges(),
|
|
37
|
+
remoteBranches: getRemoteBranches(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function branchExists(branchName) {
|
|
41
|
+
const localBranches = execGit('branch --list');
|
|
42
|
+
const remoteBranches = getRemoteBranches();
|
|
43
|
+
const localList = localBranches.split('\n').map((b) => b.trim().replace(/^\*\s*/, ''));
|
|
44
|
+
return localList.includes(branchName) || remoteBranches.includes(branchName);
|
|
45
|
+
}
|
|
46
|
+
export function createBranch(branchName) {
|
|
47
|
+
// Validate branch name to prevent command injection
|
|
48
|
+
if (!/^[a-zA-Z0-9/_-]+$/.test(branchName)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
execSync(`git checkout -b ${branchName}`, { encoding: 'utf-8' });
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function findSimilarBranches(keyword, remoteBranches) {
|
|
60
|
+
const normalizedKeyword = keyword.toLowerCase();
|
|
61
|
+
if (!normalizedKeyword) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const conflicts = [];
|
|
65
|
+
for (const branch of remoteBranches) {
|
|
66
|
+
const normalizedBranch = branch.toLowerCase();
|
|
67
|
+
const branchSuffix = normalizedBranch.split('/').pop() || '';
|
|
68
|
+
if (normalizedBranch.includes(normalizedKeyword) || (branchSuffix && normalizedKeyword.includes(branchSuffix))) {
|
|
69
|
+
conflicts.push({
|
|
70
|
+
branchName: branch,
|
|
71
|
+
similarity: 'keyword match',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return conflicts;
|
|
76
|
+
}
|
|
77
|
+
export function generateBranchName(prefix, description) {
|
|
78
|
+
const kebab = description
|
|
79
|
+
.toLowerCase()
|
|
80
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
81
|
+
.replace(/\s+/g, '-')
|
|
82
|
+
.replace(/-+/g, '-')
|
|
83
|
+
.replace(/^-|-$/g, '')
|
|
84
|
+
.substring(0, MAX_BRANCH_NAME_LENGTH);
|
|
85
|
+
return `${prefix}/${kebab}`;
|
|
86
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "software-engineer",
|
|
3
|
+
"version": "0.1.10",
|
|
4
|
+
"description": "Software Factory Pipeline - Automate development workflow with Claude AI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sf": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"prepublishOnly": "npm run build",
|
|
13
|
+
"start": "node dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"claude",
|
|
17
|
+
"ai",
|
|
18
|
+
"pipeline",
|
|
19
|
+
"automation",
|
|
20
|
+
"development",
|
|
21
|
+
"code-review"
|
|
22
|
+
],
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"chalk": "^5.3.0",
|
|
27
|
+
"commander": "^12.1.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^20.11.0",
|
|
31
|
+
"typescript": "^5.3.3"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18.0.0"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"package.json"
|
|
39
|
+
]
|
|
40
|
+
}
|