Skip to content

πŸ—οΈ Refactor architecture to improve maintainabilityΒ #67

@ryota-murakami

Description

@ryota-murakami

Problem

The current architecture has all logic in a single 389-line index.js file with multiple responsibilities, making it difficult to maintain and test.

Current issues:

  • Single file handles: CLI routing, OpenAI API, config I/O, git operations, user prompts
  • Global mutable state (openai, model, language, apiKey)
  • gitExtension() function is 202 lines (lines 184-386)
  • gptCommit() mixes business logic with I/O operations
  • Hard to test components in isolation

Current Structure

index.js (389 lines)
  β”œβ”€ Global variables (state)
  β”œβ”€ saveConfig() / loadConfig()
  β”œβ”€ maskApiKey()
  β”œβ”€ getGitSummary()
  β”œβ”€ gptCommit()
  └─ gitExtension() (CLI setup)
utils/
  └─ sanitizeCommitMessage.js

Proposed Modular Structure

src/
  β”œβ”€ index.js                    # Entry point (~50 lines)
  β”œβ”€ cli/
  β”‚   β”œβ”€ commands/
  β”‚   β”‚   β”œβ”€ commit.js          # Commit command handler
  β”‚   β”‚   β”œβ”€ model.js           # Model selection command
  β”‚   β”‚   β”œβ”€ language.js        # Language selection command
  β”‚   β”‚   β”œβ”€ prefix.js          # Prefix toggle command
  β”‚   β”‚   β”œβ”€ apiKey.js          # API key management command
  β”‚   β”‚   └─ config.js          # Config display command
  β”‚   └─ index.js               # CLI setup with Commander
  β”œβ”€ services/
  β”‚   β”œβ”€ OpenAIService.js       # OpenAI API interactions
  β”‚   β”œβ”€ GitService.js          # Git operations
  β”‚   └─ ConfigService.js       # Configuration management
  β”œβ”€ prompts/
  β”‚   β”œβ”€ modelSelection.js      # Model selection prompt
  β”‚   β”œβ”€ languageSelection.js   # Language selection prompt
  β”‚   β”œβ”€ confirmCommit.js       # Commit confirmation prompt
  β”‚   └─ apiKeyPrompts.js       # API key prompts
  └─ utils/
      β”œβ”€ sanitizeCommitMessage.js
      └─ validators.js          # Input validation utilities

Example Refactoring

Before (index.js - mixed concerns):

const gptCommit = async () => {
  const gitSummary = await getGitSummary()
  if (!gitSummary) {
    console.log('No changes to commit. Commit canceled.')
    process.exit(0)
  }

  const messages = [ /* ... */ ]
  const parameters = { /* ... */ }
  const response = await openai.chat.completions.create(parameters)
  const message = response.choices[0].message.content.trim()
  const sanitizedMessage = sanitizeCommitMessage(message)

  const confirm = await prompts({ /* ... */ })
  if (confirm.value) {
    execSync(`git commit -m "${sanitizedMessage}"`)
    console.log('Committed with the suggested message.')
  } else {
    console.log('Commit canceled.')
  }
}

After (separation of concerns):

// src/services/OpenAIService.js
export class OpenAIService {
  constructor(apiKey, model, language) {
    this.openai = new OpenAI({ apiKey })
    this.model = model
    this.language = language
  }

  async generateCommitMessage(gitDiff, usePrefix) {
    const messages = this.buildMessages(gitDiff, usePrefix)
    const response = await this.callWithRetry({ 
      model: this.model, 
      messages 
    })
    return response.choices[0].message.content.trim()
  }

  buildMessages(gitDiff, usePrefix) { /* ... */ }
  async callWithRetry(params, retries = 3) { /* ... */ }
}

// src/services/GitService.js
export class GitService {
  async getStagedDiff() {
    const { stdout } = await exec(
      "git diff --cached -- . ':(exclude)*lock.json' ':(exclude)*lock.yaml'"
    )
    return stdout.trim() || null
  }

  async commit(message) {
    execSync(`git commit -m "${message}"`)
  }
}

// src/cli/commands/commit.js
export async function commitCommand(config) {
  const gitService = new GitService()
  const openaiService = new OpenAIService(
    config.apiKey, 
    config.model, 
    config.language
  )

  const gitDiff = await gitService.getStagedDiff()
  if (!gitDiff) {
    console.log('No changes to commit.')
    return
  }

  const message = await openaiService.generateCommitMessage(
    gitDiff, 
    config.prefixEnabled
  )
  const sanitized = sanitizeCommitMessage(message)

  if (await confirmCommitPrompt(sanitized)) {
    await gitService.commit(sanitized)
    console.log('βœ… Committed successfully.')
  } else {
    console.log('Commit canceled.')
  }
}

Benefits

  • βœ… Single Responsibility: Each module has one clear purpose
  • βœ… Testability: Easy to test components in isolation
  • βœ… Maintainability: Easier to locate and modify functionality
  • βœ… Scalability: Simple to add new commands and features
  • βœ… Reusability: Services can be reused across commands
  • βœ… State Management: Eliminate global mutable state

Implementation Plan

Phase 1: Extract Services (Week 1)

  • Create src/services/OpenAIService.js
  • Create src/services/GitService.js
  • Create src/services/ConfigService.js
  • Add tests for each service
  • Update index.js to use services

Phase 2: Extract Commands (Week 2)

  • Create src/cli/commands/ directory
  • Extract each command to separate file
  • Add tests for command handlers
  • Update CLI setup to use command files

Phase 3: Extract Prompts (Week 3)

  • Create src/prompts/ directory
  • Extract prompt logic from commands
  • Add tests for prompts
  • Update commands to use prompt modules

Phase 4: Cleanup (Week 4)

  • Remove global state from index.js
  • Update all imports and exports
  • Run full test suite
  • Update documentation
  • Verify backward compatibility

Acceptance Criteria

  • index.js reduced to < 100 lines (entry point only)
  • Each service/command file < 200 lines
  • No global mutable state
  • 100% test coverage for new services
  • All existing tests pass
  • No breaking changes to CLI interface

Priority

High - Enables future development and maintenance

Related

Quality analysis report: claudedocs/quality-analysis-report.md section 1

Migration Notes

  • Maintain backward compatibility during refactoring
  • Use feature flags if needed for gradual rollout
  • Document breaking changes clearly
  • Update CLAUDE.md with new architecture

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions