Skip to content

Commit c2001ae

Browse files
committed
feat(scripts/modulegen): add generator for template builder module catalog
Reusable Go script that reads the Coder registry repo and generates module.json and .tf.tmpl files for the template builder catalog. Parses variable declarations from main.tf, frontmatter from README.md, and pinned versions from git tags. Usage: go run ./scripts/modulegen/ -registry /path/to/registry -output coderd/templatebuilder/modules Handles escaped quotes and heredoc descriptions, skips variables with heredoc defaults (cannot be represented in the builder UI), uses numeric semver sorting for version tags, and exits non-zero on failures.
1 parent 8680465 commit c2001ae

4 files changed

Lines changed: 561 additions & 0 deletions

File tree

scripts/modulegen/main.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"log"
6+
"os"
7+
"path/filepath"
8+
"sort"
9+
)
10+
11+
// ModuleConfig defines the builder catalog metadata that cannot be
12+
// inferred from the registry (category, OS compatibility, conflicts).
13+
type ModuleConfig struct {
14+
Category string `json:"category"`
15+
CompatibleOS []string `json:"compatible_os"`
16+
ConflictsWith []string `json:"conflicts_with"`
17+
SkipVars []string `json:"skip_vars,omitempty"`
18+
}
19+
20+
// moduleConfigs defines the builder-specific metadata for each module.
21+
var moduleConfigs = map[string]ModuleConfig{
22+
"code-server": {Category: "IDE", CompatibleOS: []string{"linux"}, ConflictsWith: []string{"vscode-web"}},
23+
"jetbrains": {Category: "IDE", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
24+
"vscode-desktop": {Category: "IDE", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
25+
"vscode-web": {Category: "IDE", CompatibleOS: []string{"linux"}, ConflictsWith: []string{"code-server"}},
26+
"cursor": {Category: "IDE", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
27+
"windsurf": {Category: "IDE", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
28+
"zed": {Category: "IDE", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
29+
"kiro": {Category: "IDE", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
30+
"claude-code": {Category: "AI Agent", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
31+
"aider": {Category: "AI Agent", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
32+
"goose": {Category: "AI Agent", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
33+
"amazon-q": {Category: "AI Agent", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
34+
"git-clone": {Category: "Source Control", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
35+
"git-config": {Category: "Source Control", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
36+
"git-commit-signing": {Category: "Source Control", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
37+
"dotfiles": {Category: "Utility", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
38+
"personalize": {Category: "Utility", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
39+
"filebrowser": {Category: "Utility", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
40+
"jupyterlab": {Category: "Utility", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
41+
}
42+
43+
func main() {
44+
registryPath := flag.String("registry", "", "Path to the cloned coder/registry repo (required)")
45+
outputPath := flag.String("output", "", "Output directory for generated module files (required)")
46+
flag.Parse()
47+
48+
if *registryPath == "" || *outputPath == "" {
49+
flag.Usage()
50+
os.Exit(1)
51+
}
52+
53+
moduleIDs := sortedKeys(moduleConfigs)
54+
var failures int
55+
56+
for _, id := range moduleIDs {
57+
cfg := moduleConfigs[id]
58+
log.Printf("Generating %s...", id)
59+
60+
moduleSrc := filepath.Join(*registryPath, "registry", "coder", "modules", id)
61+
if _, err := os.Stat(moduleSrc); os.IsNotExist(err) {
62+
log.Printf(" SKIP: registry source not found at %s", moduleSrc)
63+
failures++
64+
continue
65+
}
66+
67+
fm, err := parseFrontmatter(filepath.Join(moduleSrc, "README.md"))
68+
if err != nil {
69+
log.Printf(" ERROR parsing frontmatter: %v", err)
70+
failures++
71+
continue
72+
}
73+
74+
vars, err := parseVariables(filepath.Join(moduleSrc, "main.tf"), cfg.SkipVars)
75+
if err != nil {
76+
log.Printf(" ERROR parsing variables: %v", err)
77+
failures++
78+
continue
79+
}
80+
81+
version, err := latestVersion(*registryPath, id)
82+
if err != nil {
83+
log.Printf(" WARNING: could not determine version: %v", err)
84+
version = "0.0.0"
85+
}
86+
87+
manifest := ModuleManifest{
88+
ID: id,
89+
DisplayName: fm.DisplayName,
90+
Description: fm.Description,
91+
Icon: normalizeIcon(fm.Icon),
92+
Category: cfg.Category,
93+
Tags: fm.Tags,
94+
CompatibleOS: cfg.CompatibleOS,
95+
ConflictsWith: cfg.ConflictsWith,
96+
PinnedVersion: version,
97+
Variables: vars,
98+
}
99+
100+
outDir := filepath.Join(*outputPath, id)
101+
if err := os.MkdirAll(outDir, 0o755); err != nil {
102+
log.Printf(" ERROR creating directory: %v", err)
103+
failures++
104+
continue
105+
}
106+
107+
if err := writeModuleJSON(filepath.Join(outDir, "module.json"), manifest); err != nil {
108+
log.Printf(" ERROR writing module.json: %v", err)
109+
failures++
110+
continue
111+
}
112+
113+
if err := writeTFTmpl(filepath.Join(outDir, id+".tf.tmpl"), manifest); err != nil {
114+
log.Printf(" ERROR writing .tf.tmpl: %v", err)
115+
failures++
116+
continue
117+
}
118+
119+
log.Printf(" OK: %d variables, version %s", len(vars), version)
120+
}
121+
122+
if failures > 0 {
123+
log.Fatalf("Failed to generate %d module(s)", failures)
124+
}
125+
}
126+
127+
func sortedKeys(m map[string]ModuleConfig) []string {
128+
keys := make([]string, 0, len(m))
129+
for k := range m {
130+
keys = append(keys, k)
131+
}
132+
sort.Strings(keys)
133+
return keys
134+
}

0 commit comments

Comments
 (0)