mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
170 lines
6.3 KiB
Go
170 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
// ModuleConfig defines the builder catalog metadata that cannot be
|
|
// inferred from the registry (category, OS compatibility, conflicts).
|
|
type ModuleConfig struct {
|
|
Category string `json:"category"`
|
|
CompatibleOS []string `json:"compatible_os"`
|
|
ConflictsWith []string `json:"conflicts_with"`
|
|
SkipVars []string `json:"skip_vars,omitempty"`
|
|
// Namespace is the registry namespace (e.g. "coder" or "coder-labs").
|
|
// When empty, defaults to "coder".
|
|
Namespace string `json:"namespace,omitempty"`
|
|
}
|
|
|
|
// moduleConfigs defines the builder-specific metadata for each module.
|
|
var moduleConfigs = map[string]ModuleConfig{
|
|
"code-server": {Category: "IDE", CompatibleOS: []string{"linux"}, ConflictsWith: []string{"vscode-web"}},
|
|
"jetbrains": {Category: "IDE", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"vscode-desktop": {Category: "IDE", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"vscode-web": {Category: "IDE", CompatibleOS: []string{"linux"}, ConflictsWith: []string{"code-server"}},
|
|
"cursor": {Category: "IDE", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"windsurf": {Category: "IDE", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"zed": {Category: "IDE", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"kiro": {Category: "IDE", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"claude-code": {Category: "AI Agent", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"codex": {Category: "AI Agent", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}, Namespace: "coder-labs"},
|
|
"aider": {Category: "AI Agent", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"amazon-q": {Category: "AI Agent", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"antigravity": {Category: "AI Agent", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"git-clone": {Category: "Source Control", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"git-config": {Category: "Source Control", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"git-commit-signing": {Category: "Source Control", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"dotfiles": {Category: "Utility", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"personalize": {Category: "Utility", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"filebrowser": {Category: "Utility", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"jupyterlab": {Category: "Utility", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
"kasmvnc": {Category: "Utility", CompatibleOS: []string{"linux"}, ConflictsWith: []string{}},
|
|
}
|
|
|
|
func main() {
|
|
outputPath := flag.String("output", "", "Output directory for generated module files (required)")
|
|
baseURL := flag.String("registry-url", registryBaseURL, "Base URL of the Coder registry API")
|
|
flag.Parse()
|
|
|
|
if *outputPath == "" {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
args := flag.Args()
|
|
if len(args) == 0 {
|
|
log.Fatal("specify one or more registry module IDs to generate (e.g. coder/antigravity coder-labs/codex)")
|
|
}
|
|
refs, err := resolveModuleArgs(args)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
var failures int
|
|
|
|
for _, ref := range refs {
|
|
id := ref.slug
|
|
cfg := moduleConfigs[id]
|
|
namespace := ref.namespace
|
|
registryID := fmt.Sprintf("%s/%s", namespace, id)
|
|
log.Printf("Generating %s...", registryID)
|
|
|
|
regMod, err := fetchModule(ctx, *baseURL, registryID)
|
|
if err != nil {
|
|
log.Printf(" ERROR fetching module: %v", err)
|
|
failures++
|
|
continue
|
|
}
|
|
|
|
version, err := fetchLatestVersion(ctx, *baseURL, namespace, id)
|
|
if err != nil {
|
|
log.Printf(" WARNING: could not determine version: %v", err)
|
|
version = "0.0.0"
|
|
}
|
|
|
|
vars := convertVariables(regMod.Variables, cfg.SkipVars)
|
|
|
|
manifest := ModuleManifest{
|
|
ID: id,
|
|
DisplayName: regMod.DisplayName,
|
|
Description: regMod.Description,
|
|
Icon: normalizeIcon(regMod.IconURL),
|
|
Category: cfg.Category,
|
|
Tags: regMod.Tags,
|
|
CompatibleOS: cfg.CompatibleOS,
|
|
ConflictsWith: cfg.ConflictsWith,
|
|
Namespace: namespace,
|
|
PinnedVersion: version,
|
|
Variables: vars,
|
|
}
|
|
|
|
outDir := filepath.Join(*outputPath, id)
|
|
if err := os.MkdirAll(outDir, 0o755); err != nil {
|
|
log.Printf(" ERROR creating directory: %v", err)
|
|
failures++
|
|
continue
|
|
}
|
|
|
|
if err := writeModuleJSON(filepath.Join(outDir, "module.json"), manifest); err != nil {
|
|
log.Printf(" ERROR writing module.json: %v", err)
|
|
failures++
|
|
continue
|
|
}
|
|
|
|
if err := writeTFTmpl(filepath.Join(outDir, id+".tf.tmpl"), manifest); err != nil {
|
|
log.Printf(" ERROR writing .tf.tmpl: %v", err)
|
|
failures++
|
|
continue
|
|
}
|
|
|
|
log.Printf(" OK: %d variables, version %s", len(vars), version)
|
|
}
|
|
|
|
if failures > 0 {
|
|
log.Fatalf("Failed to generate %d module(s)", failures)
|
|
}
|
|
}
|
|
|
|
// moduleRef is a resolved module reference with its registry namespace and slug.
|
|
type moduleRef struct {
|
|
namespace string
|
|
slug string
|
|
}
|
|
|
|
// resolveModuleArgs parses CLI arguments in "namespace/slug" format and
|
|
// validates each against moduleConfigs. Returns an error if any argument
|
|
// is malformed, references an unknown module, or has a namespace that
|
|
// does not match the configured value.
|
|
func resolveModuleArgs(args []string) ([]moduleRef, error) {
|
|
refs := make([]moduleRef, 0, len(args))
|
|
for _, arg := range args {
|
|
parts := strings.SplitN(arg, "/", 2)
|
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
|
return nil, xerrors.Errorf("invalid module ID %q; expected namespace/slug (e.g. coder/antigravity)", arg)
|
|
}
|
|
namespace, slug := parts[0], parts[1]
|
|
cfg, ok := moduleConfigs[slug]
|
|
if !ok {
|
|
return nil, xerrors.Errorf("unknown module %q; add it to moduleConfigs first", slug)
|
|
}
|
|
expectedNS := cfg.Namespace
|
|
if expectedNS == "" {
|
|
expectedNS = "coder"
|
|
}
|
|
if namespace != expectedNS {
|
|
return nil, xerrors.Errorf("module %q namespace mismatch: got %q, moduleConfigs expects %q", slug, namespace, expectedNS)
|
|
}
|
|
refs = append(refs, moduleRef{namespace: namespace, slug: slug})
|
|
}
|
|
return refs, nil
|
|
}
|