Public Access
feat: basalt 0.0.1
This commit is contained in:
22 files changed
+1364
-1
No files matched your search
+178
@@ -0,0 +1,178 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.fernandovideira.com/FernandoJVideira/keystone/assets"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
newCmd.AddCommand(apiCmd)
|
||||
}
|
||||
|
||||
var apiCmd = &cobra.Command{
|
||||
Use: "api",
|
||||
Short: "Create a new Keystone API project",
|
||||
Long: `Create a new Keystone API project with the necessary structure and files.`,
|
||||
Run: genProject,
|
||||
}
|
||||
|
||||
func genProject(cmd *cobra.Command, args []string) {
|
||||
|
||||
projectNamePrompt := promptui.Prompt{
|
||||
Label: "Project Name",
|
||||
}
|
||||
|
||||
rawInput, err := projectNamePrompt.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return
|
||||
}
|
||||
moduleName := strings.TrimSpace(rawInput)
|
||||
projectDir := path.Base(strings.TrimSuffix(moduleName, "/"))
|
||||
|
||||
apiType := promptui.Select{
|
||||
Label: "Select API Type",
|
||||
Items: []string{"HTTP (REST)", "gRPC"},
|
||||
}
|
||||
|
||||
_, result, err := apiType.Run()
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Logic to map the selection to a boolean
|
||||
useGRPC := strings.Contains(result, "gRPC")
|
||||
|
||||
fmt.Printf("🚀 Scaffolding '%s' as %s...\n", moduleName, result)
|
||||
if err := scaffoldProject(projectDir, moduleName, useGRPC); err != nil {
|
||||
fmt.Printf("error scaffolding project: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("📦 Installing tools...")
|
||||
if err := installTools(useGRPC); err != nil {
|
||||
fmt.Printf("error installing tools: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("🧹 Tidying up Go modules...")
|
||||
if err := runCommand(projectDir, "go", "mod", "tidy"); err != nil {
|
||||
fmt.Printf("error running 'go mod tidy': %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✅ Done!")
|
||||
}
|
||||
|
||||
func scaffoldProject(projectDir, moduleName string, useGRPC bool) error {
|
||||
|
||||
err := os.MkdirAll(projectDir, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating project directory: %v", err)
|
||||
}
|
||||
|
||||
root := "embedded"
|
||||
err = fs.WalkDir(assets.EmbedFS, root, func(path string, d fs.DirEntry, err error) error {
|
||||
|
||||
relPath, _ := filepath.Rel(root, path)
|
||||
|
||||
if !useGRPC {
|
||||
// If User chose HTTP: Remove gRPC stuff
|
||||
if strings.HasPrefix(relPath, "pb") ||
|
||||
strings.HasPrefix(relPath, "proto") ||
|
||||
strings.Contains(relPath, "handlers/grpc") ||
|
||||
strings.HasPrefix(relPath, "docs/statik") ||
|
||||
strings.HasPrefix(relPath, "docs/swagger") {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// If User chose gRPC: Remove HTTP stuff
|
||||
if strings.Contains(relPath, "handlers/http") {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasSuffix(relPath, "cmd/api/main.go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
contentBytes, err := assets.EmbedFS.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading embedded file %s: %v", path, err)
|
||||
}
|
||||
content := string(contentBytes)
|
||||
|
||||
targetPath := filepath.Join(projectDir, relPath)
|
||||
|
||||
targetPath = strings.TrimSuffix(targetPath, ".tmpl")
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
return fmt.Errorf("error creating directory %s: %v", filepath.Dir(targetPath), err)
|
||||
}
|
||||
|
||||
newContent := strings.ReplaceAll(content, oldModuleName, moduleName)
|
||||
newContent = strings.ReplaceAll(newContent, "{{ .ModuleName }}", moduleName)
|
||||
|
||||
return os.WriteFile(targetPath, []byte(newContent), 0644)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error copying template files: %v\n", err)
|
||||
}
|
||||
|
||||
// Generate main.go from template
|
||||
return genFile(
|
||||
"templates/main.go.tpl",
|
||||
"main",
|
||||
filepath.Join(projectDir, "cmd/api", "main.go"),
|
||||
MainTemplateData{
|
||||
ModuleName: moduleName,
|
||||
UseGRPC: useGRPC,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func installTools(useGrpc bool) error {
|
||||
tools := []string{
|
||||
"github.com/air-verse/air@latest",
|
||||
"github.com/golangci/golangci-lint/cmd/golangci-lint@latest",
|
||||
"go.uber.org/mock/mockgen@latest",
|
||||
"github.com/sqlc-dev/sqlc/cmd/sqlc@latest",
|
||||
"github.com/golang-migrate/migrate/v4/cmd/migrate@latest",
|
||||
}
|
||||
|
||||
if useGrpc {
|
||||
tools = append(tools,
|
||||
"google.golang.org/protobuf/cmd/protoc-gen-go@latest",
|
||||
"google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest",
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest",
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest",
|
||||
)
|
||||
} else {
|
||||
tools = append(tools,
|
||||
"github.com/swaggo/swag/cmd/swag@latest",
|
||||
)
|
||||
}
|
||||
|
||||
for _, tool := range tools {
|
||||
if err := runCommand("", "go", "install", tool); err != nil {
|
||||
return fmt.Errorf("error installing tool %s: %v", tool, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "gitea.fernandovideira.com/FernandoJVideira/keystone/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var handlerTypes = map[string]string{
|
||||
"HTTP (REST)": "http",
|
||||
"gRPC": "grpc",
|
||||
}
|
||||
|
||||
func init() {
|
||||
newCmd.AddCommand(handlerCmd)
|
||||
}
|
||||
|
||||
var handlerCmd = &cobra.Command{
|
||||
Use: "handler",
|
||||
Short: "Create a new handler",
|
||||
Long: `Create a new handler for your Keystone project.`,
|
||||
Run: genHandler,
|
||||
}
|
||||
|
||||
type HandlerTemplateData struct {
|
||||
ModuleName string
|
||||
Name string
|
||||
}
|
||||
|
||||
func genHandler(cmd *cobra.Command, args []string) {
|
||||
namePrompt := promptui.Prompt{
|
||||
Label: "Handler Name",
|
||||
}
|
||||
|
||||
handlerNameInput, err := namePrompt.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return
|
||||
}
|
||||
handlerTypeInput := promptui.Select{
|
||||
Label: "Select API Type",
|
||||
Items: []string{"HTTP (REST)", "gRPC"},
|
||||
}
|
||||
|
||||
_, result, err := handlerTypeInput.Run()
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
moduleName, err := getModuleName()
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error retrieving module name: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
mappedHandlerType := handlerTypes[result]
|
||||
handlerName := strings.ToLower(handlerNameInput)
|
||||
cases := cases.Title(language.English)
|
||||
|
||||
fmt.Printf("🚀 Creating handler '%s' as %s...\n", handlerNameInput, result)
|
||||
|
||||
if err := genFile(
|
||||
fmt.Sprintf("templates/handler_%s.go.tpl", mappedHandlerType),
|
||||
handlerName,
|
||||
filepath.Join("internal/handlers", mappedHandlerType, fmt.Sprintf("%s.go", handlerName)),
|
||||
HandlerTemplateData{
|
||||
ModuleName: moduleName,
|
||||
Name: cases.String(handlerNameInput),
|
||||
},
|
||||
); err != nil {
|
||||
fmt.Printf("❌ Error creating handler: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Handler '%s' created successfully at %s\n", handlerNameInput, filepath.Join("internal/handlers", mappedHandlerType, fmt.Sprintf("%s.go", handlerName)))
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"gitea.fernandovideira.com/FernandoJVideira/keystone/assets"
|
||||
)
|
||||
|
||||
const (
|
||||
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_+"
|
||||
)
|
||||
|
||||
func randomString(length int) string {
|
||||
s, r := make([]rune, length), []rune(chars)
|
||||
|
||||
for i := range s {
|
||||
p, _ := rand.Prime(rand.Reader, len(r))
|
||||
x, y := p.Uint64(), uint64(len(r))
|
||||
s[i] = r[x%y]
|
||||
}
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func genFile(tplPath, tplName, outputPath string, data any) error {
|
||||
tplContent, err := assets.EmbedFS.ReadFile(tplPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading %s template: %w", tplPath, err)
|
||||
}
|
||||
|
||||
tpl, err := template.New(tplName).Parse(string(tplContent))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing %s template: %w", tplPath, err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
||||
return fmt.Errorf("error ensuring directory for %s: %w", outputPath, err)
|
||||
}
|
||||
|
||||
outFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating %s: %w", outputPath, err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
return tpl.Execute(outFile, data)
|
||||
}
|
||||
|
||||
func getModuleName() (string, error) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get working directory: %w", err)
|
||||
}
|
||||
|
||||
for {
|
||||
goModPath := filepath.Join(dir, "go.mod")
|
||||
file, err := os.Open(goModPath)
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if str, ok := strings.CutPrefix(line, "module "); ok {
|
||||
return strings.TrimSpace(str), nil
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return "", fmt.Errorf("failed to read %s: %w", goModPath, err)
|
||||
}
|
||||
return "", fmt.Errorf("module directive not found in %s", goModPath)
|
||||
}
|
||||
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return "", fmt.Errorf("failed to open %s: %w", goModPath, err)
|
||||
}
|
||||
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return "", fmt.Errorf("go.mod not found starting from %s", dir)
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
func runCommand(dir, name string, args ...string) error {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
newCmd.AddCommand(keyCmd)
|
||||
}
|
||||
|
||||
var size int
|
||||
var defaultSize = 32
|
||||
|
||||
var keyCmd = &cobra.Command{
|
||||
Use: "key [size]",
|
||||
Short: "Generate a new cryptographic key",
|
||||
Long: `Generate a new cryptographic key for use in Keystone projects.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var keyLength int
|
||||
if cmd.Flags().Lookup("size").Changed {
|
||||
keyLength = size
|
||||
} else {
|
||||
keyLength = defaultSize
|
||||
}
|
||||
|
||||
if keyLength <= 32 {
|
||||
fmt.Println("Warning: It is recommended to use a key size greater than 32 for better security.")
|
||||
fmt.Println("Generating a key of default size:", defaultSize)
|
||||
keyLength = defaultSize
|
||||
}
|
||||
|
||||
key := randomString(keyLength)
|
||||
println("Generated Key:", key)
|
||||
},
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const oldModuleName = "github.com/FernandoJVideira/simple_bank"
|
||||
const srcDir = "embedded/api_tmpl"
|
||||
|
||||
type MainTemplateData struct {
|
||||
ModuleName string
|
||||
UseGRPC bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(newCmd)
|
||||
}
|
||||
|
||||
var newCmd = &cobra.Command{
|
||||
Use: "new",
|
||||
Short: "Create a new Keystone project",
|
||||
Long: `Create a new Keystone project with the necessary structure and files.`,
|
||||
Run: genProject,
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "keystone",
|
||||
Short: "Keystone is a CLI tool for bootstrapping Go APIs",
|
||||
Long: `Keystone is a CLI tool built by LKResuts for internal usage to bootstrap Go APIs`,
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
func init() {
|
||||
newCmd.AddCommand(serviceCmd)
|
||||
}
|
||||
|
||||
type ServiceTemplateData struct {
|
||||
ModuleName string
|
||||
Name string
|
||||
LowerName string
|
||||
}
|
||||
|
||||
var serviceCmd = &cobra.Command{
|
||||
Use: "service",
|
||||
Short: "Create a new service",
|
||||
Long: `Create a new service for your Keystone project.`,
|
||||
Run: genService,
|
||||
}
|
||||
|
||||
func genService(cmd *cobra.Command, args []string) {
|
||||
namePrompt := promptui.Prompt{
|
||||
Label: "Service Name",
|
||||
}
|
||||
|
||||
serviceNameInput, err := namePrompt.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
moduleName, err := getModuleName()
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error retrieving module name: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
print(moduleName)
|
||||
|
||||
serviceName := strings.ToLower(strings.TrimSpace(serviceNameInput))
|
||||
cases := cases.Title(language.English)
|
||||
|
||||
fmt.Printf("🚀 Creating service '%s'...\n", serviceName)
|
||||
|
||||
if err := genFile(
|
||||
"templates/service.go.tpl",
|
||||
serviceName,
|
||||
fmt.Sprintf("internal/service/%s.go", serviceName),
|
||||
ServiceTemplateData{
|
||||
ModuleName: moduleName,
|
||||
Name: cases.String(serviceName),
|
||||
LowerName: serviceName,
|
||||
},
|
||||
); err != nil {
|
||||
fmt.Printf("❌ Error creating service: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
updateServiceFile(serviceName, cases)
|
||||
|
||||
fmt.Printf("✅ Service '%s' created successfully!\n", serviceNameInput)
|
||||
|
||||
}
|
||||
|
||||
func updateServiceFile(serviceName string, cases cases.Caser) {
|
||||
currDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error getting current directory: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
serviceFilePath := fmt.Sprintf("%s/internal/service/service.go", currDir)
|
||||
|
||||
content, err := os.ReadFile(serviceFilePath)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error reading service file: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fileContent := string(content)
|
||||
srvc := cases.String(serviceName)
|
||||
|
||||
interfaceSnippet := fmt.Sprintf(`// %sService defines operations for %s
|
||||
type %sService interface {
|
||||
// TODO: Add service methods
|
||||
}
|
||||
|
||||
`, srvc, srvc, srvc)
|
||||
|
||||
compileTimeCheck := fmt.Sprintf("\t_ %sService = (*%sService)(nil)",
|
||||
srvc, serviceName)
|
||||
|
||||
fileContent = insertInterface(fileContent, interfaceSnippet)
|
||||
fileContent = strings.Replace(fileContent, "var (", "var (\n"+compileTimeCheck, 1)
|
||||
|
||||
err = os.WriteFile(serviceFilePath, []byte(fileContent), 0644)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error writing to service file: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Service interface for '%s' added successfully!\n", serviceName)
|
||||
}
|
||||
|
||||
func insertInterface(fileContent, interfaceSnippet string) string {
|
||||
parts := strings.SplitN(fileContent, "var (", 2)
|
||||
|
||||
if len(parts) != 2 {
|
||||
return fileContent
|
||||
}
|
||||
|
||||
return parts[0] + interfaceSnippet + "var (" + parts[1]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Default version (we will change this dynamically later!)
|
||||
var version = "dev"
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Display the version of Keystone",
|
||||
Long: `Display the current version of the Keystone CLI tool.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("Keystone CLI version: %s\n", version)
|
||||
},
|
||||
}
|
||||
Reference in new issue
Block a user