@@ -4,8 +4,11 @@ package main
44
55import (
66 "fmt"
7+ "io"
8+ "net/http"
79 "os"
810 "runtime"
11+ "strings"
912
1013 "github.com/mendixlabs/mxcli/cmd/mxcli/docker"
1114 "github.com/mendixlabs/mxcli/sdk/mpr"
@@ -20,12 +23,14 @@ var setupCmd = &cobra.Command{
2023Subcommands:
2124 mxbuild Download MxBuild for the project's Mendix version
2225 mxruntime Download the Mendix runtime for the project's Mendix version
26+ mxcli Download an mxcli binary from GitHub releases
2327
2428Examples:
2529 mxcli setup mxbuild -p app.mpr
2630 mxcli setup mxbuild --version 11.6.3
2731 mxcli setup mxruntime -p app.mpr
2832 mxcli setup mxruntime --version 11.6.3
33+ mxcli setup mxcli --os linux --arch amd64
2934` ,
3035}
3136
@@ -159,14 +164,134 @@ Examples:
159164 },
160165}
161166
167+ // mxcliBinaryURL returns the GitHub releases download URL for an mxcli binary.
168+ // ver should be a release tag like "v0.4.0" or "nightly".
169+ // targetOS is "linux", "darwin", or "windows". targetArch is "amd64" or "arm64".
170+ func mxcliBinaryURL (repo , ver , targetOS , targetArch string ) string {
171+ name := fmt .Sprintf ("mxcli-%s-%s" , targetOS , targetArch )
172+ if targetOS == "windows" {
173+ name += ".exe"
174+ }
175+ return fmt .Sprintf ("https://github.com/%s/releases/download/%s/%s" , repo , ver , name )
176+ }
177+
178+ // mxcliReleaseTag returns the release tag that matches the running binary's
179+ // version string. Tagged releases use "vX.Y.Z"; nightly builds contain
180+ // "nightly" and map to the "nightly" release tag.
181+ func mxcliReleaseTag () string {
182+ v := version // package-level var set from ldflags
183+ if strings .Contains (v , "nightly" ) {
184+ return "nightly"
185+ }
186+ if ! strings .HasPrefix (v , "v" ) {
187+ v = "v" + v
188+ }
189+ // Strip build metadata after first hyphen-with-commit (e.g. "v0.4.0-3-gabcdef" -> "v0.4.0")
190+ if idx := strings .IndexByte (v , '-' ); idx > 0 {
191+ v = v [:idx ]
192+ }
193+ return v
194+ }
195+
196+ // downloadMxcliBinary downloads the mxcli binary for the given OS/arch from
197+ // GitHub releases and writes it to outputPath with executable permissions.
198+ func downloadMxcliBinary (repo , tag , targetOS , targetArch , outputPath string , w io.Writer ) error {
199+ url := mxcliBinaryURL (repo , tag , targetOS , targetArch )
200+ fmt .Fprintf (w , "Downloading mxcli %s (%s/%s)...\n " , tag , targetOS , targetArch )
201+ fmt .Fprintf (w , " URL: %s\n " , url )
202+
203+ resp , err := http .Get (url )
204+ if err != nil {
205+ return fmt .Errorf ("downloading mxcli: %w" , err )
206+ }
207+ defer resp .Body .Close ()
208+
209+ if resp .StatusCode != http .StatusOK {
210+ return fmt .Errorf ("downloading mxcli: HTTP %d from %s" , resp .StatusCode , url )
211+ }
212+
213+ if resp .ContentLength > 0 {
214+ fmt .Fprintf (w , " Size: %.1f MB\n " , float64 (resp .ContentLength )/ (1024 * 1024 ))
215+ }
216+
217+ f , err := os .OpenFile (outputPath , os .O_CREATE | os .O_WRONLY | os .O_TRUNC , 0755 )
218+ if err != nil {
219+ return fmt .Errorf ("creating output file: %w" , err )
220+ }
221+ defer f .Close ()
222+
223+ if _ , err := io .Copy (f , resp .Body ); err != nil {
224+ os .Remove (outputPath )
225+ return fmt .Errorf ("writing binary: %w" , err )
226+ }
227+
228+ fmt .Fprintf (w , " Saved to %s\n " , outputPath )
229+ return nil
230+ }
231+
232+ var setupMxcliCmd = & cobra.Command {
233+ Use : "mxcli" ,
234+ Short : "Download an mxcli binary from GitHub releases" ,
235+ Long : `Download an mxcli binary for a specific OS/architecture from GitHub releases.
236+
237+ By default, downloads the version matching the currently running binary for
238+ linux/amd64 — the typical target for devcontainers.
239+
240+ Examples:
241+ mxcli setup mxcli # Linux amd64 binary to ./mxcli
242+ mxcli setup mxcli --output /usr/local/bin/mxcli
243+ mxcli setup mxcli --os darwin --arch arm64 # macOS Apple Silicon
244+ mxcli setup mxcli --tag v0.4.0 # Specific release
245+ mxcli setup mxcli --tag nightly # Latest nightly build
246+ ` ,
247+ Run : func (cmd * cobra.Command , args []string ) {
248+ targetOS , _ := cmd .Flags ().GetString ("os" )
249+ targetArch , _ := cmd .Flags ().GetString ("arch" )
250+ output , _ := cmd .Flags ().GetString ("output" )
251+ tag , _ := cmd .Flags ().GetString ("tag" )
252+ repo , _ := cmd .Flags ().GetString ("repo" )
253+ dryRun , _ := cmd .Flags ().GetBool ("dry-run" )
254+
255+ if tag == "" {
256+ tag = mxcliReleaseTag ()
257+ }
258+
259+ if dryRun {
260+ url := mxcliBinaryURL (repo , tag , targetOS , targetArch )
261+ fmt .Fprintf (os .Stdout , "Dry run:\n " )
262+ fmt .Fprintf (os .Stdout , " Tag: %s\n " , tag )
263+ fmt .Fprintf (os .Stdout , " OS: %s\n " , targetOS )
264+ fmt .Fprintf (os .Stdout , " Arch: %s\n " , targetArch )
265+ fmt .Fprintf (os .Stdout , " URL: %s\n " , url )
266+ fmt .Fprintf (os .Stdout , " Output: %s\n " , output )
267+ return
268+ }
269+
270+ if err := downloadMxcliBinary (repo , tag , targetOS , targetArch , output , os .Stdout ); err != nil {
271+ fmt .Fprintf (os .Stderr , "Error: %v\n " , err )
272+ os .Exit (1 )
273+ }
274+
275+ fmt .Fprintf (os .Stdout , "\n mxcli ready: %s\n " , output )
276+ },
277+ }
278+
162279func init () {
163280 setupMxBuildCmd .Flags ().String ("version" , "" , "Mendix version to download (e.g., 11.6.3)" )
164281 setupMxBuildCmd .Flags ().Bool ("dry-run" , false , "Show what would be downloaded without downloading" )
165282
166283 setupMxRuntimeCmd .Flags ().String ("version" , "" , "Mendix version to download (e.g., 11.6.3)" )
167284 setupMxRuntimeCmd .Flags ().Bool ("dry-run" , false , "Show what would be downloaded without downloading" )
168285
286+ setupMxcliCmd .Flags ().String ("os" , "linux" , "Target operating system (linux, darwin, windows)" )
287+ setupMxcliCmd .Flags ().String ("arch" , "amd64" , "Target architecture (amd64, arm64)" )
288+ setupMxcliCmd .Flags ().String ("output" , "./mxcli" , "Output file path" )
289+ setupMxcliCmd .Flags ().String ("tag" , "" , "Release tag to download (default: match running version)" )
290+ setupMxcliCmd .Flags ().String ("repo" , "mendixlabs/mxcli" , "GitHub repository" )
291+ setupMxcliCmd .Flags ().Bool ("dry-run" , false , "Show what would be downloaded without downloading" )
292+
169293 setupCmd .AddCommand (setupMxBuildCmd )
170294 setupCmd .AddCommand (setupMxRuntimeCmd )
295+ setupCmd .AddCommand (setupMxcliCmd )
171296 rootCmd .AddCommand (setupCmd )
172297}
0 commit comments