Skip to content
Merged
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
41 changes: 29 additions & 12 deletions forge-cli/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,16 +241,6 @@ func collectInteractive(opts *initOptions) error {
theme := tui.DetectTheme(themeOverride)
styles := tui.NewStyleSet(theme)

// Load tool info for the tools step
allTools := builtins.All()
var toolInfos []steps.ToolInfo
for _, t := range allTools {
toolInfos = append(toolInfos, steps.ToolInfo{
Name: t.Name(),
Description: t.Description(),
})
}

// Load skill info for the skills step
var skillInfos []steps.SkillInfo
reg, regErr := local.NewEmbeddedRegistry()
Expand Down Expand Up @@ -325,7 +315,7 @@ func collectInteractive(opts *initOptions) error {
steps.NewProviderStep(styles, validateKeyFn, oauthFlowFn),
steps.NewFallbackStep(styles, validateKeyFn),
steps.NewChannelStep(styles),
steps.NewToolsStep(styles, toolInfos, validateWebSearchKeyFn),
steps.NewWebSearchStep(styles, validateWebSearchKeyFn),
steps.NewSkillsStep(styles, skillInfos),
steps.NewCompressionStep(styles),
steps.NewAuthStep(styles),
Expand Down Expand Up @@ -375,7 +365,13 @@ func collectInteractive(opts *initOptions) error {
opts.Channels = []string{ctx.Channel}
}

opts.BuiltinTools = ctx.BuiltinTools
// MERGE the wizard's builtin-tool selection into any --tools flag rather
// than overwriting it. The wizard only records web_search today; a bare
// overwrite would drop the flag's other entries from both forge.yaml and
// the init_egress DefaultToolDomains derivation whenever the user picked a
// provider. Merge + dedupe covers all three paths (flag+skip, flag+
// provider, no-flag+provider). See #263 review.
opts.BuiltinTools = mergeBuiltinTools(opts.BuiltinTools, ctx.BuiltinTools)
opts.Skills = ctx.Skills
opts.Compression = ctx.Compression

Expand Down Expand Up @@ -1417,6 +1413,27 @@ func containsStr(slice []string, val string) bool {
return false
}

// mergeBuiltinTools returns the union of a --tools flag value and the wizard's
// recorded builtin-tool selection, preserving first-seen order and dropping
// duplicates and empty entries. Merging (not overwriting) keeps a flag entry
// like http_request when the wizard also records web_search — both must reach
// forge.yaml and the init_egress domain derivation. See #263 review.
func mergeBuiltinTools(flag, fromWizard []string) []string {
seen := make(map[string]struct{}, len(flag)+len(fromWizard))
out := make([]string, 0, len(flag)+len(fromWizard))
for _, name := range append(append([]string{}, flag...), fromWizard...) {
if name == "" {
continue
}
if _, dup := seen[name]; dup {
continue
}
seen[name] = struct{}{}
out = append(out, name)
}
return out
}

// runOAuthFlow executes the OAuth browser flow for a provider and returns the access token.
func runOAuthFlow(provider string) (string, error) {
var config oauth.ProviderConfig
Expand Down
32 changes: 32 additions & 0 deletions forge-cli/cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -951,3 +951,35 @@ func writeTempFile(t *testing.T, name, content string) string {
}
return path
}

// TestMergeBuiltinTools pins the three paths the #263 review called out:
// a --tools flag merges with the wizard's web_search selection instead of
// being clobbered, empties are dropped, and duplicates are deduped.
func TestMergeBuiltinTools(t *testing.T) {
cases := []struct {
name string
flag []string
fromWizard []string
want []string
}{
{"flag+skip (wizard records nothing)", []string{"http_request"}, nil, []string{"http_request"}},
{"flag+provider (must not clobber the flag)", []string{"http_request"}, []string{"web_search"}, []string{"http_request", "web_search"}},
{"no-flag+provider", nil, []string{"web_search"}, []string{"web_search"}},
{"dedupe overlap", []string{"web_search", "http_request"}, []string{"web_search"}, []string{"web_search", "http_request"}},
{"drops empties", []string{"", "http_request"}, []string{""}, []string{"http_request"}},
{"both empty", nil, nil, []string{}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := mergeBuiltinTools(tc.flag, tc.fromWizard)
if len(got) != len(tc.want) {
t.Fatalf("got %v, want %v", got, tc.want)
}
for i := range tc.want {
if got[i] != tc.want[i] {
t.Fatalf("got %v, want %v (order matters)", got, tc.want)
}
}
})
}
}
Loading
Loading