From f1064e6cd4fb328d6b416bb003915a80dda5c623 Mon Sep 17 00:00:00 2001 From: Andrew Vasilyev Date: Fri, 27 Mar 2026 17:17:07 +0100 Subject: [PATCH 1/3] feat(init): add OpenCode tool support with skills, commands, and lint rules - Add 'opencode' to SupportedTools map in tool_templates.go with generateOpenCodeConfig() - Write skills to .opencode/skills//SKILL.md with YAML frontmatter - Write commands to .opencode/commands/ - Write lint rules to .claude/lint-rules/ (shared path used by mxcli lint) - Write opencode.json pointing to AGENTS.md and .ai-context/skills/ - Install VS Code extension for opencode tool same as claude - Add wrapSkillContent() and extractSkillDescription() helpers - Update cmd_add_tool.go Long description to list opencode - Add 13 tests (unit + integration) in init_test.go --- cmd/mxcli/cmd_add_tool.go | 1 + cmd/mxcli/init.go | 151 +++++++++++++++- cmd/mxcli/init_test.go | 336 ++++++++++++++++++++++++++++++++++++ cmd/mxcli/tool_templates.go | 21 +++ 4 files changed, 507 insertions(+), 2 deletions(-) create mode 100644 cmd/mxcli/init_test.go diff --git a/cmd/mxcli/cmd_add_tool.go b/cmd/mxcli/cmd_add_tool.go index ff3fefb..5dbd533 100644 --- a/cmd/mxcli/cmd_add_tool.go +++ b/cmd/mxcli/cmd_add_tool.go @@ -34,6 +34,7 @@ Supported Tools: - continue Continue.dev with custom commands - windsurf Windsurf (Codeium) with MDL rules - aider Aider with project configuration + - opencode OpenCode AI agent with MDL commands and skills `, Args: cobra.RangeArgs(0, 2), Run: func(cmd *cobra.Command, args []string) { diff --git a/cmd/mxcli/init.go b/cmd/mxcli/init.go index c0d0c6a..949312b 100644 --- a/cmd/mxcli/init.go +++ b/cmd/mxcli/init.go @@ -52,6 +52,7 @@ Supported Tools: - continue Continue.dev with custom commands - windsurf Windsurf (Codeium) with MDL rules - aider Aider with project configuration + - opencode OpenCode AI agent with MDL commands and skills All tools receive universal documentation in AGENTS.md and .ai-context/ `, @@ -143,6 +144,35 @@ All tools receive universal documentation in AGENTS.md and .ai-context/ } } + // Create .opencode directory for OpenCode-specific content (if OpenCode is selected) + var opencodeCommandsDir, opencodeSkillsDir string + if slices.Contains(tools, "opencode") { + opencodeDir := filepath.Join(absDir, ".opencode") + opencodeCommandsDir = filepath.Join(opencodeDir, "commands") + opencodeSkillsDir = filepath.Join(opencodeDir, "skills") + + if err := os.MkdirAll(opencodeCommandsDir, 0755); err != nil { + fmt.Fprintf(os.Stderr, "Error creating .opencode/commands directory: %v\n", err) + os.Exit(1) + } + if err := os.MkdirAll(opencodeSkillsDir, 0755); err != nil { + fmt.Fprintf(os.Stderr, "Error creating .opencode/skills directory: %v\n", err) + os.Exit(1) + } + + // Lint rules stay in .claude/lint-rules/ (read by mxcli lint). + // Ensure that directory exists even when claude tool is not selected. + if !slices.Contains(tools, "claude") { + if lintRulesDir == "" { + lintRulesDir = filepath.Join(absDir, ".claude", "lint-rules") + } + if err := os.MkdirAll(lintRulesDir, 0755); err != nil { + fmt.Fprintf(os.Stderr, "Error creating .claude/lint-rules directory: %v\n", err) + os.Exit(1) + } + } + } + // Write universal skills to .ai-context/skills/ skillCount := 0 err = fs.WalkDir(skillsFS, "skills", func(path string, d fs.DirEntry, err error) error { @@ -254,6 +284,97 @@ All tools receive universal documentation in AGENTS.md and .ai-context/ fmt.Printf(" Created %d lint rule files in .claude/lint-rules/\n", lintRuleCount) } } + + // OpenCode-specific: write commands, lint rules, and skills + if toolName == "opencode" && opencodeCommandsDir != "" { + cmdCount := 0 + err = fs.WalkDir(commandsFS, "commands", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + content, err := commandsFS.ReadFile(path) + if err != nil { + return err + } + targetPath := filepath.Join(opencodeCommandsDir, d.Name()) + if err := os.WriteFile(targetPath, content, 0644); err != nil { + return err + } + cmdCount++ + return nil + }) + if err != nil { + fmt.Fprintf(os.Stderr, " Error writing OpenCode commands: %v\n", err) + } else { + fmt.Printf(" Created %d command files in .opencode/commands/\n", cmdCount) + } + + lintRuleCount := 0 + err = fs.WalkDir(lintRulesFS, "lint-rules", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + content, err := lintRulesFS.ReadFile(path) + if err != nil { + return err + } + targetPath := filepath.Join(lintRulesDir, d.Name()) + if err := os.WriteFile(targetPath, content, 0644); err != nil { + return err + } + lintRuleCount++ + return nil + }) + if err != nil { + fmt.Fprintf(os.Stderr, " Error writing lint rules: %v\n", err) + } else { + fmt.Printf(" Created %d lint rule files in .claude/lint-rules/\n", lintRuleCount) + } + + skillCount2 := 0 + err = fs.WalkDir(skillsFS, "skills", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + // Skip README + if d.Name() == "README.md" { + return nil + } + content, err := skillsFS.ReadFile(path) + if err != nil { + return err + } + // Derive skill name from filename (strip .md) + skillName := strings.TrimSuffix(d.Name(), ".md") + // Create per-skill subdirectory + skillDir := filepath.Join(opencodeSkillsDir, skillName) + if err := os.MkdirAll(skillDir, 0755); err != nil { + return err + } + // Wrap content with OpenCode frontmatter + wrapped := wrapSkillContent(skillName, content) + targetPath := filepath.Join(skillDir, "SKILL.md") + if err := os.WriteFile(targetPath, wrapped, 0644); err != nil { + return err + } + skillCount2++ + return nil + }) + if err != nil { + fmt.Fprintf(os.Stderr, " Error writing OpenCode skills: %v\n", err) + } else { + fmt.Printf(" Created %d skill directories in .opencode/skills/\n", skillCount2) + } + } } // Write universal AGENTS.md @@ -312,8 +433,8 @@ All tools receive universal documentation in AGENTS.md and .ai-context/ } } - // Install VS Code extension if Claude is selected - if slices.Contains(tools, "claude") { + // Install VS Code extension if Claude or OpenCode is selected + if slices.Contains(tools, "claude") || slices.Contains(tools, "opencode") { installVSCodeExtension(absDir) } @@ -340,6 +461,32 @@ All tools receive universal documentation in AGENTS.md and .ai-context/ }, } +// wrapSkillContent prepends OpenCode-compatible YAML frontmatter to a skill file. +// OpenCode requires each skill to live in its own subdirectory as SKILL.md and +// the file must start with YAML frontmatter containing name, description, and +// compatibility fields. +func wrapSkillContent(skillName string, content []byte) []byte { + description := extractSkillDescription(content) + frontmatter := fmt.Sprintf("---\nname: %s\ndescription: %s\ncompatibility: opencode\n---\n\n", skillName, description) + return append([]byte(frontmatter), content...) +} + +// extractSkillDescription returns a one-line description for the skill by +// finding the first top-level markdown heading (# ...) and stripping a leading +// "Skill: " prefix if present. Falls back to the skill name if no heading is +// found. +func extractSkillDescription(content []byte) string { + for _, line := range strings.Split(string(content), "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "# ") { + desc := strings.TrimPrefix(line, "# ") + desc = strings.TrimPrefix(desc, "Skill: ") + return strings.TrimSpace(desc) + } + } + return "MDL skill" +} + func findMprFile(dir string) string { entries, err := os.ReadDir(dir) if err != nil { diff --git a/cmd/mxcli/init_test.go b/cmd/mxcli/init_test.go new file mode 100644 index 0000000..1d578b9 --- /dev/null +++ b/cmd/mxcli/init_test.go @@ -0,0 +1,336 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// runInit is a test helper that sets initTools to the given list, invokes the +// cobra Run closure against dir, then restores the original value. The +// function panics if initListTools or initAllTools are left non-default from a +// previous test. +func runInit(t *testing.T, tools []string, dir string) { + t.Helper() + prev := initTools + t.Cleanup(func() { initTools = prev }) + initTools = tools + initCmd.Run(initCmd, []string{dir}) +} + +// ── Unit tests: extractSkillDescription ────────────────────────────────────── + +func TestExtractSkillDescription_FirstHeading(t *testing.T) { + content := []byte("# My Skill\n\nSome content here.\n") + got := extractSkillDescription(content) + if got != "My Skill" { + t.Errorf("got %q, want %q", got, "My Skill") + } +} + +func TestExtractSkillDescription_SkillPrefix(t *testing.T) { + content := []byte("# Skill: Write Microflows\n\nContent.\n") + got := extractSkillDescription(content) + if got != "Write Microflows" { + t.Errorf("got %q, want %q", got, "Write Microflows") + } +} + +func TestExtractSkillDescription_Fallback(t *testing.T) { + content := []byte("No heading here.\nJust plain text.\n") + got := extractSkillDescription(content) + if got != "MDL skill" { + t.Errorf("got %q, want %q", got, "MDL skill") + } +} + +func TestExtractSkillDescription_IgnoresSubheadings(t *testing.T) { + content := []byte("## Section\n\n# Top Level\n") + got := extractSkillDescription(content) + if got != "Top Level" { + t.Errorf("got %q, want %q", got, "Top Level") + } +} + +func TestExtractSkillDescription_EmptyContent(t *testing.T) { + got := extractSkillDescription([]byte{}) + if got != "MDL skill" { + t.Errorf("got %q, want %q", got, "MDL skill") + } +} + +// ── Unit tests: wrapSkillContent ───────────────────────────────────────────── + +func TestWrapSkillContent_FrontmatterPresent(t *testing.T) { + content := []byte("# My Skill\n\nbody text\n") + wrapped := wrapSkillContent("my-skill", content) + text := string(wrapped) + + if !strings.HasPrefix(text, "---\n") { + t.Errorf("wrapped content should start with '---\\n', got: %q", text[:min(40, len(text))]) + } + if !strings.Contains(text, "name: my-skill\n") { + t.Error("frontmatter should contain 'name: my-skill'") + } + if !strings.Contains(text, "description: My Skill\n") { + t.Error("frontmatter should contain 'description: My Skill'") + } + if !strings.Contains(text, "compatibility: opencode\n") { + t.Error("frontmatter should contain 'compatibility: opencode'") + } +} + +func TestWrapSkillContent_OriginalContentPreserved(t *testing.T) { + body := "# My Skill\n\nThis is the skill body.\n" + wrapped := wrapSkillContent("my-skill", []byte(body)) + text := string(wrapped) + + if !strings.Contains(text, body) { + t.Errorf("wrapped content should contain original body; got:\n%s", text) + } +} + +func TestWrapSkillContent_FrontmatterClosedBeforeBody(t *testing.T) { + content := []byte("# Title\n\nbody\n") + wrapped := string(wrapSkillContent("s", content)) + + closingIdx := strings.Index(wrapped, "\n---\n") + bodyIdx := strings.Index(wrapped, "# Title") + + if closingIdx == -1 { + t.Fatal("closing '---' delimiter not found") + } + if bodyIdx < closingIdx { + t.Errorf("body appears before closing delimiter: closing=%d body=%d", closingIdx, bodyIdx) + } +} + +// ── Integration tests ───────────────────────────────────────────────────────── + +// fileExists is a small test helper. +func fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +// countFilesInDir returns the number of regular files directly inside dir. +func countFilesInDir(dir string) int { + entries, err := os.ReadDir(dir) + if err != nil { + return 0 + } + n := 0 + for _, e := range entries { + if !e.IsDir() { + n++ + } + } + return n +} + +// countSubDirs returns the number of subdirectories directly inside dir. +func countSubDirs(dir string) int { + entries, err := os.ReadDir(dir) + if err != nil { + return 0 + } + n := 0 + for _, e := range entries { + if e.IsDir() { + n++ + } + } + return n +} + +func TestInitOpenCode_CreatesRequiredStructure(t *testing.T) { + dir := t.TempDir() + runInit(t, []string{"opencode"}, dir) + + // opencode.json config + if !fileExists(filepath.Join(dir, "opencode.json")) { + t.Error("opencode.json should be created") + } + + // .opencode/commands/ with at least one command + commandsDir := filepath.Join(dir, ".opencode", "commands") + if n := countFilesInDir(commandsDir); n == 0 { + t.Error(".opencode/commands/ should contain command files") + } + + // .opencode/skills/ with at least one skill subdirectory + skillsDir := filepath.Join(dir, ".opencode", "skills") + if n := countSubDirs(skillsDir); n == 0 { + t.Error(".opencode/skills/ should contain skill subdirectories") + } + + // Lint rules stay in .claude/lint-rules/ for mxcli lint compatibility + lintDir := filepath.Join(dir, ".claude", "lint-rules") + if n := countFilesInDir(lintDir); n == 0 { + t.Error(".claude/lint-rules/ should contain lint rule files") + } + + // Universal files + if !fileExists(filepath.Join(dir, "AGENTS.md")) { + t.Error("AGENTS.md should be created") + } + if n := countFilesInDir(filepath.Join(dir, ".ai-context", "skills")); n == 0 { + t.Error(".ai-context/skills/ should contain skill files") + } + + // Claude-specific files should NOT be present + if fileExists(filepath.Join(dir, ".claude", "settings.json")) { + t.Error(".claude/settings.json should NOT be created for opencode-only init") + } + if fileExists(filepath.Join(dir, "CLAUDE.md")) { + t.Error("CLAUDE.md should NOT be created for opencode-only init") + } +} + +func TestInitOpenCode_EachSkillHasValidFrontmatter(t *testing.T) { + dir := t.TempDir() + runInit(t, []string{"opencode"}, dir) + + skillsDir := filepath.Join(dir, ".opencode", "skills") + entries, err := os.ReadDir(skillsDir) + if err != nil { + t.Fatalf("could not read .opencode/skills/: %v", err) + } + if len(entries) == 0 { + t.Fatal("no skill directories found — init may have failed") + } + + for _, e := range entries { + if !e.IsDir() { + continue + } + skillMD := filepath.Join(skillsDir, e.Name(), "SKILL.md") + data, err := os.ReadFile(skillMD) + if err != nil { + t.Errorf("skill %q: SKILL.md missing: %v", e.Name(), err) + continue + } + text := string(data) + if !strings.HasPrefix(text, "---\n") { + t.Errorf("skill %q: SKILL.md should start with YAML frontmatter '---'", e.Name()) + } + if !strings.Contains(text, "name: "+e.Name()) { + t.Errorf("skill %q: SKILL.md frontmatter should contain 'name: %s'", e.Name(), e.Name()) + } + if !strings.Contains(text, "compatibility: opencode") { + t.Errorf("skill %q: SKILL.md should contain 'compatibility: opencode'", e.Name()) + } + } +} + +func TestInitOpenCode_READMENotWrittenAsSkill(t *testing.T) { + dir := t.TempDir() + runInit(t, []string{"opencode"}, dir) + + readmeSkillDir := filepath.Join(dir, ".opencode", "skills", "README") + if fileExists(readmeSkillDir) { + t.Error("README should not be written as an OpenCode skill directory") + } +} + +func TestInitOpenCode_LintRulesCreatedWithoutClaudeTool(t *testing.T) { + dir := t.TempDir() + runInit(t, []string{"opencode"}, dir) // no "claude" + + lintDir := filepath.Join(dir, ".claude", "lint-rules") + if !fileExists(lintDir) { + t.Error(".claude/lint-rules/ should exist even when only opencode is selected") + } + if n := countFilesInDir(lintDir); n == 0 { + t.Error(".claude/lint-rules/ should contain .star lint rule files") + } + + // Verify they are .star files + entries, _ := os.ReadDir(lintDir) + for _, e := range entries { + if !strings.HasSuffix(e.Name(), ".star") { + t.Errorf("unexpected file in .claude/lint-rules/: %s (expected .star)", e.Name()) + } + } +} + +func TestInitOpenCode_CommandsAreMarkdown(t *testing.T) { + dir := t.TempDir() + runInit(t, []string{"opencode"}, dir) + + commandsDir := filepath.Join(dir, ".opencode", "commands") + entries, err := os.ReadDir(commandsDir) + if err != nil { + t.Fatalf("could not read .opencode/commands/: %v", err) + } + for _, e := range entries { + if !strings.HasSuffix(e.Name(), ".md") { + t.Errorf(".opencode/commands/%s: expected .md extension", e.Name()) + } + } +} + +func TestInitClaude_CreatesClaudeSpecificFiles(t *testing.T) { + dir := t.TempDir() + runInit(t, []string{"claude"}, dir) + + // Claude-specific + if !fileExists(filepath.Join(dir, "CLAUDE.md")) { + t.Error("CLAUDE.md should be created for claude tool") + } + if !fileExists(filepath.Join(dir, ".claude", "settings.json")) { + t.Error(".claude/settings.json should be created for claude tool") + } + if n := countFilesInDir(filepath.Join(dir, ".claude", "commands")); n == 0 { + t.Error(".claude/commands/ should contain command files") + } + if n := countFilesInDir(filepath.Join(dir, ".claude", "lint-rules")); n == 0 { + t.Error(".claude/lint-rules/ should contain lint rule files") + } + + // Universal files + if !fileExists(filepath.Join(dir, "AGENTS.md")) { + t.Error("AGENTS.md should be created") + } +} + +func TestInitClaude_NoOpenCodeDirCreated(t *testing.T) { + dir := t.TempDir() + runInit(t, []string{"claude"}, dir) + + if fileExists(filepath.Join(dir, ".opencode")) { + t.Error(".opencode/ should NOT be created for claude-only init") + } + if fileExists(filepath.Join(dir, "opencode.json")) { + t.Error("opencode.json should NOT be created for claude-only init") + } +} + +func TestInitBothTools_CreatesAllFiles(t *testing.T) { + dir := t.TempDir() + runInit(t, []string{"claude", "opencode"}, dir) + + // Claude files + if !fileExists(filepath.Join(dir, "CLAUDE.md")) { + t.Error("CLAUDE.md should exist when claude tool is selected") + } + if !fileExists(filepath.Join(dir, ".claude", "settings.json")) { + t.Error(".claude/settings.json should exist when claude tool is selected") + } + + // OpenCode files + if !fileExists(filepath.Join(dir, "opencode.json")) { + t.Error("opencode.json should exist when opencode tool is selected") + } + if n := countSubDirs(filepath.Join(dir, ".opencode", "skills")); n == 0 { + t.Error(".opencode/skills/ should contain skill subdirectories") + } + + // Lint rules should be present exactly once + if n := countFilesInDir(filepath.Join(dir, ".claude", "lint-rules")); n == 0 { + t.Error(".claude/lint-rules/ should contain lint rule files") + } +} diff --git a/cmd/mxcli/tool_templates.go b/cmd/mxcli/tool_templates.go index 46f4ac2..73a18bd 100644 --- a/cmd/mxcli/tool_templates.go +++ b/cmd/mxcli/tool_templates.go @@ -78,6 +78,16 @@ var SupportedTools = map[string]ToolConfig{ }, }, }, + "opencode": { + Name: "OpenCode", + Description: "OpenCode AI agent with MDL commands and skills", + Files: []ToolFile{ + { + Path: "opencode.json", + Content: generateOpenCodeConfig, + }, + }, + }, } // Universal files created for all tools @@ -331,3 +341,14 @@ func generatePlaywrightConfig() string { func generateProjectAIMD(projectName, mprPath string) string { return generateClaudeMD(projectName, mprPath) } + +func generateOpenCodeConfig(projectName, mprPath string) string { + return `{ + "$schema": "https://opencode.ai/config.json", + "instructions": [ + "AGENTS.md", + ".ai-context/skills/*.md" + ] +} +` +} From 7cf3bb0257e1883ea5434a6565b23a1bb827b9bb Mon Sep 17 00:00:00 2001 From: Andrew Vasilyev Date: Fri, 27 Mar 2026 17:17:57 +0100 Subject: [PATCH 2/3] docs: add OpenCode integration documentation - New dedicated tutorial page (docs-site/src/tutorial/opencode.md) - Add OpenCode to SUMMARY.md nav after claude-code.md - Update ai-assistants.md: count five -> six, add OpenCode row - Update other-ai-tools.md: add OpenCode section with link to dedicated page - Update mxcli-init.md Supported AI Tools table with OpenCode row - Update README.md intro sentence and Supported AI Tools table --- README.md | 3 +- docs-site/src/SUMMARY.md | 1 + docs-site/src/ide/mxcli-init.md | 1 + docs-site/src/tutorial/ai-assistants.md | 3 +- docs-site/src/tutorial/opencode.md | 191 +++++++++++++++++++++++ docs-site/src/tutorial/other-ai-tools.md | 14 +- 6 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 docs-site/src/tutorial/opencode.md diff --git a/README.md b/README.md index e5eb053..b7b3856 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ > > **Do not edit a project with mxcli while it is open in Studio Pro.** Studio Pro maintains in-memory caches that cannot be updated externally. Close the project in Studio Pro first, run mxcli, then re-open the project. -A command-line tool that enables AI coding assistants ([Claude Code](https://claude.ai/claude-code), Cursor, Continue.dev, Windsurf, Aider, and others) to read, understand, and modify Mendix application projects. +A command-line tool that enables AI coding assistants ([Claude Code](https://claude.ai/claude-code), OpenCode, Cursor, Continue.dev, Windsurf, Aider, and others) to read, understand, and modify Mendix application projects. **[Read the documentation](https://mendixlabs.github.io/mxcli/)** | **[Try it in the Playground](https://codespaces.new/mendixlabs/mxcli-playground)** -- no install needed, runs in your browser @@ -125,6 +125,7 @@ claude # or use Cursor, Continue.dev, etc. | Tool | Config File | Description | |------|------------|-------------| | **Claude Code** | `.claude/`, `CLAUDE.md` | Full integration with skills and commands | +| **OpenCode** | `.opencode/`, `opencode.json` | Skills, commands, and lint rules | | **Cursor** | `.cursorrules` | Compact MDL reference and command guide | | **Continue.dev** | `.continue/config.json` | Custom commands and slash commands | | **Windsurf** | `.windsurfrules` | Codeium's AI with MDL rules | diff --git a/docs-site/src/SUMMARY.md b/docs-site/src/SUMMARY.md index 3c2837a..f2b577a 100644 --- a/docs-site/src/SUMMARY.md +++ b/docs-site/src/SUMMARY.md @@ -26,6 +26,7 @@ - [Validating with mxcli check](tutorial/validation.md) - [Working with AI Assistants](tutorial/ai-assistants.md) - [Claude Code Integration](tutorial/claude-code.md) + - [OpenCode Integration](tutorial/opencode.md) - [Cursor / Continue.dev / Windsurf](tutorial/other-ai-tools.md) - [Skills and CLAUDE.md](tutorial/skills.md) - [The MDL + AI Workflow](tutorial/mdl-ai-workflow.md) diff --git a/docs-site/src/ide/mxcli-init.md b/docs-site/src/ide/mxcli-init.md index c448ced..404466f 100644 --- a/docs-site/src/ide/mxcli-init.md +++ b/docs-site/src/ide/mxcli-init.md @@ -26,6 +26,7 @@ mxcli init --list-tools | Tool | Flag | Config Files Created | |------|------|---------------------| | **Claude Code** (default) | `--tool claude` | `.claude/settings.json`, `CLAUDE.md`, commands, lint rules | +| **OpenCode** | `--tool opencode` | `.opencode/`, `opencode.json`, commands, skills, lint rules | | **Cursor** | `--tool cursor` | `.cursorrules` | | **Continue.dev** | `--tool continue` | `.continue/config.json` | | **Windsurf** | `--tool windsurf` | `.windsurfrules` | diff --git a/docs-site/src/tutorial/ai-assistants.md b/docs-site/src/tutorial/ai-assistants.md index 393cb7d..5cf2487 100644 --- a/docs-site/src/tutorial/ai-assistants.md +++ b/docs-site/src/tutorial/ai-assistants.md @@ -22,11 +22,12 @@ Fewer tokens means lower API costs, more of your application fits in a single pr ## Supported AI tools -mxcli supports five AI coding assistants out of the box, plus a universal format that works with any tool: +mxcli supports six AI coding assistants out of the box, plus a universal format that works with any tool: | Tool | Init Flag | Config File | Description | |------|-----------|-------------|-------------| | **Claude Code** | `--tool claude` (default) | `.claude/`, `CLAUDE.md` | Full integration with skills, commands, and lint rules | +| **OpenCode** | `--tool opencode` | `.opencode/`, `opencode.json` | Deep integration with skills, commands, and lint rules | | **Cursor** | `--tool cursor` | `.cursorrules` | Compact MDL reference and command guide | | **Continue.dev** | `--tool continue` | `.continue/config.json` | Custom commands and slash commands | | **Windsurf** | `--tool windsurf` | `.windsurfrules` | Codeium's AI with MDL rules | diff --git a/docs-site/src/tutorial/opencode.md b/docs-site/src/tutorial/opencode.md new file mode 100644 index 0000000..3ba07b8 --- /dev/null +++ b/docs-site/src/tutorial/opencode.md @@ -0,0 +1,191 @@ +# OpenCode Integration + +OpenCode is a fully supported AI integration for mxcli. It gets deep support: a dedicated configuration directory, project-level context in `AGENTS.md`, skill files that teach the AI MDL patterns, slash commands for common operations, and Starlark lint rules. + +## Initializing a project + +Use the `--tool opencode` flag: + +```bash +mxcli init --tool opencode /path/to/my-mendix-project +``` + +To set up both OpenCode and Claude Code together: + +```bash +mxcli init --tool opencode --tool claude /path/to/my-mendix-project +``` + +## What gets created + +After running `mxcli init --tool opencode`, your project directory gains: + +``` +my-mendix-project/ +├── AGENTS.md # Universal AI assistant guide (OpenCode reads this) +├── opencode.json # OpenCode configuration file +├── .opencode/ +│ ├── commands/ # Slash commands (/create-entity, etc.) +│ └── skills/ # MDL pattern guides (OpenCode skill format) +│ ├── write-microflows/ +│ │ └── SKILL.md +│ ├── create-page/ +│ │ └── SKILL.md +│ └── ... +├── .claude/ +│ └── lint-rules/ # Starlark lint rules (shared with mxcli lint) +├── .ai-context/ +│ ├── skills/ # Shared skill files (universal copy) +│ └── examples/ # Example MDL scripts +├── .devcontainer/ +│ ├── devcontainer.json # Dev container configuration +│ └── Dockerfile # Container image with mxcli, JDK, Docker +├── mxcli # CLI binary (copied into project) +└── app.mpr # Your Mendix project (already existed) +``` + +### opencode.json + +The `opencode.json` file is OpenCode's primary configuration. It points to `AGENTS.md` for instructions and to the skill files in `.opencode/skills/`: + +```json +{ + "instructions": ["AGENTS.md", ".ai-context/skills/*.md"], + "model": "anthropic/claude-sonnet-4-5" +} +``` + +### Skills + +Skills live in `.opencode/skills//SKILL.md` and use YAML frontmatter that OpenCode understands: + +```markdown +--- +name: write-microflows +description: MDL syntax and patterns for creating microflows +compatibility: opencode +--- + +# Writing Microflows +... +``` + +Each skill covers a specific area: microflow syntax, page patterns, security setup, and so on. OpenCode reads the relevant skill before generating MDL, which significantly improves output quality. + +### Commands + +The `.opencode/commands/` directory contains slash commands available inside OpenCode. These mirror the Claude Code commands: `/create-entity`, `/create-microflow`, `/create-page`, `/lint`, and others. + +### Lint rules + +Lint rules live in `.claude/lint-rules/` regardless of which tool is selected — this is where `mxcli lint` looks for custom Starlark rules. OpenCode init writes the rules there so `mxcli lint` works the same way for both tool choices. + +## Setting up the dev container + +The dev container setup is identical to the Claude Code workflow. Open your project folder in VS Code, click **"Reopen in Container"** when prompted, and wait for the container to build. + +### What's installed in the container + +| Component | Purpose | +|-----------|---------| +| **mxcli** | Mendix CLI (copied into project root) | +| **MxBuild / mx** | Mendix project validation and building | +| **JDK 21** (Adoptium) | Required by MxBuild | +| **Docker-in-Docker** | Running Mendix apps locally with `mxcli docker` | +| **Node.js** | Playwright testing support | +| **PostgreSQL client** | Database connectivity | + +## Starting OpenCode + +With the dev container running, open a terminal in VS Code and start OpenCode: + +```bash +opencode +``` + +OpenCode now has access to your project files, the mxcli binary, the skill files in `.opencode/skills/`, and the commands in `.opencode/commands/`. + +## How OpenCode works with your project + +The workflow mirrors Claude Code exactly: + +### 1. Explore + +OpenCode uses mxcli commands to understand the project before making changes: + +```sql +-- What modules exist? +SHOW MODULES; + +-- What entities are in this module? +SHOW ENTITIES IN Sales; + +-- What does this entity look like? +DESCRIBE ENTITY Sales.Customer; + +-- What microflows exist? +SHOW MICROFLOWS IN Sales; + +-- Search for something specific +SEARCH 'validation'; +``` + +### 2. Read the relevant skill + +Before writing MDL, OpenCode reads the appropriate skill file from `.opencode/skills/`. If you ask for a microflow, it reads the `write-microflows` skill. If you ask for a page, it reads `create-page`. + +### 3. Write MDL + +OpenCode generates an MDL script based on the project context and skill guidance: + +```sql +/** Customer master data */ +@Position(100, 100) +CREATE PERSISTENT ENTITY Sales.Customer ( + Name: String(200) NOT NULL, + Email: String(200) NOT NULL, + Phone: String(50), + IsActive: Boolean DEFAULT true +); +``` + +### 4. Validate + +```bash +./mxcli check script.mdl +./mxcli check script.mdl -p app.mpr --references +``` + +### 5. Execute + +```bash +./mxcli -p app.mpr -c "EXECUTE SCRIPT 'script.mdl'" +``` + +### 6. Verify + +```bash +./mxcli docker check -p app.mpr +``` + +## Adding OpenCode to an existing project + +If you already ran `mxcli init` for another tool and want to add OpenCode support: + +```bash +mxcli add-tool opencode +``` + +This creates `.opencode/`, `opencode.json`, and the lint rules without touching any existing configuration. + +## Tips for working with OpenCode + +- **Be specific about module names.** Say "Create a Customer entity in the Sales module" rather than just "Create a Customer entity." +- **Mention existing elements.** If you want an association to an existing entity, name it: "Link Order to the existing Sales.Customer entity." +- **Let the AI explore first.** OpenCode will run SHOW and DESCRIBE commands to understand what's already in the project. This leads to better results. +- **Review in Studio Pro.** After changes are applied, open the project in Studio Pro to verify the result visually. +- **Use `mxcli docker check`** to catch issues that `mxcli check` alone might miss. + +## Next steps + +To understand what the skill files contain and how they guide AI behavior, see [Skills and CLAUDE.md](skills.md). For other supported tools, see [Cursor / Continue.dev / Windsurf](other-ai-tools.md). diff --git a/docs-site/src/tutorial/other-ai-tools.md b/docs-site/src/tutorial/other-ai-tools.md index 2928a47..344c066 100644 --- a/docs-site/src/tutorial/other-ai-tools.md +++ b/docs-site/src/tutorial/other-ai-tools.md @@ -1,12 +1,15 @@ # Cursor / Continue.dev / Windsurf -Claude Code is the default integration, but mxcli also supports Cursor, Continue.dev, Windsurf, and Aider. Each tool gets its own configuration file that teaches the AI about MDL syntax and mxcli commands. +Claude Code is the default integration, but mxcli also supports OpenCode, Cursor, Continue.dev, Windsurf, and Aider. Each tool gets its own configuration file that teaches the AI about MDL syntax and mxcli commands. ## Initializing for a specific tool Use the `--tool` flag to specify which AI tool you use: ```bash +# OpenCode +mxcli init --tool opencode /path/to/my-mendix-project + # Cursor mxcli init --tool cursor /path/to/my-mendix-project @@ -60,6 +63,7 @@ On top of the universal files, each tool gets its own configuration: | Tool | Config File | Contents | |------|-------------|----------| | **Claude Code** | `.claude/`, `CLAUDE.md` | Settings, skills, commands, lint rules, project context | +| **OpenCode** | `.opencode/`, `opencode.json` | Skills, commands, lint rules, project context | | **Cursor** | `.cursorrules` | Compact MDL reference and mxcli command guide | | **Continue.dev** | `.continue/config.json` | Custom commands and slash commands | | **Windsurf** | `.windsurfrules` | MDL rules for Codeium's AI | @@ -67,6 +71,14 @@ On top of the universal files, each tool gets its own configuration: ## Tool details +### OpenCode + +OpenCode receives full integration on par with Claude Code: dedicated skills in `.opencode/skills/`, slash commands in `.opencode/commands/`, and Starlark lint rules in `.claude/lint-rules/`. See the dedicated [OpenCode Integration](opencode.md) page for the complete walkthrough. + +```bash +mxcli init --tool opencode /path/to/project +``` + ### Cursor Cursor reads its instructions from `.cursorrules` in the project root. The file mxcli generates contains a compact MDL syntax reference and a list of mxcli commands the AI can use. Cursor's Composer and Chat features will reference this file automatically. From 0eab8df91e181fd96e67625da0118092180bf96e Mon Sep 17 00:00:00 2001 From: Andrew Vasilyev Date: Fri, 27 Mar 2026 18:40:41 +0100 Subject: [PATCH 3/3] fix(init): address PR review comments - Add opencode to --tool flag help text - Fix extractSkillDescription comment (returns 'MDL skill', not the skill name) - Add yamlSingleQuote() helper; use it in wrapSkillContent to safely escape skill names and descriptions in YAML frontmatter - Update init_test.go assertions to match new single-quoted frontmatter format - Fix opencode.json doc example to match generateOpenCodeConfig actual output - Remove false panic claim from runInit test helper comment - Wire full OpenCode sidecar into cmd_add_tool.go (commands, lint-rules, skills) - Guard OpenCode lint-rule write with slices.Contains(tools, "claude") to prevent duplicate writes when both tools are selected or --all-tools is used - Capture and handle fs.WalkDir errors in cmd_add_tool.go OpenCode sidecar - Move success message in cmd_add_tool.go to after the OpenCode sidecar block - Add .opencode/skills/**/SKILL.md to generateOpenCodeConfig instructions - Update opencode.md doc example and prose to match generated config - Rename other-ai-tools.md heading to '# Other AI tools' - Fix stale link text in opencode.md to match renamed heading - Nil out vsixData in runInit test helper to prevent external CLI side effects - Fix lint-rule guard to use resolved 'tools' slice instead of 'initTools' --- cmd/mxcli/cmd_add_tool.go | 97 ++++++++++++++++++++++++ cmd/mxcli/init.go | 58 ++++++++------ cmd/mxcli/init_test.go | 88 ++++++++++++++++++--- cmd/mxcli/tool_templates.go | 1 + docs-site/src/SUMMARY.md | 2 +- docs-site/src/tutorial/opencode.md | 12 ++- docs-site/src/tutorial/other-ai-tools.md | 2 +- 7 files changed, 221 insertions(+), 39 deletions(-) diff --git a/cmd/mxcli/cmd_add_tool.go b/cmd/mxcli/cmd_add_tool.go index 5dbd533..32bd3f8 100644 --- a/cmd/mxcli/cmd_add_tool.go +++ b/cmd/mxcli/cmd_add_tool.go @@ -5,8 +5,10 @@ package main import ( "fmt" + "io/fs" "os" "path/filepath" + "strings" "github.com/spf13/cobra" ) @@ -132,6 +134,101 @@ Supported Tools: fmt.Println(" Run 'mxcli init' first to create universal documentation.") } + // OpenCode sidecar: commands, skills, lint-rules (same as mxcli init) + if toolName == "opencode" { + opencodeDir := filepath.Join(absDir, ".opencode") + opencodeCommandsDir := filepath.Join(opencodeDir, "commands") + opencodeSkillsDir := filepath.Join(opencodeDir, "skills") + lintRulesDir := filepath.Join(absDir, ".claude", "lint-rules") + + for _, dir := range []string{opencodeCommandsDir, opencodeSkillsDir, lintRulesDir} { + if err := os.MkdirAll(dir, 0755); err != nil { + fmt.Fprintf(os.Stderr, " Error creating directory %s: %v\n", dir, err) + } + } + + cmdCount := 0 + if err := fs.WalkDir(commandsFS, "commands", func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + return err + } + content, err := commandsFS.ReadFile(path) + if err != nil { + return err + } + targetPath := filepath.Join(opencodeCommandsDir, d.Name()) + if _, statErr := os.Stat(targetPath); statErr == nil { + return nil // skip existing + } + if err := os.WriteFile(targetPath, content, 0644); err != nil { + return err + } + cmdCount++ + return nil + }); err != nil { + fmt.Fprintf(os.Stderr, " Error writing OpenCode commands: %v\n", err) + } else if cmdCount > 0 { + fmt.Printf(" Created %d command files in .opencode/commands/\n", cmdCount) + } + + lintCount := 0 + if err := fs.WalkDir(lintRulesFS, "lint-rules", func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + return err + } + content, err := lintRulesFS.ReadFile(path) + if err != nil { + return err + } + targetPath := filepath.Join(lintRulesDir, d.Name()) + if _, statErr := os.Stat(targetPath); statErr == nil { + return nil // skip existing + } + if err := os.WriteFile(targetPath, content, 0644); err != nil { + return err + } + lintCount++ + return nil + }); err != nil { + fmt.Fprintf(os.Stderr, " Error writing lint rules: %v\n", err) + } else if lintCount > 0 { + fmt.Printf(" Created %d lint rule files in .claude/lint-rules/\n", lintCount) + } + + skillCount := 0 + if err := fs.WalkDir(skillsFS, "skills", func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + return err + } + if d.Name() == "README.md" { + return nil + } + content, err := skillsFS.ReadFile(path) + if err != nil { + return err + } + skillName := strings.TrimSuffix(d.Name(), ".md") + skillDir := filepath.Join(opencodeSkillsDir, skillName) + if err := os.MkdirAll(skillDir, 0755); err != nil { + return err + } + targetPath := filepath.Join(skillDir, "SKILL.md") + if _, statErr := os.Stat(targetPath); statErr == nil { + return nil // skip existing + } + wrapped := wrapSkillContent(skillName, content) + if err := os.WriteFile(targetPath, wrapped, 0644); err != nil { + return err + } + skillCount++ + return nil + }); err != nil { + fmt.Fprintf(os.Stderr, " Error writing OpenCode skills: %v\n", err) + } else if skillCount > 0 { + fmt.Printf(" Created %d skill directories in .opencode/skills/\n", skillCount) + } + } + fmt.Println("\n✓ Tool support added!") fmt.Printf("\nNext steps:\n") fmt.Printf(" 1. Open project in %s\n", toolConfig.Name) diff --git a/cmd/mxcli/init.go b/cmd/mxcli/init.go index 949312b..6a0c7dc 100644 --- a/cmd/mxcli/init.go +++ b/cmd/mxcli/init.go @@ -313,28 +313,33 @@ All tools receive universal documentation in AGENTS.md and .ai-context/ } lintRuleCount := 0 - err = fs.WalkDir(lintRulesFS, "lint-rules", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() { + // Only write lint rules from the OpenCode path when Claude is not also + // being initialised — the Claude path already writes the same files to + // .claude/lint-rules/ and we don't want duplicate log output or writes. + if !slices.Contains(tools, "claude") { + err = fs.WalkDir(lintRulesFS, "lint-rules", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + content, err := lintRulesFS.ReadFile(path) + if err != nil { + return err + } + targetPath := filepath.Join(lintRulesDir, d.Name()) + if err := os.WriteFile(targetPath, content, 0644); err != nil { + return err + } + lintRuleCount++ return nil - } - content, err := lintRulesFS.ReadFile(path) + }) if err != nil { - return err + fmt.Fprintf(os.Stderr, " Error writing lint rules: %v\n", err) + } else { + fmt.Printf(" Created %d lint rule files in .claude/lint-rules/\n", lintRuleCount) } - targetPath := filepath.Join(lintRulesDir, d.Name()) - if err := os.WriteFile(targetPath, content, 0644); err != nil { - return err - } - lintRuleCount++ - return nil - }) - if err != nil { - fmt.Fprintf(os.Stderr, " Error writing lint rules: %v\n", err) - } else { - fmt.Printf(" Created %d lint rule files in .claude/lint-rules/\n", lintRuleCount) } skillCount2 := 0 @@ -461,19 +466,28 @@ All tools receive universal documentation in AGENTS.md and .ai-context/ }, } +// yamlSingleQuote wraps s in YAML single quotes and escapes any internal +// single quotes by doubling them, so the result is safe to embed in a YAML +// value without further quoting. +func yamlSingleQuote(s string) string { + s = strings.ReplaceAll(s, "\n", " ") + s = strings.ReplaceAll(s, "'", "''") + return "'" + s + "'" +} + // wrapSkillContent prepends OpenCode-compatible YAML frontmatter to a skill file. // OpenCode requires each skill to live in its own subdirectory as SKILL.md and // the file must start with YAML frontmatter containing name, description, and // compatibility fields. func wrapSkillContent(skillName string, content []byte) []byte { description := extractSkillDescription(content) - frontmatter := fmt.Sprintf("---\nname: %s\ndescription: %s\ncompatibility: opencode\n---\n\n", skillName, description) + frontmatter := fmt.Sprintf("---\nname: %s\ndescription: %s\ncompatibility: opencode\n---\n\n", yamlSingleQuote(skillName), yamlSingleQuote(description)) return append([]byte(frontmatter), content...) } // extractSkillDescription returns a one-line description for the skill by // finding the first top-level markdown heading (# ...) and stripping a leading -// "Skill: " prefix if present. Falls back to the skill name if no heading is +// "Skill: " prefix if present. Falls back to "MDL skill" if no heading is // found. func extractSkillDescription(content []byte) string { for _, line := range strings.Split(string(content), "\n") { @@ -1147,7 +1161,7 @@ func init() { rootCmd.AddCommand(initCmd) // Add flags for tool selection - initCmd.Flags().StringSliceVar(&initTools, "tool", []string{}, "AI tool(s) to configure (claude, cursor, continue, windsurf, aider)") + initCmd.Flags().StringSliceVar(&initTools, "tool", []string{}, "AI tool(s) to configure (claude, opencode, cursor, continue, windsurf, aider)") initCmd.Flags().BoolVar(&initAllTools, "all-tools", false, "Initialize for all supported AI tools") initCmd.Flags().BoolVar(&initListTools, "list-tools", false, "List supported AI tools and exit") } diff --git a/cmd/mxcli/init_test.go b/cmd/mxcli/init_test.go index 1d578b9..9eb0bbb 100644 --- a/cmd/mxcli/init_test.go +++ b/cmd/mxcli/init_test.go @@ -3,6 +3,7 @@ package main import ( + "io" "os" "path/filepath" "strings" @@ -10,14 +11,19 @@ import ( ) // runInit is a test helper that sets initTools to the given list, invokes the -// cobra Run closure against dir, then restores the original value. The -// function panics if initListTools or initAllTools are left non-default from a -// previous test. +// cobra Run closure against dir, then restores the original values. +// vsixData is set to nil during the call to prevent installVSCodeExtension from +// invoking the external 'code' CLI or writing .vsix files on CI / dev machines. func runInit(t *testing.T, tools []string, dir string) { t.Helper() - prev := initTools - t.Cleanup(func() { initTools = prev }) + prevTools := initTools + prevVsix := vsixData + t.Cleanup(func() { + initTools = prevTools + vsixData = prevVsix + }) initTools = tools + vsixData = nil // disable VS Code extension installation during tests initCmd.Run(initCmd, []string{dir}) } @@ -72,11 +78,11 @@ func TestWrapSkillContent_FrontmatterPresent(t *testing.T) { if !strings.HasPrefix(text, "---\n") { t.Errorf("wrapped content should start with '---\\n', got: %q", text[:min(40, len(text))]) } - if !strings.Contains(text, "name: my-skill\n") { - t.Error("frontmatter should contain 'name: my-skill'") + if !strings.Contains(text, "name: 'my-skill'\n") { + t.Error("frontmatter should contain 'name: 'my-skill''") } - if !strings.Contains(text, "description: My Skill\n") { - t.Error("frontmatter should contain 'description: My Skill'") + if !strings.Contains(text, "description: 'My Skill'\n") { + t.Error("frontmatter should contain 'description: 'My Skill''") } if !strings.Contains(text, "compatibility: opencode\n") { t.Error("frontmatter should contain 'compatibility: opencode'") @@ -217,8 +223,8 @@ func TestInitOpenCode_EachSkillHasValidFrontmatter(t *testing.T) { if !strings.HasPrefix(text, "---\n") { t.Errorf("skill %q: SKILL.md should start with YAML frontmatter '---'", e.Name()) } - if !strings.Contains(text, "name: "+e.Name()) { - t.Errorf("skill %q: SKILL.md frontmatter should contain 'name: %s'", e.Name(), e.Name()) + if !strings.Contains(text, "name: '"+e.Name()+"'") { + t.Errorf("skill %q: SKILL.md frontmatter should contain 'name: '%s''", e.Name(), e.Name()) } if !strings.Contains(text, "compatibility: opencode") { t.Errorf("skill %q: SKILL.md should contain 'compatibility: opencode'", e.Name()) @@ -334,3 +340,63 @@ func TestInitBothTools_CreatesAllFiles(t *testing.T) { t.Error(".claude/lint-rules/ should contain lint rule files") } } + +// runInitAllTools is like runInit but exercises the --all-tools path. +func runInitAllTools(t *testing.T, dir string) { + t.Helper() + prevTools := initTools + prevAll := initAllTools + prevVsix := vsixData + t.Cleanup(func() { + initTools = prevTools + initAllTools = prevAll + vsixData = prevVsix + }) + initTools = []string{} + initAllTools = true + vsixData = nil + initCmd.Run(initCmd, []string{dir}) +} + +func TestInitAllTools_CreatesAllFilesWithoutDuplicateLintRules(t *testing.T) { + dir := t.TempDir() + // Capture stdout to count lint-rule log lines. + origStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + runInitAllTools(t, dir) + + w.Close() + os.Stdout = origStdout + outBytes, _ := io.ReadAll(r) + output := string(outBytes) + + // Claude artifacts + if !fileExists(filepath.Join(dir, "CLAUDE.md")) { + t.Error("CLAUDE.md should exist with --all-tools") + } + if !fileExists(filepath.Join(dir, ".claude", "settings.json")) { + t.Error(".claude/settings.json should exist with --all-tools") + } + if n := countFilesInDir(filepath.Join(dir, ".claude", "commands")); n == 0 { + t.Error(".claude/commands/ should contain command files with --all-tools") + } + + // OpenCode artifacts + if !fileExists(filepath.Join(dir, "opencode.json")) { + t.Error("opencode.json should exist with --all-tools") + } + if n := countSubDirs(filepath.Join(dir, ".opencode", "skills")); n == 0 { + t.Error(".opencode/skills/ should contain skill dirs with --all-tools") + } + + // Lint rules must exist and the creation message must appear exactly once. + if n := countFilesInDir(filepath.Join(dir, ".claude", "lint-rules")); n == 0 { + t.Error(".claude/lint-rules/ should contain lint rule files with --all-tools") + } + lintMsgCount := strings.Count(output, "lint rule files in .claude/lint-rules/") + if lintMsgCount != 1 { + t.Errorf("lint rule creation message should appear exactly once, got %d:\n%s", lintMsgCount, output) + } +} diff --git a/cmd/mxcli/tool_templates.go b/cmd/mxcli/tool_templates.go index 73a18bd..b4558fd 100644 --- a/cmd/mxcli/tool_templates.go +++ b/cmd/mxcli/tool_templates.go @@ -347,6 +347,7 @@ func generateOpenCodeConfig(projectName, mprPath string) string { "$schema": "https://opencode.ai/config.json", "instructions": [ "AGENTS.md", + ".opencode/skills/**/SKILL.md", ".ai-context/skills/*.md" ] } diff --git a/docs-site/src/SUMMARY.md b/docs-site/src/SUMMARY.md index f2b577a..0998211 100644 --- a/docs-site/src/SUMMARY.md +++ b/docs-site/src/SUMMARY.md @@ -27,7 +27,7 @@ - [Working with AI Assistants](tutorial/ai-assistants.md) - [Claude Code Integration](tutorial/claude-code.md) - [OpenCode Integration](tutorial/opencode.md) - - [Cursor / Continue.dev / Windsurf](tutorial/other-ai-tools.md) + - [Other AI tools](tutorial/other-ai-tools.md) - [Skills and CLAUDE.md](tutorial/skills.md) - [The MDL + AI Workflow](tutorial/mdl-ai-workflow.md) diff --git a/docs-site/src/tutorial/opencode.md b/docs-site/src/tutorial/opencode.md index 3ba07b8..20d9411 100644 --- a/docs-site/src/tutorial/opencode.md +++ b/docs-site/src/tutorial/opencode.md @@ -46,12 +46,16 @@ my-mendix-project/ ### opencode.json -The `opencode.json` file is OpenCode's primary configuration. It points to `AGENTS.md` for instructions and to the skill files in `.opencode/skills/`: +The `opencode.json` file is OpenCode's primary configuration. It points to `AGENTS.md` for instructions and to both the OpenCode-format skills in `.opencode/skills/` and the universal skill files in `.ai-context/skills/`: ```json { - "instructions": ["AGENTS.md", ".ai-context/skills/*.md"], - "model": "anthropic/claude-sonnet-4-5" + "$schema": "https://opencode.ai/config.json", + "instructions": [ + "AGENTS.md", + ".opencode/skills/**/SKILL.md", + ".ai-context/skills/*.md" + ] } ``` @@ -188,4 +192,4 @@ This creates `.opencode/`, `opencode.json`, and the lint rules without touching ## Next steps -To understand what the skill files contain and how they guide AI behavior, see [Skills and CLAUDE.md](skills.md). For other supported tools, see [Cursor / Continue.dev / Windsurf](other-ai-tools.md). +To understand what the skill files contain and how they guide AI behavior, see [Skills and CLAUDE.md](skills.md). For other supported tools, see [Other AI tools (Cursor, Continue.dev, Windsurf, OpenCode)](other-ai-tools.md). diff --git a/docs-site/src/tutorial/other-ai-tools.md b/docs-site/src/tutorial/other-ai-tools.md index 344c066..bec1d08 100644 --- a/docs-site/src/tutorial/other-ai-tools.md +++ b/docs-site/src/tutorial/other-ai-tools.md @@ -1,4 +1,4 @@ -# Cursor / Continue.dev / Windsurf +# Other AI tools Claude Code is the default integration, but mxcli also supports OpenCode, Cursor, Continue.dev, Windsurf, and Aider. Each tool gets its own configuration file that teaches the AI about MDL syntax and mxcli commands.