feat(skills): default --name to --group, --toolset, or single --prebuilt name (#3586)

## Description

Makes `--name` optional for `skills-generate` whenever the command
produces a single skill.

When `--name` is omitted, `resolveSkillName` picks a name using the
first rule that applies:

1. `--name`, if you set it explicitly
2. the `--group` name
3. the `--toolset` name
4. the config file name, when exactly one `--prebuilt` is given

If none of these apply (a custom `--config`, multiple `--prebuilt`
configs, or no config), `--name` is still required and the command
errors clearly.

So a group or toolset skill can now be generated with no naming flags:

```bash
toolbox --prebuilt alloydb-postgres skills-generate --group greeting
# -> skill named "greeting"
```

**Unchanged:** when no group flag is set and multiple groups exist,
`--name` still acts as a prefix (`<name>-<group>`) to keep skill names
from colliding.

Based on #3585

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com>
This commit is contained in:
Twisha Bansal
2026-07-27 11:44:39 +05:30
committed by GitHub
parent c1abd4fc4f
commit 2b33b08c3a
4 changed files with 114 additions and 7 deletions
+27 -1
View File
@@ -75,7 +75,6 @@ func NewCommand(opts *internal.ToolboxOptions) *cobra.Command {
flags.StringVar(&cmd.additionalNotes, "additional-notes", "", "Additional notes to add under the Usage section of the generated SKILL.md")
flags.StringVar(&cmd.invocationMode, "invocation-mode", "npx", "Invocation mode for the generated scripts: 'binary' or 'npx'")
flags.StringVar(&cmd.toolboxVersion, "toolbox-version", opts.VersionNum, "Version of @toolbox-sdk/server to use for npx approach")
_ = cmd.MarkFlagRequired("name")
cmd.MarkFlagsMutuallyExclusive("group", "toolset")
return cmd.Command
}
@@ -100,6 +99,13 @@ func run(cmd *skillsCmd, opts *internal.ToolboxOptions) error {
return err
}
name, err := resolveSkillName(cmd.name, cmd.group, cmd.toolset, opts.PrebuiltConfigs)
if err != nil {
opts.Logger.ErrorContext(ctx, err.Error())
return err
}
cmd.name = name
if err := os.MkdirAll(cmd.outputDir, 0755); err != nil {
errMsg := fmt.Errorf("error creating output directory: %w", err)
opts.Logger.ErrorContext(ctx, errMsg.Error())
@@ -238,6 +244,26 @@ func run(cmd *skillsCmd, opts *internal.ToolboxOptions) error {
return nil
}
// resolveSkillName returns the explicit --name when set. Otherwise, in the
// single-skill modes it defaults to the --group or --toolset name, and for
// prebuilt generation it defaults to the config name when exactly one
// --prebuilt config is given. Any other case requires --name.
func resolveSkillName(name, group, toolset string, prebuiltConfigs []string) (string, error) {
if name != "" {
return name, nil
}
if group != "" {
return group, nil
}
if toolset != "" {
return toolset, nil
}
if len(prebuiltConfigs) == 1 {
return strings.ReplaceAll(prebuiltConfigs[0], "/", "-"), nil
}
return "", fmt.Errorf("--name is required unless --group or --toolset is set, or exactly one --prebuilt config is provided")
}
func (c *skillsCmd) collectContents(ctx context.Context, opts *internal.ToolboxOptions) (map[string]skillContent, error) {
// Initialize tools and groups only; skills generation does not need live
// sources, auth services, or embedding models.
+83
View File
@@ -572,6 +572,89 @@ func TestBuildSkillContents(t *testing.T) {
}
}
func TestResolveSkillName(t *testing.T) {
tests := []struct {
name string
flagName string
group string
toolset string
prebuiltConfigs []string
want string
wantErr bool
}{
{
name: "explicit name wins",
flagName: "my-skill",
want: "my-skill",
},
{
name: "explicit name wins over prebuilt",
flagName: "my-skill",
prebuiltConfigs: []string{"alloydb-postgres"},
want: "my-skill",
},
{
name: "explicit name wins over group",
flagName: "my-skill",
group: "greeting",
want: "my-skill",
},
{
name: "defaults to group",
group: "greeting",
want: "greeting",
},
{
name: "defaults to toolset",
toolset: "greeting",
want: "greeting",
},
{
name: "group wins over prebuilt",
group: "greeting",
prebuiltConfigs: []string{"alloydb-postgres"},
want: "greeting",
},
{
name: "defaults to single prebuilt",
prebuiltConfigs: []string{"alloydb-postgres"},
want: "alloydb-postgres",
},
{
name: "sanitizes slashes in single prebuilt",
prebuiltConfigs: []string{"alloydb-postgres/some-toolset"},
want: "alloydb-postgres-some-toolset",
},
{
name: "no name and no prebuilt errors",
wantErr: true,
},
{
name: "no name and multiple prebuilts errors",
prebuiltConfigs: []string{"alloydb-postgres", "cloud-sql-postgres"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := resolveSkillName(tt.flagName, tt.group, tt.toolset, tt.prebuiltConfigs)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error, got nil (got %q)", got)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}
func TestGenerateSkill_FlagValidation(t *testing.T) {
tests := []struct {
name string
@@ -34,7 +34,7 @@ toolbox <tool-source> skills-generate \
```
- `<tool-source>`: Can be `--config`, `--configs`, `--config-folder`, and `--prebuilt`. See the [CLI Reference](../../../reference/cli.md) for details.
- `--name`: Name of the generated skill. When multiple toolsets are generated because `--toolset` is omitted, this name acts as a prefix for each skill folder (e.g., `<name>-<toolset>`).
- `--name`: (Optional) Name of the generated skill. When multiple toolsets are generated because `--toolset` is omitted, this name acts as a prefix for each skill folder (e.g., `<name>-<toolset>`). When omitted in a single-skill mode, the name defaults, in order, to: the `--group` name, then the `--toolset` name, then the single `--prebuilt` config name. Any other case (a custom `--config`, or multiple `--prebuilt` configs) requires `--name`.
- `--description`: (Optional) Description of the generated skill. When a [group](../groups/) defines its own `description`, that takes precedence and `--description` acts as a fallback for groups without one.
- `--group`: (Optional) Name of the [group](../groups/) to convert into a single skill. Uses the group's `description`, falling back to `--description`. Mutually exclusive with `--toolset`.
- `--toolset`: (Optional) Name of the toolset to convert into a skill. If not provided, one skill will be generated for every custom toolset defined. If no custom toolsets are defined, it defaults to a single skill containing all tools.
@@ -103,11 +103,10 @@ Description resolution follows the group's metadata:
- If the group defines a `description`, the generated skill uses it — this takes **precedence** over `--description`.
- If the group has no `description`, the `--description` flag is used as a fallback.
For example, to generate a skill from a `data_analyst` group:
When `--name` is omitted, the skill is named after the group (e.g., `data_analyst`):
```bash
toolbox --config tools.yaml skills-generate \
--name "data_analyst" \
--group "data_analyst" \
--description "Fallback description if the group has none"
```
@@ -118,11 +117,10 @@ Sourcing the description from the group keeps the skill's description and the gr
### Example: Prebuilt Configuration
You can also generate skills from prebuilt toolsets:
You can also generate skills from prebuilt configurations. When exactly one `--prebuilt` config is provided, `--name` is optional and defaults to the prebuilt config name, so no naming flags are required:
```bash
toolbox --prebuilt alloydb-postgres-admin skills-generate \
--name "alloydb-postgres-admin" \
--description "skill for performing administrative operations on alloydb"
```
+1 -1
View File
@@ -74,7 +74,7 @@ toolbox skills-generate --name <name> --description <description> --toolset <too
**Flags:**
- `--name`: Name of the generated skill. When multiple toolsets are generated because `--toolset` is omitted, this name acts as a prefix for each skill folder (e.g., `<name>-<toolset>`).
- `--name`: (Optional) Name of the generated skill. When multiple toolsets are generated because `--toolset` is omitted, this name acts as a prefix for each skill folder (e.g., `<name>-<toolset>`). When omitted in a single-skill mode, the name defaults, in order, to: the `--group` name, then the `--toolset` name, then the single `--prebuilt` config name; any other case requires `--name`.
- `--description`: (Optional) Description of the generated skill. When a group defines its own `description`, that takes precedence and `--description` acts as a fallback.
- `--group`: (Optional) Name of the group to convert into a single skill. Uses the group's `description`, falling back to `--description`. Mutually exclusive with `--toolset`.
- `--toolset`: (Optional) Name of the toolset to convert into a skill. If not provided, one skill will be generated for every custom toolset defined. If no custom toolsets are defined, it defaults to a single skill containing all tools.