Skip to content

Commit 57f89d6

Browse files
committed
feat: add @init command to scaffold new zig projects
Scaffolds build.zig, build.zig.zon, src/main.zig, and AGENTS.md with current Zig API patterns. Automatically generates the required fingerprint by parsing the zig build error output.
1 parent 19d4de4 commit 57f89d6

File tree

8 files changed

+249
-1
lines changed

8 files changed

+249
-1
lines changed

AGENTS.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Zig Development
44

5-
Always use `zigdoc` to discover APIs for the Zig standard library AND any third-party dependencies (modules). Assume training data is out of date.
5+
Always use `zigdoc` to discover APIs for the Zig standard library and any third-party dependencies.
66

77
Examples:
88
```bash
@@ -14,6 +14,8 @@ zigdoc vaxis.Window
1414

1515
## Common Zig Patterns
1616

17+
These patterns reflect current Zig APIs and may differ from older documentation.
18+
1719
**ArrayList:**
1820
```zig
1921
var list: std.ArrayList(u32) = .empty;
@@ -29,6 +31,18 @@ defer writer.flush();
2931
try writer.print("hello {s}\n", .{"world"});
3032
```
3133

34+
**build.zig executable/test:**
35+
```zig
36+
b.addExecutable(.{
37+
.name = "foo",
38+
.root_module = b.createModule(.{
39+
.root_source_file = b.path("src/main.zig"),
40+
.target = target,
41+
.optimize = optimize,
42+
}),
43+
});
44+
```
45+
3246
## Zig Code Style
3347

3448
**Naming:**

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ Examples:
2828
Options:
2929
-h, --help Show this help message
3030
--dump-imports Dump module imports from build.zig as JSON
31+
32+
Commands:
33+
@init Initialize a new Zig project with AGENTS.md
34+
```
35+
36+
## Project Initialization
37+
38+
`zigdoc @init` scaffolds a minimal Zig project with an `AGENTS.md` file containing up-to-date API patterns and coding conventions for AI assistants.
39+
40+
```bash
41+
mkdir my-project && cd my-project
42+
zigdoc @init
3143
```
3244

3345
## Examples

build_readme.zig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ pub fn main() !void {
3535
\\```
3636
\\{s}```
3737
\\
38+
\\## Project Initialization
39+
\\
40+
\\`zigdoc @init` scaffolds a minimal Zig project with an `AGENTS.md` file containing up-to-date API patterns and coding conventions for AI assistants.
41+
\\
42+
\\```bash
43+
\\mkdir my-project && cd my-project
44+
\\zigdoc @init
45+
\\```
46+
\\
3847
\\## Examples
3948
\\
4049
\\```bash

src/main.zig

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ const log = std.log.scoped(.zigdoc);
66
const build_runner_0_14 = @embedFile("build_runner_0.14.zig");
77
const build_runner_0_15 = @embedFile("build_runner_0.15.zig");
88

9+
const template_build_zig = @embedFile("templates/build.zig.template");
10+
const template_main_zig = @embedFile("templates/main.zig.template");
11+
const template_build_zig_zon = @embedFile("templates/build.zig.zon.template");
12+
const template_agents_md = @embedFile("templates/AGENTS.md.template");
13+
914
pub fn main() !void {
1015
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
1116
const gpa, const is_debug = gpa: {
@@ -42,6 +47,11 @@ pub fn main() !void {
4247
return;
4348
}
4449

50+
if (std.mem.eql(u8, symbol.?, "@init")) {
51+
try initProject(arena.allocator());
52+
return;
53+
}
54+
4555
Walk.init(arena.allocator());
4656
Walk.Decl.init(arena.allocator());
4757

@@ -84,10 +94,71 @@ fn printUsage() !void {
8494
\\ -h, --help Show this help message
8595
\\ --dump-imports Dump module imports from build.zig as JSON
8696
\\
97+
\\Commands:
98+
\\ @init Initialize a new Zig project with AGENTS.md
99+
\\
87100
);
88101
try stdout_writer.interface.flush();
89102
}
90103

104+
fn initProject(allocator: std.mem.Allocator) !void {
105+
const cwd = std.fs.cwd();
106+
107+
// Check if project already exists
108+
if (cwd.access("build.zig", .{})) |_| {
109+
std.debug.print("Error: build.zig already exists\n", .{});
110+
return error.ProjectExists;
111+
} else |_| {}
112+
113+
// Get project name from current directory
114+
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
115+
const cwd_path = try cwd.realpath(".", &path_buf);
116+
const name = std.fs.path.basename(cwd_path);
117+
118+
// Create src directory
119+
try cwd.makeDir("src");
120+
121+
// Write files with substitutions
122+
try cwd.writeFile(.{ .sub_path = "build.zig", .data = try substitute(allocator, template_build_zig, name) });
123+
try cwd.writeFile(.{ .sub_path = "build.zig.zon", .data = try substitute(allocator, template_build_zig_zon, name) });
124+
try cwd.writeFile(.{ .sub_path = "src/main.zig", .data = template_main_zig });
125+
try cwd.writeFile(.{ .sub_path = "AGENTS.md", .data = template_agents_md });
126+
127+
// Run zig build to get suggested fingerprint from error message
128+
const result = std.process.Child.run(.{
129+
.allocator = allocator,
130+
.argv = &.{ "zig", "build" },
131+
}) catch {
132+
std.debug.print("Initialized Zig project '{s}' (run 'zig build' to generate fingerprint)\n", .{name});
133+
return;
134+
};
135+
136+
// Parse fingerprint from error: "suggested value: 0x..."
137+
if (std.mem.indexOf(u8, result.stderr, "suggested value: ")) |start| {
138+
const fp_start = start + "suggested value: ".len;
139+
const fp_end = std.mem.indexOfPos(u8, result.stderr, fp_start, "\n") orelse result.stderr.len;
140+
const fingerprint = result.stderr[fp_start..fp_end];
141+
142+
// Read current build.zig.zon and insert fingerprint
143+
const zon_content = try cwd.readFileAlloc(allocator, "build.zig.zon", 64 * 1024);
144+
const new_zon = try std.mem.replaceOwned(
145+
u8,
146+
allocator,
147+
zon_content,
148+
".version = \"0.0.0\",",
149+
try std.fmt.allocPrint(allocator, ".version = \"0.0.0\",\n .fingerprint = {s},", .{fingerprint}),
150+
);
151+
try cwd.writeFile(.{ .sub_path = "build.zig.zon", .data = new_zon });
152+
}
153+
154+
std.debug.print("Initialized Zig project '{s}'\n", .{name});
155+
}
156+
157+
fn substitute(allocator: std.mem.Allocator, template: []const u8, name: []const u8) ![]const u8 {
158+
const sanitized = try std.mem.replaceOwned(u8, allocator, name, "-", "_");
159+
return std.mem.replaceOwned(u8, allocator, template, "{{name}}", sanitized);
160+
}
161+
91162
fn dumpImports(arena: *std.heap.ArenaAllocator) !void {
92163
// Check if build.zig exists
93164
std.fs.cwd().access("build.zig", .{}) catch {

src/templates/AGENTS.md.template

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# AGENTS.md
2+
3+
## Zig Development
4+
5+
Always use `zigdoc` to discover APIs for the Zig standard library and any third-party dependencies.
6+
7+
Examples:
8+
```bash
9+
zigdoc std.fs
10+
zigdoc std.posix.getuid
11+
zigdoc ghostty-vt.Terminal
12+
zigdoc vaxis.Window
13+
```
14+
15+
## Common Zig Patterns
16+
17+
These patterns reflect current Zig APIs and may differ from older documentation.
18+
19+
**ArrayList:**
20+
```zig
21+
var list: std.ArrayList(u32) = .empty;
22+
defer list.deinit(allocator);
23+
try list.append(allocator, 42);
24+
```
25+
26+
**stdout/stderr Writer:**
27+
```zig
28+
var buf: [4096]u8 = undefined;
29+
const writer = std.fs.File.stdout().writer(&buf);
30+
defer writer.flush();
31+
try writer.print("hello {s}\n", .{"world"});
32+
```
33+
34+
**build.zig executable/test:**
35+
```zig
36+
b.addExecutable(.{
37+
.name = "foo",
38+
.root_module = b.createModule(.{
39+
.root_source_file = b.path("src/main.zig"),
40+
.target = target,
41+
.optimize = optimize,
42+
}),
43+
});
44+
```
45+
46+
## Zig Code Style
47+
48+
**Naming:**
49+
- `camelCase` for functions and methods
50+
- `snake_case` for variables and parameters
51+
- `PascalCase` for types, structs, and enums
52+
- `SCREAMING_SNAKE_CASE` for constants
53+
54+
**Struct initialization:** Prefer explicit type annotation with anonymous literals:
55+
```zig
56+
const foo: Type = .{ .field = value }; // Good
57+
const foo = Type{ .field = value }; // Avoid
58+
```
59+
60+
**File structure:**
61+
1. `//!` doc comment describing the module
62+
2. `const Self = @This();` (for self-referential types)
63+
3. Imports: `std` → `builtin` → project modules
64+
4. `const log = std.log.scoped(.module_name);`
65+
66+
**Functions:** Order methods as `init` → `deinit` → public API → private helpers
67+
68+
**Memory:** Pass allocators explicitly, use `errdefer` for cleanup on error
69+
70+
**Documentation:** Use `///` for public API, `//` for implementation notes. Always explain *why*, not just *what*.
71+
72+
**Tests:** Inline in the same file, register in src/main.zig test block
73+
74+
## Safety Conventions
75+
76+
Inspired by [TigerStyle](https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md).
77+
78+
**Assertions:**
79+
- Add assertions that catch real bugs, not trivially true statements
80+
- Focus on API boundaries and state transitions where invariants matter
81+
- Good: bounds checks, null checks before dereference, state machine transitions
82+
- Avoid: asserting something immediately after setting it, checking internal function arguments
83+
84+
**Function size:**
85+
- Soft limit of 70 lines per function
86+
- Centralize control flow (switch/if) in parent functions
87+
- Push pure computation to helper functions
88+
89+
**Comments:**
90+
- Explain *why* the code exists, not *what* it does
91+
- Document non-obvious thresholds, timing values, protocol details

src/templates/build.zig.template

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
const exe = b.addExecutable(.{
8+
.name = "{{name}}",
9+
.root_module = b.createModule(.{
10+
.root_source_file = b.path("src/main.zig"),
11+
.target = target,
12+
.optimize = optimize,
13+
}),
14+
});
15+
16+
b.installArtifact(exe);
17+
18+
const run_cmd = b.addRunArtifact(exe);
19+
run_cmd.step.dependOn(b.getInstallStep());
20+
if (b.args) |args| {
21+
run_cmd.addArgs(args);
22+
}
23+
24+
const run_step = b.step("run", "Run the application");
25+
run_step.dependOn(&run_cmd.step);
26+
27+
const test_step = b.step("test", "Run unit tests");
28+
const exe_tests = b.addTest(.{
29+
.root_module = b.createModule(.{
30+
.root_source_file = b.path("src/main.zig"),
31+
.target = target,
32+
.optimize = optimize,
33+
}),
34+
});
35+
test_step.dependOn(&b.addRunArtifact(exe_tests).step);
36+
37+
const fmt_check = b.addFmt(.{ .paths = &.{ "src", "build.zig", "build.zig.zon" } });
38+
test_step.dependOn(&fmt_check.step);
39+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.{
2+
.name = .{{name}},
3+
.version = "0.0.0",
4+
.paths = .{
5+
"build.zig",
6+
"build.zig.zon",
7+
"src",
8+
},
9+
}

src/templates/main.zig.template

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const std = @import("std");
2+
3+
pub fn main() void {}

0 commit comments

Comments
 (0)