From bc648655e6bc25a65d18c00bc0b5848e33f4893f Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Wed, 18 Mar 2026 23:19:36 -0700 Subject: [PATCH] feat: add optional filepath argument to run command --- cmd/platform/run.go | 16 +++++++++-- cmd/platform/run_test.go | 34 +++++++++++++++++++++++ docs/reference/hooks/index.md | 2 +- internal/pkg/platform/localserver.go | 14 +++++++--- internal/pkg/platform/localserver_test.go | 30 ++++++++++---------- internal/pkg/platform/run.go | 2 ++ 6 files changed, 77 insertions(+), 21 deletions(-) diff --git a/cmd/platform/run.go b/cmd/platform/run.go index 5e606ad2..2ff17862 100644 --- a/cmd/platform/run.go +++ b/cmd/platform/run.go @@ -47,13 +47,14 @@ var runAppSelectPromptFunc = prompts.AppSelectPrompt func NewRunCommand(clients *shared.ClientFactory) *cobra.Command { cmd := &cobra.Command{ - Use: "run", + Use: "run [app-path]", Aliases: []string{"dev", "start-dev"}, // Aliases a few proposed alternative names Short: "Start a local server to develop and run the app locally", Long: `Start a local server to develop and run the app locally while watching for file changes`, + Args: cobra.MaximumNArgs(1), Example: style.ExampleCommandsf([]style.ExampleCommand{ {Command: "platform run", Meaning: "Start a local development server"}, - {Command: "platform run --activity-level debug", Meaning: "Run a local development server with debug activity"}, + {Command: "platform run ./src/app.py", Meaning: "Run a local development server with a custom app entry point"}, {Command: "platform run --cleanup", Meaning: "Run a local development server with cleanup"}, }), PreRunE: func(cmd *cobra.Command, args []string) error { @@ -96,6 +97,16 @@ func RunRunCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []str } ctx := cmd.Context() + var appPath string + if len(args) > 0 { + appPath = args[0] + if _, err := clients.Fs.Stat(appPath); err != nil { + return slackerror.New(slackerror.ErrNotFound). + WithMessage("The app path %q could not be found", appPath). + WithRemediation("Check that the file exists and the path is correct") + } + } + // Get the workspace from the flag or prompt selection, err := runAppSelectPromptFunc(ctx, clients, prompts.ShowLocalOnly, prompts.ShowAllApps) if err != nil { @@ -136,6 +147,7 @@ func RunRunCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []str Activity: !runFlags.noActivity, ActivityLevel: runFlags.activityLevel, App: selection.App, + AppPath: appPath, Auth: selection.Auth, Cleanup: runFlags.cleanup, ShowTriggers: triggers.ShowTriggers(clients, runFlags.hideTriggers), diff --git a/cmd/platform/run_test.go b/cmd/platform/run_test.go index ef656c67..1173cc7e 100644 --- a/cmd/platform/run_test.go +++ b/cmd/platform/run_test.go @@ -28,6 +28,7 @@ import ( "github.com/slackapi/slack-cli/internal/slackerror" "github.com/slackapi/slack-cli/internal/style" "github.com/slackapi/slack-cli/test/testutil" + "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -54,6 +55,7 @@ func (m *RunPkgMock) Run(ctx context.Context, clients *shared.ClientFactory, run func TestRunCommand_Flags(t *testing.T) { tests := map[string]struct { + setup func(cm *shared.ClientsMock) cmdArgs []string appFlag string tokenFlag string @@ -186,6 +188,35 @@ func TestRunCommand_Flags(t *testing.T) { }, expectedErr: slackerror.New(slackerror.ErrProcessInterrupted), }, + "Positional arg sets AppPath": { + setup: func(cm *shared.ClientsMock) { + _ = afero.WriteFile(cm.Fs, "./src/app.py", []byte(""), 0644) + }, + cmdArgs: []string{"./src/app.py"}, + selectedAppAuth: prompts.SelectedApp{ + App: types.NewApp(), + Auth: types.SlackAuth{}, + }, + expectedRunArgs: platform.RunArgs{ + Activity: true, + ActivityLevel: "info", + App: types.NewApp(), + AppPath: "./src/app.py", + Auth: types.SlackAuth{}, + Cleanup: false, + ShowTriggers: true, + }, + }, + "Error if app path does not exist": { + cmdArgs: []string{"./nonexistent/app.py"}, + selectedAppAuth: prompts.SelectedApp{ + App: types.NewApp(), + Auth: types.SlackAuth{}, + }, + expectedErr: slackerror.New(slackerror.ErrNotFound). + WithMessage("The app path %q could not be found", "./nonexistent/app.py"). + WithRemediation("Check that the file exists and the path is correct"), + }, "Error if no apps are available when using a remote manifest source": { selectedAppErr: slackerror.New(slackerror.ErrMissingOptions), expectedErr: slackerror.New(slackerror.ErrAppNotFound). @@ -214,6 +245,9 @@ func TestRunCommand_Flags(t *testing.T) { clients.Config.AppFlag = tc.appFlag clients.Config.TokenFlag = tc.tokenFlag }) + if tc.setup != nil { + tc.setup(clientsMock) + } appSelectMock := prompts.NewAppSelectMock() appSelectMock.On("AppSelectPrompt", mock.Anything, mock.Anything, prompts.ShowLocalOnly, prompts.ShowAllApps).Return(tc.selectedAppAuth, tc.selectedAppErr) diff --git a/docs/reference/hooks/index.md b/docs/reference/hooks/index.md index b41343a7..4d9d2d5d 100644 --- a/docs/reference/hooks/index.md +++ b/docs/reference/hooks/index.md @@ -216,7 +216,7 @@ The application's app-level token and bot access token will be provided as envir All Bolt SDKs leverage this `start` hook operating mode. -A custom start path can be set with the `SLACK_CLI_CUSTOM_FILE_PATH` variable. +A custom start path can be provided as a positional argument to the `run` command (e.g., `slack run ./src/app.py`), which sets both the `SLACK_APP_PATH` and `SLACK_CLI_CUSTOM_FILE_PATH` environment variables for the hook process. ##### Output diff --git a/internal/pkg/platform/localserver.go b/internal/pkg/platform/localserver.go index 28b87041..320f3154 100644 --- a/internal/pkg/platform/localserver.go +++ b/internal/pkg/platform/localserver.go @@ -81,6 +81,7 @@ type LocalServer struct { token string localHostedContext LocalHostedContext cliConfig hooks.SDKCLIConfig + appPath string Connection WebSocketConnection delegateCmd hooks.ShellCommand // track running delegated process delegateCmdMutex sync.Mutex // protect concurrent access @@ -279,11 +280,16 @@ func (r *LocalServer) stopDelegateProcess(ctx context.Context) { // connection for running app locally to script hook start func (r *LocalServer) StartDelegate(ctx context.Context) error { // Set up hook execution options + env := map[string]string{ + "SLACK_CLI_XAPP": r.token, + "SLACK_CLI_XOXB": r.localHostedContext.BotAccessToken, + } + if r.appPath != "" { + env["SLACK_APP_PATH"] = r.appPath + env["SLACK_CLI_CUSTOM_FILE_PATH"] = r.appPath + } var sdkManagedConnectionStartHookOpts = hooks.HookExecOpts{ - Env: map[string]string{ - "SLACK_CLI_XAPP": r.token, - "SLACK_CLI_XOXB": r.localHostedContext.BotAccessToken, - }, + Env: env, Exec: hooks.ShellExec{}, Hook: r.clients.SDKConfig.Hooks.Start, } diff --git a/internal/pkg/platform/localserver_test.go b/internal/pkg/platform/localserver_test.go index e0b061b3..15378a52 100644 --- a/internal/pkg/platform/localserver_test.go +++ b/internal/pkg/platform/localserver_test.go @@ -140,13 +140,14 @@ func Test_LocalServer_Start(t *testing.T) { TeamID: "justiceleague", } server := LocalServer{ - clients, - "ABC123", - localContext, - clients.SDKConfig, - conn, - nil, - sync.Mutex{}, + clients: clients, + token: "ABC123", + localHostedContext: localContext, + cliConfig: clients.SDKConfig, + appPath: "", + Connection: conn, + delegateCmd: nil, + delegateCmdMutex: sync.Mutex{}, } if tc.fakeDialer != nil { orig := *WebsocketDialerDial @@ -349,13 +350,14 @@ func Test_LocalServer_Listen(t *testing.T) { TeamID: "justiceleague", } server := LocalServer{ - clients, - "ABC123", - localContext, - clients.SDKConfig, - conn, - nil, - sync.Mutex{}, + clients: clients, + token: "ABC123", + localHostedContext: localContext, + cliConfig: clients.SDKConfig, + appPath: "", + Connection: conn, + delegateCmd: nil, + delegateCmdMutex: sync.Mutex{}, } tc.Test(t, ctx, clientsMock, &server, conn) }) diff --git a/internal/pkg/platform/run.go b/internal/pkg/platform/run.go index d78a7d74..f2bf3967 100644 --- a/internal/pkg/platform/run.go +++ b/internal/pkg/platform/run.go @@ -36,6 +36,7 @@ type RunArgs struct { Activity bool ActivityLevel string App types.App + AppPath string Auth types.SlackAuth Cleanup bool ShowTriggers bool @@ -126,6 +127,7 @@ func Run(ctx context.Context, clients *shared.ClientFactory, runArgs RunArgs) (t token: localInstallResult.APIAccessTokens.AppLevel, localHostedContext: localHostedContext, cliConfig: cliConfig, + appPath: runArgs.AppPath, Connection: nil, }