Skip to content

Commit 8b3126f

Browse files
committed
Add interactive setup flow to ask init
- Add charmbracelet/huh for terminal UI forms - Interactive agent selection with auto-detection of existing agents - Starter pack selection to install curated skills on first run - Add --yes flag for non-interactive CI/scripting use - Add CreateConfigWithAgents to save selected agents to ask.yaml
1 parent 55240d3 commit 8b3126f

File tree

4 files changed

+321
-35
lines changed

4 files changed

+321
-35
lines changed

cmd/init.go

Lines changed: 232 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,62 +3,259 @@ package cmd
33
import (
44
"fmt"
55
"os"
6+
"sort"
7+
"strings"
68

9+
"github.com/charmbracelet/huh"
10+
"github.com/fatih/color"
711
"github.com/spf13/cobra"
812
"github.com/yeasy/ask/internal/config"
13+
"github.com/yeasy/ask/internal/installer"
914
)
1015

1116
// initCmd represents the init command
1217
var initCmd = &cobra.Command{
1318
Use: "init",
1419
Short: "Initialize a new ASK project",
15-
Long: `Initialize a new Agent Skills Kit project.
16-
This will create ask.yaml and the skills directory (default: .agent/skills/).`,
17-
Run: func(_ *cobra.Command, _ []string) {
18-
if _, err := os.Stat("ask.yaml"); err == nil {
19-
fmt.Println("ask.yaml already exists in this directory.")
20-
return
21-
}
20+
Long: `Initialize a new Agent Skills Kit project with interactive setup.
21+
22+
This walks you through selecting your AI agents and optionally
23+
installing a starter skill pack. Use --yes to skip prompts.`,
24+
Example: ` # Interactive setup
25+
ask init
26+
27+
# Non-interactive with defaults
28+
ask init --yes`,
29+
Run: runInteractiveInit,
30+
}
31+
32+
func runInteractiveInit(cmd *cobra.Command, _ []string) {
33+
if _, err := os.Stat("ask.yaml"); err == nil {
34+
fmt.Println("ask.yaml already exists in this directory.")
35+
return
36+
}
37+
38+
yes, _ := cmd.Flags().GetBool("yes")
39+
40+
if yes {
41+
runNonInteractiveInit()
42+
return
43+
}
44+
45+
// Welcome banner
46+
fmt.Println()
47+
fmt.Println(color.New(color.FgCyan, color.Bold).Sprint(" Welcome to ASK — the package manager for agent skills!"))
48+
fmt.Println()
49+
50+
// --- Step 1: Detect and select agents ---
51+
cwd, _ := os.Getwd()
52+
detected := config.DetectExistingToolDirs(cwd)
53+
detectedSet := make(map[string]bool)
54+
for _, d := range detected {
55+
detectedSet[d.Name] = true
56+
}
2257

23-
// Create skills directory using default path
24-
skillsDir := config.DefaultSkillsDir
25-
if err := os.MkdirAll(skillsDir, 0755); err != nil {
26-
fmt.Printf("Error creating skills directory: %v\n", err)
27-
os.Exit(1)
58+
// Build agent options sorted by: detected first, then alphabetical
59+
type agentOption struct {
60+
key string
61+
display string
62+
detected bool
63+
}
64+
var agentOpts []agentOption
65+
for _, name := range config.GetSupportedAgentNames() {
66+
ac := config.SupportedAgents[config.AgentType(name)]
67+
label := ac.Name
68+
if detectedSet[name] {
69+
label += color.New(color.FgGreen).Sprint(" (detected)")
2870
}
71+
agentOpts = append(agentOpts, agentOption{
72+
key: name,
73+
display: label,
74+
detected: detectedSet[name],
75+
})
76+
}
77+
sort.Slice(agentOpts, func(i, j int) bool {
78+
if agentOpts[i].detected != agentOpts[j].detected {
79+
return agentOpts[i].detected
80+
}
81+
return agentOpts[i].key < agentOpts[j].key
82+
})
2983

30-
err := config.CreateDefaultConfig()
31-
if err != nil {
32-
fmt.Printf("Error creating ask.yaml: %v\n", err)
33-
os.Exit(1)
84+
// Pre-select detected agents
85+
var selectedAgents []string
86+
for _, opt := range agentOpts {
87+
if opt.detected {
88+
selectedAgents = append(selectedAgents, opt.key)
3489
}
90+
}
91+
92+
huhOpts := make([]huh.Option[string], 0, len(agentOpts))
93+
for _, opt := range agentOpts {
94+
huhOpts = append(huhOpts, huh.NewOption(opt.display, opt.key))
95+
}
96+
97+
err := huh.NewForm(
98+
huh.NewGroup(
99+
huh.NewMultiSelect[string]().
100+
Title("Which agents do you use?").
101+
Description("Space to toggle, Enter to confirm").
102+
Options(huhOpts...).
103+
Value(&selectedAgents),
104+
),
105+
).Run()
106+
if err != nil {
107+
fmt.Printf("Error: %v\n", err)
108+
os.Exit(1)
109+
}
110+
111+
// --- Step 2: Create config and directories ---
112+
skillsDir := config.DefaultSkillsDir
113+
if err := os.MkdirAll(skillsDir, 0755); err != nil {
114+
fmt.Printf("Error creating skills directory: %v\n", err)
115+
os.Exit(1)
116+
}
35117

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

40-
fmt.Println("✓ Initialized ASK project")
41-
fmt.Println(" Created: ask.yaml")
42-
fmt.Printf(" Created: %s/\n", skillsDir)
123+
fmt.Println()
124+
color.Green("✓ Initialized ASK project")
125+
fmt.Println(" Created: ask.yaml")
126+
fmt.Printf(" Created: %s/\n", skillsDir)
43127

44-
if len(detected) > 0 {
45-
fmt.Println()
46-
fmt.Println(" Detected agents:")
47-
for _, t := range detected {
48-
fmt.Printf(" • %s (%s)\n", t.Name, t.SkillsDir)
128+
if len(selectedAgents) > 0 {
129+
names := make([]string, 0, len(selectedAgents))
130+
for _, a := range selectedAgents {
131+
if ac, ok := config.SupportedAgents[config.AgentType(a)]; ok {
132+
names = append(names, ac.Name)
49133
}
50-
fmt.Println()
51-
fmt.Println(" Skills will be synced to all detected agents automatically.")
52134
}
135+
fmt.Printf(" Agents: %s\n", strings.Join(names, ", "))
136+
}
137+
138+
// --- Step 3: Offer starter pack ---
139+
packChoices := make([]huh.Option[string], 0, len(skillPacks)+1)
140+
for _, pack := range skillPacks {
141+
label := fmt.Sprintf("%s — %s", pack.Name, pack.Description)
142+
packChoices = append(packChoices, huh.NewOption(label, pack.Name))
143+
}
144+
packChoices = append(packChoices, huh.NewOption("Skip for now", "skip"))
145+
146+
var selectedPack string
147+
err = huh.NewForm(
148+
huh.NewGroup(
149+
huh.NewSelect[string]().
150+
Title("\nInstall a starter pack?").
151+
Description("Get productive quickly with curated skills").
152+
Options(packChoices...).
153+
Value(&selectedPack),
154+
),
155+
).Run()
156+
if err != nil {
157+
// User canceled, that's fine
158+
selectedPack = "skip"
159+
}
160+
161+
if selectedPack != "skip" {
162+
installStarterPack(selectedPack, selectedAgents)
163+
}
164+
165+
// --- Step 4: Next steps ---
166+
fmt.Println()
167+
color.Cyan("Next steps:")
168+
fmt.Println(" ask search Search for skills")
169+
fmt.Println(" ask install <name> Install a skill")
170+
fmt.Println(" ask list View installed skills")
171+
fmt.Println(" ask doctor Check your setup")
172+
fmt.Println()
173+
}
174+
175+
func runNonInteractiveInit() {
176+
skillsDir := config.DefaultSkillsDir
177+
if err := os.MkdirAll(skillsDir, 0755); err != nil {
178+
fmt.Printf("Error creating skills directory: %v\n", err)
179+
os.Exit(1)
180+
}
181+
182+
if err := config.CreateDefaultConfig(); err != nil {
183+
fmt.Printf("Error creating ask.yaml: %v\n", err)
184+
os.Exit(1)
185+
}
186+
187+
cwd, _ := os.Getwd()
188+
detected := config.DetectExistingToolDirs(cwd)
189+
190+
color.Green("✓ Initialized ASK project")
191+
fmt.Println(" Created: ask.yaml")
192+
fmt.Printf(" Created: %s/\n", skillsDir)
193+
194+
if len(detected) > 0 {
195+
names := make([]string, 0, len(detected))
196+
for _, d := range detected {
197+
names = append(names, d.Name)
198+
}
199+
fmt.Printf(" Detected agents: %s\n", strings.Join(names, ", "))
200+
fmt.Println(" Skills will be synced to all detected agents automatically.")
201+
}
202+
203+
fmt.Println()
204+
fmt.Println("Next steps:")
205+
fmt.Println(" ask search Browse available skills")
206+
fmt.Println(" ask install <name> Install a skill")
207+
fmt.Println(" ask doctor Check your setup")
208+
}
209+
210+
func installStarterPack(packName string, agents []string) {
211+
var pack *struct {
212+
Name string
213+
Description string
214+
Skills []string
215+
}
216+
for i := range skillPacks {
217+
if skillPacks[i].Name == packName {
218+
pack = &skillPacks[i]
219+
break
220+
}
221+
}
222+
if pack == nil {
223+
return
224+
}
225+
226+
cfg, err := config.LoadConfig()
227+
if err != nil {
228+
def := config.DefaultConfig()
229+
cfg = &def
230+
}
231+
232+
opts := installer.InstallOptions{
233+
Agents: agents,
234+
Config: cfg,
235+
}
236+
237+
fmt.Printf("\nInstalling %s pack (%d skills)...\n\n", pack.Name, len(pack.Skills))
238+
239+
var succeeded, failed int
240+
for _, skillInput := range pack.Skills {
241+
err := installer.Install(skillInput, opts)
242+
if err != nil {
243+
fmt.Printf(" ✗ %s: %v\n", skillInput, err)
244+
failed++
245+
} else {
246+
color.Green(" ✓ %s", skillInput)
247+
succeeded++
248+
}
249+
}
53250

54-
fmt.Println()
55-
fmt.Println("Next steps:")
56-
fmt.Println(" ask search Browse available skills")
57-
fmt.Println(" ask install <name> Install a skill")
58-
fmt.Println(" ask doctor Check your setup")
59-
},
251+
fmt.Printf("\nDone! %d installed", succeeded)
252+
if failed > 0 {
253+
fmt.Printf(", %d failed", failed)
254+
}
255+
fmt.Println(".")
60256
}
61257

62258
func init() {
63259
rootCmd.AddCommand(initCmd)
260+
initCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode with defaults")
64261
}

go.mod

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,22 @@ require (
1313
)
1414

1515
require (
16+
github.com/atotto/clipboard v0.1.4 // indirect
17+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
1618
github.com/bep/debounce v1.2.1 // indirect
19+
github.com/catppuccin/go v0.3.0 // indirect
20+
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
21+
github.com/charmbracelet/bubbletea v1.3.6 // indirect
22+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
23+
github.com/charmbracelet/huh v1.0.0 // indirect
24+
github.com/charmbracelet/lipgloss v1.1.0 // indirect
25+
github.com/charmbracelet/x/ansi v0.9.3 // indirect
26+
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
27+
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
28+
github.com/charmbracelet/x/term v0.2.1 // indirect
1729
github.com/davecgh/go-spew v1.1.1 // indirect
30+
github.com/dustin/go-humanize v1.0.1 // indirect
31+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
1832
github.com/fsnotify/fsnotify v1.9.0 // indirect
1933
github.com/go-ole/go-ole v1.3.0 // indirect
2034
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
@@ -29,9 +43,16 @@ require (
2943
github.com/leaanthony/gosod v1.0.4 // indirect
3044
github.com/leaanthony/slicer v1.6.0 // indirect
3145
github.com/leaanthony/u v1.1.1 // indirect
46+
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
3247
github.com/mattn/go-colorable v0.1.13 // indirect
3348
github.com/mattn/go-isatty v0.0.20 // indirect
49+
github.com/mattn/go-localereader v0.0.1 // indirect
50+
github.com/mattn/go-runewidth v0.0.16 // indirect
3451
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
52+
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
53+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
54+
github.com/muesli/cancelreader v0.2.2 // indirect
55+
github.com/muesli/termenv v0.16.0 // indirect
3556
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
3657
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
3758
github.com/pkg/errors v0.9.1 // indirect
@@ -49,6 +70,7 @@ require (
4970
github.com/valyala/fasttemplate v1.2.2 // indirect
5071
github.com/wailsapp/go-webview2 v1.0.22 // indirect
5172
github.com/wailsapp/mimetype v1.4.1 // indirect
73+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
5274
go.yaml.in/yaml/v3 v3.0.4 // indirect
5375
golang.org/x/crypto v0.33.0 // indirect
5476
golang.org/x/net v0.35.0 // indirect

0 commit comments

Comments
 (0)