ajhahn.de
← eeco
Go 50 lines
package cockpit

import (
	"errors"
	"fmt"
	"strings"
)

// cursorRenderer emits a Playbook as a modern Cursor rule file
// (.cursor/rules/<name>.mdc): YAML frontmatter (description / globs /
// alwaysApply) followed by the shared advisory body. Cursor rules are
// advisory — Cursor does not enforce a tool allowlist from an .mdc at runtime
// (its enforced settings live in a separate permissions layer, out of scope
// until C4) — so every emitted file carries the ADVISORY banner. Like Claude,
// Cursor is per-playbook (one .mdc per playbook), so it flows through the
// unchanged per-playbook emit machinery.
type cursorRenderer struct{}

func (cursorRenderer) Target() string { return "cursor" }

// Enforcement reports that Cursor rules are advisory only. It satisfies the
// Fidelity interface.
func (cursorRenderer) Enforcement() Enforcement { return EnforcementAdvisory }

func (cursorRenderer) RelPath(p Playbook) string {
	return ".cursor/rules/" + p.Name + ".mdc"
}

// Render produces the .mdc bytes. It is deterministic and rejects a Playbook
// whose single-line frontmatter field (description) would span multiple lines
// — the frontmatter is line-oriented, mirroring the Claude renderer's guard.
func (cursorRenderer) Render(p Playbook) ([]byte, error) {
	desc := strings.TrimSpace(p.Description)
	if strings.ContainsAny(desc, "\r\n") {
		return nil, errors.New("playbook description must be a single line")
	}

	var b strings.Builder
	b.WriteString("---\n")
	fmt.Fprintf(&b, "description: %s\n", desc)
	b.WriteString("globs:\n")
	b.WriteString("alwaysApply: false\n")
	b.WriteString("---\n")
	fmt.Fprintf(&b, "# %s\n\n", deriveTitle(p.Name))
	b.WriteString(advisoryBanner)
	b.WriteString("\n\n")
	renderPlaybookBody(&b, p, "##")
	return []byte(b.String()), nil
}