├── go.mod ├── go.sum ├── README.md └── main.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Phillip-England/seed 2 | 3 | go 1.23.3 4 | 5 | require ( 6 | github.com/Phillip-England/mood v0.0.5 7 | github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 8 | ) 9 | 10 | require golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Phillip-England/mood v0.0.5 h1:IUcm9OSqav6q+3y+AAV9ds6RQeR4v94DoTslTqYs4cU= 2 | github.com/Phillip-England/mood v0.0.5/go.mod h1:py37wX8i4ziEUTphSnNJ6j5PxCN/nXM1ZGlrcGO2xOc= 3 | github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= 4 | github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= 5 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= 6 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # seed 2 | Generate go skeleton projects quickly. 3 | 4 | ## Installation 5 | Install on your `GOPATH` using: 6 | ```bash 7 | go install github.com/Phillip-England/seed@latest 8 | ``` 9 | 10 | If you prefer to build from source, with go installed on your system, clone the repo, install deps, then build: 11 | ```bash 12 | git clone https://github.com/Phillip-England/seed 13 | cd seed 14 | go mod tidy 15 | go build -o seed 16 | ``` 17 | 18 | ## Usage 19 | If you run `seed plant` without any further args, seed will assume you want the project generated in your current directory. 20 | 21 | To specify, a target directory, just pass the name of the directory you'd like to generate. This will generate an app directory containing our project (assuming ./app doesn't already exist). 22 | ```bash 23 | seed plant app 24 | ``` 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/Phillip-England/mood" 9 | "github.com/eiannone/keyboard" 10 | ) 11 | 12 | func main() { 13 | 14 | app := mood.New() 15 | 16 | app.SetDefault(func(app *mood.Mood) error { 17 | fmt.Println("seed - generate skeleton projects with ease") 18 | fmt.Println("run 'seed plant' to get started") 19 | return nil 20 | }) 21 | 22 | app.At("plant", func(app *mood.Mood) error { 23 | 24 | out := app.GetArgOr(1, ".") 25 | if out != "." { 26 | if len(out) >= 2 { 27 | if string(out[0:2]) != "./" { 28 | out = "./" + out 29 | } 30 | } 31 | } 32 | menuPosition := 0 33 | menuLimit := 2 34 | clear() 35 | printMenu(menuPosition) 36 | err := keyboard.Open() 37 | if err != nil { 38 | return err 39 | } 40 | defer keyboard.Close() 41 | 42 | for { 43 | _, key, err := keyboard.GetKey() 44 | if err != nil { 45 | fmt.Println(`error reading key:`, err) 46 | continue 47 | } 48 | switch key { 49 | case keyboard.KeyArrowDown: 50 | menuPosition++ 51 | if menuPosition > menuLimit { 52 | menuPosition = 0 53 | } 54 | clear() 55 | printMenu(menuPosition) 56 | case keyboard.KeyArrowUp: 57 | menuPosition-- 58 | if menuPosition < 0 { 59 | menuPosition = menuLimit 60 | } 61 | clear() 62 | printMenu(menuPosition) 63 | case keyboard.KeyCtrlZ: 64 | clear() 65 | return nil 66 | case keyboard.KeyCtrlX: 67 | clear() 68 | return nil 69 | case keyboard.KeyCtrlC: 70 | clear() 71 | return nil 72 | case keyboard.KeyEnter: 73 | clear() 74 | skeletonType, err := getSkeletonType(menuPosition) 75 | if err != nil { 76 | return err 77 | } 78 | switch skeletonType { 79 | case "server": 80 | err := GenerateGoServer(out) 81 | if err != nil { 82 | return err 83 | } 84 | return nil 85 | case "cli": 86 | err := GenerateGoCli(out) 87 | if err != nil { 88 | return err 89 | } 90 | return nil 91 | case "library": 92 | err := GenerateGoLibrary(out) 93 | if err != nil { 94 | return err 95 | } 96 | return nil 97 | } 98 | return nil 99 | } 100 | } 101 | }) 102 | 103 | err := app.Run() 104 | if err != nil { 105 | fmt.Println(err.Error()) 106 | } 107 | 108 | } 109 | 110 | func GenerateGoLibrary(out string) error { 111 | if out != "." { 112 | err := makeDir(out) 113 | if err != nil { 114 | return fmt.Errorf(`cannot overwrite dir [%s] because it already exists`, out) 115 | } 116 | } 117 | skeleton := NewLibrarySkeleton(out) 118 | err := makeFile(skeleton.LibGoPath) 119 | if err != nil { 120 | return err 121 | } 122 | err = makeFile(skeleton.TestGoPath) 123 | if err != nil { 124 | return err 125 | } 126 | err = writeFile(skeleton.LibGoPath, `package lib 127 | 128 | func Add(a, b int) int { 129 | return a + b 130 | } 131 | `) 132 | if err != nil { 133 | return err 134 | } 135 | err = writeFile(skeleton.TestGoPath, `package lib 136 | 137 | import "testing" 138 | 139 | func TestAdd(t *testing.T) { 140 | result := Add(2, 3) 141 | expected := 5 142 | 143 | if result != expected { 144 | t.Errorf("Expected %d, but got %d", expected, result) 145 | } 146 | } 147 | `) 148 | if err != nil { 149 | return err 150 | } 151 | clear() 152 | if out == "." { 153 | fmt.Println("to initialize the project, run:\n") 154 | fmt.Println("1. go mod init github.com/github-name/repo-name\n") 155 | fmt.Println("thank you for using seed 🌱") 156 | return nil 157 | } 158 | fmt.Println("to initialize the project, run:\n") 159 | fmt.Println("1. cd " + strings.Replace(out, "./", "", 1)) 160 | fmt.Println("2. go mod init github.com/github-name/repo-name\n") 161 | fmt.Println("thank you for using seed 🌱") 162 | return nil 163 | } 164 | 165 | func GenerateGoCli(out string) error { 166 | if out != "." { 167 | err := makeDir(out) 168 | if err != nil { 169 | return fmt.Errorf(`cannot overwrite dir [%s] because it already exists`, out) 170 | } 171 | } 172 | skeleton := NewCliSkeleton(out) 173 | err := makeFile(skeleton.MainGoPath) 174 | if err != nil { 175 | return err 176 | } 177 | err = writeFile(skeleton.MainGoPath, `package main 178 | 179 | import ( 180 | "fmt" 181 | "os" 182 | "github.com/Phillip-England/mood" 183 | ) 184 | 185 | func main() { 186 | 187 | app := mood.New() 188 | 189 | app.SetDefault(NewDefaultCmd) 190 | app.At("help", NewHelpCmd) 191 | 192 | err := app.Run() 193 | if err != nil { 194 | panic(err) 195 | } 196 | 197 | } 198 | 199 | //====================== 200 | // DefaultCmd 201 | //====================== 202 | 203 | type DefaultCmd struct{} 204 | 205 | func NewDefaultCmd(app *mood.App) (mood.Cmd, error) { 206 | return DefaultCmd{}, nil 207 | } 208 | 209 | func (cmd DefaultCmd) Execute(app *mood.App) error { 210 | fmt.Println("working..") 211 | return nil 212 | } 213 | 214 | //====================== 215 | // HelpCmd 216 | //====================== 217 | 218 | type HelpCmd struct{} 219 | 220 | func NewHelpCmd(app *mood.App) (mood.Cmd, error) { 221 | return HelpCmd{}, nil 222 | } 223 | 224 | func (cmd HelpCmd) Execute(app *mood.App) error { 225 | fmt.Println("helping..") 226 | return nil 227 | }`) 228 | if err != nil { 229 | return err 230 | } 231 | clear() 232 | if out == "." { 233 | fmt.Println("to install required packages run:\n") 234 | fmt.Println("1. go mod init github.com/github-name/repo-name") 235 | fmt.Println("2. go mod tidy\n") 236 | fmt.Println("thank you for using seed 🌱") 237 | return nil 238 | } 239 | fmt.Println("to install required packages run:\n") 240 | fmt.Println("1. cd " + strings.Replace(out, "./", "", 1)) 241 | fmt.Println("2. go mod init github.com/github-name/repo-name") 242 | fmt.Println("3. go mod tidy\n") 243 | fmt.Println("thank you for using seed 🌱") 244 | return nil 245 | } 246 | 247 | func GenerateGoServer(out string) error { 248 | if out != "." { 249 | err := makeDir(out) 250 | if err != nil { 251 | return fmt.Errorf(`cannot overwrite dir [%s] because it already exists`, out) 252 | } 253 | } 254 | skeleton := NewServerSkeleton(out) 255 | err := makeDir(skeleton.TemplatePath) 256 | if err != nil { 257 | return err 258 | } 259 | err = makeDir(skeleton.StaticPath) 260 | if err != nil { 261 | return err 262 | } 263 | err = makeFile(skeleton.IndexHtmlPath) 264 | if err != nil { 265 | return err 266 | } 267 | err = makeFile(skeleton.IndexJsPath) 268 | if err != nil { 269 | return err 270 | } 271 | err = makeFile(skeleton.IndexCssPath) 272 | if err != nil { 273 | return err 274 | } 275 | err = makeFile(skeleton.MainGoPath) 276 | if err != nil { 277 | return err 278 | } 279 | err = writeFile(skeleton.IndexHtmlPath, ` 280 | 281 | 282 | 283 | 284 | 285 | {{ .Title }} 286 | 287 | 288 |

Hello, World!

289 | 290 | 291 | `) 292 | if err != nil { 293 | return err 294 | } 295 | err = writeFile(skeleton.IndexCssPath, `* { 296 | margin: 0; 297 | padding: 0; 298 | box-sizing: border-box; 299 | } 300 | 301 | html { 302 | font-size: 16px; 303 | } 304 | 305 | body { 306 | line-height: 1.5; 307 | background-color: var(--color-white); 308 | color: var(--color-black); 309 | } 310 | 311 | :root { 312 | --color-black: #000; 313 | --color-white: #fff 314 | }`) 315 | err = writeFile(skeleton.IndexJsPath, `console.log('connected!')`) 316 | if err != nil { 317 | return err 318 | } 319 | err = writeFile(skeleton.MainGoPath, `package main 320 | 321 | import ( 322 | "net/http" 323 | 324 | "github.com/Phillip-England/vii" 325 | ) 326 | 327 | func main() { 328 | app := vii.NewApp() 329 | app.Use(vii.MwLogger, vii.MwTimeout(10)) 330 | app.Static("./static") 331 | app.Favicon() 332 | err := app.Templates("./templates", nil) 333 | if err != nil { 334 | panic(err) 335 | } 336 | app.At("GET /", func(w http.ResponseWriter, r *http.Request) { 337 | vii.ExecuteTemplate(w, r, "index.html", map[string]interface{}{ 338 | "Title": "Some Title", 339 | }) 340 | }) 341 | app.Serve("8080") 342 | } 343 | `) 344 | clear() 345 | if out == "." { 346 | fmt.Println("to install required packages run:\n") 347 | fmt.Println("1. go mod init github.com/github-name/repo-name") 348 | fmt.Println("2. go mod tidy\n") 349 | fmt.Println("thank you for using seed 🌱") 350 | return nil 351 | } 352 | fmt.Println("to install required packages run:\n") 353 | fmt.Println("1. cd " + strings.Replace(out, "./", "", 1)) 354 | fmt.Println("2. go mod init github.com/github-name/repo-name") 355 | fmt.Println("3. go mod tidy\n") 356 | fmt.Println("thank you for using seed 🌱") 357 | return nil 358 | } 359 | 360 | type LibrarySkeleton struct { 361 | Root string 362 | LibGoPath string 363 | TestGoPath string 364 | } 365 | 366 | func NewLibrarySkeleton(root string) LibrarySkeleton { 367 | var fileName string 368 | if root == "." { 369 | fileName = "lib.go" 370 | } else { 371 | fileName = strings.Replace(root, "./", "", 1) + ".go" 372 | } 373 | var testName string 374 | if root == "." { 375 | testName = "lib_test.go" 376 | } else { 377 | testName = strings.Replace(root, "./", "", 1) + "_test.go" 378 | } 379 | return LibrarySkeleton{ 380 | Root: root, 381 | LibGoPath: root + "/" + fileName, 382 | TestGoPath: root + "/" + testName, 383 | } 384 | } 385 | 386 | type CliSkeleton struct { 387 | Root string 388 | MainGoPath string 389 | } 390 | 391 | func NewCliSkeleton(root string) CliSkeleton { 392 | return CliSkeleton{ 393 | Root: root, 394 | MainGoPath: root + "/main.go", 395 | } 396 | } 397 | 398 | type ServerSkeleton struct { 399 | Root string 400 | TemplatePath string 401 | StaticPath string 402 | IndexHtmlPath string 403 | IndexCssPath string 404 | IndexJsPath string 405 | MainGoPath string 406 | } 407 | 408 | func NewServerSkeleton(root string) ServerSkeleton { 409 | skeleton := ServerSkeleton{ 410 | Root: root, 411 | TemplatePath: root + "/templates", 412 | IndexHtmlPath: root + "/templates/index.html", 413 | IndexCssPath: root + "/static/index.css", 414 | StaticPath: root + "/static", 415 | IndexJsPath: root + "/static/index.js", 416 | MainGoPath: root + "/main.go", 417 | } 418 | return skeleton 419 | } 420 | 421 | func printMenu(selectPosition int) { 422 | msg := `select a skeleton: 423 | 1. server 424 | 2. cli 425 | 3. library 426 | ` 427 | lines := strings.Split(msg, "\n") 428 | newLines := []string{} 429 | for i, line := range lines { 430 | if i == 0 { 431 | newLines = append(newLines, line) 432 | continue 433 | } 434 | if i == selectPosition+1 { 435 | newLines = append(newLines, line+" *") 436 | continue 437 | } 438 | newLines = append(newLines, line) 439 | } 440 | fmt.Println(strings.Join(newLines, "\n")) 441 | } 442 | 443 | func clear() { 444 | fmt.Print("\033[2J\033[H") 445 | } 446 | 447 | func getSkeletonType(menuPosition int) (string, error) { 448 | switch menuPosition { 449 | case 0: 450 | return "server", nil 451 | case 1: 452 | return "cli", nil 453 | case 2: 454 | return "library", nil 455 | 456 | } 457 | return "", fmt.Errorf("provided invalid position [%d] to getSkeletonType", menuPosition) 458 | } 459 | 460 | func makeDir(path string) error { 461 | info, err := os.Stat(path) 462 | if err == nil && info.IsDir() { 463 | return fmt.Errorf("directory '%s' already exists", path) 464 | } 465 | if err != nil && !os.IsNotExist(err) { 466 | return fmt.Errorf("error checking directory: %w", err) 467 | } 468 | if err := os.Mkdir(path, 0755); err != nil { 469 | return fmt.Errorf("failed to create directory: %w", err) 470 | } 471 | return nil 472 | } 473 | 474 | func makeFile(path string) error { 475 | info, err := os.Stat(path) 476 | if err == nil && !info.IsDir() { 477 | return fmt.Errorf("file '%s' already exists", path) 478 | } 479 | if err != nil && !os.IsNotExist(err) { 480 | return fmt.Errorf("error checking file: %w", err) 481 | } 482 | file, err := os.Create(path) 483 | if err != nil { 484 | return fmt.Errorf("failed to create file: %w", err) 485 | } 486 | defer file.Close() 487 | 488 | return nil 489 | } 490 | 491 | func writeFile(path, content string) error { 492 | info, err := os.Stat(path) 493 | if os.IsNotExist(err) { 494 | return fmt.Errorf("file '%s' does not exist", path) 495 | } 496 | if err != nil { 497 | return fmt.Errorf("error checking file: %w", err) 498 | } 499 | if info.IsDir() { 500 | return fmt.Errorf("'%s' is a directory, not a file", path) 501 | } 502 | file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0644) 503 | if err != nil { 504 | return fmt.Errorf("failed to open file: %w", err) 505 | } 506 | defer file.Close() 507 | _, err = file.WriteString(content) 508 | if err != nil { 509 | return fmt.Errorf("failed to write to file: %w", err) 510 | } 511 | 512 | return nil 513 | } 514 | --------------------------------------------------------------------------------