@@ -52,6 +52,7 @@ Supported Tools:
5252 - continue Continue.dev with custom commands
5353 - windsurf Windsurf (Codeium) with MDL rules
5454 - aider Aider with project configuration
55+ - opencode OpenCode AI agent with MDL commands and skills
5556
5657All tools receive universal documentation in AGENTS.md and .ai-context/
5758` ,
@@ -143,6 +144,35 @@ All tools receive universal documentation in AGENTS.md and .ai-context/
143144 }
144145 }
145146
147+ // Create .opencode directory for OpenCode-specific content (if OpenCode is selected)
148+ var opencodeCommandsDir , opencodeSkillsDir string
149+ if slices .Contains (tools , "opencode" ) {
150+ opencodeDir := filepath .Join (absDir , ".opencode" )
151+ opencodeCommandsDir = filepath .Join (opencodeDir , "commands" )
152+ opencodeSkillsDir = filepath .Join (opencodeDir , "skills" )
153+
154+ if err := os .MkdirAll (opencodeCommandsDir , 0755 ); err != nil {
155+ fmt .Fprintf (os .Stderr , "Error creating .opencode/commands directory: %v\n " , err )
156+ os .Exit (1 )
157+ }
158+ if err := os .MkdirAll (opencodeSkillsDir , 0755 ); err != nil {
159+ fmt .Fprintf (os .Stderr , "Error creating .opencode/skills directory: %v\n " , err )
160+ os .Exit (1 )
161+ }
162+
163+ // Lint rules stay in .claude/lint-rules/ (read by mxcli lint).
164+ // Ensure that directory exists even when claude tool is not selected.
165+ if ! slices .Contains (tools , "claude" ) {
166+ if lintRulesDir == "" {
167+ lintRulesDir = filepath .Join (absDir , ".claude" , "lint-rules" )
168+ }
169+ if err := os .MkdirAll (lintRulesDir , 0755 ); err != nil {
170+ fmt .Fprintf (os .Stderr , "Error creating .claude/lint-rules directory: %v\n " , err )
171+ os .Exit (1 )
172+ }
173+ }
174+ }
175+
146176 // Write universal skills to .ai-context/skills/
147177 skillCount := 0
148178 err = fs .WalkDir (skillsFS , "skills" , func (path string , d fs.DirEntry , err error ) error {
@@ -254,6 +284,102 @@ All tools receive universal documentation in AGENTS.md and .ai-context/
254284 fmt .Printf (" Created %d lint rule files in .claude/lint-rules/\n " , lintRuleCount )
255285 }
256286 }
287+
288+ // OpenCode-specific: write commands, lint rules, and skills
289+ if toolName == "opencode" && opencodeCommandsDir != "" {
290+ cmdCount := 0
291+ err = fs .WalkDir (commandsFS , "commands" , func (path string , d fs.DirEntry , err error ) error {
292+ if err != nil {
293+ return err
294+ }
295+ if d .IsDir () {
296+ return nil
297+ }
298+ content , err := commandsFS .ReadFile (path )
299+ if err != nil {
300+ return err
301+ }
302+ targetPath := filepath .Join (opencodeCommandsDir , d .Name ())
303+ if err := os .WriteFile (targetPath , content , 0644 ); err != nil {
304+ return err
305+ }
306+ cmdCount ++
307+ return nil
308+ })
309+ if err != nil {
310+ fmt .Fprintf (os .Stderr , " Error writing OpenCode commands: %v\n " , err )
311+ } else {
312+ fmt .Printf (" Created %d command files in .opencode/commands/\n " , cmdCount )
313+ }
314+
315+ lintRuleCount := 0
316+ // Only write lint rules from the OpenCode path when Claude is not also
317+ // being initialised — the Claude path already writes the same files to
318+ // .claude/lint-rules/ and we don't want duplicate log output or writes.
319+ if ! slices .Contains (tools , "claude" ) {
320+ err = fs .WalkDir (lintRulesFS , "lint-rules" , func (path string , d fs.DirEntry , err error ) error {
321+ if err != nil {
322+ return err
323+ }
324+ if d .IsDir () {
325+ return nil
326+ }
327+ content , err := lintRulesFS .ReadFile (path )
328+ if err != nil {
329+ return err
330+ }
331+ targetPath := filepath .Join (lintRulesDir , d .Name ())
332+ if err := os .WriteFile (targetPath , content , 0644 ); err != nil {
333+ return err
334+ }
335+ lintRuleCount ++
336+ return nil
337+ })
338+ if err != nil {
339+ fmt .Fprintf (os .Stderr , " Error writing lint rules: %v\n " , err )
340+ } else {
341+ fmt .Printf (" Created %d lint rule files in .claude/lint-rules/\n " , lintRuleCount )
342+ }
343+ }
344+
345+ skillCount2 := 0
346+ err = fs .WalkDir (skillsFS , "skills" , func (path string , d fs.DirEntry , err error ) error {
347+ if err != nil {
348+ return err
349+ }
350+ if d .IsDir () {
351+ return nil
352+ }
353+ // Skip README
354+ if d .Name () == "README.md" {
355+ return nil
356+ }
357+ content , err := skillsFS .ReadFile (path )
358+ if err != nil {
359+ return err
360+ }
361+ // Derive skill name from filename (strip .md)
362+ skillName := strings .TrimSuffix (d .Name (), ".md" )
363+ // Create per-skill subdirectory
364+ skillDir := filepath .Join (opencodeSkillsDir , skillName )
365+ if err := os .MkdirAll (skillDir , 0755 ); err != nil {
366+ return err
367+ }
368+ // Wrap content with OpenCode frontmatter
369+ wrapped := wrapSkillContent (skillName , content )
370+ targetPath := filepath .Join (skillDir , "SKILL.md" )
371+ if err := os .WriteFile (targetPath , wrapped , 0644 ); err != nil {
372+ return err
373+ }
374+ skillCount2 ++
375+ return nil
376+ })
377+ if err != nil {
378+ fmt .Fprintf (os .Stderr , " Error writing OpenCode skills: %v\n " , err )
379+ } else {
380+ fmt .Printf (" Created %d skill directories in .opencode/skills/\n " , skillCount2 )
381+ }
382+ }
257383 }
258384
259385 // Write universal AGENTS.md
@@ -312,8 +438,8 @@ All tools receive universal documentation in AGENTS.md and .ai-context/
312438 }
313439 }
314440
315- // Install VS Code extension if Claude is selected
316- if slices .Contains (tools , "claude" ) {
441+ // Install VS Code extension if Claude or OpenCode is selected
442+ if slices .Contains (tools , "claude" ) || slices . Contains ( tools , "opencode" ) {
317443 installVSCodeExtension (absDir )
318444 }
319445
@@ -340,6 +466,41 @@ All tools receive universal documentation in AGENTS.md and .ai-context/
340466 },
341467}
342468
469+ // yamlSingleQuote wraps s in YAML single quotes and escapes any internal
470+ // single quotes by doubling them, so the result is safe to embed in a YAML
471+ // value without further quoting.
472+ func yamlSingleQuote (s string ) string {
473+ s = strings .ReplaceAll (s , "\n " , " " )
474+ s = strings .ReplaceAll (s , "'" , "''" )
475+ return "'" + s + "'"
476+ }
477+
478+ // wrapSkillContent prepends OpenCode-compatible YAML frontmatter to a skill file.
479+ // OpenCode requires each skill to live in its own subdirectory as SKILL.md and
480+ // the file must start with YAML frontmatter containing name, description, and
481+ // compatibility fields.
482+ func wrapSkillContent (skillName string , content []byte ) []byte {
483+ description := extractSkillDescription (content )
484+ frontmatter := fmt .Sprintf ("---\n name: %s\n description: %s\n compatibility: opencode\n ---\n \n " , yamlSingleQuote (skillName ), yamlSingleQuote (description ))
485+ return append ([]byte (frontmatter ), content ... )
486+ }
487+
488+ // extractSkillDescription returns a one-line description for the skill by
489+ // finding the first top-level markdown heading (# ...) and stripping a leading
490+ // "Skill: " prefix if present. Falls back to "MDL skill" if no heading is
491+ // found.
492+ func extractSkillDescription (content []byte ) string {
493+ for _ , line := range strings .Split (string (content ), "\n " ) {
494+ line = strings .TrimSpace (line )
495+ if strings .HasPrefix (line , "# " ) {
496+ desc := strings .TrimPrefix (line , "# " )
497+ desc = strings .TrimPrefix (desc , "Skill: " )
498+ return strings .TrimSpace (desc )
499+ }
500+ }
501+ return "MDL skill"
502+ }
503+
343504func findMprFile (dir string ) string {
344505 entries , err := os .ReadDir (dir )
345506 if err != nil {
@@ -1000,7 +1161,7 @@ func init() {
10001161 rootCmd .AddCommand (initCmd )
10011162
10021163 // Add flags for tool selection
1003- initCmd .Flags ().StringSliceVar (& initTools , "tool" , []string {}, "AI tool(s) to configure (claude, cursor, continue, windsurf, aider)" )
1164+ initCmd .Flags ().StringSliceVar (& initTools , "tool" , []string {}, "AI tool(s) to configure (claude, opencode, cursor, continue, windsurf, aider)" )
10041165 initCmd .Flags ().BoolVar (& initAllTools , "all-tools" , false , "Initialize for all supported AI tools" )
10051166 initCmd .Flags ().BoolVar (& initListTools , "list-tools" , false , "List supported AI tools and exit" )
10061167}
0 commit comments