diff --git a/cmd/kosli/docs.go b/cmd/kosli/docs.go
index c8dde1f49..a18f72346 100644
--- a/cmd/kosli/docs.go
+++ b/cmd/kosli/docs.go
@@ -1,18 +1,13 @@
package main
import (
- "bytes"
"encoding/json"
"fmt"
"io"
"net/http"
- "net/url"
- "os"
- "path"
- "path/filepath"
"strings"
- "unicode"
+ "github.com/kosli-dev/cli/internal/docgen"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)
@@ -27,6 +22,7 @@ type docsOptions struct {
dest string
topCmd *cobra.Command
generateHeaders bool
+ mintlify bool
}
func newDocsCmd(out io.Writer) *cobra.Command {
@@ -47,273 +43,76 @@ func newDocsCmd(out io.Writer) *cobra.Command {
f := cmd.Flags()
f.StringVar(&o.dest, "dir", "./", "The directory to which documentation is written.")
f.BoolVar(&o.generateHeaders, "generate-headers", true, "Generate standard headers for markdown files.")
+ f.BoolVar(&o.mintlify, "mintlify", false, "Generate Mintlify-compatible MDX output instead of Hugo.")
return cmd
}
func (o *docsOptions) run() error {
if o.generateHeaders {
- linkHandler := func(name string) string {
- base := strings.TrimSuffix(name, path.Ext(name))
- return "/client_reference/" + strings.ToLower(base) + "/"
- }
-
- hdrFunc := func(filename string, beta, deprecated bool, summary string) string {
- base := filepath.Base(filename)
- name := strings.TrimSuffix(base, path.Ext(base))
- title := strings.ToLower(strings.ReplaceAll(name, "_", " "))
- return fmt.Sprintf("---\ntitle: \"%s\"\nbeta: %t\ndeprecated: %t\nsummary: \"%s\"\n---\n\n", title, beta, deprecated, summary)
- }
-
- return MereklyGenMarkdownTreeCustom(o.topCmd, o.dest, hdrFunc, linkHandler)
- }
- return doc.GenMarkdownTree(o.topCmd, o.dest)
-}
-
-func MereklyGenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender func(string, bool, bool, string) string, linkHandler func(string) string) error {
- for _, c := range cmd.Commands() {
- // skip all unavailable commands except deprecated ones
- if (!c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand()) && c.Deprecated == "" {
- continue
- }
- if err := MereklyGenMarkdownTreeCustom(c, dir, filePrepender, linkHandler); err != nil {
- return err
- }
- }
-
- if !cmd.HasParent() || !cmd.HasSubCommands() {
- basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + ".md"
- filename := filepath.Join(dir, basename)
- summary := cmd.Short
- f, err := os.Create(filename)
- if err != nil {
- return err
- }
- defer func() {
- if err := f.Close(); err != nil {
- logger.Warn("failed to close file %s: %v", filename, err)
- }
- }()
-
- if _, err := io.WriteString(f, filePrepender(filename, isBeta(cmd), isDeprecated(cmd), summary)); err != nil {
- return err
- }
- if err := KosliGenMarkdownCustom(cmd, f, linkHandler); err != nil {
- return err
- }
- }
- return nil
-}
-
-// KosliGenMarkdownCustom creates custom markdown output.
-func KosliGenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error {
- cmd.InitDefaultHelpCmd()
- cmd.InitDefaultHelpFlag()
-
- buf := new(bytes.Buffer)
- name := cmd.CommandPath()
-
- buf.WriteString("# " + name + "\n\n")
-
- if isBeta(cmd) {
- buf.WriteString("{{% hint warning %}}\n")
- fmt.Fprintf(buf, "**%s** is a beta feature. ", name)
- fmt.Fprintf(buf, "Beta features provide early access to product functionality. ")
- fmt.Fprintf(buf, "These features may change between releases without warning, or can be removed in a ")
- fmt.Fprintf(buf, "future release.\n")
- fmt.Fprintf(buf, "Please contact us to enable this feature for your organization.\n")
- // fmt.Fprintf(buf, "You can enable beta features by using the `kosli enable beta` command.")
- buf.WriteString("{{% /hint %}}\n")
- }
-
- if isDeprecated(cmd) {
- buf.WriteString("{{% hint danger %}}\n")
- fmt.Fprintf(buf, "**%s** is deprecated. %s ", name, cmd.Deprecated)
- fmt.Fprintf(buf, "Deprecated commands will be removed in a future release.\n")
- buf.WriteString("{{% /hint %}}\n")
- }
-
- if len(cmd.Long) > 0 {
- buf.WriteString("## Synopsis\n\n")
- if cmd.Runnable() {
- fmt.Fprintf(buf, "```shell\n%s\n```\n\n", cmd.UseLine())
- }
- buf.WriteString(strings.ReplaceAll(cmd.Long, "^", "`") + "\n\n")
- }
-
- if err := printOptions(buf, cmd, name); err != nil {
- return err
- }
-
- urlSafeName := url.QueryEscape(name)
- liveExamplesBuf := new(bytes.Buffer)
- for _, ci := range []string{"GitHub", "GitLab"} {
- if liveYamlDocExists(ci, urlSafeName) {
- fmt.Fprintf(liveExamplesBuf, "{{< tab \"%v\" >}}", ci)
- fmt.Fprintf(liveExamplesBuf, "View an example of the `%s` command in %s.\n\n", name, ci)
- fmt.Fprintf(liveExamplesBuf, "In [this YAML file](%v)", yamlURL(ci, urlSafeName))
- if liveEventDocExists(ci, urlSafeName) {
- fmt.Fprintf(liveExamplesBuf, ", which created [this Kosli Event](%v).", eventURL(ci, urlSafeName))
- }
- liveExamplesBuf.WriteString("{{< /tab >}}")
- }
- }
- liveExamples := liveExamplesBuf.String()
- if len(liveExamples) > 0 {
- buf.WriteString("## Live Examples in different CI systems\n\n")
- buf.WriteString("{{< tabs \"live-examples\" \"col-no-wrap\" >}}")
- buf.WriteString(liveExamples)
- buf.WriteString("{{< /tabs >}}\n\n")
- }
-
- liveCliFullCommand, liveCliURL, liveCliExists := liveCliDocExists(name)
- if liveCliExists {
- buf.WriteString("## Live Example\n\n")
- buf.WriteString("{{< raw-html >}}")
- fmt.Fprintf(buf, "To view a live example of '%s' you can run the commands below (for the cyber-dojo demo organization).
Run the commands below and view the output.", name, liveCliURL)
- buf.WriteString("
")
- buf.WriteString("export KOSLI_ORG=cyber-dojo\n")
- buf.WriteString("export KOSLI_API_TOKEN=Pj_XT2deaVA6V1qrTlthuaWsmjVt4eaHQwqnwqjRO3A # read-only\n")
- buf.WriteString(liveCliFullCommand)
- buf.WriteString("")
- buf.WriteString("{{< / raw-html >}}\n\n")
- }
-
- if len(cmd.Example) > 0 {
- // This is an attempt to tidy up the non-live examples, so they each have their own title.
- // Note: The contents of the title lines could also contain < and > characters which will
- // be lost if simply embedded in a md ## section.
- buf.WriteString("## Examples Use Cases\n\n")
- url := "https://docs.kosli.com/getting_started/install/#assigning-flags-via-environment-variables"
- message := fmt.Sprintf("These examples all assume that the flags `--api-token`, `--org`, `--host`, (and `--flow`, `--trail` when required), are [set/provided](%v). \n\n", url)
- buf.WriteString(message)
-
- // Some non-title lines contain a # character, (eg in a snappish) so we have to
- // split on newlines first and then only split on # in the first position
- example := strings.TrimSpace(cmd.Example)
- lines := strings.Split(example, "\n")
-
- // Some commands have #titles spanning several lines (that is, each title line starts with a # character)
- if name == "kosli report approval" {
- fmt.Fprintf(buf, "```shell\n%s\n```\n\n", example)
- } else if name == "kosli request approval" {
- fmt.Fprintf(buf, "```shell\n%s\n```\n\n", example)
- } else if name == "kosli snapshot server" {
- fmt.Fprintf(buf, "```shell\n%s\n```\n\n", example)
- } else if lines[0][0] != '#' {
- // Some commands, eg 'kosli assert snapshot' have no #title
- // and their example starts immediately with the kosli command.
- fmt.Fprintf(buf, "```shell\n%s\n```\n\n", example)
+ var formatter docgen.Formatter
+ if o.mintlify {
+ formatter = docgen.MintlifyFormatter{}
} else {
- // The rest we can format nicely
- all := hashTitledExamples(lines)
- for i := 0; i < len(all); i++ {
- exampleLines := all[i]
- // Some titles have a trailing colon, some don't
- title := strings.Trim(exampleLines[0], ":")
- if len(title) > 0 {
- fmt.Fprintf(buf, "##### %s\n\n", strings.TrimSpace(title[1:]))
- fmt.Fprintf(buf, "```shell\n%s\n```\n\n", strings.Join(exampleLines[1:], "\n"))
- }
- }
+ formatter = docgen.HugoFormatter{}
}
- }
- _, err := buf.WriteTo(w)
- return err
-}
-
-func hashTitledExamples(lines []string) [][]string {
- // Some non-title lines contain a # character, so we have split on newlines first
- // and then split on # which are the first character in their line
- result := make([][]string, 0)
- example := make([]string, 0)
- for _, line := range lines {
- if strings.HasPrefix(line, "#") {
- result = append(result, example) // See result[1:] at end
- example = make([]string, 0)
- }
- if !isSetWithEnvVar(line) {
- example = append(example, choppedLineContinuation(line))
+ metaFn := func(cmd *cobra.Command) docgen.CommandMeta {
+ return docgen.CommandMeta{
+ Name: cmd.CommandPath(),
+ Beta: isBeta(cmd),
+ Deprecated: isDeprecated(cmd),
+ DeprecMsg: cmd.Deprecated,
+ Summary: cmd.Short,
+ Long: cmd.Long,
+ UseLine: cmd.UseLine(),
+ Runnable: cmd.Runnable(),
+ Example: cmd.Example,
+ }
}
- }
- result = append(result, example)
- return result[1:]
-}
-func isSetWithEnvVar(line string) bool {
- trimmed_line := strings.TrimSpace(line)
- if strings.HasPrefix(trimmed_line, "--api-token ") {
- return true
- } else if strings.HasPrefix(trimmed_line, "--host ") {
- return true
- } else if strings.HasPrefix(trimmed_line, "--org ") {
- return true
- } else if strings.HasPrefix(trimmed_line, "--flow ") {
- return true
- } else if strings.HasPrefix(trimmed_line, "--trail ") {
- return true
- } else {
- return false
+ return docgen.GenMarkdownTree(o.topCmd, o.dest, formatter, metaFn, &kosliLiveDocProvider{})
}
+ return doc.GenMarkdownTree(o.topCmd, o.dest)
}
-func choppedLineContinuation(line string) string {
- trimmed_line := strings.TrimRightFunc(line, unicode.IsSpace)
- return strings.TrimSuffix(trimmed_line, "\\")
-}
-
-func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) error {
- flags := cmd.NonInheritedFlags()
- flags.SetOutput(buf)
- if flags.HasAvailableFlags() {
- buf.WriteString("## Flags\n")
- buf.WriteString("| Flag | Description |\n")
- buf.WriteString("| :--- | :--- |\n")
- usages := CommandsInTable(flags)
- fmt.Fprint(buf, usages)
- buf.WriteString("\n\n")
- }
-
- parentFlags := cmd.InheritedFlags()
- parentFlags.SetOutput(buf)
- if parentFlags.HasAvailableFlags() {
- buf.WriteString("## Flags inherited from parent commands\n")
- buf.WriteString("| Flag | Description |\n")
- buf.WriteString("| :--- | :--- |\n")
- usages := CommandsInTable(parentFlags)
- fmt.Fprint(buf, usages)
- buf.WriteString("\n\n")
- }
- return nil
-}
+// kosliLiveDocProvider implements docgen.LiveDocProvider using HTTP calls
+// to the Kosli live docs API.
+type kosliLiveDocProvider struct{}
const baseURL = "https://app.kosli.com/api/v2/livedocs/cyber-dojo"
-func liveYamlDocExists(ci string, command string) bool {
+func (p *kosliLiveDocProvider) YamlDocExists(ci, command string) bool {
url := fmt.Sprintf("%s/yaml_exists?ci=%s&command=%s", baseURL, strings.ToLower(ci), command)
return liveDocExists(url)
}
-func liveEventDocExists(ci string, command string) bool {
+func (p *kosliLiveDocProvider) EventDocExists(ci, command string) bool {
url := fmt.Sprintf("%s/event_exists?ci=%s&command=%s", baseURL, strings.ToLower(ci), command)
return liveDocExists(url)
}
-func liveCliDocExists(command string) (string, string, bool) {
+func (p *kosliLiveDocProvider) YamlURL(ci, command string) string {
+ return fmt.Sprintf("%s/yaml?ci=%s&command=%s", baseURL, strings.ToLower(ci), command)
+}
+
+func (p *kosliLiveDocProvider) EventURL(ci, command string) string {
+ return fmt.Sprintf("%s/event?ci=%s&command=%s", baseURL, strings.ToLower(ci), command)
+}
+
+func (p *kosliLiveDocProvider) CLIDocExists(command string) (string, string, bool) {
fullCommand, ok := liveCliMap[command]
if ok {
plussed := strings.ReplaceAll(fullCommand, " ", "+")
- exists_url := fmt.Sprintf("%s/cli_exists?command=%s", baseURL, plussed)
+ existsURL := fmt.Sprintf("%s/cli_exists?command=%s", baseURL, plussed)
url := fmt.Sprintf("%s/cli?command=%s", baseURL, plussed)
- return fullCommand, url, liveDocExists(exists_url)
- } else {
- return "", "", false
+ return fullCommand, url, liveDocExists(existsURL)
}
+ return "", "", false
}
func liveDocExists(url string) bool {
- response, err := http.Get(url)
+ response, err := http.Get(url) //nolint:gosec
if err != nil {
return false
}
@@ -331,14 +130,6 @@ func liveDocExists(url string) bool {
return exists
}
-func yamlURL(ci string, command string) string {
- return fmt.Sprintf("%s/yaml?ci=%s&command=%s", baseURL, strings.ToLower(ci), command)
-}
-
-func eventURL(ci string, command string) string {
- return fmt.Sprintf("%s/event?ci=%s&command=%s", baseURL, strings.ToLower(ci), command)
-}
-
var liveCliMap = map[string]string{
"kosli list environments": "kosli list environments --output=json",
"kosli get environment": "kosli get environment aws-prod --output=json",
diff --git a/cmd/kosli/docs_test.go b/cmd/kosli/docs_test.go
index 2f383f6e0..b2979f7f5 100644
--- a/cmd/kosli/docs_test.go
+++ b/cmd/kosli/docs_test.go
@@ -24,7 +24,7 @@ func (suite *DocsCommandTestSuite) TestDocsCmd() {
// Then:
// - make test_integration_single TARGET=TestDocsCommandTestSuite
// will tell you where the new snyk.md master file lives.
- // Then copy it to ./cmd/kosli/testdata/output/docs/
+ // Then copy it to ./cmd/kosli/testdata/output/docs/hugo/
// and undo the changes above.
global = &GlobalOpts{}
tempDirName, err := os.MkdirTemp("", "generatedDocs")
@@ -46,7 +46,57 @@ func (suite *DocsCommandTestSuite) TestDocsCmd() {
actualFile := filepath.Join(tempDirName, "snyk.md")
require.FileExists(suite.T(), actualFile)
- err = compareTwoFiles(actualFile, goldenPath("output/docs/snyk.md"))
+ err = compareTwoFiles(actualFile, goldenPath("output/docs/hugo/snyk.md"))
+ require.NoError(suite.T(), err)
+}
+
+func (suite *DocsCommandTestSuite) TestDocsCmdMintlifySnyk() {
+ global = &GlobalOpts{}
+ tempDirName, err := os.MkdirTemp("", "generatedDocsMintlify")
+ require.NoError(suite.T(), err)
+ defer func() {
+ if err := os.RemoveAll(tempDirName); err != nil {
+ require.NoError(suite.T(), err, "failed to remove temp dir %s", tempDirName)
+ }
+ }()
+
+ o := &docsOptions{
+ dest: tempDirName,
+ topCmd: newAttestSnykCmd(os.Stdout),
+ generateHeaders: true,
+ mintlify: true,
+ }
+ err = o.run()
+ require.NoError(suite.T(), err)
+
+ actualFile := filepath.Join(tempDirName, "snyk.md")
+ require.FileExists(suite.T(), actualFile)
+ err = compareTwoFiles(actualFile, goldenPath("output/docs/mintlify/snyk.md"))
+ require.NoError(suite.T(), err)
+}
+
+func (suite *DocsCommandTestSuite) TestDocsCmdMintlifyDeprecated() {
+ global = &GlobalOpts{}
+ tempDirName, err := os.MkdirTemp("", "generatedDocsMintlifyDeprecated")
+ require.NoError(suite.T(), err)
+ defer func() {
+ if err := os.RemoveAll(tempDirName); err != nil {
+ require.NoError(suite.T(), err, "failed to remove temp dir %s", tempDirName)
+ }
+ }()
+
+ o := &docsOptions{
+ dest: tempDirName,
+ topCmd: newReportArtifactCmd(os.Stdout),
+ generateHeaders: true,
+ mintlify: true,
+ }
+ err = o.run()
+ require.NoError(suite.T(), err)
+
+ actualFile := filepath.Join(tempDirName, "artifact.md")
+ require.FileExists(suite.T(), actualFile)
+ err = compareTwoFiles(actualFile, goldenPath("output/docs/mintlify/artifact.md"))
require.NoError(suite.T(), err)
}
diff --git a/cmd/kosli/printCommandsInTable.go b/cmd/kosli/printCommandsInTable.go
deleted file mode 100644
index bf1d2902a..000000000
--- a/cmd/kosli/printCommandsInTable.go
+++ /dev/null
@@ -1,89 +0,0 @@
-package main
-
-import (
- "bytes"
- "fmt"
- "strings"
-
- "github.com/spf13/pflag"
-)
-
-func CommandsInTable(f *pflag.FlagSet) string {
- buf := new(bytes.Buffer)
-
- lines := make([]string, 0, 100)
-
- maxlen := 0
- f.VisitAll(func(flag *pflag.Flag) {
- if flag.Hidden {
- return
- }
-
- line := ""
- if flag.Shorthand != "" && flag.ShorthandDeprecated == "" {
- line = fmt.Sprintf(" -%s, --%s", flag.Shorthand, flag.Name)
- } else {
- line = fmt.Sprintf(" --%s", flag.Name)
- }
-
- varname, usage := pflag.UnquoteUsage(flag)
- if varname != "" {
- line += " " + varname
- }
- if flag.NoOptDefVal != "" {
- switch flag.Value.Type() {
- case "string":
- line += fmt.Sprintf("[=\"%s\"]", flag.NoOptDefVal)
- case "bool":
- if flag.NoOptDefVal != "true" {
- line += fmt.Sprintf("[=%s]", flag.NoOptDefVal)
- }
- case "count":
- if flag.NoOptDefVal != "+1" {
- line += fmt.Sprintf("[=%s]", flag.NoOptDefVal)
- }
- default:
- line += fmt.Sprintf("[=%s]", flag.NoOptDefVal)
- }
- }
-
- // This special character will be replaced with spacing once the
- // correct alignment is calculated
- line += "\x00"
- if len(line) > maxlen {
- maxlen = len(line)
- }
-
- line += usage
- defaultZero := []string{"", "0", "[]", "")
+ b.WriteString("export KOSLI_ORG=cyber-dojo\n")
+ b.WriteString("export KOSLI_API_TOKEN=Pj_XT2deaVA6V1qrTlthuaWsmjVt4eaHQwqnwqjRO3A # read-only\n")
+ b.WriteString(fullCommand)
+ b.WriteString("")
+ b.WriteString("{{< / raw-html >}}\n\n")
+ return b.String()
+}
+
+func (HugoFormatter) ExampleUseCases(commandName, example string) string {
+ var b strings.Builder
+ b.WriteString("## Examples Use Cases\n\n")
+ url := "https://docs.kosli.com/getting_started/install/#assigning-flags-via-environment-variables"
+ fmt.Fprintf(&b, "These examples all assume that the flags `--api-token`, `--org`, `--host`, (and `--flow`, `--trail` when required), are [set/provided](%v). \n\n", url)
+
+ example = strings.TrimSpace(example)
+ lines := strings.Split(example, "\n")
+
+ if commandName == "kosli report approval" ||
+ commandName == "kosli request approval" ||
+ commandName == "kosli snapshot server" {
+ fmt.Fprintf(&b, "```shell\n%s\n```\n\n", example)
+ } else if lines[0][0] != '#' {
+ fmt.Fprintf(&b, "```shell\n%s\n```\n\n", example)
+ } else {
+ all := HashTitledExamples(lines)
+ for i := 0; i < len(all); i++ {
+ exampleLines := all[i]
+ title := strings.Trim(exampleLines[0], ":")
+ if len(title) > 0 {
+ fmt.Fprintf(&b, "##### %s\n\n", strings.TrimSpace(title[1:]))
+ fmt.Fprintf(&b, "```shell\n%s\n```\n\n", strings.Join(exampleLines[1:], "\n"))
+ }
+ }
+ }
+ return b.String()
+}
+
+func (HugoFormatter) LinkHandler(name string) string {
+ base := strings.TrimSuffix(name, path.Ext(name))
+ return "/client_reference/" + strings.ToLower(base) + "/"
+}
diff --git a/internal/docgen/hugo_test.go b/internal/docgen/hugo_test.go
new file mode 100644
index 000000000..ef8184a23
--- /dev/null
+++ b/internal/docgen/hugo_test.go
@@ -0,0 +1,127 @@
+package docgen
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestHugoFrontMatter(t *testing.T) {
+ f := HugoFormatter{}
+ meta := CommandMeta{Name: "kosli attest snyk", Summary: "Report a snyk attestation"}
+ got := f.FrontMatter(meta)
+ if !strings.Contains(got, `title: "kosli attest snyk"`) {
+ t.Errorf("expected title in front matter, got:\n%s", got)
+ }
+ if !strings.Contains(got, "beta: false") {
+ t.Errorf("expected beta: false, got:\n%s", got)
+ }
+ if !strings.Contains(got, `summary: "Report a snyk attestation"`) {
+ t.Errorf("expected summary, got:\n%s", got)
+ }
+}
+
+func TestHugoBetaWarning(t *testing.T) {
+ f := HugoFormatter{}
+ got := f.BetaWarning("kosli foo")
+ if !strings.Contains(got, "{{% hint warning %}}") {
+ t.Error("expected Hugo hint warning shortcode")
+ }
+ if !strings.Contains(got, "**kosli foo** is a beta feature") {
+ t.Error("expected command name in warning")
+ }
+}
+
+func TestHugoDeprecatedWarning(t *testing.T) {
+ f := HugoFormatter{}
+ got := f.DeprecatedWarning("kosli artifact", "see kosli attest commands")
+ if !strings.Contains(got, "{{% hint danger %}}") {
+ t.Error("expected Hugo hint danger shortcode")
+ }
+ if !strings.Contains(got, "**kosli artifact** is deprecated. see kosli attest commands") {
+ t.Error("expected deprecation message")
+ }
+}
+
+func TestHugoSynopsis(t *testing.T) {
+ f := HugoFormatter{}
+ meta := CommandMeta{
+ Long: "Report a ^snyk^ attestation.",
+ UseLine: "snyk [IMAGE-NAME] [flags]",
+ Runnable: true,
+ }
+ got := f.Synopsis(meta)
+ if !strings.Contains(got, "## Synopsis") {
+ t.Error("expected Synopsis heading")
+ }
+ if !strings.Contains(got, "```shell\nsnyk [IMAGE-NAME] [flags]\n```") {
+ t.Error("expected shell code block with usage line")
+ }
+ if !strings.Contains(got, "Report a `snyk` attestation.") {
+ t.Error("expected carets replaced with backticks")
+ }
+}
+
+func TestHugoSynopsisNotRunnable(t *testing.T) {
+ f := HugoFormatter{}
+ meta := CommandMeta{Long: "Some description", Runnable: false}
+ got := f.Synopsis(meta)
+ if strings.Contains(got, "```shell") {
+ t.Error("should not contain code block for non-runnable command")
+ }
+}
+
+func TestHugoLiveCIExamples(t *testing.T) {
+ f := HugoFormatter{}
+ examples := []CIExample{
+ {CI: "GitHub", YamlURL: "http://yaml", EventURL: "http://event"},
+ }
+ got := f.LiveCIExamples(examples, "kosli attest snyk")
+ if !strings.Contains(got, `{{< tabs "live-examples"`) {
+ t.Error("expected Hugo tabs shortcode")
+ }
+ if !strings.Contains(got, `{{< tab "GitHub" >}}`) {
+ t.Error("expected GitHub tab")
+ }
+}
+
+func TestHugoLiveCIExamplesEmpty(t *testing.T) {
+ f := HugoFormatter{}
+ got := f.LiveCIExamples(nil, "cmd")
+ if got != "" {
+ t.Error("expected empty string for no examples")
+ }
+}
+
+func TestHugoLiveCLIExample(t *testing.T) {
+ f := HugoFormatter{}
+ got := f.LiveCLIExample("kosli list environments", "kosli list environments --output=json", "http://example.com")
+ if !strings.Contains(got, "{{< raw-html >}}") {
+ t.Error("expected raw-html shortcode")
+ }
+ if !strings.Contains(got, "kosli list environments --output=json") {
+ t.Error("expected CLI command in output")
+ }
+}
+
+func TestHugoExampleUseCases(t *testing.T) {
+ f := HugoFormatter{}
+ example := "# report a snyk attestation\nkosli attest snyk foo"
+ got := f.ExampleUseCases("kosli attest snyk", example)
+ if !strings.Contains(got, "## Examples Use Cases") {
+ t.Error("expected heading")
+ }
+ if !strings.Contains(got, "##### report a snyk attestation") {
+ t.Error("expected hash-titled example")
+ }
+ if !strings.Contains(got, "https://docs.kosli.com/getting_started") {
+ t.Error("expected full URL for Hugo format")
+ }
+}
+
+func TestHugoLinkHandler(t *testing.T) {
+ f := HugoFormatter{}
+ got := f.LinkHandler("kosli_attest_snyk.md")
+ if got != "/client_reference/kosli_attest_snyk/" {
+ t.Errorf("expected trailing slash link, got: %s", got)
+ }
+}
diff --git a/internal/docgen/mintlify.go b/internal/docgen/mintlify.go
new file mode 100644
index 000000000..bcb5b63e8
--- /dev/null
+++ b/internal/docgen/mintlify.go
@@ -0,0 +1,192 @@
+package docgen
+
+import (
+ "fmt"
+ "path"
+ "regexp"
+ "strings"
+)
+
+// MintlifyFormatter generates Mintlify-compatible MDX documentation.
+type MintlifyFormatter struct{}
+
+func (MintlifyFormatter) Title(name string) string {
+ return "" // Mintlify renders title from front matter
+}
+
+func (MintlifyFormatter) FrontMatter(meta CommandMeta) string {
+ desc := sanitizeDescription(meta.Summary)
+ return fmt.Sprintf("---\ntitle: \"%s\"\nbeta: %t\ndeprecated: %t\ndescription: \"%s\"\n---\n\n",
+ meta.Name, meta.Beta, meta.Deprecated, desc)
+}
+
+func (MintlifyFormatter) BetaWarning(name string) string {
+ var b strings.Builder
+ b.WriteString("")
+ b.WriteString("export KOSLI_ORG=cyber-dojo\n")
+ b.WriteString("export KOSLI_API_TOKEN=Pj_XT2deaVA6V1qrTlthuaWsmjVt4eaHQwqnwqjRO3A # read-only\n")
+ b.WriteString(fullCommand)
+ b.WriteString("\n\n")
+ return b.String()
+}
+
+func (MintlifyFormatter) ExampleUseCases(commandName, example string) string {
+ var b strings.Builder
+ b.WriteString("## Examples Use Cases\n\n")
+ url := "/getting_started/install/#assigning-flags-via-environment-variables"
+ fmt.Fprintf(&b, "These examples all assume that the flags `--api-token`, `--org`, `--host`, (and `--flow`, `--trail` when required), are [set/provided](%v). \n\n", url)
+
+ example = strings.TrimSpace(example)
+ lines := strings.Split(example, "\n")
+
+ if commandName == "kosli report approval" ||
+ commandName == "kosli request approval" ||
+ commandName == "kosli snapshot server" {
+ fmt.Fprintf(&b, "```shell\n%s\n```\n\n", example)
+ } else if lines[0][0] != '#' {
+ fmt.Fprintf(&b, "```shell\n%s\n```\n\n", example)
+ } else {
+ all := HashTitledExamples(lines)
+ b.WriteString(" alone
+ s = angleBracketPattern.ReplaceAllString(s, "`$1`")
+
+ return s
+}
diff --git a/internal/docgen/mintlify_test.go b/internal/docgen/mintlify_test.go
new file mode 100644
index 000000000..f64b9c4ff
--- /dev/null
+++ b/internal/docgen/mintlify_test.go
@@ -0,0 +1,248 @@
+package docgen
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestMintlifyFrontMatter(t *testing.T) {
+ f := MintlifyFormatter{}
+ meta := CommandMeta{Name: "kosli attest snyk", Summary: "Report a snyk attestation"}
+ got := f.FrontMatter(meta)
+ if !strings.Contains(got, `title: "kosli attest snyk"`) {
+ t.Errorf("expected title, got:\n%s", got)
+ }
+ if !strings.Contains(got, `description: "Report a snyk attestation"`) {
+ t.Errorf("expected description, got:\n%s", got)
+ }
+}
+
+func TestMintlifyFrontMatterSanitizesDescription(t *testing.T) {
+ f := MintlifyFormatter{}
+ meta := CommandMeta{Name: "cmd", Summary: "Use ^foo^ with \"quotes\""}
+ got := f.FrontMatter(meta)
+ if !strings.Contains(got, "description: \"Use `foo` with 'quotes'\"") {
+ t.Errorf("expected sanitized description, got:\n%s", got)
+ }
+}
+
+func TestMintlifyFrontMatterTruncatesLongDescription(t *testing.T) {
+ f := MintlifyFormatter{}
+ long := strings.Repeat("a", 250)
+ meta := CommandMeta{Name: "cmd", Summary: long}
+ got := f.FrontMatter(meta)
+ if !strings.Contains(got, "...") {
+ t.Error("expected truncated description")
+ }
+}
+
+func TestMintlifyBetaWarning(t *testing.T) {
+ f := MintlifyFormatter{}
+ got := f.BetaWarning("kosli foo")
+ if !strings.Contains(got, "") {
+ t.Error("expected Warning component")
+ }
+ if !strings.Contains(got, " ") {
+ t.Error("expected closing Warning component")
+ }
+ if !strings.Contains(got, "**kosli foo** is a beta feature") {
+ t.Error("expected command name in warning")
+ }
+}
+
+func TestMintlifyDeprecatedWarning(t *testing.T) {
+ f := MintlifyFormatter{}
+ got := f.DeprecatedWarning("kosli artifact", "see kosli attest commands")
+ if !strings.Contains(got, "") {
+ t.Error("expected Warning component")
+ }
+ if !strings.Contains(got, "**kosli artifact** is deprecated") {
+ t.Error("expected deprecation message")
+ }
+}
+
+func TestMintlifySynopsis(t *testing.T) {
+ f := MintlifyFormatter{}
+ meta := CommandMeta{
+ Long: "Report a ^snyk^ attestation.",
+ UseLine: "snyk [IMAGE-NAME] [flags]",
+ Runnable: true,
+ }
+ got := f.Synopsis(meta)
+ if !strings.Contains(got, "## Synopsis") {
+ t.Error("expected Synopsis heading")
+ }
+ if !strings.Contains(got, "Report a `snyk` attestation.") {
+ t.Error("expected carets replaced")
+ }
+}
+
+func TestMintlifyLiveCIExamples(t *testing.T) {
+ f := MintlifyFormatter{}
+ examples := []CIExample{
+ {CI: "GitHub", YamlURL: "http://yaml", EventURL: "http://event"},
+ {CI: "GitLab", YamlURL: "http://yaml2"},
+ }
+ got := f.LiveCIExamples(examples, "kosli attest snyk")
+ if !strings.Contains(got, "") {
+ t.Error("expected Tabs component")
+ }
+ if !strings.Contains(got, ``) {
+ t.Error("expected GitHub tab")
+ }
+ if !strings.Contains(got, ``) {
+ t.Error("expected GitLab tab")
+ }
+ if !strings.Contains(got, " ") {
+ t.Error("expected closing Tabs")
+ }
+}
+
+func TestMintlifyLiveCLIExample(t *testing.T) {
+ f := MintlifyFormatter{}
+ got := f.LiveCLIExample("kosli list environments", "kosli list environments --output=json", "http://example.com")
+ // Should NOT contain Hugo shortcode wrappers
+ if strings.Contains(got, "{{< raw-html >}}") {
+ t.Error("should not contain Hugo shortcode")
+ }
+ if !strings.Contains(got, "") {
+ t.Error("expected raw HTML pre tag")
+ }
+}
+
+func TestMintlifyExampleUseCases(t *testing.T) {
+ f := MintlifyFormatter{}
+ example := "# report a snyk attestation\nkosli attest snyk foo"
+ got := f.ExampleUseCases("kosli attest snyk", example)
+ if !strings.Contains(got, "") {
+ t.Error("expected AccordionGroup component")
+ }
+ if !strings.Contains(got, ``) {
+ t.Error("expected Accordion with title")
+ }
+ if !strings.Contains(got, " ") {
+ t.Error("expected closing AccordionGroup")
+ }
+ if strings.Contains(got, "https://docs.kosli.com") {
+ t.Error("expected bare docs.kosli.com URLs to be linkified")
+ }
+}
+
+func TestMintlifyLinkHandler(t *testing.T) {
+ f := MintlifyFormatter{}
+ got := f.LinkHandler("kosli_attest_snyk.md")
+ if got != "/client_reference/kosli_attest_snyk" {
+ t.Errorf("expected no trailing slash, got: %s", got)
+ }
+}
+
+func TestLinkifyKosliDocsURLs(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want string
+ }{
+ {
+ name: "bare URL becomes markdown link",
+ input: "A boolean flag https://docs.kosli.com/faq/#boolean-flags (default false)",
+ want: "A boolean flag [docs](/faq/#boolean-flags) (default false)",
+ },
+ {
+ name: "URL followed by comma",
+ input: "(defaulted in some CIs: https://docs.kosli.com/ci-defaults, otherwise defaults to HEAD ).",
+ want: "(defaulted in some CIs: [docs](/ci-defaults), otherwise defaults to HEAD ).",
+ },
+ {
+ name: "URL followed by space and closing paren",
+ input: "(defaulted in some CIs: https://docs.kosli.com/ci-defaults ).",
+ want: "(defaulted in some CIs: [docs](/ci-defaults) ).",
+ },
+ {
+ name: "long path URL",
+ input: "see https://docs.kosli.com/integrations/ci_cd/#defaulted-kosli-command-flags-from-ci-variables .",
+ want: "see [docs](/integrations/ci_cd/#defaulted-kosli-command-flags-from-ci-variables) .",
+ },
+ {
+ name: "non-kosli URL untouched",
+ input: "see https://example.com/foo for details",
+ want: "see https://example.com/foo for details",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := linkifyKosliDocsURLs(tt.input)
+ if got != tt.want {
+ t.Errorf("linkifyKosliDocsURLs(%q):\ngot: %q\nwant: %q", tt.input, got, tt.want)
+ }
+ })
+ }
+}
+
+func TestEscapeMintlifyProse(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want string
+ }{
+ {
+ name: "curly braces escaped",
+ input: "Use {expression} here",
+ want: "Use \\{expression\\} here",
+ },
+ {
+ name: "uppercase angle brackets converted",
+ input: "Use as input",
+ want: "Use `IMAGE-NAME` as input",
+ },
+ {
+ name: "lowercase HTML tags preserved",
+ input: "Use link",
+ want: "Use link",
+ },
+ {
+ name: "code fence content not escaped",
+ input: "text ```\n{code}\n\n``` more {text}",
+ want: "text ```\n{code}\n\n``` more \\{text\\}",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := escapeMintlifyProse(tt.input)
+ if got != tt.want {
+ t.Errorf("escapeMintlifyProse(%q):\ngot: %q\nwant: %q", tt.input, got, tt.want)
+ }
+ })
+ }
+}
+
+func TestSanitizeDescription(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want string
+ }{
+ {
+ name: "carets to backticks",
+ input: "Use ^foo^ bar",
+ want: "Use `foo` bar",
+ },
+ {
+ name: "quotes escaped",
+ input: `Say "hello"`,
+ want: "Say 'hello'",
+ },
+ {
+ name: "truncation",
+ input: strings.Repeat("x", 250),
+ want: strings.Repeat("x", 197) + "...",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := sanitizeDescription(tt.input)
+ if got != tt.want {
+ t.Errorf("sanitizeDescription(%q) = %q, want %q", tt.input, got, tt.want)
+ }
+ })
+ }
+}