|
| 1 | +//! Command-line argument parsing for opencoder. |
| 2 | +//! |
| 3 | +//! Handles parsing of CLI arguments, provider presets, and displays |
| 4 | +//! usage/help information. |
| 5 | + |
| 6 | +const std = @import("std"); |
| 7 | +const config = @import("config.zig"); |
| 8 | +const Allocator = std.mem.Allocator; |
| 9 | + |
| 10 | +/// CLI parsing errors |
| 11 | +pub const ParseError = error{ |
| 12 | + MissingRequiredArgs, |
| 13 | + UnknownOption, |
| 14 | + UnknownProvider, |
| 15 | + InvalidProjectDir, |
| 16 | + MissingOptionValue, |
| 17 | +}; |
| 18 | + |
| 19 | +/// Result of CLI parsing |
| 20 | +pub const ParseResult = union(enum) { |
| 21 | + /// Show help and exit |
| 22 | + help, |
| 23 | + /// Show version and exit |
| 24 | + version, |
| 25 | + /// Run with configuration |
| 26 | + run: config.Config, |
| 27 | +}; |
| 28 | + |
| 29 | +/// Parse command-line arguments |
| 30 | +pub fn parse(allocator: Allocator) ParseError!ParseResult { |
| 31 | + var args = std.process.args(); |
| 32 | + _ = args.skip(); // Skip program name |
| 33 | + |
| 34 | + var cfg = config.Config.loadFromEnv(); |
| 35 | + var planning_model: ?[]const u8 = null; |
| 36 | + var execution_model: ?[]const u8 = null; |
| 37 | + var project_dir: ?[]const u8 = null; |
| 38 | + var user_hint: ?[]const u8 = null; |
| 39 | + |
| 40 | + while (args.next()) |arg| { |
| 41 | + if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) { |
| 42 | + return .help; |
| 43 | + } else if (std.mem.eql(u8, arg, "--version")) { |
| 44 | + return .version; |
| 45 | + } else if (std.mem.eql(u8, arg, "-v") or std.mem.eql(u8, arg, "--verbose")) { |
| 46 | + cfg.verbose = true; |
| 47 | + } else if (std.mem.eql(u8, arg, "--provider")) { |
| 48 | + const provider_str = args.next() orelse return ParseError.MissingOptionValue; |
| 49 | + const provider = config.Provider.fromString(provider_str) orelse return ParseError.UnknownProvider; |
| 50 | + const models = config.getProviderModels(provider); |
| 51 | + planning_model = models.planning; |
| 52 | + execution_model = models.execution; |
| 53 | + } else if (std.mem.eql(u8, arg, "-P") or std.mem.eql(u8, arg, "--planning-model")) { |
| 54 | + planning_model = args.next() orelse return ParseError.MissingOptionValue; |
| 55 | + } else if (std.mem.eql(u8, arg, "-E") or std.mem.eql(u8, arg, "--execution-model")) { |
| 56 | + execution_model = args.next() orelse return ParseError.MissingOptionValue; |
| 57 | + } else if (std.mem.eql(u8, arg, "-p") or std.mem.eql(u8, arg, "--project")) { |
| 58 | + project_dir = args.next() orelse return ParseError.MissingOptionValue; |
| 59 | + } else if (std.mem.startsWith(u8, arg, "-")) { |
| 60 | + return ParseError.UnknownOption; |
| 61 | + } else { |
| 62 | + // Positional argument is the user hint |
| 63 | + user_hint = arg; |
| 64 | + } |
| 65 | + } |
| 66 | + |
| 67 | + // Validate required arguments |
| 68 | + if (planning_model == null or execution_model == null) { |
| 69 | + return ParseError.MissingRequiredArgs; |
| 70 | + } |
| 71 | + |
| 72 | + // Set project directory (default to current directory) |
| 73 | + const final_project_dir = project_dir orelse |
| 74 | + std.posix.getenv("OPENCODER_PROJECT_DIR") orelse |
| 75 | + "."; |
| 76 | + |
| 77 | + // Validate project directory exists |
| 78 | + std.fs.cwd().access(final_project_dir, .{}) catch { |
| 79 | + return ParseError.InvalidProjectDir; |
| 80 | + }; |
| 81 | + |
| 82 | + // Resolve to absolute path |
| 83 | + const abs_path = std.fs.cwd().realpathAlloc(allocator, final_project_dir) catch { |
| 84 | + return ParseError.InvalidProjectDir; |
| 85 | + }; |
| 86 | + |
| 87 | + cfg.planning_model = planning_model.?; |
| 88 | + cfg.execution_model = execution_model.?; |
| 89 | + cfg.project_dir = abs_path; |
| 90 | + cfg.user_hint = user_hint; |
| 91 | + |
| 92 | + return .{ .run = cfg }; |
| 93 | +} |
| 94 | + |
| 95 | +const usage_text = |
| 96 | + \\opencoder v |
| 97 | +++ config.version ++ |
| 98 | + \\ - Autonomous OpenCode Runner |
| 99 | + \\ |
| 100 | + \\Usage: |
| 101 | + \\ opencoder --provider PROVIDER [OPTIONS] [HINT] |
| 102 | + \\ opencoder -P MODEL -E MODEL [OPTIONS] [HINT] |
| 103 | + \\ |
| 104 | + \\Required Arguments (choose one): |
| 105 | + \\ --provider PROVIDER Use a provider preset (github, anthropic, openai, opencode) |
| 106 | + \\ -P, --planning-model MODEL Model for planning/evaluation (e.g., anthropic/claude-sonnet-4) |
| 107 | + \\ -E, --execution-model MODEL Model for task execution (e.g., anthropic/claude-haiku) |
| 108 | + \\ |
| 109 | + \\Optional Arguments: |
| 110 | + \\ -p, --project DIR Project directory (default: $OPENCODER_PROJECT_DIR or $PWD) |
| 111 | + \\ -v, --verbose Enable verbose logging |
| 112 | + \\ -h, --help Show this help message |
| 113 | + \\ --version Show version |
| 114 | + \\ HINT Optional instruction/hint for what to build (e.g., "build a REST API") |
| 115 | + \\ |
| 116 | + \\Provider Presets: |
| 117 | + \\ github Planning: claude-opus-4.5, Execution: claude-sonnet-4.5 |
| 118 | + \\ anthropic Planning: claude-sonnet-4, Execution: claude-haiku |
| 119 | + \\ openai Planning: gpt-4, Execution: gpt-4o-mini |
| 120 | + \\ opencode Planning: glm-4.7-free, Execution: minimax-m2.1-free |
| 121 | + \\ |
| 122 | + \\Environment Variables: |
| 123 | + \\ OPENCODER_PROJECT_DIR Default project directory |
| 124 | + \\ OPENCODER_MAX_RETRIES Max retries per operation (default: 3) |
| 125 | + \\ OPENCODER_BACKOFF_BASE Base seconds for exponential backoff (default: 10) |
| 126 | + \\ OPENCODER_LOG_RETENTION Days to keep old logs (default: 30) |
| 127 | + \\ |
| 128 | + \\Examples: |
| 129 | + \\ # Using provider preset (recommended) |
| 130 | + \\ opencoder --provider github |
| 131 | + \\ opencoder --provider github "build a todo app" |
| 132 | + \\ opencoder --provider anthropic "create a REST API" |
| 133 | + \\ |
| 134 | + \\ # Using explicit models |
| 135 | + \\ opencoder -P anthropic/claude-sonnet-4 -E anthropic/claude-haiku |
| 136 | + \\ opencoder -P anthropic/claude-sonnet-4 -E anthropic/claude-haiku "build a todo app" |
| 137 | + \\ |
| 138 | + \\Directory Structure: |
| 139 | + \\ $PROJECT_DIR/.opencoder/ |
| 140 | + \\ ├── state.json # Current execution state |
| 141 | + \\ ├── current_plan.md # Active task plan |
| 142 | + \\ ├── alerts.log # Critical error alerts |
| 143 | + \\ ├── history/ # Archived completed plans |
| 144 | + \\ └── logs/ |
| 145 | + \\ ├── main.log # Main rotating log |
| 146 | + \\ └── cycles/ # Per-cycle detailed logs |
| 147 | + \\ |
| 148 | +; |
| 149 | + |
| 150 | +/// Print usage/help information |
| 151 | +pub fn printUsage(file: std.fs.File) void { |
| 152 | + _ = file.write(usage_text) catch {}; |
| 153 | +} |
| 154 | + |
| 155 | +/// Print version information |
| 156 | +pub fn printVersion(file: std.fs.File) void { |
| 157 | + _ = file.write("opencoder " ++ config.version ++ "\n") catch {}; |
| 158 | +} |
| 159 | + |
| 160 | +/// Format error message for CLI errors |
| 161 | +pub fn formatError(err: ParseError, file: std.fs.File) void { |
| 162 | + const msg = switch (err) { |
| 163 | + ParseError.MissingRequiredArgs => "Error: Either --provider or both --planning-model and --execution-model are required\nUse -h or --help for usage information\n", |
| 164 | + ParseError.UnknownOption => "Error: Unknown option\nUse -h or --help for usage information\n", |
| 165 | + ParseError.UnknownProvider => "Error: Unknown provider\nAvailable providers: github, anthropic, openai, opencode\n", |
| 166 | + ParseError.InvalidProjectDir => "Error: Project directory does not exist or is not accessible\n", |
| 167 | + ParseError.MissingOptionValue => "Error: Option requires a value\nUse -h or --help for usage information\n", |
| 168 | + }; |
| 169 | + _ = file.write(msg) catch {}; |
| 170 | +} |
| 171 | + |
| 172 | +// ============================================================================ |
| 173 | +// Tests |
| 174 | +// ============================================================================ |
| 175 | + |
| 176 | +test "usage_text contains expected strings" { |
| 177 | + try std.testing.expect(std.mem.indexOf(u8, usage_text, "opencoder") != null); |
| 178 | + try std.testing.expect(std.mem.indexOf(u8, usage_text, "--provider") != null); |
| 179 | + try std.testing.expect(std.mem.indexOf(u8, usage_text, "--planning-model") != null); |
| 180 | + try std.testing.expect(std.mem.indexOf(u8, usage_text, "--execution-model") != null); |
| 181 | +} |
| 182 | + |
| 183 | +test "usage_text contains version" { |
| 184 | + try std.testing.expect(std.mem.indexOf(u8, usage_text, config.version) != null); |
| 185 | +} |
0 commit comments