feat(docs): add --target hugo sync mode for core.help
Some checks failed
Security Scan / Secret Detection (push) Failing after 20s
Security Scan / Dependency & Config Scan (push) Failing after 18s
Security Scan / Go Vulnerability Check (push) Failing after 1m16s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-02-21 02:27:28 +00:00
parent 349647d4aa
commit 2e24bb59f6

View file

@ -1,12 +1,15 @@
package docs
import (
"bytes"
"fmt"
"path/filepath"
"strings"
"forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/go/pkg/i18n"
"forge.lthn.ai/core/go/pkg/io"
"forge.lthn.ai/core/go/pkg/repos"
)
// Flag variables for sync command
@ -14,6 +17,7 @@ var (
docsSyncRegistryPath string
docsSyncDryRun bool
docsSyncOutputDir string
docsSyncTarget string
)
var docsSyncCmd = &cli.Command{
@ -21,7 +25,7 @@ var docsSyncCmd = &cli.Command{
Short: i18n.T("cmd.docs.sync.short"),
Long: i18n.T("cmd.docs.sync.long"),
RunE: func(cmd *cli.Command, args []string) error {
return runDocsSync(docsSyncRegistryPath, docsSyncOutputDir, docsSyncDryRun)
return runDocsSync(docsSyncRegistryPath, docsSyncOutputDir, docsSyncDryRun, docsSyncTarget)
},
}
@ -29,6 +33,7 @@ func init() {
docsSyncCmd.Flags().StringVar(&docsSyncRegistryPath, "registry", "", i18n.T("common.flag.registry"))
docsSyncCmd.Flags().BoolVar(&docsSyncDryRun, "dry-run", false, i18n.T("cmd.docs.sync.flag.dry_run"))
docsSyncCmd.Flags().StringVar(&docsSyncOutputDir, "output", "", i18n.T("cmd.docs.sync.flag.output"))
docsSyncCmd.Flags().StringVar(&docsSyncTarget, "target", "php", "Target format: php (default) or hugo")
}
// packageOutputName maps repo name to output folder name
@ -57,13 +62,21 @@ func shouldSyncRepo(repoName string) bool {
return true
}
func runDocsSync(registryPath string, outputDir string, dryRun bool) error {
// Find or use provided registry
func runDocsSync(registryPath string, outputDir string, dryRun bool, target string) error {
reg, basePath, err := loadRegistry(registryPath)
if err != nil {
return err
}
switch target {
case "hugo":
return runHugoSync(reg, basePath, outputDir, dryRun)
default:
return runPHPSync(reg, basePath, outputDir, dryRun)
}
}
func runPHPSync(reg *repos.Registry, basePath string, outputDir string, dryRun bool) error {
// Default output to core-php/docs/packages relative to registry
if outputDir == "" {
outputDir = filepath.Join(basePath, "core-php", "docs", "packages")
@ -158,3 +171,157 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error {
return nil
}
// hugoOutputName maps repo name to Hugo content section and folder.
func hugoOutputName(repoName string) (string, string) {
if repoName == "cli" {
return "getting-started", ""
}
if repoName == "core" {
return "cli", ""
}
if strings.HasPrefix(repoName, "go-") {
return "go", repoName
}
if strings.HasPrefix(repoName, "core-") {
return "php", strings.TrimPrefix(repoName, "core-")
}
return "go", repoName
}
// injectFrontMatter prepends Hugo front matter to markdown content if missing.
func injectFrontMatter(content []byte, title string, weight int) []byte {
if bytes.HasPrefix(bytes.TrimSpace(content), []byte("---")) {
return content
}
fm := fmt.Sprintf("---\ntitle: %q\nweight: %d\n---\n\n", title, weight)
return append([]byte(fm), content...)
}
// titleFromFilename derives a human-readable title from a filename.
func titleFromFilename(filename string) string {
name := strings.TrimSuffix(filepath.Base(filename), ".md")
name = strings.ReplaceAll(name, "-", " ")
name = strings.ReplaceAll(name, "_", " ")
words := strings.Fields(name)
for i, w := range words {
if len(w) > 0 {
words[i] = strings.ToUpper(w[:1]) + w[1:]
}
}
return strings.Join(words, " ")
}
// copyWithFrontMatter copies a markdown file, injecting front matter if missing.
func copyWithFrontMatter(src, dst string, weight int) error {
if err := io.Local.EnsureDir(filepath.Dir(dst)); err != nil {
return err
}
content, err := io.Local.Read(src)
if err != nil {
return err
}
title := titleFromFilename(src)
result := injectFrontMatter([]byte(content), title, weight)
return io.Local.Write(dst, string(result))
}
func runHugoSync(reg *repos.Registry, basePath string, outputDir string, dryRun bool) error {
if outputDir == "" {
outputDir = filepath.Join(basePath, "docs-site", "content")
}
var docsInfo []RepoDocInfo
for _, repo := range reg.List() {
if repo.Name == "core-template" || repo.Name == "core-claude" {
continue
}
info := scanRepoDocs(repo)
if info.HasDocs {
docsInfo = append(docsInfo, info)
}
}
if len(docsInfo) == 0 {
cli.Text("No documentation found")
return nil
}
cli.Print("\n Hugo sync: %d repos with docs → %s\n\n", len(docsInfo), outputDir)
for _, info := range docsInfo {
section, folder := hugoOutputName(info.Name)
target := section
if folder != "" {
target = section + "/" + folder
}
fileCount := len(info.DocsFiles) + len(info.KBFiles)
if info.Readme != "" {
fileCount++
}
cli.Print(" %s → %s/ (%d files)\n", repoNameStyle.Render(info.Name), target, fileCount)
}
if dryRun {
cli.Print("\n Dry run — no files written\n")
return nil
}
cli.Blank()
if !confirm("Sync to Hugo content directory?") {
cli.Text("Aborted")
return nil
}
cli.Blank()
var synced int
for _, info := range docsInfo {
section, folder := hugoOutputName(info.Name)
destDir := filepath.Join(outputDir, section)
if folder != "" {
destDir = filepath.Join(destDir, folder)
}
weight := 10
docsDir := filepath.Join(info.Path, "docs")
for _, f := range info.DocsFiles {
src := filepath.Join(docsDir, f)
dst := filepath.Join(destDir, f)
if err := copyWithFrontMatter(src, dst, weight); err != nil {
cli.Print(" %s %s: %s\n", errorStyle.Render("✗"), f, err)
continue
}
weight += 10
}
if info.Readme != "" && folder != "" {
dst := filepath.Join(destDir, "_index.md")
if err := copyWithFrontMatter(info.Readme, dst, 1); err != nil {
cli.Print(" %s README: %s\n", errorStyle.Render("✗"), err)
}
}
if len(info.KBFiles) > 0 {
suffix := strings.TrimPrefix(info.Name, "go-")
kbDestDir := filepath.Join(outputDir, "kb", suffix)
kbDir := filepath.Join(info.Path, "KB")
kbWeight := 10
for _, f := range info.KBFiles {
src := filepath.Join(kbDir, f)
dst := filepath.Join(kbDestDir, f)
if err := copyWithFrontMatter(src, dst, kbWeight); err != nil {
cli.Print(" %s KB/%s: %s\n", errorStyle.Render("✗"), f, err)
continue
}
kbWeight += 10
}
}
cli.Print(" %s %s\n", successStyle.Render("✓"), info.Name)
synced++
}
cli.Print("\n Synced %d repos to Hugo content\n", synced)
return nil
}