feat: basalt 0.0.1

This commit is contained in:
FernandoJVideira committed 2026-01-27 17:33:14 +00:00
1 parent 487d1d324d
commit 5e31fc2f23
22 files changed
+1364 -1

No files matched your search

+178
View File
@@ -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
}
+7
View File
@@ -0,0 +1,7 @@
package main
import "gitea.fernandovideira.com/FernandoJVideira/keystone/cmd"
func main() {
cmd.Execute()
}
+83
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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]
}
+23
View File
@@ -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)
},
}