diff --git a/go.mod b/go.mod index 3f4a114..3146250 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,14 @@ require ( github.com/arduino/go-paths-helper v1.14.0 github.com/arduino/go-windows-runas v1.0.1 github.com/codeclysm/extract/v4 v4.0.0 + github.com/jedib0t/go-pretty/v6 v6.6.8 github.com/leonelquinteros/gotext v1.7.2 github.com/schollz/progressbar/v3 v3.18.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 go.bug.st/cleanup v1.0.0 + go.bug.st/f v0.4.0 ) require ( @@ -20,6 +22,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/juju/errors v1.0.0 // indirect github.com/klauspost/compress v1.18.0 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect @@ -27,5 +30,6 @@ require ( github.com/ulikunitz/xz v0.5.12 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/term v0.35.0 // indirect + golang.org/x/text v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 06378a9..c17d162 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM= github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -30,6 +32,7 @@ github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2Em github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -45,17 +48,21 @@ github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= go.bug.st/cleanup v1.0.0 h1:XVj1HZxkBXeq3gMT7ijWUpHyIC1j8XAoNSyQ06CskgA= go.bug.st/cleanup v1.0.0/go.mod h1:EqVmTg2IBk4znLbPD28xne3abjsJftMdqqJEjhn70bk= +go.bug.st/f v0.4.0 h1:Vstqb950nMA+PhAlRxUw8QL1ntHy/gXHNyyzjkQLJ10= +go.bug.st/f v0.4.0/go.mod h1:bMo23205ll7UW63KwO1ut5RdlJ9JK8RyEEr88CmOF5Y= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/list.go b/list.go new file mode 100644 index 0000000..3902cf0 --- /dev/null +++ b/list.go @@ -0,0 +1,75 @@ +// This file is part of arduino-flasher-cli. +// +// Copyright 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-flasher-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package main + +import ( + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" + + "github.com/arduino/arduino-flasher-cli/feedback" + "github.com/arduino/arduino-flasher-cli/i18n" + "github.com/arduino/arduino-flasher-cli/tablestyle" + "github.com/arduino/arduino-flasher-cli/updater" +) + +func newListCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List the available Linux images", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + runListCommand() + }, + } + return cmd +} + +func runListCommand() { + client := updater.NewClient() + + manifest, err := client.GetInfoManifest() + if err != nil { + feedback.Fatal(i18n.Tr("error retrieving the manifest: %v", err), feedback.ErrBadArgument) + } + + feedback.PrintResult(listResult{Latest: manifest.Latest, Releases: manifest.Releases}) +} + +type listResult struct { + Latest updater.Release `json:"latest"` + Releases []updater.Release `json:"releases"` +} + +// Data implements Result interface +func (lr listResult) Data() interface{} { + return lr +} + +// String implements Result interface +func (lr listResult) String() string { + t := table.NewWriter() + t.SetStyle(tablestyle.CustomCleanStyle) + t.AppendHeader(table.Row{"VERSION", "LATEST"}) + + for i := len(lr.Releases) - 1; i >= 0; i-- { + row := table.Row{lr.Releases[i].Version} + if lr.Releases[i].Version == lr.Latest.Version { + row = append(row, "✓") + } + t.AppendRow(row) + } + return t.Render() +} diff --git a/main.go b/main.go index 8dae992..5d89119 100644 --- a/main.go +++ b/main.go @@ -51,6 +51,7 @@ func main() { rootCmd.AddCommand( newFlashCmd(), newInstallDriversCmd(), + newListCmd(), &cobra.Command{ Use: "version", Short: "Print the version number of Arduino Flasher CLI", diff --git a/tablestyle/tablestyle.go b/tablestyle/tablestyle.go new file mode 100644 index 0000000..22081ce --- /dev/null +++ b/tablestyle/tablestyle.go @@ -0,0 +1,41 @@ +// This file is part of arduino-flasher-cli. +// +// Copyright 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-flasher-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package tablestyle + +import ( + "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" +) + +var ( + CustomCleanStyle = table.Style{ + Name: "CustomClean", + Box: table.BoxStyle{PaddingRight: " "}, + Format: table.FormatOptions{ + Footer: text.FormatUpper, + Header: text.FormatUpper, + Row: text.FormatDefault, + RowAlign: text.AlignCenter, + }, + Options: table.Options{ + DrawBorder: false, + SeparateColumns: true, + SeparateFooter: false, + SeparateHeader: false, + SeparateRows: false, + }, + } +) diff --git a/updater/flasher.go b/updater/flasher.go index 4ba32e3..98284e9 100644 --- a/updater/flasher.go +++ b/updater/flasher.go @@ -18,7 +18,6 @@ package updater import ( "context" "fmt" - "net/url" "runtime" "strings" @@ -31,14 +30,7 @@ import ( func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes bool) error { if !imagePath.Exist() { - updateURL := "https://downloads.arduino.cc" - - parsedURL, err := url.Parse(updateURL) - if err != nil { - return fmt.Errorf("invalid UPDATE_URL: %v", err) - } - - client := NewClient(parsedURL, "debian-im/Stable") + client := NewClient() tempImagePath, err := DownloadAndExtract(client, version, func(target string) (bool, error) { feedback.Printf("Found Debian image version: %s", target) diff --git a/updater/http_client.go b/updater/http_client.go index 7a9a24f..240dc98 100644 --- a/updater/http_client.go +++ b/updater/http_client.go @@ -23,12 +23,16 @@ import ( "io" "net/http" "net/url" + + "go.bug.st/f" ) +var baseURL = f.Must(url.Parse("https://downloads.arduino.cc")) + +const pathRelease = "debian-im/Stable" + // Client holds the base URL, command name, allows custom HTTP client, and optional headers. type Client struct { - BaseURL *url.URL - CmdName string HTTPClient HTTPDoer Headers map[string]string // Optional headers to add to each request } @@ -56,10 +60,8 @@ func WithHTTPClient(client HTTPDoer) Option { } // NewClient creates a new Client with optional configuration. -func NewClient(baseURL *url.URL, cmdName string, opts ...Option) *Client { +func NewClient(opts ...Option) *Client { c := &Client{ - BaseURL: baseURL, - CmdName: cmdName, HTTPClient: http.DefaultClient, Headers: nil, } @@ -78,7 +80,7 @@ func (c *Client) addHeaders(req *http.Request) { // GetInfoManifest fetches and decodes the Debian images info.json. func (c *Client) GetInfoManifest() (Manifest, error) { - manifestURL := c.BaseURL.JoinPath(c.CmdName, "info.json").String() + manifestURL := baseURL.JoinPath(pathRelease, "info.json").String() req, err := http.NewRequest("GET", manifestURL, nil) if err != nil { return Manifest{}, fmt.Errorf("failed to create request: %w", err)