* feat(help): Add CLI help command Fixes #136 * chore: remove binary * feat(mcp): Add TCP transport Fixes #126 * feat(io): Migrate pkg/mcp to use Medium abstraction Fixes #103 * chore(io): Migrate internal/cmd/docs/* to Medium abstraction Fixes #113 * chore(io): Migrate internal/cmd/dev/* to Medium abstraction Fixes #114 * chore(io): Migrate internal/cmd/setup/* to Medium abstraction * chore(io): Complete migration of internal/cmd/dev/* to Medium abstraction * chore(io): Migrate internal/cmd/sdk, pkgcmd, and workspace to Medium abstraction * style: fix formatting in internal/variants Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(io): simplify local Medium implementation Rewrote to match the simpler TypeScript pattern: - path() sanitizes and returns string directly - Each method calls path() once - No complex symlink validation - Less code, less attack surface Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(mcp): update sandboxing tests for simplified Medium The simplified io/local.Medium implementation: - Sanitizes .. to . (no error, path is cleaned) - Allows absolute paths through (caller validates if needed) - Follows symlinks (no traversal blocking) Update tests to match this simplified behavior. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(updater): resolve PkgVersion duplicate declaration Remove var PkgVersion from updater.go since go generate creates const PkgVersion in version.go. Track version.go in git to ensure builds work without running go generate first. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
157 lines
4.3 KiB
Go
157 lines
4.3 KiB
Go
package docs
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/host-uk/core/pkg/cli"
|
|
"github.com/host-uk/core/pkg/i18n"
|
|
"github.com/host-uk/core/pkg/io"
|
|
)
|
|
|
|
// Flag variables for sync command
|
|
var (
|
|
docsSyncRegistryPath string
|
|
docsSyncDryRun bool
|
|
docsSyncOutputDir string
|
|
)
|
|
|
|
var docsSyncCmd = &cli.Command{
|
|
Use: "sync",
|
|
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)
|
|
},
|
|
}
|
|
|
|
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"))
|
|
}
|
|
|
|
// packageOutputName maps repo name to output folder name
|
|
func packageOutputName(repoName string) string {
|
|
// core -> go (the Go framework)
|
|
if repoName == "core" {
|
|
return "go"
|
|
}
|
|
// core-admin -> admin, core-api -> api, etc.
|
|
if strings.HasPrefix(repoName, "core-") {
|
|
return strings.TrimPrefix(repoName, "core-")
|
|
}
|
|
return repoName
|
|
}
|
|
|
|
// shouldSyncRepo returns true if this repo should be synced
|
|
func shouldSyncRepo(repoName string) bool {
|
|
// Skip core-php (it's the destination)
|
|
if repoName == "core-php" {
|
|
return false
|
|
}
|
|
// Skip template
|
|
if repoName == "core-template" {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func runDocsSync(registryPath string, outputDir string, dryRun bool) error {
|
|
// Find or use provided registry
|
|
reg, basePath, err := loadRegistry(registryPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Default output to core-php/docs/packages relative to registry
|
|
if outputDir == "" {
|
|
outputDir = filepath.Join(basePath, "core-php", "docs", "packages")
|
|
}
|
|
|
|
// Scan all repos for docs
|
|
var docsInfo []RepoDocInfo
|
|
for _, repo := range reg.List() {
|
|
if !shouldSyncRepo(repo.Name) {
|
|
continue
|
|
}
|
|
info := scanRepoDocs(repo)
|
|
if info.HasDocs && len(info.DocsFiles) > 0 {
|
|
docsInfo = append(docsInfo, info)
|
|
}
|
|
}
|
|
|
|
if len(docsInfo) == 0 {
|
|
cli.Text(i18n.T("cmd.docs.sync.no_docs_found"))
|
|
return nil
|
|
}
|
|
|
|
cli.Print("\n%s %s\n\n", dimStyle.Render(i18n.T("cmd.docs.sync.found_label")), i18n.T("cmd.docs.sync.repos_with_docs", map[string]interface{}{"Count": len(docsInfo)}))
|
|
|
|
// Show what will be synced
|
|
var totalFiles int
|
|
for _, info := range docsInfo {
|
|
totalFiles += len(info.DocsFiles)
|
|
outName := packageOutputName(info.Name)
|
|
cli.Print(" %s → %s %s\n",
|
|
repoNameStyle.Render(info.Name),
|
|
docsFileStyle.Render("packages/"+outName+"/"),
|
|
dimStyle.Render(i18n.T("cmd.docs.sync.files_count", map[string]interface{}{"Count": len(info.DocsFiles)})))
|
|
|
|
for _, f := range info.DocsFiles {
|
|
cli.Print(" %s\n", dimStyle.Render(f))
|
|
}
|
|
}
|
|
|
|
cli.Print("\n%s %s\n",
|
|
dimStyle.Render(i18n.Label("total")),
|
|
i18n.T("cmd.docs.sync.total_summary", map[string]interface{}{"Files": totalFiles, "Repos": len(docsInfo), "Output": outputDir}))
|
|
|
|
if dryRun {
|
|
cli.Print("\n%s\n", dimStyle.Render(i18n.T("cmd.docs.sync.dry_run_notice")))
|
|
return nil
|
|
}
|
|
|
|
// Confirm
|
|
cli.Blank()
|
|
if !confirm(i18n.T("cmd.docs.sync.confirm")) {
|
|
cli.Text(i18n.T("common.prompt.abort"))
|
|
return nil
|
|
}
|
|
|
|
// Sync docs
|
|
cli.Blank()
|
|
var synced int
|
|
for _, info := range docsInfo {
|
|
outName := packageOutputName(info.Name)
|
|
repoOutDir := filepath.Join(outputDir, outName)
|
|
|
|
// Clear existing directory
|
|
io.Local.Delete(repoOutDir) // Recursive delete
|
|
|
|
if err := io.Local.EnsureDir(repoOutDir); err != nil {
|
|
cli.Print(" %s %s: %s\n", errorStyle.Render("✗"), info.Name, err)
|
|
continue
|
|
}
|
|
|
|
// Copy all docs files
|
|
docsDir := filepath.Join(info.Path, "docs")
|
|
for _, f := range info.DocsFiles {
|
|
src := filepath.Join(docsDir, f)
|
|
dst := filepath.Join(repoOutDir, f)
|
|
// Ensure parent dir
|
|
io.Local.EnsureDir(filepath.Dir(dst))
|
|
|
|
if err := io.Copy(io.Local, src, io.Local, dst); err != nil {
|
|
cli.Print(" %s %s: %s\n", errorStyle.Render("✗"), f, err)
|
|
}
|
|
}
|
|
|
|
cli.Print(" %s %s → packages/%s/\n", successStyle.Render("✓"), info.Name, outName)
|
|
synced++
|
|
}
|
|
|
|
cli.Print("\n%s %s\n", successStyle.Render(i18n.T("i18n.done.sync")), i18n.T("cmd.docs.sync.synced_packages", map[string]interface{}{"Count": synced}))
|
|
|
|
return nil
|
|
}
|