179 lines
4.5 KiB
Go
179 lines
4.5 KiB
Go
// Package auto implements the ctx auto plugin — stack detection and routing.
|
|
package auto
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/ricarneiro/ctx/internal/core"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func init() {
|
|
core.Register(&plugin{})
|
|
}
|
|
|
|
type plugin struct{}
|
|
|
|
func (p *plugin) Name() string { return "auto" }
|
|
func (p *plugin) Version() string { return "0.1.0" }
|
|
func (p *plugin) ShortDescription() string { return "Detect stack and route to appropriate plugin" }
|
|
|
|
func (p *plugin) Command(ctx *core.Context) *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "auto",
|
|
Short: p.ShortDescription(),
|
|
Long: "Auto-detect the project stack and route to the appropriate ctx plugin.",
|
|
}
|
|
cmd.AddCommand(newDetectCmd(ctx))
|
|
cmd.AddCommand(newProjectCmd(ctx))
|
|
return cmd
|
|
}
|
|
|
|
// --- detect subcommand ---
|
|
|
|
func newDetectCmd(ctx *core.Context) *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "detect",
|
|
Short: "List detected stacks in the current directory",
|
|
Long: "Scan the current directory and list detected technology stacks without running any plugin.",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return runDetect(ctx)
|
|
},
|
|
}
|
|
}
|
|
|
|
func runDetect(ctx *core.Context) error {
|
|
stacks, err := Detect(ctx.WorkDir)
|
|
if err != nil {
|
|
fmt.Fprintln(ctx.Stderr, err.Error())
|
|
return fmt.Errorf("exit 1")
|
|
}
|
|
formatDetect(ctx, stacks)
|
|
return nil
|
|
}
|
|
|
|
func formatDetect(ctx *core.Context, stacks []Stack) {
|
|
w := ctx.Stdout
|
|
fmt.Fprintln(w, "# Stack detection")
|
|
fmt.Fprintln(w)
|
|
fmt.Fprintf(w, "**directory:** %s\n\n", ctx.WorkDir)
|
|
|
|
if len(stacks) == 0 {
|
|
fmt.Fprintln(w, "No known stack detected.")
|
|
fmt.Fprintln(w)
|
|
plugins := core.All()
|
|
names := make([]string, len(plugins))
|
|
for i, p := range plugins {
|
|
names[i] = p.Name()
|
|
}
|
|
fmt.Fprintf(w, "Available plugins: %s\n", strings.Join(names, ", "))
|
|
fmt.Fprintln(w, "Run `ctx <plugin> --help` to see what each plugin offers.")
|
|
return
|
|
}
|
|
|
|
fmt.Fprintln(w, "## Detected stacks")
|
|
for _, s := range stacks {
|
|
fmt.Fprintf(w, "- **%s** (confidence: %s)\n", s.Name, s.Confidence)
|
|
for _, e := range s.Evidence {
|
|
fmt.Fprintf(w, " - %s\n", e)
|
|
}
|
|
}
|
|
fmt.Fprintln(w)
|
|
|
|
fmt.Fprintln(w, "## Suggested commands")
|
|
for _, s := range stacks {
|
|
fmt.Fprintf(w, "- `ctx %s project`\n", s.Name)
|
|
}
|
|
}
|
|
|
|
// --- project subcommand ---
|
|
|
|
func newProjectCmd(ctx *core.Context) *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "project",
|
|
Short: "Detect stack and emit project context summary",
|
|
Long: "Auto-detect the project stack and run the `project` command of each matching plugin.",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return runProject(ctx)
|
|
},
|
|
}
|
|
}
|
|
|
|
func runProject(ctx *core.Context) error {
|
|
stacks, err := Detect(ctx.WorkDir)
|
|
if err != nil {
|
|
fmt.Fprintln(ctx.Stderr, err.Error())
|
|
return fmt.Errorf("exit 1")
|
|
}
|
|
|
|
if len(stacks) == 0 {
|
|
formatDetect(ctx, stacks)
|
|
return nil
|
|
}
|
|
|
|
first := true
|
|
for _, stack := range stacks {
|
|
if !first {
|
|
fmt.Fprintln(ctx.Stdout, "---")
|
|
fmt.Fprintln(ctx.Stdout)
|
|
}
|
|
first = false
|
|
|
|
p := core.Get(stack.Name)
|
|
if p == nil {
|
|
fmt.Fprintf(ctx.Stdout,
|
|
"# %s\n\nNo plugin registered for stack `%s`.\n\n",
|
|
stack.Name, stack.Name)
|
|
continue
|
|
}
|
|
|
|
pluginCmd := p.Command(ctx)
|
|
|
|
// If the root command itself is a placeholder, the whole plugin is unimplemented.
|
|
if pluginCmd.Annotations["placeholder"] == "true" {
|
|
writePlaceholder(ctx, stack.Name)
|
|
continue
|
|
}
|
|
|
|
sub := findSubcommand(pluginCmd, "project")
|
|
if sub == nil {
|
|
fmt.Fprintf(ctx.Stdout,
|
|
"# %s\n\nThe `%s` plugin does not have a `project` command.\n\n",
|
|
stack.Name, stack.Name)
|
|
continue
|
|
}
|
|
|
|
if sub.Annotations["placeholder"] == "true" {
|
|
writePlaceholder(ctx, stack.Name)
|
|
continue
|
|
}
|
|
|
|
// Execute the project subcommand directly (no cobra routing overhead).
|
|
if sub.RunE != nil {
|
|
if err := sub.RunE(sub, []string{}); err != nil {
|
|
fmt.Fprintf(ctx.Stderr, "error running %s project: %v\n", stack.Name, err)
|
|
}
|
|
} else if sub.Run != nil {
|
|
sub.Run(sub, []string{})
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// findSubcommand returns the named subcommand of cmd, or nil if not found.
|
|
func findSubcommand(cmd *cobra.Command, name string) *cobra.Command {
|
|
for _, sub := range cmd.Commands() {
|
|
if sub.Name() == name {
|
|
return sub
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writePlaceholder(ctx *core.Context, name string) {
|
|
fmt.Fprintf(ctx.Stdout,
|
|
"# %s (placeholder)\n\nThe `%s` plugin detected this stack but its `project` command is not yet implemented.\nThis will be available in a future version of ctx.\n\n",
|
|
name, name)
|
|
}
|