From 2e2b2187b957e9af78797a67ec5c6874615fae02 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sun, 8 Feb 2026 21:35:45 -1000 Subject: Initial project: task model, executor, API server, CLI, storage, reporter Claudomator automation toolkit for Claude Code with: - Task model with YAML parsing, validation, state machine (49 tests, 0 races) - SQLite storage for tasks and executions - Executor pool with bounded concurrency, timeout, cancellation - REST API + WebSocket for mobile PWA integration - Webhook/multi-notifier system - CLI: init, run, serve, list, status commands - Console, JSON, HTML reporters with cost tracking Co-Authored-By: Claude Opus 4.6 --- internal/cli/run.go | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 internal/cli/run.go (limited to 'internal/cli/run.go') diff --git a/internal/cli/run.go b/internal/cli/run.go new file mode 100644 index 0000000..e74b247 --- /dev/null +++ b/internal/cli/run.go @@ -0,0 +1,128 @@ +package cli + +import ( + "context" + "fmt" + "log/slog" + "os" + "os/signal" + "syscall" + + "github.com/claudomator/claudomator/internal/executor" + "github.com/claudomator/claudomator/internal/storage" + "github.com/claudomator/claudomator/internal/task" + "github.com/spf13/cobra" +) + +func newRunCmd() *cobra.Command { + var ( + parallel int + dryRun bool + ) + + cmd := &cobra.Command{ + Use: "run ", + Short: "Run task(s) from a YAML file", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runTasks(args[0], parallel, dryRun) + }, + } + + cmd.Flags().IntVarP(¶llel, "parallel", "p", 3, "max concurrent executions") + cmd.Flags().BoolVar(&dryRun, "dry-run", false, "validate without executing") + + return cmd +} + +func runTasks(file string, parallel int, dryRun bool) error { + tasks, err := task.ParseFile(file) + if err != nil { + return fmt.Errorf("parsing: %w", err) + } + + // Validate all tasks. + for i := range tasks { + if err := task.Validate(&tasks[i]); err != nil { + return fmt.Errorf("task %q: %w", tasks[i].Name, err) + } + } + + if dryRun { + fmt.Printf("Validated %d task(s) successfully.\n", len(tasks)) + for _, t := range tasks { + fmt.Printf(" - %s (model: %s, timeout: %v)\n", t.Name, t.Claude.Model, t.Timeout.Duration) + } + return nil + } + + // Setup infrastructure. + if err := cfg.EnsureDirs(); err != nil { + return fmt.Errorf("creating dirs: %w", err) + } + + store, err := storage.Open(cfg.DBPath) + if err != nil { + return fmt.Errorf("opening db: %w", err) + } + defer store.Close() + + level := slog.LevelInfo + if verbose { + level = slog.LevelDebug + } + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level})) + + runner := &executor.ClaudeRunner{ + BinaryPath: cfg.ClaudeBinaryPath, + Logger: logger, + LogDir: cfg.LogDir, + } + pool := executor.NewPool(parallel, runner, store, logger) + + // Handle graceful shutdown. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) + go func() { + <-sigCh + fmt.Fprintln(os.Stderr, "\nShutting down...") + cancel() + }() + + // Submit all tasks. + fmt.Printf("Dispatching %d task(s) (max concurrency: %d)...\n", len(tasks), parallel) + for i := range tasks { + if err := store.CreateTask(&tasks[i]); err != nil { + return fmt.Errorf("storing task: %w", err) + } + if err := store.UpdateTaskState(tasks[i].ID, task.StateQueued); err != nil { + return fmt.Errorf("queuing task: %w", err) + } + tasks[i].State = task.StateQueued + if err := pool.Submit(ctx, &tasks[i]); err != nil { + logger.Warn("could not submit task", "name", tasks[i].Name, "error", err) + } + } + + // Wait for all results. + completed, failed := 0, 0 + for i := 0; i < len(tasks); i++ { + result := <-pool.Results() + if result.Err != nil { + failed++ + fmt.Printf(" FAIL %s: %v\n", result.TaskID, result.Err) + } else { + completed++ + fmt.Printf(" OK %s (cost: $%.4f)\n", result.TaskID, result.Execution.CostUSD) + } + } + + fmt.Printf("\nDone: %d completed, %d failed\n", completed, failed) + if failed > 0 { + return fmt.Errorf("%d task(s) failed", failed) + } + return nil +} -- cgit v1.2.3