Commit 8a69149

Karn Wong <[email protected]>
2025-09-29 12:57:50
feat(getSmart): add nvme
1 parent d64d949
cmd/get/get_smart.go
@@ -0,0 +1,19 @@
+package get
+
+import (
+	"github.com/kahnwong/swissknife/internal/get"
+	"github.com/spf13/cobra"
+)
+
+var getSmartCmd = &cobra.Command{
+	Use:   "smart",
+	Short: "Get disk SMART info",
+	Long:  "Get disk SMART info. Equivalent of `sudo smartctl -a /dev/nvme0n1p`",
+	Run: func(cmd *cobra.Command, args []string) {
+		get.Smart()
+	},
+}
+
+func init() {
+	Cmd.AddCommand(getSmartCmd)
+}
cmd/root.go
@@ -5,6 +5,8 @@ import (
 
 	"github.com/kahnwong/swissknife/cmd/generate"
 	"github.com/kahnwong/swissknife/cmd/get"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
 	"github.com/spf13/cobra"
 )
 
@@ -27,6 +29,8 @@ func Execute() {
 }
 
 func init() {
+	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
+
 	rootCmd.AddCommand(get.Cmd)
 	rootCmd.AddCommand(generate.Cmd)
 }
internal/get/ip.go
@@ -15,7 +15,7 @@ type PublicIPResponse struct {
 	Ip string `json:"ip"`
 }
 
-type IPLocation struct {
+type IPLocationResponse struct {
 	Ip         string `json:"ip"`
 	Country    string `json:"country"`
 	RegionName string `json:"regionName"`
@@ -56,8 +56,8 @@ func getPublicIP() PublicIPResponse {
 	return response
 }
 
-func getIPLocation(ip string) IPLocation {
-	var response IPLocation
+func getIPLocation(ip string) IPLocationResponse {
+	var response IPLocationResponse
 	err := requests.
 		URL(fmt.Sprintf("http://ip-api.com/json/%s", ip)).
 		ToJSON(&response).
internal/get/smart.go
@@ -0,0 +1,55 @@
+package get
+
+import (
+	"fmt"
+	"runtime"
+
+	"github.com/anatol/smart.go"
+	"github.com/kahnwong/swissknife/configs/color"
+	"github.com/rs/zerolog/log"
+	"github.com/shirou/gopsutil/v4/disk"
+)
+
+func Smart() {
+	if runtime.GOOS == "linux" {
+		device := getRootDiskVolume()
+
+		isNvme := true
+		dev, err := smart.OpenNVMe(device)
+		if err != nil {
+			log.Fatal().Err(err).Msgf("Failed to open %s device. You probaby have to run as sudo", device)
+		}
+		defer func(dev smart.Device) {
+			err = dev.Close()
+			if err != nil {
+				log.Fatal().Err(err).Msgf("Failed to close %s device", device)
+			}
+		}(dev)
+
+		if isNvme {
+			sm, err := dev.ReadSMART()
+			if err != nil {
+				log.Fatal().Msg("Failed to read SMART info")
+			}
+			fmt.Printf("%s: %v\n", color.Green("Percent Used"), sm.PercentUsed)
+		}
+	} else {
+		log.Error().Msgf("%s is not supported\n", runtime.GOOS)
+	}
+}
+
+func getRootDiskVolume() string {
+	partitions, err := disk.Partitions(false)
+	if err != nil {
+		log.Fatal().Msg("Failed to get disk partitions")
+	}
+
+	var volume string
+	for _, partition := range partitions {
+		if partition.Mountpoint == "/" {
+			volume = partition.Device
+		}
+	}
+
+	return volume
+}
go.mod
@@ -3,6 +3,7 @@ module github.com/kahnwong/swissknife
 go 1.25.1
 
 require (
+	github.com/anatol/smart.go v0.0.0-20241126061019-f03d79b340d2
 	github.com/atotto/clipboard v0.1.4
 	github.com/carlmjohnson/requests v0.25.1
 	github.com/charmbracelet/bubbles v0.21.0
go.sum
@@ -1,3 +1,7 @@
+github.com/anatol/smart.go v0.0.0-20241126061019-f03d79b340d2 h1:ha5+W2OHFJOEY4uTne98HGD9vQltSYtdyzNCbyhp7rY=
+github.com/anatol/smart.go v0.0.0-20241126061019-f03d79b340d2/go.mod h1:NoOw2eVExhZPtVxpYJzEvdRc5OSbCQZJT2vSe3dflPg=
+github.com/anatol/vmtest v0.0.0-20230711210602-87511df0d4bc h1:xMQuzBhj6hXQZufedPQM2OiGX2UcQHSptXtG3+28S8Q=
+github.com/anatol/vmtest v0.0.0-20230711210602-87511df0d4bc/go.mod h1:NC+g66bgkUjV1unIJXhHO35RHxVViWUzNeeKAkkO7DU=
 github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
 github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
@@ -51,6 +55,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
 github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc=
 github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFPuZ8=
 github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@@ -107,6 +113,8 @@ github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8O
 github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
 github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
 github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
+github.com/tmc/scp v0.0.0-20170824174625-f7b48647feef h1:7D6Nm4D6f0ci9yttWaKjM1TMAXrH5Su72dojqYGntFY=
+github.com/tmc/scp v0.0.0-20170824174625-f7b48647feef/go.mod h1:WLFStEdnJXpjK8kd4qKLwQKX/1vrDzp5BcDyiZJBHJM=
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
 github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=