Skip to content
Merged
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
152 changes: 98 additions & 54 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/spf13/cobra"
"stackroost/config"
"stackroost/internal"
"stackroost/internal/logger"
)

var rootCmd = &cobra.Command{
Expand All @@ -23,6 +24,8 @@ var createDomainCmd = &cobra.Command{
Use: "create-domain",
Short: "Create a web server configuration for a domain",
Run: func(cmd *cobra.Command, args []string) {
logger.Info("Starting create-domain command execution")

domain, _ := cmd.Flags().GetString("name")
port, _ := cmd.Flags().GetString("port")
serverType, _ := cmd.Flags().GetString("server")
Expand All @@ -31,14 +34,18 @@ var createDomainCmd = &cobra.Command{
createDir, _ := cmd.Flags().GetBool("useridr")

if internal.IsNilOrEmpty(domain) {
fmt.Println("Error: --name flag is required and cannot be empty")
logger.Error("Domain name is required and cannot be empty")
os.Exit(1)
}

if internal.IsNilOrEmpty(port) {
logger.Info("No port specified, defaulting to 80")
port = "80"
}

username := strings.Split(domain, ".")[0]

logger.Debug(fmt.Sprintf("Extracted username: %s from domain: %s", username, domain))

ext := ".conf"
var configPath string

Expand All @@ -49,142 +56,138 @@ var createDomainCmd = &cobra.Command{
configPath = filepath.Join("/etc/caddy/sites-available", domain+ext)
case "apache":
configPath = filepath.Join("/etc/apache2/sites-available", domain+ext)
default:
fmt.Println("Error: Unsupported server type. Supported types are: apache, nginx, caddy")
default:
logger.Error(fmt.Sprintf("Unsupported server type: %s. Supported types are: apache, nginx, caddy", serverType))
os.Exit(1)
}

if _, err := os.Stat(configPath); err == nil {
fmt.Printf(" Configuration for '%s' already exists at %s\n", domain, configPath)
fmt.Println(" Aborting to prevent overwriting existing configuration.")
logger.Error(fmt.Sprintf("Configuration for '%s' already exists at %s", domain, configPath))
os.Exit(1)
}

fmt.Println(" Starting setup for domain:", domain)
logger.Info(fmt.Sprintf("Initiating setup for domain: %s with server type: %s", domain, serverType))

// Shell user creation
if shellUser {
if internal.IsNilOrEmpty(password) {
fmt.Println(" Error: --pass is required when --shelluser is true")
logger.Error("Password is required when shelluser is enabled")
os.Exit(1)
}

fmt.Println("🔧 Creating system user:", username)
logger.Info(fmt.Sprintf("Creating system user: %s", username))

userAddCmd := fmt.Sprintf("id -u %s || useradd -m -s /bin/bash %s", username, username)
setPassCmd := fmt.Sprintf("echo '%s:%s' | chpasswd", username, password)

if err := internal.RunCommand("sudo", "bash", "-c", userAddCmd); err != nil {
fmt.Printf(" Failed to create user: %v\n", err)
logger.Error(fmt.Sprintf("Failed to create user %s: %v", username, err))
os.Exit(1)
}

if err := internal.RunCommand("sudo", "bash", "-c", setPassCmd); err != nil {
fmt.Printf(" Failed to set password: %v\n", err)
logger.Error(fmt.Sprintf("Failed to set password for user %s: %v", username, err))
os.Exit(1)
}

fmt.Printf(" User '%s' created with shell access\n", username)
logger.Success(fmt.Sprintf("User '%s' created with shell access", username))
}

// Create user directory
if createDir {
fmt.Println(" Creating public_html directory for user...")
logger.Info("Creating public_html directory for user")

publicHtmlPath := fmt.Sprintf("/home/%s/public_html", username)
if err := os.MkdirAll(publicHtmlPath, 0755); err != nil {
fmt.Printf(" Failed to create directory: %v\n", err)
logger.Error(fmt.Sprintf("Failed to create directory %s: %v", publicHtmlPath, err))
os.Exit(1)
}

if err := internal.RunCommand("sudo", "chown", "-R", fmt.Sprintf("%s:%s", username, username), fmt.Sprintf("/home/%s", username)); err != nil {
fmt.Printf(" Failed to assign ownership: %v\n", err)
logger.Error(fmt.Sprintf("Failed to assign ownership for %s: %v", username, err))
os.Exit(1)
}

fmt.Printf(" Directory '%s' created and owned by '%s'\n", publicHtmlPath, username)
logger.Success(fmt.Sprintf("Directory '%s' created and owned by '%s'", publicHtmlPath, username))
}

fmt.Println(" Generating Apache configuration...")
logger.Info(fmt.Sprintf("Generating %s configuration", serverType))

configGen, err := config.NewWebServerConfig(serverType)
if err != nil {
fmt.Printf(" Error: %v\n", err)
logger.Error(fmt.Sprintf("Failed to create config generator: %v", err))
os.Exit(1)
}

// Use extracted username in DocumentRoot
configContent, err := configGen.Generate(domain, port, username)
if err != nil {
fmt.Printf(" Error generating config: %v\n", err)
logger.Error(fmt.Sprintf("Failed to generate config for %s: %v", domain, err))
os.Exit(1)
}

if err := writeConfigFile(domain, configContent, configGen.GetFileExtension()); err != nil {
fmt.Printf(" Error writing config file: %v\n", err)
logger.Error(fmt.Sprintf("Failed to write config file: %v", err))
os.Exit(1)
}

fmt.Println(" Configuration file created.")
logger.Success("Configuration file created")

filename := fmt.Sprintf("%s%s", domain, configGen.GetFileExtension())

switch serverType {
case "apache":
fmt.Println(" Enabling site with a2ensite...")
logger.Info("Enabling site with a2ensite")
if err := internal.RunCommand("sudo", "a2ensite", filename); err != nil {
fmt.Printf(" Failed to enable site: %v\n", err)
logger.Error(fmt.Sprintf("Failed to enable site %s: %v", filename, err))
os.Exit(1)
}

fmt.Println("Reloading Apache server...")
logger.Info("Reloading Apache server")
if err := internal.RunCommand("sudo", "systemctl", "reload", "apache2"); err != nil {
fmt.Printf(" Failed to reload apache: %v\n", err)
logger.Error(fmt.Sprintf("Failed to reload Apache: %v", err))
os.Exit(1)
}

case "nginx":
sitePath := filepath.Join("/etc/nginx/sites-available", filename)
linkPath := filepath.Join("/etc/nginx/sites-enabled", filename)
fmt.Println(" Enabling Nginx site...")
logger.Info("Enabling Nginx site")
if _, err := os.Stat(linkPath); os.IsNotExist(err) {
if err := internal.RunCommand("sudo", "ln", "-s", sitePath, linkPath); err != nil {
fmt.Printf(" Failed to enable nginx site: %v\n", err)
logger.Error(fmt.Sprintf("Failed to enable Nginx site %s: %v", filename, err))
os.Exit(1)
}
}

fmt.Println("Reloading Nginx server...")
logger.Info("Reloading Nginx server")
if err := internal.RunCommand("sudo", "systemctl", "reload", "nginx"); err != nil {
fmt.Printf(" Failed to reload nginx: %v\n", err)
logger.Error(fmt.Sprintf("Failed to reload Nginx: %v", err))
os.Exit(1)
}

case "caddy":
sitePath := filepath.Join("/etc/caddy/sites-available", filename)
linkPath := filepath.Join("/etc/caddy/sites-enabled", filename)
fmt.Println(" Enabling Caddy site...")

logger.Info("Enabling Caddy site")
if _, err := os.Stat(linkPath); os.IsNotExist(err) {
if err := internal.RunCommand("sudo", "ln", "-s", sitePath, linkPath); err != nil {
fmt.Printf(" Failed to enable caddy site: %v\n", err)
logger.Error(fmt.Sprintf("Failed to enable Caddy site %s: %v", filename, err))
os.Exit(1)
}
}
}

fmt.Println("Reloading Caddy server...")
if err := internal.RunCommand("sudo", "systemctl", "reload", "caddy"); err != nil {
fmt.Printf(" Failed to reload caddy: %v\n", err)
os.Exit(1)
logger.Info("Reloading Caddy server")
if err := internal.RunCommand("sudo", "systemctl", "reload", "caddy"); err != nil {
logger.Error(fmt.Sprintf("Failed to reload Caddy: %v", err))
os.Exit(1)
}
}

fmt.Printf("🎉 %s configuration created and enabled for %s on port %s\n", serverType, domain, port)
logger.Success(fmt.Sprintf("%s configuration created and enabled for %s on port %s", serverType, domain, port))
},
}

func init() {
rootCmd.AddCommand(createDomainCmd)
createDomainCmd.Flags().StringP("name", "n", "", "Domain name for the configuration (e.g., mahesh.spark.dev)")
createDomainCmd.Flags().StringP("name", "n", "", "Domain name for the configuration (e.g., example.com)")
createDomainCmd.Flags().Bool("shelluser", false, "Create a shell user for the domain")
createDomainCmd.Flags().String("pass", "", "Password for the shell user")
createDomainCmd.Flags().Bool("useridr", false, "Create user directory /home/<user>/public_html")
Expand All @@ -195,42 +198,83 @@ func init() {

func Execute() {
if err := rootCmd.Execute(); err != nil {
logger.Error(fmt.Sprintf("Command execution failed: %v", err))
fmt.Println("Error:", err)
os.Exit(1)
}
}

func printWelcome() {
fmt.Println("Welcome to StackRoost CLI!")
fmt.Println("Your terminal assistant for managing Linux servers.")
reset := "\033[0m"
bold := "\033[1m"
gray := "\033[38;2;180;180;180m"

title := "WELCOME TO STACKROOST CLI"
subTitle := "Smart Linux Server Manager"

startR, startG, startB := 135, 206, 235
endR, endG, endB := 255, 0, 0

length := len(title)

fmt.Println()
fmt.Print(bold)

for i, ch := range title {
t := float64(i) / float64(length-1)
r := int(float64(startR)*(1-t) + float64(endR)*t)
g := int(float64(startG)*(1-t) + float64(endG)*t)
b := int(float64(startB)*(1-t) + float64(endB)*t)
fmt.Printf("\033[38;2;%d;%d;%dm%c", r, g, b, ch)
}

fmt.Println(reset)
fmt.Println()

fmt.Printf("%s\033[38;2;135;206;235m%s%s\n\n", bold, subTitle, reset)

fmt.Println(gray + "Welcome to StackRoost — your powerful CLI for managing Linux domains," + reset)
fmt.Println(gray + "creating shell users, and configuring Apache · Nginx · Caddy effortlessly." + reset)
fmt.Println()

fmt.Printf("%s\033[38;2;200;200;200mtry: %sstackroost --help%s\n\n", bold, reset, reset)
}

func writeConfigFile(domain, content, extension string) error {
var outputDir string
logger.Info(fmt.Sprintf("Writing configuration file for %s", domain))

var configPath string
if extension == ".conf" {
if strings.HasPrefix(content, "server") {
outputDir = "/etc/nginx/sites-available"
configPath = "/etc/nginx/sites-available"
logger.Debug("Detected nginx configuration")
} else if strings.Contains(content, "php_fastcgi") {
outputDir = "/etc/caddy/sites-available"
configPath = "/etc/caddy/sites-available"
logger.Debug("Detected caddy configuration")
} else {
outputDir = "/etc/apache2/sites-available"
configPath = "/etc/apache2/sites-available"
logger.Debug("Detected apache configuration")
}
}

if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("failed to create output directory: %v", err)
if err := os.MkdirAll(configPath, 0755); err != nil {
logger.Error(fmt.Sprintf("Failed to create output directory %s: %v", configPath, err))
return err
}

filename := fmt.Sprintf("%s%s", domain, extension)
outputPath := filepath.Join(outputDir, filename)
outputPath := filepath.Join(configPath, filename)

if _, err := os.Stat(outputPath); err == nil {
return fmt.Errorf("configuration for '%s' already exists at %s", domain, outputPath)
logger.Error(fmt.Sprintf("Configuration already exists at %s for domain %s", outputPath, domain))
return fmt.Errorf("configuration exists")
}

if err := os.WriteFile(outputPath, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write config file: %v", err)
logger.Error(fmt.Sprintf("Failed to write config file %s: %v", outputPath, err))
return err
}

logger.Success(fmt.Sprintf("Configuration file written to %s", outputPath))
return nil
}
}
4 changes: 0 additions & 4 deletions config/apache/apache.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ package apache

import "fmt"

// ApacheConfig implements the WebServerConfig interface for Apache
type ApacheConfig struct{}

// Generate creates an Apache virtual host configuration
func (a *ApacheConfig) Generate(domain, port, username string) (string, error) {
vhostTemplate := `<VirtualHost *:%s>
ServerName %s
Expand All @@ -23,8 +21,6 @@ func (a *ApacheConfig) Generate(domain, port, username string) (string, error) {
return fmt.Sprintf(vhostTemplate, port, domain, domain, username, domain, domain, username), nil
}


// GetFileExtension returns the file extension for Apache config files
func (a *ApacheConfig) GetFileExtension() string {
return ".conf"
}
1 change: 0 additions & 1 deletion config/caddy/caddy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package caddy

import "fmt"

// CaddyConfig implements WebServerConfig for Caddy
type CaddyConfig struct{}

func (c *CaddyConfig) Generate(domain, port, username string) (string, error) {
Expand Down
2 changes: 0 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@ import (
"stackroost/config/caddy"
)

// WebServerConfig defines the interface for generating web server configurations
type WebServerConfig interface {
Generate(domain, port, username string) (string, error)
GetFileExtension() string
}

// NewWebServerConfig creates a new configuration generator based on the server type
func NewWebServerConfig(serverType string) (WebServerConfig, error) {
switch serverType {
case "apache":
Expand Down
1 change: 0 additions & 1 deletion config/nginx/nginx.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package nginx

import "fmt"

// NginxConfig implements WebServerConfig for Nginx
type NginxConfig struct{}

func (n *NginxConfig) Generate(domain, port, username string) (string, error) {
Expand Down
Loading