diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e8867fe..5878086 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,6 +5,9 @@ on: branches: [main] paths: - 'docs/site/**' + - 'cli/cmd/**' + - 'tools/docs-gen/**' + - '.github/workflows/docs.yml' workflow_dispatch: permissions: @@ -22,12 +25,14 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: - go-version-file: docs/site/go.mod + go-version-file: go.mod cache: true - uses: peaceiris/actions-hugo@v3 with: hugo-version: latest extended: true + - name: Generate CLI reference + run: go run ./tools/docs-gen -o docs/site/content/cli/reference - run: cd docs/site && hugo --minify - uses: actions/upload-pages-artifact@v3 with: diff --git a/.gitignore b/.gitignore index 895e475..b90e473 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ CLAUDE.md docs/site/public/ docs/site/resources/ docs/site/.hugo_build.lock + +# Auto-generated CLI reference (produced by tools/docs-gen in CI) +docs/site/content/cli/reference/skillsctl*.md diff --git a/Makefile b/Makefile index 88fe8a1..c751a8f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: proto lint test test-backend test-cli build-cli build-backend e2e clean +.PHONY: proto lint test test-backend test-cli build-cli build-backend docs-cli e2e clean proto: cd proto && buf lint @@ -25,5 +25,9 @@ build-backend: e2e: ./e2e/scripts/run-e2e.sh +docs-cli: + go run ./tools/docs-gen -o docs/site/content/cli/reference + clean: rm -f skillsctl skillsctl-server coverage.out + rm -f docs/site/content/cli/reference/skillsctl*.md diff --git a/cli/internal/config/config.go b/cli/internal/config/config.go deleted file mode 100644 index 6d17847..0000000 --- a/cli/internal/config/config.go +++ /dev/null @@ -1,38 +0,0 @@ -package config - -import ( - "os" - "path/filepath" - - "github.com/spf13/viper" -) - -type Config struct { - APIURL string `mapstructure:"api_url"` - SkillsDir string `mapstructure:"skills_dir"` - Auth Auth `mapstructure:"auth"` -} - -type Auth struct { - OIDCIssuer string `mapstructure:"oidc_issuer"` - ClientID string `mapstructure:"client_id"` -} - -func Load() *Config { - home, _ := os.UserHomeDir() - - viper.SetDefault("api_url", "http://localhost:8080") - viper.SetDefault("skills_dir", filepath.Join(home, ".claude", "skills")) - - viper.SetConfigName("config") - viper.SetConfigType("yaml") - viper.AddConfigPath(filepath.Join(home, ".config", "skillsctl")) - viper.SetEnvPrefix("SKILLCTL") - viper.AutomaticEnv() - - _ = viper.ReadInConfig() // ok if config file not found - - cfg := &Config{} - _ = viper.Unmarshal(cfg) - return cfg -} diff --git a/cli/internal/config/config_test.go b/cli/internal/config/config_test.go deleted file mode 100644 index 9418d30..0000000 --- a/cli/internal/config/config_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package config_test - -import ( - "strings" - "testing" - - "github.com/nebari-dev/skillsctl/cli/internal/config" -) - -func TestDefaults(t *testing.T) { - cfg := config.Load() - - tests := []struct { - name string - got string - want string - }{ - {"api_url default", cfg.APIURL, "http://localhost:8080"}, - {"skills_dir ends with .claude/skills", "", ""}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.name == "skills_dir ends with .claude/skills" { - if !strings.HasSuffix(cfg.SkillsDir, ".claude/skills") { - t.Errorf("skills_dir %q does not end with .claude/skills", cfg.SkillsDir) - } - return - } - if tt.got != tt.want { - t.Errorf("got %q, want %q", tt.got, tt.want) - } - }) - } -} diff --git a/docs/site/content/cli/reference/_index.md b/docs/site/content/cli/reference/_index.md new file mode 100644 index 0000000..c3b7627 --- /dev/null +++ b/docs/site/content/cli/reference/_index.md @@ -0,0 +1,11 @@ +--- +title: "Reference" +weight: 99 +bookCollapseSection: true +--- + +# CLI reference + +Auto-generated from the Cobra command definitions in `cli/cmd/`. Every command, flag, and default is extracted from the source at build time, so this section always reflects the current release. + +For narrative explanations and worked examples, see the hand-written command pages (`install`, `publish`, `explore`, `config`, `auth`). diff --git a/go.mod b/go.mod index 52f61b0..fd0b808 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/nebari-dev/skillsctl -go 1.25.7 +go 1.25.8 require ( connectrpc.com/connect v1.19.1 @@ -16,6 +16,7 @@ require ( ) require ( + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect @@ -26,6 +27,7 @@ require ( github.com/ncruces/go-strftime v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect diff --git a/go.sum b/go.sum index 36e5e80..f20f909 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -45,6 +46,7 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= diff --git a/tools/docs-gen/main.go b/tools/docs-gen/main.go new file mode 100644 index 0000000..6d82fe7 --- /dev/null +++ b/tools/docs-gen/main.go @@ -0,0 +1,57 @@ +// Command docs-gen renders the skillsctl CLI reference as Hugo-compatible +// Markdown by walking the Cobra command tree. +package main + +import ( + "flag" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" + + clicmd "github.com/nebari-dev/skillsctl/cli/cmd" +) + +func main() { + out := flag.String("o", "", "output directory (required)") + flag.Parse() + + if *out == "" { + fmt.Fprintln(os.Stderr, "docs-gen: -o is required") + flag.Usage() + os.Exit(2) + } + if flag.NArg() > 0 { + fmt.Fprintf(os.Stderr, "docs-gen: unexpected positional arguments: %v\n", flag.Args()) + os.Exit(2) + } + + if err := run(clicmd.NewRootCmd(), *out); err != nil { + log.Fatalf("docs-gen: %v", err) + } + fmt.Printf("wrote CLI reference to %s\n", *out) +} + +func run(root *cobra.Command, out string) error { + if err := os.MkdirAll(out, 0o755); err != nil { + return fmt.Errorf("mkdir %s: %w", out, err) + } + root.DisableAutoGenTag = true + + filePrepender := func(filename string) string { + base := strings.TrimSuffix(filepath.Base(filename), ".md") + title := strings.ReplaceAll(base, "_", " ") + return fmt.Sprintf("---\ntitle: %q\n---\n\n", title) + } + + linkHandler := func(name string) string { + base := strings.TrimSuffix(name, ".md") + return fmt.Sprintf("{{< relref \"/cli/reference/%s\" >}}", base) + } + + return doc.GenMarkdownTreeCustom(root, out, filePrepender, linkHandler) +} diff --git a/tools/docs-gen/main_test.go b/tools/docs-gen/main_test.go new file mode 100644 index 0000000..0e49d93 --- /dev/null +++ b/tools/docs-gen/main_test.go @@ -0,0 +1,44 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" + + clicmd "github.com/nebari-dev/skillsctl/cli/cmd" +) + +func TestRun(t *testing.T) { + out := t.TempDir() + if err := run(clicmd.NewRootCmd(), out); err != nil { + t.Fatalf("run: %v", err) + } + + wantFiles := []string{ + "skillsctl.md", + "skillsctl_install.md", + "skillsctl_publish.md", + "skillsctl_explore.md", + "skillsctl_auth.md", + "skillsctl_config.md", + } + + for _, name := range wantFiles { + t.Run(name, func(t *testing.T) { + path := filepath.Join(out, name) + body, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + content := string(body) + + if !strings.HasPrefix(content, "---\ntitle: ") { + t.Errorf("%s: missing Hugo frontmatter; first 40 bytes: %q", name, content[:min(len(content), 40)]) + } + if strings.Contains(content, "Auto generated by spf13/cobra") { + t.Errorf("%s: DisableAutoGenTag should suppress the auto-gen tag", name) + } + }) + } +}