Skip to content

Commit e5a219c

Browse files
author
Claude Dev
committed
wip
1 parent 345b09c commit e5a219c

10 files changed

Lines changed: 1961 additions & 10 deletions

File tree

.claude/skills/mendix/generate-domain-model.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ create persistent entity Module.Photo (
201201
) extends System.Image;
202202
```
203203

204-
**Note:** `mxcli syntax entity` output may show EXTENDS after `)` — this is misleading. Always place EXTENDS before `(`.
204+
**Note:** EXTENDS must always be placed **before** the opening parenthesis — never after `)`. The `mxcli syntax` reference now correctly shows `[EXTENDS Module.Parent]` in the primary template before `(`.
205205

206206
#### System Attributes (Auditing)
207207

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ TEST_PARALLEL ?= $(_85PCT)
5454
# Hard ceiling on how long the full test suite may run.
5555
TEST_TIMEOUT ?= 180s
5656

57-
.PHONY: build mdlrun build-local install-local build-debug release release-launcher release-daemon release-local-bins clean test _test-inner test-mdl report report-bench report-reset-baseline bench-baseline grammar sync-skills sync-commands sync-lint-rules sync-changelog sync-examples sync-all docs documentation docs-site docs-serve source-tree sbom sbom-report lint lint-go fmt vet update-helpdesk-golden test-helpdesk-regression setup install install-daemon install-global test-section-check update-snapshots validate-snapshots validate-academy-capstone test-integration-profiled test-profile-check test-profile-record
57+
.PHONY: build mdlrun build-local install-local build-debug release release-mxcli release-win-amd64 release-launcher release-daemon release-local-bins clean test _test-inner test-mdl report report-bench report-reset-baseline bench-baseline grammar sync-skills sync-commands sync-lint-rules sync-changelog sync-examples sync-all docs documentation docs-site docs-serve source-tree sbom sbom-report lint lint-go fmt vet update-helpdesk-golden test-helpdesk-regression setup install install-daemon install-global test-section-check update-snapshots validate-snapshots validate-academy-capstone test-integration-profiled test-profile-check test-profile-record
5858

5959
setup:
6060
git config core.hooksPath .githooks
@@ -196,6 +196,12 @@ release-mxcli: sync-all
196196
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build $(RELEASE_LDFLAGS) -trimpath -o $(BUILD_DIR)/$(BINARY_NAME)-windows-arm64.exe $(CMD_PATH)
197197
@echo "mxcli binaries built in $(BUILD_DIR)/."
198198

199+
# Build mxcli for Windows amd64 only.
200+
release-win-amd64: sync-all
201+
@mkdir -p $(BUILD_DIR)
202+
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build $(RELEASE_LDFLAGS) -trimpath -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(CMD_PATH)
203+
@echo "Built $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe"
204+
199205
# Run tests. TEST_P/TEST_PARALLEL default to 85% nproc.
200206
# Uses nice(1) — NOT cpulimit(1), whose SIGSTOP/SIGCONT breaks Go's runtime.
201207
test: test-showcase

cmd/mxcli/cmd_new.go

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import (
77
"os"
88
"os/exec"
99
"path/filepath"
10+
"regexp"
1011
"runtime"
12+
"sort"
13+
"strings"
1114

1215
"github.com/mendixlabs/mxcli/cmd/mxcli/docker"
1316
"github.com/spf13/cobra"
@@ -28,13 +31,26 @@ Examples:
2831
mxcli new MyApp
2932
mxcli new MyApp --version 11.8.0
3033
mxcli new MyApp --version 10.24.0 --output-dir ./projects/my-app
34+
mxcli new --list-versions
3135
`,
32-
Args: cobra.ExactArgs(1),
36+
Args: cobra.MaximumNArgs(1),
3337
Run: func(cmd *cobra.Command, args []string) {
38+
listVersions, _ := cmd.Flags().GetBool("list-versions")
39+
if listVersions {
40+
listMendixVersions()
41+
return
42+
}
43+
44+
if len(args) < 1 {
45+
fmt.Fprintln(os.Stderr, "Error: app name is required")
46+
fmt.Fprintln(os.Stderr, "Usage: mxcli new <app-name> [--version X.Y.Z]")
47+
os.Exit(1)
48+
}
3449
appName := args[0]
3550
mendixVersion, _ := cmd.Flags().GetString("version")
3651
outputDir, _ := cmd.Flags().GetString("output-dir")
3752
skipInit, _ := cmd.Flags().GetBool("skip-init")
53+
force, _ := cmd.Flags().GetBool("force")
3854

3955
if mendixVersion == "" {
4056
fmt.Fprintln(os.Stderr, "Error: --version is required (e.g., --version 11.8.0)")
@@ -53,8 +69,13 @@ Examples:
5369

5470
// Check if directory already exists and has content
5571
if entries, err := os.ReadDir(absDir); err == nil && len(entries) > 0 {
56-
fmt.Fprintf(os.Stderr, "Error: directory %s already exists and is not empty\n", absDir)
57-
os.Exit(1)
72+
if force {
73+
fmt.Printf(" Directory %s is not empty (--force), proceeding...\n", absDir)
74+
} else {
75+
fmt.Fprintf(os.Stderr, "Error: directory %s already exists and is not empty\n", absDir)
76+
fmt.Fprintf(os.Stderr, " Use --force to override, or choose a different --output-dir\n")
77+
os.Exit(1)
78+
}
5879
}
5980

6081
// Step 1: Resolve mx binary.
@@ -154,6 +175,96 @@ Examples:
154175
},
155176
}
156177

178+
// listMendixVersions lists available Mendix versions from all sources.
179+
func listMendixVersions() {
180+
all := map[string]string{} // version → source label
181+
add := func(version, source string) {
182+
if version != "" {
183+
all[version] = source
184+
}
185+
}
186+
187+
// 1. Cached downloads (~/.mxcli/mxbuild/<version>/)
188+
for _, v := range allCachedMxBuildVersions() {
189+
add(v, "cached download")
190+
}
191+
192+
// 2. Windows: C:\Program Files\Mendix\<version>\modeler\mxbuild.exe
193+
if runtime.GOOS == "windows" {
194+
for _, env := range []string{"PROGRAMFILES", "PROGRAMW6432", "PROGRAMFILES(X86)"} {
195+
if d := os.Getenv(env); d != "" {
196+
entries, _ := os.ReadDir(filepath.Join(d, "Mendix"))
197+
for _, e := range entries {
198+
if e.IsDir() {
199+
if _, err := os.Stat(filepath.Join(d, "Mendix", e.Name(), "modeler", "mxbuild.exe")); err == nil {
200+
add(e.Name(), "Studio Pro")
201+
}
202+
}
203+
}
204+
}
205+
}
206+
if sd := os.Getenv("SystemDrive"); sd != "" {
207+
root := sd + string(os.PathSeparator)
208+
for _, dir := range []string{"Program Files", "Program Files (x86)"} {
209+
entries, _ := os.ReadDir(filepath.Join(root, dir, "Mendix"))
210+
for _, e := range entries {
211+
if e.IsDir() {
212+
if _, err := os.Stat(filepath.Join(root, dir, "Mendix", e.Name(), "modeler", "mxbuild.exe")); err == nil {
213+
add(e.Name(), "Studio Pro")
214+
}
215+
}
216+
}
217+
}
218+
}
219+
}
220+
221+
// 3. macOS: /Applications/Mendix Studio Pro *.app
222+
if runtime.GOOS == "darwin" {
223+
matches, _ := filepath.Glob("/Applications/Mendix Studio Pro *.app")
224+
re := regexp.MustCompile(`^Mendix Studio Pro (\d+\.\d+\.\d+)`)
225+
for _, match := range matches {
226+
base := strings.TrimSuffix(filepath.Base(match), ".app")
227+
if m := re.FindStringSubmatch(base); m != nil {
228+
add(m[1], "Studio Pro")
229+
}
230+
}
231+
}
232+
233+
if len(all) == 0 {
234+
fmt.Println("No Mendix versions found.")
235+
fmt.Println()
236+
fmt.Println("To find available Mendix versions, visit:")
237+
fmt.Println(" https://docs.mendix.com/releasenotes/studio-pro/")
238+
fmt.Println()
239+
fmt.Println("Usage:")
240+
fmt.Println(" mxcli new MyApp --version X.Y.Z")
241+
fmt.Println()
242+
fmt.Println("mxcli automatically downloads the required version on first use.")
243+
return
244+
}
245+
246+
var versions []string
247+
for v := range all {
248+
versions = append(versions, v)
249+
}
250+
sort.Slice(versions, func(i, j int) bool {
251+
// Prefer newer versions first (simple semver comparison by string).
252+
// This works for X.Y.Z format when all are the same length.
253+
return versions[i] > versions[j]
254+
})
255+
256+
fmt.Println("Available Mendix versions:")
257+
for _, v := range versions {
258+
fmt.Printf(" %-12s (%s)\n", v, all[v])
259+
}
260+
fmt.Println()
261+
fmt.Println("Usage:")
262+
fmt.Println(" mxcli new MyApp --version X.Y.Z")
263+
fmt.Println()
264+
fmt.Println("Cached downloads are stored in ~/.mxcli/mxbuild/<version>/.")
265+
fmt.Println("mxcli automatically downloads the required version on first use.")
266+
}
267+
157268
// cleanupDuplicateLocaleFiles removes duplicate locale files that mx create-project
158269
// generates in themesource/atlas_core/. MxBuild crashes when multiple translation.json
159270
// files map to the same locale key (e.g., "en-US").
@@ -201,9 +312,11 @@ func cleanupDuplicateLocaleFiles(projectDir string) int {
201312
}
202313

203314
func init() {
315+
newCmd.Flags().Bool("list-versions", false, "List cached Mendix versions and show how to find available ones")
204316
newCmd.Flags().String("version", "", "Mendix version (e.g., 11.8.0) — required")
205317
newCmd.Flags().String("output-dir", "", "Output directory (default: ./<app-name>)")
206318
newCmd.Flags().Bool("skip-init", false, "Skip AI tooling initialization (mxcli init)")
319+
newCmd.Flags().Bool("force", false, "Allow creating project in a non-empty directory")
207320

208321
rootCmd.AddCommand(newCmd)
209322
}

cmd/mxcli/init_claudemd.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,25 @@ func generateClaudeMD(projectName, mprFile string, inDevcontainer bool) string {
289289
w(bt3 + "bash\n./mxcli check script.mdl -p " + mprPath + " --references\n" + bt3 + "\n\n")
290290
w("The reference checker is smart - it automatically skips references to objects that are created within the same script.\n\n")
291291

292+
// ── Build & Run ──────────────────────────────────────────────────
293+
w("## Build & Run the App\n\n")
294+
w("After making MDL changes, build and run the app locally (no Docker required):\n\n")
295+
w(bt3 + "bash\n")
296+
w("# Build PAD package (compiles SCSS, runs mxbuild)\n./mxcli build -p " + mprPath + "\n\n")
297+
w("# Start the app (HSQLDB embedded)\n./mxcli run -p " + mprPath + " --admin-password MyPassword\n")
298+
w(bt3 + "\n\n")
299+
w("The app is available at **http://localhost:8080**. Login: " + bt + "MxAdmin" + bt + " / " + bt + "MyPassword" + bt + ".\n\n")
300+
w("### Hot Reload\n\n")
301+
w("After applying MDL changes while the app is running, use reload (no restart needed):\n\n")
302+
w(bt3 + "bash\n")
303+
w("./mxcli exec changes.mdl -p " + mprPath + " # Apply MDL changes\n")
304+
w("./mxcli reload -p " + mprPath + " # Build + hot reload (~55s)\n")
305+
w("./mxcli reload -p " + mprPath + " --model-only # Reload only (~100ms)\n")
306+
w(bt3 + "\n\n")
307+
w("### Use PostgreSQL (instead of HSQLDB)\n\n")
308+
w(bt3 + "bash\n./mxcli run -p " + mprPath + " --db postgres://user:pass@localhost:5432/mendix\n" + bt3 + "\n\n")
309+
w("See the " + bt + "run-app" + bt + " skill for Docker-based workflows and detailed options.\n\n")
310+
292311
// ── Linting ─────────────────────────────────────────────────────
293312
w("## Linting\n\n")
294313
w("Check your project for common issues:\n\n")

cmd/mxcli/setup.go

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,18 +285,19 @@ Examples:
285285

286286
// setupCompletionsCmd installs shell completion scripts.
287287
var setupCompletionsCmd = &cobra.Command{
288-
Use: "completions [bash|zsh|fish]",
288+
Use: "completions [bash|zsh|fish|powershell]",
289289
Short: "Install shell completion scripts for bash/zsh/fish/powershell",
290290
Args: cobra.MaximumNArgs(1),
291-
ValidArgs: []string{"bash", "zsh", "fish"},
291+
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
292292
RunE: func(cmd *cobra.Command, args []string) error {
293293
shell := detectShell()
294294
if len(args) > 0 {
295295
shell = args[0]
296296
}
297+
297298
path := completionPath(shell)
298299
if path == "" {
299-
return fmt.Errorf("unsupported shell: %s (try: mxcli setup completions bash|zsh|fish, or mxcli completion bash|zsh|fish|powershell)", shell)
300+
return fmt.Errorf("unsupported shell: %s (supported: bash, zsh, fish, powershell)", shell)
300301
}
301302

302303
dir := filepath.Dir(path)
@@ -323,6 +324,15 @@ var setupCompletionsCmd = &cobra.Command{
323324
if err := rootCmd.GenFishCompletion(f, true); err != nil {
324325
return err
325326
}
327+
case "powershell":
328+
if err := rootCmd.GenPowerShellCompletionWithDesc(f); err != nil {
329+
return err
330+
}
331+
if msg, err := installPowerShellCompletion(path); err != nil {
332+
fmt.Fprintf(os.Stderr, " Warning: %v\n", err)
333+
} else if msg != "" {
334+
fmt.Fprint(os.Stdout, msg)
335+
}
326336
}
327337
fmt.Fprintf(os.Stdout, "Shell completions installed: %s\n", path)
328338
fmt.Fprintf(os.Stdout, "Restart your shell or run: source %s\n", path)
@@ -358,8 +368,64 @@ func completionPath(shell string) string {
358368
return filepath.Join(home, ".zsh", "completions", "_mxcli")
359369
case "fish":
360370
return filepath.Join(home, ".config", "fish", "completions", "mxcli.fish")
371+
case "powershell":
372+
return filepath.Join(home, ".config", "powershell", "completions", "mxcli.ps1")
361373
}
362374
return ""
375+
}
376+
377+
// installPowerShellCompletion adds a dot-source line to the PowerShell profile
378+
// to load the completion script. Idempotent: skips if the marker already exists.
379+
func installPowerShellCompletion(completionPath string) (string, error) {
380+
home, err := os.UserHomeDir()
381+
if err != nil {
382+
return "", fmt.Errorf("find home dir: %w", err)
383+
}
384+
385+
// Determine profile path: prefer PowerShell 7, fall back to 5.1 on Windows.
386+
profilePaths := []string{
387+
filepath.Join(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
388+
}
389+
if runtime.GOOS == "windows" {
390+
profilePaths = []string{
391+
filepath.Join(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1"),
392+
filepath.Join(home, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1"),
393+
}
394+
}
395+
396+
dotSourceLine := ". \"" + completionPath + "\""
397+
marker := "# mxcli completion"
398+
399+
for _, profilePath := range profilePaths {
400+
// Read existing profile.
401+
data, err := os.ReadFile(profilePath)
402+
if err != nil {
403+
if !os.IsNotExist(err) {
404+
continue // permission error, try next path
405+
}
406+
// Profile doesn't exist — create directory.
407+
if err := os.MkdirAll(filepath.Dir(profilePath), 0755); err != nil {
408+
continue
409+
}
410+
} else if strings.Contains(string(data), marker) {
411+
// Already installed — skip silently for all paths.
412+
return "", nil
413+
}
414+
415+
// Append with marker block.
416+
block := "\n" + marker + "\n" + dotSourceLine + "\n# mxcli completion (end)\n"
417+
f, err := os.OpenFile(profilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
418+
if err != nil {
419+
continue
420+
}
421+
if _, err := f.WriteString(block); err != nil {
422+
f.Close()
423+
continue
424+
}
425+
f.Close()
426+
return fmt.Sprintf(" Added to PowerShell profile: %s\n", profilePath), nil
427+
}
428+
return "", nil
363429
}
364430

365431
func init() {

cmd/mxcli/syntax/features_domain_model.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func init() {
2424
"entity", "create entity", "persistent", "non-persistent",
2525
"generalization", "extends", "event handler", "attribute",
2626
},
27-
Syntax: "CREATE PERSISTENT ENTITY Module.Name (\n Attr: Type [constraints],\n ...\n) [INDEX (attr1)] [COMMENT 'text'];\n\nCREATE NON_PERSISTENT ENTITY Module.Name (...);\n\nCREATE PERSISTENT ENTITY Module.Name EXTENDS Module.Parent (...);",
27+
Syntax: "CREATE PERSISTENT ENTITY Module.Name [EXTENDS Module.Parent] (\n Attr: Type [constraints],\n ...\n) [INDEX (attr1)] [COMMENT 'text'];\n\nCREATE NON_PERSISTENT ENTITY Module.Name (...);\n\nCREATE PERSISTENT ENTITY Module.Name EXTENDS Module.Parent (...);",
2828
Example: "CREATE PERSISTENT ENTITY MyModule.Customer (\n Name: String(100) NOT NULL ERROR 'Name is required',\n Email: String(200) UNIQUE,\n Balance: Decimal DEFAULT 0,\n IsActive: Boolean DEFAULT true,\n Status: Enumeration(MyModule.CustomerType)\n)\nINDEX (Email)\nCOMMENT 'Stores customer information';",
2929
SeeAlso: []string{"domain-model.entity.create", "domain-model.entity.alter", "domain-model.entity.attributes"},
3030
})
@@ -37,7 +37,7 @@ func init() {
3737
"non-persistent", "extends", "generalization",
3838
"index", "event handler", "before commit", "after commit",
3939
},
40-
Syntax: "CREATE PERSISTENT ENTITY Module.Name (\n Attr: Type [NOT NULL [ERROR 'msg']] [UNIQUE [ERROR 'msg']] [DEFAULT val],\n ...\n)\n[INDEX (attr1, attr2)]\n[ON BEFORE|AFTER CREATE|COMMIT|DELETE|ROLLBACK CALL Module.MF [RAISE ERROR]]\n[COMMENT 'text'];\n\nCREATE NON_PERSISTENT ENTITY Module.Name (...);\nCREATE PERSISTENT ENTITY Module.Name EXTENDS Module.Parent (...);",
40+
Syntax: "CREATE PERSISTENT ENTITY Module.Name [EXTENDS Module.Parent] (\n Attr: Type [NOT NULL [ERROR 'msg']] [UNIQUE [ERROR 'msg']] [DEFAULT val],\n ...\n)\n[INDEX (attr1, attr2)]\n[ON BEFORE|AFTER CREATE|COMMIT|DELETE|ROLLBACK CALL Module.MF [RAISE ERROR]]\n[COMMENT 'text'];\n\nCREATE NON_PERSISTENT ENTITY Module.Name (...);\nCREATE PERSISTENT ENTITY Module.Name EXTENDS Module.Parent (...);",
4141
Example: "-- Persistent with constraints and index\nCREATE PERSISTENT ENTITY Shop.Order (\n OrderNumber: String(20) NOT NULL,\n Total: Decimal DEFAULT 0,\n CreatedAt: DateTime\n)\nINDEX (OrderNumber)\nON BEFORE COMMIT CALL Shop.ValidateOrder($currentObject) RAISE ERROR;\n\n-- With generalization\nCREATE PERSISTENT ENTITY Shop.ProductImage EXTENDS System.Image (\n Caption: String(200)\n);",
4242
SeeAlso: []string{"domain-model.entity.alter", "domain-model.entity.attributes"},
4343
})

0 commit comments

Comments
 (0)