From bdbf159c0a416423cfeac167be0ea3dd7abe425b Mon Sep 17 00:00:00 2001 From: Michael Brooks Date: Tue, 27 Oct 2026 22:08:44 -0700 Subject: [PATCH 1/4] feat: print the agent skill with the skill command and flag Bundle the slack-cli skill from the Slack Skills Plugin into the CLI so any coding agent with the CLI installed can read it with `slack skill` or `slack --skill`, without installing the plugin first. --- cmd/root.go | 13 +- cmd/root_test.go | 14 ++ cmd/skill/SKILL.md | 251 +++++++++++++++++++++++++ cmd/skill/skill.go | 73 +++++++ cmd/skill/skill_test.go | 58 ++++++ docs/reference/commands/slack.md | 2 + docs/reference/commands/slack_skill.md | 47 +++++ 7 files changed, 457 insertions(+), 1 deletion(-) create mode 100644 cmd/skill/SKILL.md create mode 100644 cmd/skill/skill.go create mode 100644 cmd/skill/skill_test.go create mode 100644 docs/reference/commands/slack_skill.md diff --git a/cmd/root.go b/cmd/root.go index d88d851b..318448c8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -42,6 +42,7 @@ import ( "github.com/slackapi/slack-cli/cmd/platform" "github.com/slackapi/slack-cli/cmd/project" "github.com/slackapi/slack-cli/cmd/sandbox" + skillcmd "github.com/slackapi/slack-cli/cmd/skill" "github.com/slackapi/slack-cli/cmd/triggers" "github.com/slackapi/slack-cli/cmd/upgrade" versioncmd "github.com/slackapi/slack-cli/cmd/version" @@ -88,9 +89,11 @@ var AliasMap = map[string]*AliasInfo{ var processName = cmdutil.GetProcessName() func NewRootCommand(clients *shared.ClientFactory, updateNotification *update.UpdateNotification) *cobra.Command { + var skillFlag bool + // rootCmd is the base Cobra command and all subcommands, that are part of the cmd package, // will be are added to the root command. - return &cobra.Command{ + rootCmd := &cobra.Command{ Use: fmt.Sprintf("%v [flags]", processName), Short: "Slack command-line tool", SilenceErrors: true, // we will bubble up and handle errors ourselves. @@ -142,10 +145,17 @@ func NewRootCommand(clients *shared.ClientFactory, updateNotification *update.Up return updateNotification.PrintAndPromptUpdates(cmd, version.Raw()) }, RunE: func(cmd *cobra.Command, args []string) error { + if skillFlag { + return skillcmd.RunSkillCommand(clients, cmd) + } return cmd.Help() }, } + // Support `--skill` as a shortcut to the `skill` command, similar to `--version` + rootCmd.Flags().BoolVar(&skillFlag, "skill", false, "print the agent skill for the Slack CLI") + + return rootCmd } // Init bootstraps the CLI process. Put things that do not rely on specific arguments or flags passed to the CLI in here. If you need flag/argument values, InitConfig below. @@ -196,6 +206,7 @@ func Init(ctx context.Context) (*cobra.Command, *shared.ClientFactory) { docs.NewCommand(clients), doctor.NewDoctorCommand(clients), feedback.NewFeedbackCommand(clients), + skillcmd.NewCommand(clients), } for _, topLevelCommand := range topLevelCommands { diff --git a/cmd/root_test.go b/cmd/root_test.go index e1a964dd..439101a1 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -218,6 +218,20 @@ func Test_NewSuggestion(t *testing.T) { require.Regexp(t, `Did you mean this\?\s+create`, err.Error(), "should suggest the create command") } +func Test_SkillFlag(t *testing.T) { + ctx := slackcontext.MockContext(t.Context()) + clientsMock := shared.NewClientsMock() + clients := shared.NewClientFactory(clientsMock.MockClientFactory()) + + // Call RunE directly since executing the command runs the global initializers of other tests + cmd := NewRootCommand(clients, nil) + cmd.SetContext(ctx) + require.NoError(t, cmd.ParseFlags([]string{"--skill"})) + err := cmd.RunE(cmd, []string{}) + require.NoError(t, err) + assert.True(t, strings.HasPrefix(clientsMock.GetStdoutOutput(), "---\nname: slack-cli\n")) +} + func Test_Aliases(t *testing.T) { ctx := slackcontext.MockContext(t.Context()) diff --git a/cmd/skill/SKILL.md b/cmd/skill/SKILL.md new file mode 100644 index 00000000..be1f7c80 --- /dev/null +++ b/cmd/skill/SKILL.md @@ -0,0 +1,251 @@ +--- +name: slack-cli +description: "Use when a developer works with the Slack CLI (slack command) to create, run, or manage a Slack app from the terminal: logging in or authenticating (slack login), adding a team or switching workspaces, running locally (slack run), deploying, editing the app manifest, calling Web API methods (slack api), or searching Slack docs (slack docs search)." +--- + +# Slack CLI + +Use the Slack CLI to create, run, and manage Slack apps — including calling Web API methods directly and searching Slack developer documentation from the terminal. + +For initial setup (sandbox creation, project scaffolding from templates), use the `slack:create-slack-app` skill instead. + +--- + +## Step 1: Detect the Slack CLI + +Resolve the path to the **public** Slack CLI before running any other command. We refer to the resolved value as `SLACK_CMD` — substitute it everywhere below. **Try 1a → 1b → 1c in order; do not skip ahead.** + +### 1a. Standard install path (preferred — no verification needed) + +The official install scripts place the binary at a fixed per-user path. If the file exists, use it directly: only the install script writes to that path. + +- macOS / Linux: `~/.slack/bin/slack` +- Windows: `%USERPROFILE%\AppData\Local\slack-cli\bin\slack.exe` (or `%USERPROFILE%\.slack-cli\bin\slack.exe` as a fallback) + +POSIX shell: + +```bash +[ -x "$HOME/.slack/bin/slack" ] && echo "$HOME/.slack/bin/slack" +``` + +PowerShell: + +```powershell +$p = "$env:USERPROFILE\AppData\Local\slack-cli\bin\slack.exe" +if (Test-Path $p) { $p } elseif (Test-Path "$env:USERPROFILE\.slack-cli\bin\slack.exe") { "$env:USERPROFILE\.slack-cli\bin\slack.exe" } +``` + +If a path is returned, set `SLACK_CMD` to that **absolute path** and proceed to Step 2. Do not also run `_fingerprint`. + +### 1b. `slack` on PATH (verify with fingerprint) + +If 1a found nothing, try `slack` on PATH: + +```bash +slack _fingerprint 2>/dev/null +``` + +If it outputs `d41d8cd98f00b204e9800998ecf8427e`, set `SLACK_CMD=slack` and proceed. + +### 1c. Ask about an alias, or install + +If 1b fails or returns a different value, ask the developer using AskUserQuestion: + +- "The `slack` command on your system doesn't appear to be the public Slack CLI. Do you have it installed under a different name or alias?" +- Options: "Yes, it's aliased as..." (let them provide the alias), "No, I need to install it" +- If they provide an alias, verify it with ` _fingerprint 2>/dev/null` and set `SLACK_CMD=`. +- If they need to install it, run: + + ```text + curl -fsSL https://downloads.slack-edge.com/slack-cli/install.sh | bash + ``` + + Then re-run **1a** — the install script will have written `~/.slack/bin/slack`. + +**Common mistakes:** Don't use `which slack` to discover the binary — `which` resolves any shell alias and defeats the point of 1a. In Git Bash on Windows, use the POSIX probe form, not PowerShell. + +--- + +## Step 2: Command Discovery via Help + +**Always run `SLACK_CMD --help` before constructing a command you have not used in the current session.** Do not guess at flags — the help output is the source of truth. + +- `SLACK_CMD help` — lists all available command groups +- `SLACK_CMD --help` — shows subcommands, flags, and usage examples + +### Resolving `--app` and `--team` values + +When a command requires `--app` or `--team`: + +- **App ID**: Run `SLACK_CMD app list` from the project directory to see installed apps and their IDs. +- **Team ID**: Run `SLACK_CMD auth list` to see authenticated workspaces and their team IDs. + +--- + +## Step 3: Searching Documentation (`slack docs search`) + +Search Slack's developer documentation directly from the terminal. + +```bash +SLACK_CMD docs search "" --output=text --limit=5 +``` + +Use `--output=text` for concise terminal-readable results. Use this when you are already running CLI commands and want to: + +- Check a Slack feature before implementing it +- Verify API behavior or required scopes +- Look up Block Kit elements, event types, or method parameters + +--- + +## Step 4: Calling Web API Methods (`slack api`) + +Call any Slack Web API method directly. Run `SLACK_CMD api --help` for full details and examples. + +Parameters are passed as positional `key=value` pairs (NOT `--key=value` flags): + +```bash +SLACK_CMD api chat.postMessage channel=C0123456789 text="Hello from the CLI" +``` + +**Important distinction**: `--team`, `--token`, `--json`, and `--data` are meta-flags (prefixed with `--`). API method parameters use positional `key=value` syntax without dashes. + +**Reference**: Full method list at . + +--- + +## Step 5: Authentication (`slack auth`) + +### When to run this flow + +Slack auth is **per-team**, not a single boolean. Run the seamless login flow below whenever any of these is true: + +- The developer is not authenticated to any team yet. +- The developer wants to **add a new team / workspace / sandbox**, even if `SLACK_CMD auth list` already shows other teams. +- The developer asks to log in, switch teams, or re-authenticate. + +`SLACK_CMD auth list` showing other teams is **not** a reason to skip login — those are different teams. Ask the developer which team they want, then run the flow. + +The flow below is the same for every kind of workspace. `/slackauthticket` works in any workspace the developer belongs to, whether that is a developer sandbox, a Free Team, or a production workspace, so there is no sandbox-specific login. Which target to prefer is the `slack:create-slack-app` skill's call (**Step 3: Choose Where to Install the App**). + +### Inspect existing auth (optional) + +```bash +SLACK_CMD auth list +``` + +Use this to show the developer which teams are already authenticated, or to confirm a successful login. Do not treat a non-empty list as "auth complete" when the developer asked to log in to a new team. + +### Run the seamless login flow + +The agent drives this end-to-end — **no separate terminal window, no browser confirmation**. + +`SLACK_CMD login --no-prompt` makes the CLI emit a single-use ticket and exit immediately instead of waiting on stdin. Slack itself renders the challenge code **inside a workspace modal** when the developer sends the `/slackauthticket` slash command — there is no browser step. The agent submits the challenge back to the CLI to complete login. + +**1. Start login and capture the ticket** + +```bash +SLACK_CMD login --no-prompt +``` + +The CLI prints a `/slackauthticket ` slash command and exits. Capture the ticket — you will need it in step 4. Sample output: + +```text +📋 Run the following slash command from any Slack channel in the workspace + you'd like to authenticate + + /slackauthticket eyJ0eXAiOiJKV1QiLCJh… + +? Slack will then show you a challenge code. Submit it via: + slack login --ticket --challenge +``` + +**2. Hand the slash command to the developer** + +Show the full `/slackauthticket …` line and ask the developer to paste it into the message box of the Slack workspace they want to authenticate, then send it. Slack responds with a modal containing a short challenge code (e.g. `JDt1IK7X`). + +**3. Collect the challenge code** + +Use `AskUserQuestion` to ask the developer for the challenge code shown in the Slack modal. Wait for their answer — do not guess or default. + +**4. Complete login** + +```bash +SLACK_CMD login --ticket --challenge +``` + +On success the CLI returns the team name and ID. Verify with `SLACK_CMD auth list` and report the team back to the developer. + +**Troubleshooting**: tickets are single-use and time-limited. If step 4 fails with an invalid/expired ticket or wrong challenge, restart from step 1 with a fresh `SLACK_CMD login --no-prompt` — do not retry the same ticket. + +Use `--team ` on individual commands to target a specific workspace without switching globally. + +### Red flags — STOP + +If you catch yourself thinking any of these, you are about to regress to the old broken flow: + +| Rationalization | Reality | +|---|---| +| "`auth list` already shows teams, so login isn't needed." | Auth is per-team. The developer asked for a _new_ team — drive the flow. | +| "`slack login` needs browser confirmation, so I can't drive it." | False with `--no-prompt`. The challenge code appears in Slack's modal, not a browser. The agent runs both `slack login` invocations itself. | +| "I should tell the developer to run `slack login` in a separate terminal." | Never. Step 5 is the agent's job from start to finish. | + +**All of these mean: run `SLACK_CMD login --no-prompt` yourself and follow the four numbered steps above.** + +--- + +## Step 6: Running an App Locally (`slack run`) + +Run `SLACK_CMD run --help` for all available flags. + +### Resolve the app or team target + +Run `SLACK_CMD app list` from the project directory to check for installed apps: + +- **If an app exists**: Use `--app=` in the run command. +- **If no apps are listed**: Use `--team=` instead (get the team ID from `SLACK_CMD auth list`). + +### Start the dev server in the background + +Run this command as a **background process** so the developer can continue working: + +**If an app ID was found:** + +```bash +cd && SLACK_CMD run --org-workspace-grant=all --app= +``` + +**If no apps — use the team ID:** + +```bash +cd && SLACK_CMD run --org-workspace-grant=all --team= +``` + +Tell the developer the app is running in the background. They can ask: + +- "What's the status of the dev server?" — to check on it +- "Show me the output from slack run" — to see activity logs +- "Stop the dev server" — to terminate the process + +--- + +## Step 7: Managing the Manifest + +Run `SLACK_CMD manifest --help` for subcommands (`validate`, `info`) and the `--source local|remote` flag. + +Use `SLACK_CMD manifest validate` before deploying or when something seems wrong with the app configuration. + +--- + +## Step 8: Other Commands + +For any other command group (e.g., `trigger`, `datastore`, `env`, `collaborator`, `external-auth`, `deploy`), run `SLACK_CMD --help` to discover subcommands and flags. Run `SLACK_CMD help` to see all available command groups. + +--- + +## Notes + +- `SLACK_CMD` is a placeholder — always substitute the actual command name resolved in Step 1. +- **Always run `--help`** before constructing a command you have not used in the current session. +- Interactive commands (e.g., `slack trigger create` without `--trigger-def`) cannot be run in the background. Tell the developer to run these in a **new terminal window**. `slack login` is **not** in this category — drive it inline using the `--no-prompt` / `--ticket` / `--challenge` flow in Step 5. +- `slack run` runs locally for development. `slack deploy` deploys to Slack's hosted infrastructure, unless `.slack/hooks.json` defines a `deploy` hook, in which case it runs that hook instead (see the `slack:deploy-slack-app` skill). These are different operations, so do not confuse them. diff --git a/cmd/skill/skill.go b/cmd/skill/skill.go new file mode 100644 index 00000000..005dee7b --- /dev/null +++ b/cmd/skill/skill.go @@ -0,0 +1,73 @@ +// Copyright 2022-2026 Salesforce, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package skill + +import ( + _ "embed" + "strings" + + "github.com/opentracing/opentracing-go" + "github.com/slackapi/slack-cli/internal/shared" + "github.com/slackapi/slack-cli/internal/style" + "github.com/spf13/cobra" +) + +// skillFile is the "slack-cli" agent skill from the Slack Skills Plugin. The +// copy here ships with each release so the skill matches the commands of the +// installed version. +// +// https://github.com/slackapi/slack-skills-plugin/blob/main/skills/slack-cli/SKILL.md +// +//go:embed SKILL.md +var skillFile string + +func NewCommand(clients *shared.ClientFactory) *cobra.Command { + cmd := &cobra.Command{ + Use: "skill", + Aliases: []string{"skills"}, + Short: "Print the agent skill for the Slack CLI", + Long: strings.Join([]string{ + "Print the agent skill that teaches coding agents to use the Slack CLI.", + "", + "The skill is a markdown file with frontmatter that follows the Agent Skills", + "format. Agents can read the output or save it to a skills directory.", + }, "\n"), + Example: style.ExampleCommandsf([]style.ExampleCommand{ + { + Meaning: "Print the agent skill using the command", + Command: "skill", + }, + { + Meaning: "Print the agent skill using the flag", + Command: "--skill", + }, + }), + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return RunSkillCommand(clients, cmd) + }, + } + return cmd +} + +// RunSkillCommand writes the agent skill to stdout without formatting +func RunSkillCommand(clients *shared.ClientFactory, cmd *cobra.Command) error { + ctx := cmd.Context() + span, _ := opentracing.StartSpanFromContext(ctx, "cmd.skill") + defer span.Finish() + + _, err := clients.IO.WriteOut().Write([]byte(skillFile)) + return err +} diff --git a/cmd/skill/skill_test.go b/cmd/skill/skill_test.go new file mode 100644 index 00000000..e6846255 --- /dev/null +++ b/cmd/skill/skill_test.go @@ -0,0 +1,58 @@ +// Copyright 2022-2026 Salesforce, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package skill + +import ( + "testing" + + "github.com/slackapi/slack-cli/internal/shared" + "github.com/slackapi/slack-cli/internal/slackcontext" + "github.com/slackapi/slack-cli/test/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_NewCommand(t *testing.T) { + ctx := slackcontext.MockContext(t.Context()) + clientsMock := shared.NewClientsMock() + clients := shared.NewClientFactory(clientsMock.MockClientFactory()) + + cmd := NewCommand(clients) + cmd.SetArgs([]string{}) + testutil.MockCmdIO(clients.IO, cmd) + + err := cmd.ExecuteContext(ctx) + require.NoError(t, err) + assert.Equal(t, skillFile, clientsMock.GetStdoutOutput()) + assert.Equal(t, []string{"skills"}, cmd.Aliases) +} + +func Test_NewCommand_Args(t *testing.T) { + ctx := slackcontext.MockContext(t.Context()) + clientsMock := shared.NewClientsMock() + clients := shared.NewClientFactory(clientsMock.MockClientFactory()) + + cmd := NewCommand(clients) + cmd.SetArgs([]string{"unexpected"}) + testutil.MockCmdIO(clients.IO, cmd) + + err := cmd.ExecuteContext(ctx) + assert.Error(t, err) +} + +func Test_skillFile(t *testing.T) { + assert.Contains(t, skillFile, "---\nname: slack-cli\n", "should start with skill frontmatter") + assert.Contains(t, skillFile, "\ndescription: ", "should include a skill description") +} diff --git a/docs/reference/commands/slack.md b/docs/reference/commands/slack.md index 1505ef89..255a9a37 100644 --- a/docs/reference/commands/slack.md +++ b/docs/reference/commands/slack.md @@ -22,6 +22,7 @@ slack [flags] -f, --force ignore warnings and continue executing command -h, --help help for slack --no-color remove styles and formatting from outputs + --skill print the agent skill for the Slack CLI -s, --skip-update skip checking for latest version of CLI -w, --team string select workspace or organization by team name or ID --token string set the access token associated with a team @@ -68,6 +69,7 @@ $ slack docs # Open Slack developer docs * [slack run](/tools/slack-cli/reference/commands/slack_run/) - Start a local server to develop and run the app locally * [slack samples](/tools/slack-cli/reference/commands/slack_samples/) - List available sample apps * [slack sandbox](/tools/slack-cli/reference/commands/slack_sandbox/) - Manage developer sandboxes +* [slack skill](/tools/slack-cli/reference/commands/slack_skill/) - Print the agent skill for the Slack CLI * [slack sync](/tools/slack-cli/reference/commands/slack_sync/) - Sync the app manifest between project and app settings * [slack trigger](/tools/slack-cli/reference/commands/slack_trigger/) - List details of existing triggers * [slack uninstall](/tools/slack-cli/reference/commands/slack_uninstall/) - Uninstall the app from a team diff --git a/docs/reference/commands/slack_skill.md b/docs/reference/commands/slack_skill.md new file mode 100644 index 00000000..b7b2fac2 --- /dev/null +++ b/docs/reference/commands/slack_skill.md @@ -0,0 +1,47 @@ +# `slack skill` + +Print the agent skill for the Slack CLI + +## Description + +Print the agent skill that teaches coding agents to use the Slack CLI. + +The skill is a markdown file with frontmatter that follows the Agent Skills +format. Agents can read the output or save it to a skills directory. + +``` +slack skill [flags] +``` + +## Flags + +``` + -h, --help help for skill +``` + +## Global flags + +``` + --accessible use accessible prompts for screen readers + -a, --app string use a specific app ID or environment + --config-dir string use a custom path for system config directory + -e, --experiment strings use the experiment(s) in the command + -f, --force ignore warnings and continue executing command + --no-color remove styles and formatting from outputs + -s, --skip-update skip checking for latest version of CLI + -w, --team string select workspace or organization by team name or ID + --token string set the access token associated with a team + -v, --verbose print debug logging and additional info +``` + +## Examples + +``` +$ slack skill # Print the agent skill using the command +$ slack --skill # Print the agent skill using the flag +``` + +## See also + +* [slack](/tools/slack-cli/reference/commands/slack/) - Slack command-line tool + From 57bdc43061846fd5d3237b3773145d79a1c3eb16 Mon Sep 17 00:00:00 2001 From: Michael Brooks Date: Wed, 28 Oct 2026 08:48:22 -0700 Subject: [PATCH 2/4] fix: skip the update check for the skill command and flag An update notice printed after the skill would end up in the skill when the output is saved to a file. --- internal/update/update.go | 2 +- internal/update/update_test.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/internal/update/update.go b/internal/update/update.go index a42a5a53..bfd977d4 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -215,7 +215,7 @@ func (u *UpdateNotification) isCI() bool { func (u *UpdateNotification) isIgnoredCommand() bool { // Commands that skip update checks only when used alone or with flags // "manifest" is included because it's an alias that runs "manifest info" - ignoredCommands := []string{"_fingerprint", "manifest", "manifest info", "version"} + ignoredCommands := []string{"--skill", "_fingerprint", "manifest", "manifest info", "skill", "skills", "version"} // Commands that skip update checks regardless of any arguments // "api" is a leaf command that accepts positional args, not subcommands ignoredPrefixes := []string{"api"} diff --git a/internal/update/update_test.go b/internal/update/update_test.go index bfdd58af..c5f1114b 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -194,6 +194,22 @@ func Test_UpdateNotification_isIgnoredCommand(t *testing.T) { command: "auth", expected: false, }, + "skill command": { + command: "skill", + expected: true, + }, + "skills alias": { + command: "skills", + expected: true, + }, + "skill flag": { + command: "--skill", + expected: true, + }, + "skill flag with flags": { + command: "--skill --verbose", + expected: true, + }, } { t.Run(name, func(t *testing.T) { if tc.command != "" { From 781ba2ce9f8b4b70477fa838ff70a586850b4c5d Mon Sep 17 00:00:00 2001 From: Michael Brooks Date: Wed, 28 Oct 2026 10:14:33 -0700 Subject: [PATCH 3/4] refactor: define the skill flag beside the version flag in init Keeps the root command constructor as it was, apart from the check in its run function. --- cmd/root.go | 13 +++++-------- cmd/root_test.go | 4 ++-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 318448c8..57f437f0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -89,11 +89,9 @@ var AliasMap = map[string]*AliasInfo{ var processName = cmdutil.GetProcessName() func NewRootCommand(clients *shared.ClientFactory, updateNotification *update.UpdateNotification) *cobra.Command { - var skillFlag bool - // rootCmd is the base Cobra command and all subcommands, that are part of the cmd package, // will be are added to the root command. - rootCmd := &cobra.Command{ + return &cobra.Command{ Use: fmt.Sprintf("%v [flags]", processName), Short: "Slack command-line tool", SilenceErrors: true, // we will bubble up and handle errors ourselves. @@ -145,17 +143,13 @@ func NewRootCommand(clients *shared.ClientFactory, updateNotification *update.Up return updateNotification.PrintAndPromptUpdates(cmd, version.Raw()) }, RunE: func(cmd *cobra.Command, args []string) error { - if skillFlag { + if skill, _ := cmd.Flags().GetBool("skill"); skill { return skillcmd.RunSkillCommand(clients, cmd) } return cmd.Help() }, } - // Support `--skill` as a shortcut to the `skill` command, similar to `--version` - rootCmd.Flags().BoolVar(&skillFlag, "skill", false, "print the agent skill for the Slack CLI") - - return rootCmd } // Init bootstraps the CLI process. Put things that do not rely on specific arguments or flags passed to the CLI in here. If you need flag/argument values, InitConfig below. @@ -173,6 +167,9 @@ func Init(ctx context.Context) (*cobra.Command, *shared.ClientFactory) { rootCmd.Version = version.Raw() rootCmd.SetVersionTemplate(versioncmd.Template() + "\n") + // Support `--skill` as a shortcut to the `skill` command, similar to `--version` + rootCmd.Flags().Bool("skill", false, "print the agent skill for the Slack CLI") + // Add subcommands (each subcommand may add their own child subcommands) // Please keep these sorted subCommands := []*cobra.Command{ diff --git a/cmd/root_test.go b/cmd/root_test.go index 439101a1..607b8b47 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -220,11 +220,11 @@ func Test_NewSuggestion(t *testing.T) { func Test_SkillFlag(t *testing.T) { ctx := slackcontext.MockContext(t.Context()) + cmd, clients := Init(ctx) clientsMock := shared.NewClientsMock() - clients := shared.NewClientFactory(clientsMock.MockClientFactory()) + clients.IO = clientsMock.IO // Call RunE directly since executing the command runs the global initializers of other tests - cmd := NewRootCommand(clients, nil) cmd.SetContext(ctx) require.NoError(t, cmd.ParseFlags([]string{"--skill"})) err := cmd.RunE(cmd, []string{}) From 12fb1cbcc8e9fbe86de3f7614cf15548d9509b22 Mon Sep 17 00:00:00 2001 From: Michael Brooks Date: Wed, 28 Oct 2026 10:43:06 -0700 Subject: [PATCH 4/4] fix: print the skill when the skill command also gets the skill flag Agents that combine the command and flag, as in `slack skill --skill` or `slack --skill skill`, now get the skill instead of an unknown flag error. --- cmd/skill/skill.go | 6 ++++++ cmd/skill/skill_test.go | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/cmd/skill/skill.go b/cmd/skill/skill.go index 005dee7b..e71c7b45 100644 --- a/cmd/skill/skill.go +++ b/cmd/skill/skill.go @@ -59,6 +59,12 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command { return RunSkillCommand(clients, cmd) }, } + + // Accept the root `--skill` flag here too, so `slack skill --skill` and + // `slack --skill skill` print the skill instead of erroring + cmd.Flags().Bool("skill", false, "print the agent skill for the Slack CLI") + _ = cmd.Flags().MarkHidden("skill") + return cmd } diff --git a/cmd/skill/skill_test.go b/cmd/skill/skill_test.go index e6846255..f5d26d91 100644 --- a/cmd/skill/skill_test.go +++ b/cmd/skill/skill_test.go @@ -39,6 +39,20 @@ func Test_NewCommand(t *testing.T) { assert.Equal(t, []string{"skills"}, cmd.Aliases) } +func Test_NewCommand_SkillFlag(t *testing.T) { + ctx := slackcontext.MockContext(t.Context()) + clientsMock := shared.NewClientsMock() + clients := shared.NewClientFactory(clientsMock.MockClientFactory()) + + cmd := NewCommand(clients) + cmd.SetArgs([]string{"--skill"}) + testutil.MockCmdIO(clients.IO, cmd) + + err := cmd.ExecuteContext(ctx) + require.NoError(t, err) + assert.Equal(t, skillFile, clientsMock.GetStdoutOutput()) +} + func Test_NewCommand_Args(t *testing.T) { ctx := slackcontext.MockContext(t.Context()) clientsMock := shared.NewClientsMock()