Skip to content
This repository was archived by the owner on Aug 18, 2025. It is now read-only.

Commit 9b0c7bc

Browse files
author
mirkobrombin
committed
first commit
0 parents  commit 9b0c7bc

File tree

11 files changed

+462
-0
lines changed

11 files changed

+462
-0
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# rfw-cli
2+
3+
`rfw-cli` is the official command-line interface (CLI) tool for the **rfw** framework. It allows you to create, build, and run **rfw** projects from the command line.
4+
5+
## Installation
6+
7+
Ensure you have Go installed on your machine. Then, install `rfw-cli` with the following command:
8+
9+
```bash
10+
go install github.com/rfwlab/rfw-cli@latest
11+
```
12+
13+
## Usage
14+
15+
To create a new **rfw** project, run the following command:
16+
17+
```bash
18+
rfw-cli init github.com/username/project-name
19+
```
20+
21+
## Server
22+
23+
To start the **rfw** server, run the following command:
24+
25+
```bash
26+
rfw-cli dev
27+
```
28+
29+
To set a custom port and expose to the network, use the following flags:
30+
31+
```bash
32+
rfw-cli dev --port 8080 --host
33+
```

cmd/root.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package cmd
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"os"
7+
8+
"github.com/rfwlab/rfw-cli/internal/initproj"
9+
"github.com/rfwlab/rfw-cli/internal/server"
10+
"github.com/rfwlab/rfw-cli/internal/utils"
11+
)
12+
13+
func Execute() {
14+
if len(os.Args) < 2 {
15+
showHelp()
16+
return
17+
}
18+
19+
switch os.Args[1] {
20+
case "init":
21+
initProject(os.Args[2:])
22+
case "dev":
23+
startServer(os.Args[2:])
24+
case "-h", "--help":
25+
showHelp()
26+
default:
27+
fmt.Printf("Unknown command: %s\n", os.Args[1])
28+
showHelp()
29+
}
30+
}
31+
32+
func startServer(args []string) {
33+
devFlags := flag.NewFlagSet("dev", flag.ExitOnError)
34+
port := devFlags.String("port", "8080", "Port from which the server will serve")
35+
host := devFlags.Bool("host", false, "Expose the server to the network")
36+
37+
err := devFlags.Parse(args)
38+
if err != nil {
39+
fmt.Println("Error parsing flags:", err)
40+
os.Exit(1)
41+
}
42+
43+
fmt.Println("Starting server on port", *port)
44+
45+
srv := server.NewServer(*port, *host)
46+
if err := srv.Start(); err != nil {
47+
utils.Fatal("Server failed to start: ", err)
48+
}
49+
}
50+
51+
func initProject(args []string) {
52+
initFlags := flag.NewFlagSet("init", flag.ExitOnError)
53+
54+
err := initFlags.Parse(args)
55+
if err != nil {
56+
fmt.Println("Error parsing flags:", err)
57+
os.Exit(1)
58+
}
59+
60+
remainingArgs := initFlags.Args()
61+
if len(remainingArgs) < 1 {
62+
fmt.Println("Please specify a project name: rfw-cli init <project-name>")
63+
return
64+
}
65+
66+
projectName := remainingArgs[0]
67+
if err := initproj.InitProject(projectName); err != nil {
68+
utils.Fatal("Failed to initialize project: ", err)
69+
}
70+
}
71+
72+
func showHelp() {
73+
helpMessage := `
74+
Usage:
75+
rfw-cli <command> [options]
76+
77+
Commands:
78+
init <project-name> Initialize a new project
79+
dev [--port <port>] [--host] Start the development server
80+
-h, --help Show this help message
81+
82+
Examples:
83+
rfw-cli init my-project
84+
rfw-cli dev --port 9090 --host
85+
`
86+
fmt.Println(helpMessage)
87+
}

go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/rfwlab/rfw-cli
2+
3+
go 1.22.3
4+
5+
require github.com/fatih/color v1.17.0
6+
7+
require (
8+
github.com/mattn/go-colorable v0.1.13 // indirect
9+
github.com/mattn/go-isatty v0.0.20 // indirect
10+
golang.org/x/sys v0.18.0 // indirect
11+
)

go.sum

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
2+
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
3+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
4+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
5+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
6+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
7+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
8+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
9+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
10+
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
11+
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

internal/initproj/init.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package initproj
2+
3+
import (
4+
"fmt"
5+
"io/fs"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"strings"
10+
)
11+
12+
func InitProject(projectName string) error {
13+
if projectName == "" {
14+
return fmt.Errorf("project name cannot be empty")
15+
}
16+
17+
projectName = strings.Split(projectName, "/")[len(strings.Split(projectName, "/"))-1]
18+
19+
projectPath := projectName
20+
if len(os.Args) > 3 {
21+
projectPath = os.Args[3]
22+
}
23+
24+
if _, err := os.Stat(projectPath); !os.IsNotExist(err) {
25+
return fmt.Errorf("project directory already exists")
26+
}
27+
28+
if err := os.Mkdir(projectPath, 0755); err != nil {
29+
return fmt.Errorf("failed to create project directory: %w", err)
30+
}
31+
32+
err := fs.WalkDir(TemplatesFS, ".", func(path string, d fs.DirEntry, err error) error {
33+
if err != nil {
34+
return err
35+
}
36+
37+
if path == "." {
38+
return nil
39+
}
40+
41+
relPath := strings.TrimPrefix(path, "./")
42+
targetPath := filepath.Join(projectPath, relPath)
43+
44+
if d.IsDir() {
45+
return os.MkdirAll(targetPath, 0755)
46+
}
47+
48+
content, err := TemplatesFS.ReadFile(path)
49+
if err != nil {
50+
return err
51+
}
52+
53+
contentStr := strings.ReplaceAll(string(content), "{{packageName}}", projectName)
54+
55+
return os.WriteFile(targetPath, []byte(contentStr), 0644)
56+
})
57+
if err != nil {
58+
return fmt.Errorf("failed to copy template files: %w", err)
59+
}
60+
61+
if err := copyWasmExec(projectName); err != nil {
62+
return fmt.Errorf("failed to copy wasm_exec.js: %w", err)
63+
}
64+
65+
fmt.Printf("Project '%s' initialized successfully.\n", projectName)
66+
return nil
67+
}
68+
69+
func copyWasmExec(projectDir string) error {
70+
cmd := exec.Command("go", "env", "GOROOT")
71+
output, err := cmd.Output()
72+
if err != nil {
73+
return fmt.Errorf("failed to get GOROOT: %w", err)
74+
}
75+
goRoot := strings.TrimSpace(string(output))
76+
77+
srcPath := filepath.Join(goRoot, "misc", "wasm", "wasm_exec.js")
78+
destPath := filepath.Join(projectDir, "wasm_exec.js")
79+
80+
input, err := os.ReadFile(srcPath)
81+
if err != nil {
82+
return fmt.Errorf("failed to read wasm_exec.js: %w", err)
83+
}
84+
85+
if err := os.WriteFile(destPath, input, 0644); err != nil {
86+
return fmt.Errorf("failed to write wasm_exec.js: %w", err)
87+
}
88+
89+
return nil
90+
}

internal/initproj/template/main.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package main
2+
3+
import "fmt"
4+
5+
func main() {
6+
fmt.Println("Welcome to {{packageName}}!")
7+
fmt.Println("The init command is not fully implemented yet, template will be created as soon as the framework reach a stable structure.")
8+
}

internal/initproj/templates.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package initproj
2+
3+
import (
4+
"embed"
5+
)
6+
7+
//go:embed template/*
8+
var TemplatesFS embed.FS

internal/server/server.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package server
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"net/http"
7+
"os"
8+
"os/signal"
9+
"strings"
10+
"syscall"
11+
12+
"github.com/rfwlab/rfw-cli/internal/utils"
13+
)
14+
15+
type Server struct {
16+
Port string
17+
Host bool
18+
stopCh chan os.Signal
19+
}
20+
21+
func NewServer(port string, host bool) *Server {
22+
return &Server{
23+
Port: port,
24+
Host: host,
25+
stopCh: make(chan os.Signal, 1),
26+
}
27+
}
28+
29+
func (s *Server) Start() error {
30+
fs := http.FileServer(http.Dir("."))
31+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
32+
utils.LogServeRequest(r)
33+
s.handleFileRequest(w, r, fs)
34+
})
35+
36+
signal.Notify(s.stopCh, syscall.SIGINT, syscall.SIGTERM)
37+
38+
localIP := ""
39+
var err error
40+
if s.Host {
41+
localIP, err = utils.GetLocalIP()
42+
if err != nil {
43+
return err
44+
}
45+
}
46+
47+
utils.ClearScreen()
48+
utils.PrintStartupInfo(s.Port, localIP, s.Host)
49+
50+
go func() {
51+
if err := http.ListenAndServe(":"+s.Port, nil); err != nil {
52+
utils.Fatal("Server failed: ", err)
53+
}
54+
}()
55+
56+
go s.listenForCommands()
57+
58+
<-s.stopCh
59+
utils.Info("Server stopped.")
60+
return nil
61+
}
62+
63+
func (s *Server) handleFileRequest(w http.ResponseWriter, r *http.Request, fs http.Handler) {
64+
if _, err := os.Stat("." + r.URL.Path); os.IsNotExist(err) {
65+
http.ServeFile(w, r, "./index.html")
66+
} else {
67+
fs.ServeHTTP(w, r)
68+
}
69+
}
70+
71+
func (s *Server) listenForCommands() {
72+
reader := bufio.NewReader(os.Stdin)
73+
for {
74+
input, _ := reader.ReadString('\n')
75+
input = strings.TrimSpace(input)
76+
77+
switch strings.ToLower(input) {
78+
case "h":
79+
utils.PrintHelp()
80+
case "u":
81+
utils.ClearScreen()
82+
localIP, err := utils.GetLocalIP()
83+
if err != nil {
84+
utils.Fatal("Failed to get local IP address: ", err)
85+
}
86+
utils.PrintStartupInfo(s.Port, localIP, s.Host)
87+
case "c", "q":
88+
utils.Info("Closing the server...")
89+
s.stopCh <- syscall.SIGINT
90+
return
91+
case "o":
92+
utils.Info("Opening the browser...")
93+
url := fmt.Sprintf("http://localhost:%s/", s.Port)
94+
utils.OpenBrowser(url)
95+
default:
96+
utils.Info("Unknown command. Press 'h' for help.")
97+
}
98+
}
99+
}

internal/utils/network.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package utils
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"os/exec"
7+
"runtime"
8+
)
9+
10+
func GetLocalIP() (string, error) {
11+
addrs, err := net.InterfaceAddrs()
12+
if err != nil {
13+
return "", err
14+
}
15+
16+
for _, addr := range addrs {
17+
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() && ipNet.IP.To4() != nil {
18+
return ipNet.IP.String(), nil
19+
}
20+
}
21+
22+
return "", fmt.Errorf("no local IP address found")
23+
}
24+
25+
func OpenBrowser(url string) {
26+
var cmd *exec.Cmd
27+
switch runtime.GOOS {
28+
case "windows":
29+
cmd = exec.Command("cmd", "/c", "start", url)
30+
case "darwin":
31+
cmd = exec.Command("open", url)
32+
default:
33+
cmd = exec.Command("xdg-open", url)
34+
}
35+
36+
if err := cmd.Start(); err != nil {
37+
fmt.Printf("Failed to open browser: %v\n", err)
38+
}
39+
}

0 commit comments

Comments
 (0)