Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import (
"github.com/zxh326/kite/pkg/handlers"
"github.com/zxh326/kite/pkg/middleware"
"github.com/zxh326/kite/pkg/model"
"github.com/zxh326/kite/pkg/plugin"
"github.com/zxh326/kite/pkg/rbac"
"k8s.io/klog/v2"
)

func initializeApp() (*cluster.ClusterManager, error) {
func initializeApp() (*cluster.ClusterManager, *plugin.PluginManager, error) {
common.LoadEnvs()
if klog.V(1).Enabled() {
gin.SetMode(gin.DebugMode)
Expand All @@ -30,10 +31,20 @@ func initializeApp() (*cluster.ClusterManager, error) {
handlers.InitTemplates()
internal.LoadConfigFromEnv()

return cluster.NewClusterManager()
cm, err := cluster.NewClusterManager()
if err != nil {
return nil, nil, err
}

pm := plugin.NewPluginManager(common.PluginDir)
if err := pm.LoadPlugins(); err != nil {
klog.Warningf("Failed to load plugins: %v", err)
}

return cm, pm, nil
}

func buildEngine(cm *cluster.ClusterManager) *gin.Engine {
func buildEngine(cm *cluster.ClusterManager, pm *plugin.PluginManager) *gin.Engine {
r := gin.New()
r.Use(middleware.Metrics())
if !common.DisableGZIP {
Expand All @@ -43,9 +54,10 @@ func buildEngine(cm *cluster.ClusterManager) *gin.Engine {
r.Use(gin.Recovery())
r.Use(middleware.Logger())
r.Use(middleware.DevCORS(common.CORSAllowedOrigins))
r.Use(pm.PluginMiddleware())

base := r.Group(common.Base)
setupAPIRouter(base, cm)
setupAPIRouter(base, cm, pm)
setupStatic(r)

return r
Expand Down
60 changes: 60 additions & 0 deletions cmd/kite-plugin/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
"fmt"
"os"
"os/exec"
"path/filepath"
)

// runBuild compiles the plugin Go binary and, if a frontend/ directory
// exists, builds the frontend bundle too.
func runBuild() error {
// Check we're in a plugin directory (has manifest.yaml)
if _, err := os.Stat("manifest.yaml"); err != nil {
return fmt.Errorf("manifest.yaml not found — are you in a plugin directory?")
}

fmt.Println("→ Building plugin binary...")

// Determine plugin name from current directory
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("get working directory: %w", err)
}
pluginName := filepath.Base(wd)

// Build Go binary
build := exec.Command("go", "build", "-o", pluginName, ".")
build.Stdout = os.Stdout
build.Stderr = os.Stderr
if err := build.Run(); err != nil {
return fmt.Errorf("go build failed: %w", err)
}
fmt.Printf(" ✓ Built binary: ./%s\n", pluginName)

// Build frontend if present
if info, err := os.Stat("frontend"); err == nil && info.IsDir() {
fmt.Println("→ Building frontend...")

install := exec.Command("pnpm", "install")
install.Dir = "frontend"
install.Stdout = os.Stdout
install.Stderr = os.Stderr
if err := install.Run(); err != nil {
return fmt.Errorf("pnpm install failed: %w", err)
}

bundle := exec.Command("pnpm", "build")
bundle.Dir = "frontend"
bundle.Stdout = os.Stdout
bundle.Stderr = os.Stderr
if err := bundle.Run(); err != nil {
return fmt.Errorf("frontend build failed: %w", err)
}
fmt.Println(" ✓ Frontend built: frontend/dist/")
}

fmt.Println("\n✓ Plugin build complete")
return nil
}
119 changes: 119 additions & 0 deletions cmd/kite-plugin/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package main

import (
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
)

func runInit(args []string) error {
if len(args) < 1 {
return fmt.Errorf("usage: kite-plugin init <name> [--with-frontend]")
}

name := args[0]
if name == "" || strings.ContainsAny(name, " /\\") {
return fmt.Errorf("invalid plugin name: %q (no spaces or slashes)", name)
}

withFrontend := false
for _, a := range args[1:] {
if a == "--with-frontend" {
withFrontend = true
}
}

if _, err := os.Stat(name); err == nil {
return fmt.Errorf("directory %q already exists", name)
}

if err := os.MkdirAll(name, 0o755); err != nil {
return fmt.Errorf("create directory: %w", err)
}

data := scaffoldData{
Name: name,
NameTitle: toTitle(name),
WithFrontend: withFrontend,
}

// Backend files
files := []scaffoldFile{
{Path: "main.go", Tmpl: mainGoTmpl},
{Path: "manifest.yaml", Tmpl: manifestYamlTmpl},
{Path: "go.mod", Tmpl: goModTmpl},
{Path: "Makefile", Tmpl: makefileTmpl},
{Path: "README.md", Tmpl: readmeTmpl},
}

// Frontend files
if withFrontend {
files = append(files,
scaffoldFile{Path: "frontend/package.json", Tmpl: frontendPackageJsonTmpl},
scaffoldFile{Path: "frontend/vite.config.ts", Tmpl: frontendViteConfigTmpl},
scaffoldFile{Path: "frontend/tsconfig.json", Tmpl: frontendTsconfigTmpl},
scaffoldFile{Path: "frontend/src/PluginPage.tsx", Tmpl: frontendPluginPageTmpl},
scaffoldFile{Path: "frontend/src/Settings.tsx", Tmpl: frontendSettingsTmpl},
)
}

for _, f := range files {
if err := writeTemplate(filepath.Join(name, f.Path), f.Tmpl, data); err != nil {
return fmt.Errorf("write %s: %w", f.Path, err)
}
}

fmt.Printf("✓ Plugin %q created successfully\n", name)
fmt.Printf("\nNext steps:\n")
fmt.Printf(" cd %s\n", name)
fmt.Printf(" go mod tidy\n")
if withFrontend {
fmt.Printf(" cd frontend && pnpm install && cd ..\n")
}
fmt.Printf(" kite-plugin build\n")

return nil
}

type scaffoldData struct {
Name string
NameTitle string
WithFrontend bool
}

type scaffoldFile struct {
Path string
Tmpl string
}

func writeTemplate(path, tmplStr string, data scaffoldData) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}

t, err := template.New("").Parse(tmplStr)
if err != nil {
return fmt.Errorf("parse template: %w", err)
}

f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()

return t.Execute(f, data)
}

func toTitle(s string) string {
parts := strings.Split(s, "-")
for i, p := range parts {
if len(p) > 0 {
parts[i] = strings.ToUpper(p[:1]) + p[1:]
}
}
return strings.Join(parts, "")
}
68 changes: 68 additions & 0 deletions cmd/kite-plugin/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Command kite-plugin provides developer tools for creating, building,
// validating, and packaging Kite plugins.
package main

import (
"fmt"
"os"
)

const usage = `kite-plugin — Kite Plugin Developer CLI

Usage:
kite-plugin <command> [options]

Commands:
init <name> [--with-frontend] Create a new plugin project
build Build plugin binary (and frontend if present)
validate Validate manifest.yaml and structure
package Package plugin as .tar.gz for distribution

Options:
-h, --help Show this help message

Examples:
kite-plugin init my-plugin --with-frontend
kite-plugin build
kite-plugin validate
kite-plugin package
`

func main() {
if len(os.Args) < 2 {
fmt.Print(usage)
os.Exit(0)
}

cmd := os.Args[1]
args := os.Args[2:]

switch cmd {
case "init":
if err := runInit(args); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
case "build":
if err := runBuild(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
case "validate":
if err := runValidate(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
case "package":
if err := runPackage(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
case "-h", "--help", "help":
fmt.Print(usage)
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n\n", cmd)
fmt.Print(usage)
os.Exit(1)
}
}
Loading