Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 232 additions & 35 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,62 +3,259 @@ package cmd
import (
"fmt"
"os"
"sort"
"strings"

"github.com/charmbracelet/huh"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/yeasy/ask/internal/config"
"github.com/yeasy/ask/internal/installer"
)

// initCmd represents the init command
var initCmd = &cobra.Command{
Use: "init",
Short: "Initialize a new ASK project",
Long: `Initialize a new Agent Skills Kit project.
This will create ask.yaml and the skills directory (default: .agent/skills/).`,
Run: func(_ *cobra.Command, _ []string) {
if _, err := os.Stat("ask.yaml"); err == nil {
fmt.Println("ask.yaml already exists in this directory.")
return
}
Long: `Initialize a new Agent Skills Kit project with interactive setup.

This walks you through selecting your AI agents and optionally
installing a starter skill pack. Use --yes to skip prompts.`,
Example: ` # Interactive setup
ask init

# Non-interactive with defaults
ask init --yes`,
Run: runInteractiveInit,
}

func runInteractiveInit(cmd *cobra.Command, _ []string) {
if _, err := os.Stat("ask.yaml"); err == nil {
fmt.Println("ask.yaml already exists in this directory.")
return
}

yes, _ := cmd.Flags().GetBool("yes")

if yes {
runNonInteractiveInit()
return
}

// Welcome banner
fmt.Println()
fmt.Println(color.New(color.FgCyan, color.Bold).Sprint(" Welcome to ASK — the package manager for agent skills!"))
fmt.Println()

// --- Step 1: Detect and select agents ---
cwd, _ := os.Getwd()
detected := config.DetectExistingToolDirs(cwd)
detectedSet := make(map[string]bool)
for _, d := range detected {
detectedSet[d.Name] = true
}

// Create skills directory using default path
skillsDir := config.DefaultSkillsDir
if err := os.MkdirAll(skillsDir, 0755); err != nil {
fmt.Printf("Error creating skills directory: %v\n", err)
os.Exit(1)
// Build agent options sorted by: detected first, then alphabetical
type agentOption struct {
key string
display string
detected bool
}
var agentOpts []agentOption
for _, name := range config.GetSupportedAgentNames() {
ac := config.SupportedAgents[config.AgentType(name)]
label := ac.Name
if detectedSet[name] {
label += color.New(color.FgGreen).Sprint(" (detected)")
}
agentOpts = append(agentOpts, agentOption{
key: name,
display: label,
detected: detectedSet[name],
})
}
sort.Slice(agentOpts, func(i, j int) bool {
if agentOpts[i].detected != agentOpts[j].detected {
return agentOpts[i].detected
}
return agentOpts[i].key < agentOpts[j].key
})

err := config.CreateDefaultConfig()
if err != nil {
fmt.Printf("Error creating ask.yaml: %v\n", err)
os.Exit(1)
// Pre-select detected agents
var selectedAgents []string
for _, opt := range agentOpts {
if opt.detected {
selectedAgents = append(selectedAgents, opt.key)
}
}

huhOpts := make([]huh.Option[string], 0, len(agentOpts))
for _, opt := range agentOpts {
huhOpts = append(huhOpts, huh.NewOption(opt.display, opt.key))
}

err := huh.NewForm(
huh.NewGroup(
huh.NewMultiSelect[string]().
Title("Which agents do you use?").
Description("Space to toggle, Enter to confirm").
Options(huhOpts...).
Value(&selectedAgents),
),
).Run()
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}

// --- Step 2: Create config and directories ---
skillsDir := config.DefaultSkillsDir
if err := os.MkdirAll(skillsDir, 0755); err != nil {
fmt.Printf("Error creating skills directory: %v\n", err)
os.Exit(1)
}

// Detect existing agent directories
cwd, _ := os.Getwd()
detected := config.DetectExistingToolDirs(cwd)
if err := config.CreateConfigWithAgents(selectedAgents); err != nil {
fmt.Printf("Error creating ask.yaml: %v\n", err)
os.Exit(1)
}

fmt.Println("✓ Initialized ASK project")
fmt.Println(" Created: ask.yaml")
fmt.Printf(" Created: %s/\n", skillsDir)
fmt.Println()
color.Green("✓ Initialized ASK project")
fmt.Println(" Created: ask.yaml")
fmt.Printf(" Created: %s/\n", skillsDir)

if len(detected) > 0 {
fmt.Println()
fmt.Println(" Detected agents:")
for _, t := range detected {
fmt.Printf(" • %s (%s)\n", t.Name, t.SkillsDir)
if len(selectedAgents) > 0 {
names := make([]string, 0, len(selectedAgents))
for _, a := range selectedAgents {
if ac, ok := config.SupportedAgents[config.AgentType(a)]; ok {
names = append(names, ac.Name)
}
fmt.Println()
fmt.Println(" Skills will be synced to all detected agents automatically.")
}
fmt.Printf(" Agents: %s\n", strings.Join(names, ", "))
}

// --- Step 3: Offer starter pack ---
packChoices := make([]huh.Option[string], 0, len(skillPacks)+1)
for _, pack := range skillPacks {
label := fmt.Sprintf("%s — %s", pack.Name, pack.Description)
packChoices = append(packChoices, huh.NewOption(label, pack.Name))
}
packChoices = append(packChoices, huh.NewOption("Skip for now", "skip"))

var selectedPack string
err = huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("\nInstall a starter pack?").
Description("Get productive quickly with curated skills").
Options(packChoices...).
Value(&selectedPack),
),
).Run()
if err != nil {
// User canceled, that's fine
selectedPack = "skip"
}

if selectedPack != "skip" {
installStarterPack(selectedPack, selectedAgents)
}

// --- Step 4: Next steps ---
fmt.Println()
color.Cyan("Next steps:")
fmt.Println(" ask search Search for skills")
fmt.Println(" ask install <name> Install a skill")
fmt.Println(" ask list View installed skills")
fmt.Println(" ask doctor Check your setup")
fmt.Println()
}

func runNonInteractiveInit() {
skillsDir := config.DefaultSkillsDir
if err := os.MkdirAll(skillsDir, 0755); err != nil {
fmt.Printf("Error creating skills directory: %v\n", err)
os.Exit(1)
}

if err := config.CreateDefaultConfig(); err != nil {
fmt.Printf("Error creating ask.yaml: %v\n", err)
os.Exit(1)
}

cwd, _ := os.Getwd()
detected := config.DetectExistingToolDirs(cwd)

color.Green("✓ Initialized ASK project")
fmt.Println(" Created: ask.yaml")
fmt.Printf(" Created: %s/\n", skillsDir)

if len(detected) > 0 {
names := make([]string, 0, len(detected))
for _, d := range detected {
names = append(names, d.Name)
}
fmt.Printf(" Detected agents: %s\n", strings.Join(names, ", "))
fmt.Println(" Skills will be synced to all detected agents automatically.")
}

fmt.Println()
fmt.Println("Next steps:")
fmt.Println(" ask search Browse available skills")
fmt.Println(" ask install <name> Install a skill")
fmt.Println(" ask doctor Check your setup")
}

func installStarterPack(packName string, agents []string) {
var pack *struct {
Name string
Description string
Skills []string
}
for i := range skillPacks {
if skillPacks[i].Name == packName {
pack = &skillPacks[i]
break
}
}
if pack == nil {
return
}

cfg, err := config.LoadConfig()
if err != nil {
def := config.DefaultConfig()
cfg = &def
}

opts := installer.InstallOptions{
Agents: agents,
Config: cfg,
}

fmt.Printf("\nInstalling %s pack (%d skills)...\n\n", pack.Name, len(pack.Skills))

var succeeded, failed int
for _, skillInput := range pack.Skills {
err := installer.Install(skillInput, opts)
if err != nil {
fmt.Printf(" ✗ %s: %v\n", skillInput, err)
failed++
} else {
color.Green(" ✓ %s", skillInput)
succeeded++
}
}

fmt.Println()
fmt.Println("Next steps:")
fmt.Println(" ask search Browse available skills")
fmt.Println(" ask install <name> Install a skill")
fmt.Println(" ask doctor Check your setup")
},
fmt.Printf("\nDone! %d installed", succeeded)
if failed > 0 {
fmt.Printf(", %d failed", failed)
}
fmt.Println(".")
}

func init() {
rootCmd.AddCommand(initCmd)
initCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode with defaults")
}
30 changes: 26 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,33 @@ module github.com/yeasy/ask
go 1.24.2

require (
github.com/charmbracelet/huh v1.0.0
github.com/fatih/color v1.18.0
github.com/schollz/progressbar/v3 v3.19.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/wailsapp/wails/v2 v2.11.0
golang.org/x/sync v0.19.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
github.com/charmbracelet/bubbletea v1.3.6 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
Expand All @@ -29,9 +44,16 @@ require (
github.com/leaanthony/gosod v1.0.4 // indirect
github.com/leaanthony/slicer v1.6.0 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
Expand All @@ -49,11 +71,11 @@ require (
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wailsapp/go-webview2 v1.0.22 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.29.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.28.0 // indirect
)
Loading