Skip to content

Commit dd810f5

Browse files
akoclaude
andcommitted
feat: add setup mxcli subcommand to download platform-specific binary
Adds `mxcli setup mxcli` to download the correct mxcli binary from GitHub releases, solving the issue where Windows users get a non-functional binary inside Linux devcontainers (#91). - Defaults to linux/amd64, auto-detects release tag from running version - Flags: --os, --arch, --output, --tag, --repo, --dry-run - Updates devcontainer postCreateCommand to auto-download if binary is missing or wrong platform - Warns Windows users during `mxcli init` about platform mismatch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8d81345 commit dd810f5

File tree

3 files changed

+132
-1
lines changed

3 files changed

+132
-1
lines changed

cmd/mxcli/init.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,12 @@ Container Runtime:
464464
} else {
465465
fmt.Println("\nCreated .devcontainer/ configuration")
466466
}
467+
if runtime.GOOS == "windows" {
468+
fmt.Println("\n⚠ You are running on Windows. The devcontainer is Linux-based,")
469+
fmt.Println(" so the Windows mxcli.exe will not work inside it.")
470+
fmt.Println(" The devcontainer will auto-download the correct Linux binary on first start.")
471+
fmt.Println(" Or run: mxcli setup mxcli --os linux --output ./mxcli")
472+
}
467473
}
468474

469475
// Create .playwright/cli.config.json for playwright-cli

cmd/mxcli/setup.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ package main
44

55
import (
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{
2023
Subcommands:
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
2428
Examples:
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, "\nmxcli ready: %s\n", output)
276+
},
277+
}
278+
162279
func 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
}

cmd/mxcli/tool_templates.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ func generateDevcontainerJSON(projectName, mprPath, containerRuntime string) str
295295
"containerEnv": {
296296
%s
297297
},
298-
"postCreateCommand": "curl -fsSL https://claude.ai/install.sh | bash && if [ -f ./mxcli ] && ! file ./mxcli | grep -q Linux; then echo './mxcli is not a Linux binary. Replace it with the linux-amd64 or linux-arm64 build.'; fi",
298+
"postCreateCommand": "curl -fsSL https://claude.ai/install.sh | bash && if [ -f ./mxcli ] && file ./mxcli | grep -q Linux; then echo 'mxcli binary OK'; else ./mxcli setup mxcli --output ./mxcli 2>/dev/null || { ARCH=$(uname -m); [ \"$ARCH\" = x86_64 ] && ARCH=amd64; [ \"$ARCH\" = aarch64 ] && ARCH=arm64; curl -fsSL https://github.com/mendixlabs/mxcli/releases/latest/download/mxcli-linux-${ARCH} -o ./mxcli && chmod +x ./mxcli; }; fi",
299299
"customizations": {
300300
"vscode": {
301301
"extensions": [

0 commit comments

Comments
 (0)