From 2e24bb59f6194095b29cf82f813e10aae8b43472 Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 21 Feb 2026 02:27:28 +0000 Subject: [PATCH] feat(docs): add --target hugo sync mode for core.help Co-Authored-By: Virgil --- cmd/docs/cmd_sync.go | 173 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 170 insertions(+), 3 deletions(-) diff --git a/cmd/docs/cmd_sync.go b/cmd/docs/cmd_sync.go index ef4de487..fb2d6cb7 100644 --- a/cmd/docs/cmd_sync.go +++ b/cmd/docs/cmd_sync.go @@ -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 +}