Skip to content

Commit 1635801

Browse files
committed
refactor: rewrite opencoder in Zig
Replace the bash script implementation with a native Zig application for better performance, type safety, and cross-platform support. - Add build.zig with test targets and cross-compilation support - Add build.zig.zon package manifest (v0.1.0) - Implement modular architecture: - config.zig: Provider presets and configuration - cli.zig: Argument parsing and help display - logger.zig: Logging with timestamps and file output - fs.zig: Directory structure and file utilities - state.zig: JSON-based state persistence - plan.zig: Plan parsing and task extraction - executor.zig: OpenCode CLI spawning with retries - evaluator.zig: Plan completion evaluation - loop.zig: Main autonomous loop with signal handling - main.zig: Entry point Targets Zig 0.15.2 for compatibility with latest stable release. Signed-off-by: leocavalcante <[email protected]>
1 parent a46b482 commit 1635801

File tree

12 files changed

+2268
-0
lines changed

12 files changed

+2268
-0
lines changed

build.zig

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const std = @import("std");
2+
3+
pub fn build(b: *std.Build) void {
4+
const target = b.standardTargetOptions(.{});
5+
const optimize = b.standardOptimizeOption(.{});
6+
7+
// Main executable
8+
const exe = b.addExecutable(.{
9+
.name = "opencoder",
10+
.root_module = b.createModule(.{
11+
.root_source_file = b.path("src/main.zig"),
12+
.target = target,
13+
.optimize = optimize,
14+
}),
15+
});
16+
17+
b.installArtifact(exe);
18+
19+
// Run command
20+
const run_cmd = b.addRunArtifact(exe);
21+
run_cmd.step.dependOn(b.getInstallStep());
22+
23+
if (b.args) |args| {
24+
run_cmd.addArgs(args);
25+
}
26+
27+
const run_step = b.step("run", "Run the opencoder CLI");
28+
run_step.dependOn(&run_cmd.step);
29+
30+
// Unit tests
31+
const test_targets = [_][]const u8{
32+
"src/config.zig",
33+
"src/cli.zig",
34+
"src/logger.zig",
35+
"src/fs.zig",
36+
"src/state.zig",
37+
"src/plan.zig",
38+
"src/executor.zig",
39+
"src/evaluator.zig",
40+
"src/loop.zig",
41+
};
42+
43+
const test_step = b.step("test", "Run unit tests");
44+
45+
for (test_targets) |test_file| {
46+
const unit_tests = b.addTest(.{
47+
.root_module = b.createModule(.{
48+
.root_source_file = b.path(test_file),
49+
.target = target,
50+
.optimize = optimize,
51+
}),
52+
});
53+
54+
const run_unit_tests = b.addRunArtifact(unit_tests);
55+
test_step.dependOn(&run_unit_tests.step);
56+
}
57+
}

build.zig.zon

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.{
2+
.name = .opencoder,
3+
.version = "0.1.0",
4+
.fingerprint = 0x767bc0e5b4c45b53,
5+
.minimum_zig_version = "0.14.0",
6+
.paths = .{
7+
"src",
8+
"build.zig",
9+
"build.zig.zon",
10+
"README.md",
11+
"LICENSE",
12+
},
13+
}

src/cli.zig

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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

Comments
 (0)