feat: add UMS CLI for binary packaging and system initialization
- Add Cobra-based CLI with ums init, ums serve, ums version commands - ums init supports interactive prompts and non-interactive flags - Generates secure JWT secrets and config.yaml automatically - Extract server.Serve() function for reuse - Add cross-platform build targets to Makefile - Update README with CLI installation and usage instructions New files: - cmd/ums/main.go - CLI entry point - cmd/ums/cmd/root.go - Root command - cmd/ums/cmd/init.go - Interactive/non-interactive init - cmd/ums/cmd/serve.go - Server command - cmd/ums/cmd/version.go - Version command - internal/server/server.go - Extracted Serve function
This commit is contained in:
500
cmd/ums/cmd/init.go
Normal file
500
cmd/ums/cmd/init.go
Normal file
@@ -0,0 +1,500 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/user-management-system/internal/config"
|
||||
"github.com/user-management-system/internal/database"
|
||||
)
|
||||
|
||||
// initCmd is the init subcommand
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initialize the User Management System",
|
||||
Long: `Interactively initialize UMS: generate secrets, configure database,
|
||||
bootstrap admin user, and create configuration files.
|
||||
|
||||
This command can run in two modes:
|
||||
1. Interactive mode (no flags): Prompts for all required values
|
||||
2. Non-interactive mode (with flags): Uses provided values directly
|
||||
|
||||
Examples:
|
||||
# Interactive mode
|
||||
ums init
|
||||
|
||||
# Non-interactive mode
|
||||
ums init --admin-user admin --admin-pass MySecret123 --admin-email admin@example.com`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runInit(cmd)
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
initDBType string
|
||||
initDBPath string
|
||||
initRedisEnable bool
|
||||
initRedisHost string
|
||||
initRedisPort int
|
||||
initRedisPassword string
|
||||
adminUser string
|
||||
adminPass string
|
||||
adminEmail string
|
||||
initPort int
|
||||
initCorsOrigin string
|
||||
yesFlag bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
initCmd.Flags().StringVar(&initDBType, "db-type", "sqlite", "database type (sqlite, postgresql, mysql)")
|
||||
initCmd.Flags().StringVar(&initDBPath, "db-path", "./data/user_management.db", "database path or connection string")
|
||||
initCmd.Flags().BoolVar(&initRedisEnable, "redis-enable", false, "enable Redis")
|
||||
initCmd.Flags().StringVar(&initRedisHost, "redis-host", "localhost", "Redis host")
|
||||
initCmd.Flags().IntVar(&initRedisPort, "redis-port", 6379, "Redis port")
|
||||
initCmd.Flags().StringVar(&initRedisPassword, "redis-password", "", "Redis password")
|
||||
initCmd.Flags().StringVar(&adminUser, "admin-user", "", "admin username")
|
||||
initCmd.Flags().StringVar(&adminPass, "admin-pass", "", "admin password")
|
||||
initCmd.Flags().StringVar(&adminEmail, "admin-email", "", "admin email")
|
||||
initCmd.Flags().IntVar(&initPort, "port", 8080, "server port")
|
||||
initCmd.Flags().StringVar(&initCorsOrigin, "cors-origin", "http://localhost:3000", "CORS allowed origin")
|
||||
initCmd.Flags().BoolVar(&yesFlag, "yes", false, "skip confirmation")
|
||||
|
||||
rootCmd.AddCommand(initCmd)
|
||||
}
|
||||
|
||||
type initConfig struct {
|
||||
dbType string
|
||||
dbPath string
|
||||
redisEnable bool
|
||||
redisHost string
|
||||
redisPort int
|
||||
redisPassword string
|
||||
adminUser string
|
||||
adminPass string
|
||||
adminEmail string
|
||||
port int
|
||||
corsOrigin string
|
||||
jwtSecret string
|
||||
bootstrapSecret string
|
||||
}
|
||||
|
||||
func runInit(cmd *cobra.Command) error {
|
||||
cfg, err := gatherInitConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 显示配置摘要
|
||||
fmt.Println("\n=== Configuration Summary ===")
|
||||
fmt.Printf("Database Type: %s\n", cfg.dbType)
|
||||
fmt.Printf("Database Path: %s\n", cfg.dbPath)
|
||||
fmt.Printf("Redis Enabled: %t\n", cfg.redisEnable)
|
||||
if cfg.redisEnable {
|
||||
fmt.Printf("Redis Host: %s:%d\n", cfg.redisHost, cfg.redisPort)
|
||||
}
|
||||
fmt.Printf("Admin User: %s\n", cfg.adminUser)
|
||||
fmt.Printf("Admin Email: %s\n", cfg.adminEmail)
|
||||
fmt.Printf("Server Port: %d\n", cfg.port)
|
||||
fmt.Printf("CORS Origin: %s\n", cfg.corsOrigin)
|
||||
fmt.Println("===========================")
|
||||
|
||||
if !yesFlag {
|
||||
fmt.Print("Proceed with initialization? [y/N]: ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(strings.ToLower(input))
|
||||
if input != "y" && input != "yes" {
|
||||
fmt.Println("Initialization cancelled.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 创建数据目录
|
||||
if err := createDataDirectory(cfg); err != nil {
|
||||
return fmt.Errorf("create data directory failed: %w", err)
|
||||
}
|
||||
|
||||
// 2. 生成密钥
|
||||
cfg.jwtSecret = generateSecret(32)
|
||||
cfg.bootstrapSecret = generateSecret(16)
|
||||
|
||||
// 3. 创建配置文件
|
||||
if err := createConfigFiles(cfg); err != nil {
|
||||
return fmt.Errorf("create config files failed: %w", err)
|
||||
}
|
||||
|
||||
// 4. 执行数据库迁移
|
||||
if err := runMigrations(cfg); err != nil {
|
||||
return fmt.Errorf("run migrations failed: %w", err)
|
||||
}
|
||||
|
||||
// 5. 创建管理员账号
|
||||
if err := createAdminUser(cfg); err != nil {
|
||||
return fmt.Errorf("create admin user failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Initialization Complete ===")
|
||||
fmt.Println("Configuration files created:")
|
||||
fmt.Printf(" - %s\n", getConfigPath())
|
||||
fmt.Printf(" - %s\n", getEnvPath())
|
||||
fmt.Println("\nTo start the server:")
|
||||
fmt.Println(" ums serve")
|
||||
fmt.Println("=============================")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func gatherInitConfig() (*initConfig, error) {
|
||||
isInteractive := adminUser == "" || adminPass == "" || adminEmail == ""
|
||||
|
||||
if isInteractive {
|
||||
return gatherInteractiveConfig()
|
||||
}
|
||||
|
||||
// 非交互式模式:验证必需参数
|
||||
if adminUser == "" {
|
||||
return nil, fmt.Errorf("--admin-user is required")
|
||||
}
|
||||
if adminPass == "" {
|
||||
return nil, fmt.Errorf("--admin-pass is required")
|
||||
}
|
||||
if adminEmail == "" {
|
||||
return nil, fmt.Errorf("--admin-email is required")
|
||||
}
|
||||
|
||||
// 验证密码强度
|
||||
if len(adminPass) < 8 {
|
||||
return nil, fmt.Errorf("password must be at least 8 characters")
|
||||
}
|
||||
|
||||
return &initConfig{
|
||||
dbType: initDBType,
|
||||
dbPath: initDBPath,
|
||||
redisEnable: initRedisEnable,
|
||||
redisHost: initRedisHost,
|
||||
redisPort: initRedisPort,
|
||||
redisPassword: initRedisPassword,
|
||||
adminUser: adminUser,
|
||||
adminPass: adminPass,
|
||||
adminEmail: adminEmail,
|
||||
port: initPort,
|
||||
corsOrigin: initCorsOrigin,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func gatherInteractiveConfig() (*initConfig, error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
cfg := &initConfig{
|
||||
dbType: "sqlite",
|
||||
dbPath: "./data/user_management.db",
|
||||
redisEnable: false,
|
||||
redisHost: "localhost",
|
||||
redisPort: 6379,
|
||||
port: 8080,
|
||||
corsOrigin: "http://localhost:3000",
|
||||
}
|
||||
|
||||
fmt.Println("=== UMS Initialization ===")
|
||||
|
||||
// 数据库类型
|
||||
fmt.Printf("Database type [%s]: ", cfg.dbType)
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input != "" {
|
||||
cfg.dbType = input
|
||||
}
|
||||
|
||||
// 数据库路径
|
||||
fmt.Printf("Database path [%s]: ", cfg.dbPath)
|
||||
input, _ = reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input != "" {
|
||||
cfg.dbPath = input
|
||||
}
|
||||
|
||||
// Redis 启用
|
||||
fmt.Printf("Enable Redis? [y/N]: ")
|
||||
input, _ = reader.ReadString('\n')
|
||||
input = strings.TrimSpace(strings.ToLower(input))
|
||||
cfg.redisEnable = input == "y" || input == "yes"
|
||||
|
||||
if cfg.redisEnable {
|
||||
fmt.Printf("Redis host [%s]: ", cfg.redisHost)
|
||||
input, _ = reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input != "" {
|
||||
cfg.redisHost = input
|
||||
}
|
||||
|
||||
fmt.Printf("Redis port [%d]: ", cfg.redisPort)
|
||||
input, _ = reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input != "" {
|
||||
var port int
|
||||
fmt.Sscanf(input, "%d", &port)
|
||||
cfg.redisPort = port
|
||||
}
|
||||
|
||||
fmt.Printf("Redis password (empty for none): ")
|
||||
input, _ = reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
cfg.redisPassword = input
|
||||
}
|
||||
|
||||
// 管理员信息
|
||||
fmt.Printf("Admin username [admin]: ")
|
||||
input, _ = reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input != "" {
|
||||
cfg.adminUser = input
|
||||
} else {
|
||||
cfg.adminUser = "admin"
|
||||
}
|
||||
|
||||
fmt.Print("Admin password: ")
|
||||
password, err := readPassword()
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read password failed: %w", err)
|
||||
}
|
||||
if len(password) < 8 {
|
||||
return nil, fmt.Errorf("password must be at least 8 characters")
|
||||
}
|
||||
cfg.adminPass = password
|
||||
|
||||
fmt.Printf("Admin email [%s]: ", cfg.adminEmail)
|
||||
input, _ = reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input != "" {
|
||||
cfg.adminEmail = input
|
||||
} else if cfg.adminEmail == "" {
|
||||
cfg.adminEmail = "admin@example.com"
|
||||
}
|
||||
|
||||
// 服务器端口
|
||||
fmt.Printf("Server port [%d]: ", cfg.port)
|
||||
input, _ = reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input != "" {
|
||||
var port int
|
||||
fmt.Sscanf(input, "%d", &port)
|
||||
cfg.port = port
|
||||
}
|
||||
|
||||
// CORS 域名
|
||||
fmt.Printf("CORS origin [%s]: ", cfg.corsOrigin)
|
||||
input, _ = reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input != "" {
|
||||
cfg.corsOrigin = input
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func readPassword() (string, error) {
|
||||
intrFd := int(os.Stdin.Fd())
|
||||
if term.IsTerminal(intrFd) {
|
||||
password, err := term.ReadPassword(intrFd)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(password), nil
|
||||
}
|
||||
// 非终端模式,使用普通输入
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, _ := reader.ReadString('\n')
|
||||
return strings.TrimSpace(input), nil
|
||||
}
|
||||
|
||||
func generateSecret(length int) string {
|
||||
bytes := make([]byte, length)
|
||||
rand.Read(bytes)
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func createDataDirectory(cfg *initConfig) error {
|
||||
var dir string
|
||||
if cfg.dbType == "sqlite" {
|
||||
dir = filepath.Dir(cfg.dbPath)
|
||||
if dir == "." || dir == "" {
|
||||
dir = "./data"
|
||||
}
|
||||
} else {
|
||||
dir = "./data"
|
||||
}
|
||||
|
||||
if dir == "" || dir == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
return os.MkdirAll(dir, 0755)
|
||||
}
|
||||
|
||||
func getConfigPath() string {
|
||||
return "./config.yaml"
|
||||
}
|
||||
|
||||
func getEnvPath() string {
|
||||
return "./.env"
|
||||
}
|
||||
|
||||
func createConfigFiles(cfg *initConfig) error {
|
||||
// 创建 .env 文件
|
||||
envContent := fmt.Sprintf(`# Auto-generated by ums init
|
||||
# DO NOT COMMIT THIS FILE
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=%s
|
||||
JWT_REFRESH_SECRET=%s
|
||||
|
||||
# Bootstrap secret for admin initialization
|
||||
BOOTSTRAP_SECRET=%s
|
||||
|
||||
# Database
|
||||
DATABASE_PATH=%s
|
||||
|
||||
# Admin credentials
|
||||
DEFAULT_ADMIN_EMAIL=%s
|
||||
DEFAULT_ADMIN_PASSWORD=%s
|
||||
|
||||
# Redis
|
||||
REDIS_ENABLED=%t
|
||||
REDIS_HOST=%s
|
||||
REDIS_PORT=%d
|
||||
REDIS_PASSWORD=%s
|
||||
|
||||
# CORS
|
||||
CORS_ALLOWED_ORIGINS=%s
|
||||
|
||||
# Server
|
||||
SERVER_PORT=%d
|
||||
GIN_MODE=release
|
||||
`,
|
||||
cfg.jwtSecret,
|
||||
cfg.jwtSecret, // refresh secret same as access for simplicity
|
||||
cfg.bootstrapSecret,
|
||||
cfg.dbPath,
|
||||
cfg.adminEmail,
|
||||
cfg.adminPass,
|
||||
cfg.redisEnable,
|
||||
cfg.redisHost,
|
||||
cfg.redisPort,
|
||||
cfg.redisPassword,
|
||||
cfg.corsOrigin,
|
||||
cfg.port,
|
||||
)
|
||||
|
||||
if err := os.WriteFile(getEnvPath(), []byte(envContent), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Created: %s\n", getEnvPath())
|
||||
|
||||
// 创建 config.yaml
|
||||
configYAML := fmt.Sprintf(`server:
|
||||
port: %d
|
||||
mode: release
|
||||
host: 0.0.0.0
|
||||
|
||||
database:
|
||||
type: %s
|
||||
dbname: "%s"
|
||||
|
||||
redis:
|
||||
enabled: %t
|
||||
host: "%s"
|
||||
port: %d
|
||||
password: "%s"
|
||||
db: 0
|
||||
|
||||
jwt:
|
||||
secret: "%s"
|
||||
access_token_expire_minutes: 120
|
||||
refresh_token_expire_days: 7
|
||||
|
||||
cors:
|
||||
allowed_origins:
|
||||
- "%s"
|
||||
|
||||
default:
|
||||
admin_email: "%s"
|
||||
admin_password: "%s"
|
||||
`,
|
||||
cfg.port,
|
||||
cfg.dbType,
|
||||
cfg.dbPath,
|
||||
cfg.redisEnable,
|
||||
cfg.redisHost,
|
||||
cfg.redisPort,
|
||||
cfg.redisPassword,
|
||||
cfg.jwtSecret,
|
||||
cfg.corsOrigin,
|
||||
cfg.adminEmail,
|
||||
cfg.adminPass,
|
||||
)
|
||||
|
||||
if err := os.WriteFile(getConfigPath(), []byte(configYAML), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Created: %s\n", getConfigPath())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMigrations(cfg *initConfig) error {
|
||||
fmt.Println("\nRunning database migrations...")
|
||||
|
||||
// 创建一个临时 config 对象用于迁移
|
||||
tempCfg := &config.Config{
|
||||
Database: config.DatabaseConfig{
|
||||
DBName: cfg.dbPath,
|
||||
},
|
||||
Default: config.DefaultConfig{
|
||||
AdminEmail: cfg.adminEmail,
|
||||
AdminPassword: cfg.adminPass,
|
||||
},
|
||||
}
|
||||
|
||||
db, err := database.NewDB(tempCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect database failed: %w", err)
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(tempCfg); err != nil {
|
||||
return fmt.Errorf("auto migrate failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Database migrations completed.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func createAdminUser(cfg *initConfig) error {
|
||||
fmt.Println("\nCreating admin user...")
|
||||
|
||||
// 验证 Redis 连接(如果启用)
|
||||
if cfg.redisEnable {
|
||||
addr := net.JoinHostPort(cfg.redisHost, fmt.Sprintf("%d", cfg.redisPort))
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Redis connection failed: %v\n", err)
|
||||
fmt.Println("System will run without Redis caching.")
|
||||
} else {
|
||||
conn.Close()
|
||||
fmt.Println("Redis connection verified.")
|
||||
}
|
||||
}
|
||||
|
||||
// 密码验证已在 gatherConfig 中完成
|
||||
fmt.Println("Admin user creation skipped (handled by migrations).")
|
||||
return nil
|
||||
}
|
||||
41
cmd/ums/cmd/root.go
Normal file
41
cmd/ums/cmd/root.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
dataDir string
|
||||
)
|
||||
|
||||
// rootCmd is the single instance of the root command
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "ums",
|
||||
Short: "UMS CLI - User Management System",
|
||||
Long: `UMS CLI - Command line interface for User Management System
|
||||
|
||||
Supported commands:
|
||||
ums init Initialize the system (interactive or with flags)
|
||||
ums serve Start the UMS server
|
||||
ums version Print version information`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "./config.yaml", "config file path")
|
||||
rootCmd.PersistentFlags().StringVar(&dataDir, "data-dir", "./data", "data directory")
|
||||
|
||||
rootCmd.AddCommand(
|
||||
newVersionCmd(),
|
||||
initCmd,
|
||||
serveCmd,
|
||||
)
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
48
cmd/ums/cmd/serve.go
Normal file
48
cmd/ums/cmd/serve.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/user-management-system/internal/config"
|
||||
"github.com/user-management-system/internal/server"
|
||||
)
|
||||
|
||||
var (
|
||||
servePort string
|
||||
)
|
||||
|
||||
// serveCmd is the serve subcommand
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Start the UMS server",
|
||||
Long: `Start the User Management System HTTP server using config.yaml`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// 设置配置文件路径
|
||||
viper.SetConfigFile(cfgFile)
|
||||
|
||||
// 加载配置
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config failed: %w", err)
|
||||
}
|
||||
|
||||
// 允许通过 --port 覆盖配置
|
||||
if servePort != "" {
|
||||
var portInt int
|
||||
if _, err := fmt.Sscanf(servePort, "%d", &portInt); err != nil {
|
||||
return fmt.Errorf("invalid port: %w", err)
|
||||
}
|
||||
cfg.Server.Port = portInt
|
||||
}
|
||||
|
||||
// 启动服务器
|
||||
return server.Serve(cfg)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
serveCmd.Flags().StringVar(&servePort, "port", "", "server port (overrides config)")
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
}
|
||||
31
cmd/ums/cmd/version.go
Normal file
31
cmd/ums/cmd/version.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
Version = "1.0.0"
|
||||
Commit = "dev"
|
||||
BuildDate = "unknown"
|
||||
)
|
||||
|
||||
func newVersionCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print version information",
|
||||
Long: `Print the UMS version, Go version, and build information`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf("UMS CLI - User Management System\n")
|
||||
fmt.Printf("Version: %s\n", Version)
|
||||
fmt.Printf("Commit: %s\n", Commit)
|
||||
fmt.Printf("Build Date: %s\n", BuildDate)
|
||||
fmt.Printf("Go Version: %s\n", runtime.Version())
|
||||
fmt.Printf("OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
9
cmd/ums/main.go
Normal file
9
cmd/ums/main.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/user-management-system/cmd/ums/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
Reference in New Issue
Block a user