From a0f86d78f3c640faf9762f68a90c801510900941 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Mon, 22 Jun 2026 16:15:09 -0400 Subject: [PATCH 1/3] feat: cleanup app creation outputs when passed --- cmd/app/link.go | 31 +++++++++++++++++++++++++++++++ cmd/project/create.go | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/cmd/app/link.go b/cmd/app/link.go index 044f56c3..72092faa 100644 --- a/cmd/app/link.go +++ b/cmd/app/link.go @@ -192,6 +192,37 @@ func LinkExistingApp(ctx context.Context, clients *shared.ClientFactory, app *ty return nil } +// LinkExistingAppQuiet links an existing app without verbose output. It resolves +// the team and environment, validates the app, saves it, and prints only the app +// summary. Used by `create --app` where the surrounding create output is enough. +func LinkExistingAppQuiet(ctx context.Context, clients *shared.ClientFactory, app *types.App) error { + linkedApp, auth, err := promptExistingApp(ctx, clients) + if err != nil { + return err + } + *app = linkedApp + + appIDs := []string{app.AppID} + _, err = clients.API().GetAppStatus(ctx, auth.Token, appIDs, app.TeamID) + if err != nil { + return err + } + + err = saveAppToJSON(ctx, clients, *app) + if err != nil { + clients.IO.PrintDebug(ctx, "Error saving app to file when linking existing app: %s", err) + return err + } + + clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ + Emoji: "house", + Text: "App", + Secondary: formatListSuccess([]types.App{*app}), + })) + + return nil +} + // LinkAppFooterSection displays the details of app that was added to the project. func LinkAppFooterSection(ctx context.Context, clients *shared.ClientFactory, app *types.App) { clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ diff --git a/cmd/project/create.go b/cmd/project/create.go index 1adff451..ae8f614c 100644 --- a/cmd/project/create.go +++ b/cmd/project/create.go @@ -229,7 +229,7 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args [] defer func() { _ = os.Chdir(originalDir) }() - if err := app.LinkExistingApp(ctx, clients, &types.App{}, false); err != nil { + if err := app.LinkExistingAppQuiet(ctx, clients, &types.App{}); err != nil { return err } } From 7b615b7ae8abf5ca69a5b66e1e9658124216e88c Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 25 Jun 2026 12:09:25 -0400 Subject: [PATCH 2/3] refactor: separate link logic from output in LinkExistingApp --- cmd/app/link.go | 159 +++++++++++++++++------------------------- cmd/project/create.go | 2 +- cmd/project/init.go | 2 +- 3 files changed, 66 insertions(+), 97 deletions(-) diff --git a/cmd/app/link.go b/cmd/app/link.go index 72092faa..e62e9837 100644 --- a/cmd/app/link.go +++ b/cmd/app/link.go @@ -80,7 +80,7 @@ func NewLinkCommand(clients *shared.ClientFactory) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() clients.IO.PrintTrace(ctx, slacktrace.AppLinkStart) - return LinkCommandRunE(ctx, clients, app) + return LinkCommandRunE(ctx, clients, app, false, false) }, PostRunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -93,47 +93,14 @@ func NewLinkCommand(clients *shared.ClientFactory) *cobra.Command { } // LinkCommandRunE saves details about the provided application -func LinkCommandRunE(ctx context.Context, clients *shared.ClientFactory, app *types.App) (err error) { - // Add empty line between executed command and first output - clients.IO.PrintInfo(ctx, false, "") - - err = LinkExistingApp(ctx, clients, app, false) - if err != nil { - return err - } - - return nil -} - -// LinkAppHeaderSection displays a section explaining how to find existing apps. -// External callers can use extraSecondaryText to show additional information. -// When shouldConfirm is true, additional information is included in the header -// explaining how to link apps, in case the user declines. -func LinkAppHeaderSection(ctx context.Context, clients *shared.ClientFactory, shouldConfirm bool) { - var secondaryText = []string{ - "Add an existing app from app settings", - "Find your existing apps at: " + style.Underline("https://api.slack.com/apps"), - } - - if shouldConfirm { - secondaryText = append(secondaryText, "Manually add apps later with "+style.Commandf("app link", true)) +func LinkCommandRunE(ctx context.Context, clients *shared.ClientFactory, app *types.App, shouldConfirm bool, quiet bool) (err error) { + if !quiet { + // Add empty line between executed command and first output + clients.IO.PrintInfo(ctx, false, "") + // Header section + LinkAppHeaderSection(ctx, clients, shouldConfirm) } - clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ - Emoji: "house", - Text: "App Link", - Secondary: secondaryText, - })) -} - -// LinkExistingApp prompts for an existing App ID and saves the details to the project. -// When shouldConfirm is true, a confirmation prompt will ask the user if they want to -// link an existing app and additional information is included in the header. -// The shouldConfirm option is encouraged for third-party callers. -func LinkExistingApp(ctx context.Context, clients *shared.ClientFactory, app *types.App, shouldConfirm bool) (err error) { - // Header section - LinkAppHeaderSection(ctx, clients, shouldConfirm) - // Confirm to add an existing app; useful for third-party callers if shouldConfirm { proceed, err := clients.IO.ConfirmPrompt(ctx, LinkAppConfirmPromptText, true) @@ -150,57 +117,80 @@ func LinkExistingApp(ctx context.Context, clients *shared.ClientFactory, app *ty } } - // App Manifest section - manifestSource, err := clients.Config.ProjectConfig.GetManifestSource(ctx) + if !quiet { + // App Manifest section + manifestSource, err := clients.Config.ProjectConfig.GetManifestSource(ctx) + if err != nil { + return err + } + + configPath := filepath.Join(config.ProjectConfigDirName, config.ProjectConfigJSONFilename) + clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ + Emoji: "books", + Text: "App Manifest", + Secondary: []string{ + "Manifest source is gathered from " + style.Highlight(manifestSource.Human()), + "Manifest source is configured in " + style.Highlight(configPath), + }, + })) + } + + err = LinkExistingApp(ctx, clients, app) if err != nil { return err } - configPath := filepath.Join(config.ProjectConfigDirName, config.ProjectConfigJSONFilename) + // App summary section clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ - Emoji: "books", - Text: "App Manifest", - Secondary: []string{ - "Manifest source is gathered from " + style.Highlight(manifestSource.Human()), - "Manifest source is configured in " + style.Highlight(configPath), - }, + Emoji: "house", + Text: "App", + Secondary: formatListSuccess([]types.App{*app}), })) - // Prompt to get app details - var auth *types.SlackAuth - *app, auth, err = promptExistingApp(ctx, clients) - if err != nil { - return err + if !quiet { + // Footer section + clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ + Emoji: "house_with_garden", + Text: "App Link", + Secondary: []string{ + "Added existing app to project", + }, + })) } - appIDs := []string{app.AppID} - _, err = clients.API().GetAppStatus(ctx, auth.Token, appIDs, app.TeamID) - if err != nil { - return err - } + return nil +} - // Save the app to the project - err = saveAppToJSON(ctx, clients, *app) - if err != nil { - clients.IO.PrintDebug(ctx, "Error saving app to file when linking existing app: %s", err) - return err +// LinkAppHeaderSection displays a section explaining how to find existing apps. +// External callers can use extraSecondaryText to show additional information. +// When shouldConfirm is true, additional information is included in the header +// explaining how to link apps, in case the user declines. +func LinkAppHeaderSection(ctx context.Context, clients *shared.ClientFactory, shouldConfirm bool) { + var secondaryText = []string{ + "Add an existing app from app settings", + "Find your existing apps at: " + style.Underline("https://api.slack.com/apps"), } - // Footer section - LinkAppFooterSection(ctx, clients, app) + if shouldConfirm { + secondaryText = append(secondaryText, "Manually add apps later with "+style.Commandf("app link", true)) + } - return nil + clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ + Emoji: "house", + Text: "App Link", + Secondary: secondaryText, + })) } -// LinkExistingAppQuiet links an existing app without verbose output. It resolves -// the team and environment, validates the app, saves it, and prints only the app -// summary. Used by `create --app` where the surrounding create output is enough. -func LinkExistingAppQuiet(ctx context.Context, clients *shared.ClientFactory, app *types.App) error { - linkedApp, auth, err := promptExistingApp(ctx, clients) +// LinkExistingApp resolves app details, validates the app, and saves it to the +// project. It produces no output — callers handle their own display. +func LinkExistingApp(ctx context.Context, clients *shared.ClientFactory, app *types.App) (err error) { + // Prompt to get app details + var auth *types.SlackAuth + *app, auth, err = promptExistingApp(ctx, clients) if err != nil { return err } - *app = linkedApp appIDs := []string{app.AppID} _, err = clients.API().GetAppStatus(ctx, auth.Token, appIDs, app.TeamID) @@ -208,37 +198,16 @@ func LinkExistingAppQuiet(ctx context.Context, clients *shared.ClientFactory, ap return err } + // Save the app to the project err = saveAppToJSON(ctx, clients, *app) if err != nil { clients.IO.PrintDebug(ctx, "Error saving app to file when linking existing app: %s", err) return err } - clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ - Emoji: "house", - Text: "App", - Secondary: formatListSuccess([]types.App{*app}), - })) - return nil } -// LinkAppFooterSection displays the details of app that was added to the project. -func LinkAppFooterSection(ctx context.Context, clients *shared.ClientFactory, app *types.App) { - clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ - Emoji: "house", - Text: "App", - Secondary: formatListSuccess([]types.App{*app}), - })) - clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ - Emoji: "house_with_garden", - Text: "App Link", - Secondary: []string{ - "Added existing app to project", - }, - })) -} - // promptExistingApp gathers details to represent app information func promptExistingApp(ctx context.Context, clients *shared.ClientFactory) (types.App, *types.SlackAuth, error) { slackAuth, err := prompts.PromptTeamSlackAuth(ctx, clients, "Select the existing app team", nil) diff --git a/cmd/project/create.go b/cmd/project/create.go index ae8f614c..ce42735e 100644 --- a/cmd/project/create.go +++ b/cmd/project/create.go @@ -229,7 +229,7 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args [] defer func() { _ = os.Chdir(originalDir) }() - if err := app.LinkExistingAppQuiet(ctx, clients, &types.App{}); err != nil { + if err := app.LinkCommandRunE(ctx, clients, &types.App{}, false, true); err != nil { return err } } diff --git a/cmd/project/init.go b/cmd/project/init.go index 9f9d5f90..3348119b 100644 --- a/cmd/project/init.go +++ b/cmd/project/init.go @@ -110,7 +110,7 @@ func projectInitCommandRunE(clients *shared.ClientFactory, cmd *cobra.Command, a _ = create.InstallProjectDependencies(ctx, clients, projectDirPath) // Add an existing app to the project - err = app.LinkExistingApp(ctx, clients, &types.App{}, true) + err = app.LinkCommandRunE(ctx, clients, &types.App{}, true, false) if err != nil { // Display the error but continue to init clients.IO.PrintError(ctx, "%s", err.Error()) From bfde92824544d3102e9a10ce580054e55f164d41 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 25 Jun 2026 17:19:12 -0400 Subject: [PATCH 3/3] refactor: remove booleans from LinkCommandRunE, callers own their output --- cmd/app/link.go | 77 ++++++++++++++++--------------------------- cmd/app/list.go | 6 ++-- cmd/app/list_test.go | 2 +- cmd/project/create.go | 8 ++++- cmd/project/init.go | 22 ++++++++++--- 5 files changed, 57 insertions(+), 58 deletions(-) diff --git a/cmd/app/link.go b/cmd/app/link.go index e62e9837..f5944800 100644 --- a/cmd/app/link.go +++ b/cmd/app/link.go @@ -80,7 +80,7 @@ func NewLinkCommand(clients *shared.ClientFactory) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() clients.IO.PrintTrace(ctx, slacktrace.AppLinkStart) - return LinkCommandRunE(ctx, clients, app, false, false) + return LinkCommandRunE(ctx, clients, app) }, PostRunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -93,47 +93,28 @@ func NewLinkCommand(clients *shared.ClientFactory) *cobra.Command { } // LinkCommandRunE saves details about the provided application -func LinkCommandRunE(ctx context.Context, clients *shared.ClientFactory, app *types.App, shouldConfirm bool, quiet bool) (err error) { - if !quiet { - // Add empty line between executed command and first output - clients.IO.PrintInfo(ctx, false, "") - // Header section - LinkAppHeaderSection(ctx, clients, shouldConfirm) - } +func LinkCommandRunE(ctx context.Context, clients *shared.ClientFactory, app *types.App) (err error) { + // Add empty line between executed command and first output + clients.IO.PrintInfo(ctx, false, "") - // Confirm to add an existing app; useful for third-party callers - if shouldConfirm { - proceed, err := clients.IO.ConfirmPrompt(ctx, LinkAppConfirmPromptText, true) - if err != nil { - clients.IO.PrintDebug(ctx, "Error prompting to add an existing app: %s", err) - return err - } + // Header section + LinkAppHeaderSection(ctx, clients, false) - // Add newline to match the trailing newline inserted from the footer section - clients.IO.PrintInfo(ctx, false, "") - - if !proceed { - return nil - } + // App Manifest section + manifestSource, err := clients.Config.ProjectConfig.GetManifestSource(ctx) + if err != nil { + return err } - if !quiet { - // App Manifest section - manifestSource, err := clients.Config.ProjectConfig.GetManifestSource(ctx) - if err != nil { - return err - } - - configPath := filepath.Join(config.ProjectConfigDirName, config.ProjectConfigJSONFilename) - clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ - Emoji: "books", - Text: "App Manifest", - Secondary: []string{ - "Manifest source is gathered from " + style.Highlight(manifestSource.Human()), - "Manifest source is configured in " + style.Highlight(configPath), - }, - })) - } + configPath := filepath.Join(config.ProjectConfigDirName, config.ProjectConfigJSONFilename) + clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ + Emoji: "books", + Text: "App Manifest", + Secondary: []string{ + "Manifest source is gathered from " + style.Highlight(manifestSource.Human()), + "Manifest source is configured in " + style.Highlight(configPath), + }, + })) err = LinkExistingApp(ctx, clients, app) if err != nil { @@ -144,19 +125,17 @@ func LinkCommandRunE(ctx context.Context, clients *shared.ClientFactory, app *ty clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ Emoji: "house", Text: "App", - Secondary: formatListSuccess([]types.App{*app}), + Secondary: FormatListSuccess([]types.App{*app}), })) - if !quiet { - // Footer section - clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ - Emoji: "house_with_garden", - Text: "App Link", - Secondary: []string{ - "Added existing app to project", - }, - })) - } + // Footer section + clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ + Emoji: "house_with_garden", + Text: "App Link", + Secondary: []string{ + "Added existing app to project", + }, + })) return nil } diff --git a/cmd/app/list.go b/cmd/app/list.go index 290eeb6e..216988c7 100644 --- a/cmd/app/list.go +++ b/cmd/app/list.go @@ -73,13 +73,13 @@ func runListCommand(cmd *cobra.Command, clients *shared.ClientFactory) error { clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ Emoji: "house_buildings", Text: "Apps", - Secondary: formatListSuccess(envs), + Secondary: FormatListSuccess(envs), })) return nil } -// formatListSuccess formats details about the list of project apps -func formatListSuccess(apps []types.App) (secondaryText []string) { +// FormatListSuccess formats details about the list of project apps +func FormatListSuccess(apps []types.App) (secondaryText []string) { for _, app := range apps { if app.AppID == "" { continue diff --git a/cmd/app/list_test.go b/cmd/app/list_test.go index caffcf6d..04d7546b 100644 --- a/cmd/app/list_test.go +++ b/cmd/app/list_test.go @@ -218,7 +218,7 @@ func TestAppsListFormat(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { listFlags = tc.Flags - formattedList := formatListSuccess(tc.Apps) + formattedList := FormatListSuccess(tc.Apps) for ii, value := range formattedList { formattedList[ii] = strings.TrimRight(value, ":") } diff --git a/cmd/project/create.go b/cmd/project/create.go index ce42735e..18f66d1d 100644 --- a/cmd/project/create.go +++ b/cmd/project/create.go @@ -229,9 +229,15 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args [] defer func() { _ = os.Chdir(originalDir) }() - if err := app.LinkCommandRunE(ctx, clients, &types.App{}, false, true); err != nil { + linkedApp := &types.App{} + if err := app.LinkExistingApp(ctx, clients, linkedApp); err != nil { return err } + clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ + Emoji: "house", + Text: "App", + Secondary: app.FormatListSuccess([]types.App{*linkedApp}), + })) } printCreateSuccess(ctx, clients, appDirPath) diff --git a/cmd/project/init.go b/cmd/project/init.go index 3348119b..c4e541b8 100644 --- a/cmd/project/init.go +++ b/cmd/project/init.go @@ -109,11 +109,25 @@ func projectInitCommandRunE(clients *shared.ClientFactory, cmd *cobra.Command, a // Existing projects initialized always default to config.ManifestSourceLocal. _ = create.InstallProjectDependencies(ctx, clients, projectDirPath) - // Add an existing app to the project - err = app.LinkCommandRunE(ctx, clients, &types.App{}, true, false) + // Prompt to add an existing app to the project + app.LinkAppHeaderSection(ctx, clients, true) + proceed, err := clients.IO.ConfirmPrompt(ctx, app.LinkAppConfirmPromptText, true) if err != nil { - // Display the error but continue to init - clients.IO.PrintError(ctx, "%s", err.Error()) + clients.IO.PrintDebug(ctx, "Error prompting to add an existing app: %s", err) + } + if proceed { + linkedApp := &types.App{} + err = app.LinkExistingApp(ctx, clients, linkedApp) + if err != nil { + // Display the error but continue to init + clients.IO.PrintError(ctx, "%s", err.Error()) + } else { + clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ + Emoji: "house", + Text: "App", + Secondary: app.FormatListSuccess([]types.App{*linkedApp}), + })) + } } printNextStepSection(ctx, clients, projectDirPath)