Implements horizontally-scalable worker pool architecture: - claudebox-sidecar: HTTP server for Claude Code, git, and SDLC ops - rdev-worker: standalone worker binary polling rdev-api for tasks - HTTP client adapter for sidecar communication - HPA with custom Prometheus metrics for autoscaling - ServiceMonitor for metrics scraping Code review fixes applied: - URL-encode query parameters in GitStatus (Critical #1) - Remove unused shellQuote function (Critical #2) - Use stdlib strings.Split/TrimSpace (Critical #3) - Add version injection via ldflags (Warning #4) - Add debug logging for swallowed git/sdlc errors (Warning #5, #6) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
101 lines
2.4 KiB
Go
101 lines
2.4 KiB
Go
package claudebox
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
// SDLCRunner executes SDLC CLI commands locally.
|
|
type SDLCRunner struct {
|
|
workDir string
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// SDLCRunnerConfig holds configuration for the SDLC runner.
|
|
type SDLCRunnerConfig struct {
|
|
// WorkDir is the default working directory.
|
|
WorkDir string
|
|
|
|
// Logger is an optional logger for debug output.
|
|
Logger *slog.Logger
|
|
}
|
|
|
|
// NewSDLCRunner creates a new SDLC runner.
|
|
func NewSDLCRunner(cfg SDLCRunnerConfig) *SDLCRunner {
|
|
logger := cfg.Logger
|
|
if logger == nil {
|
|
logger = slog.Default()
|
|
}
|
|
return &SDLCRunner{
|
|
workDir: cfg.WorkDir,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// SDLCResult contains the result of an SDLC command.
|
|
type SDLCResult struct {
|
|
Success bool
|
|
Output string
|
|
Data json.RawMessage // Parsed JSON from sdlc --json output
|
|
Error error
|
|
}
|
|
|
|
// Run executes an SDLC CLI command.
|
|
func (s *SDLCRunner) Run(ctx context.Context, workDir, command string, args []string) *SDLCResult {
|
|
result := &SDLCResult{}
|
|
|
|
// Ensure .sdlc/ is initialized
|
|
if err := s.ensureInit(ctx, workDir); err != nil {
|
|
// Log but continue - command might still work
|
|
s.logger.Debug("sdlc init failed, continuing with command", "error", err, "work_dir", workDir)
|
|
}
|
|
|
|
// Build the command
|
|
sdlcArgs := []string{command}
|
|
sdlcArgs = append(sdlcArgs, args...)
|
|
sdlcArgs = append(sdlcArgs, "--json")
|
|
|
|
cmd := exec.CommandContext(ctx, "sdlc", sdlcArgs...)
|
|
cmd.Dir = workDir
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
result.Error = fmt.Errorf("%s: %s", err, stderr.String())
|
|
result.Output = stdout.String()
|
|
return result
|
|
}
|
|
|
|
result.Success = true
|
|
result.Output = stdout.String()
|
|
|
|
// Try to parse JSON output
|
|
output := strings.TrimSpace(stdout.String())
|
|
if output != "" && (output[0] == '{' || output[0] == '[') {
|
|
result.Data = json.RawMessage(output)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// ensureInit checks if .sdlc/ exists and runs `sdlc init` if it doesn't.
|
|
func (s *SDLCRunner) ensureInit(ctx context.Context, workDir string) error {
|
|
// Check if .sdlc/ directory exists
|
|
cmd := exec.CommandContext(ctx, "test", "-d", workDir+"/.sdlc")
|
|
if cmd.Run() == nil {
|
|
return nil // Already initialized
|
|
}
|
|
|
|
// Run sdlc init
|
|
initCmd := exec.CommandContext(ctx, "sdlc", "init", "--json")
|
|
initCmd.Dir = workDir
|
|
return initCmd.Run()
|
|
}
|