Skip to content

feat: add --app flag support to slack create for linking existing apps#565

Open
srtaalej wants to merge 12 commits into
mainfrom
ale-app-id-flag
Open

feat: add --app flag support to slack create for linking existing apps#565
srtaalej wants to merge 12 commits into
mainfrom
ale-app-id-flag

Conversation

@srtaalej

@srtaalej srtaalej commented May 28, 2026

Copy link
Copy Markdown
Contributor

Changelog

slack create now accepts the --app flag alongside --template to scaffold a project and automatically link it to an existing app via app link.

Summary

This PR adds --app [ID] and --environment flag support to slack create. When used with --template, the CLI will:

  1. Scaffold the project from the template (existing behavior)
  2. Run the existing app link flow to save the app ID to the project

The --environment flag (local/deployed) is passed through to app link to determine which file the app is saved to. If omitted, the user is prompted.

Flag validation:

  • --app without --template returns an error
  • --environment without --app returns an error

Example

slack create my-project -t slack-samples/bolt-js-starter-template --app A0123456789 --environment local

Testing

make test testdir=cmd/project testname=TestCreateCommand_AppFlag
make test testdir=cmd/app testname=TestLinkCommand

Manual verification:

  1. ./bin/slack create my-project -t slack-samples/bolt-js-starter-template --app <real-app-id> --environment local
  2. Confirmed .slack/apps.dev.json contains linked app with correct team/app IDs
  3. Confirmed --app without --template returns helpful error
  4. Confirmed --environment without --app returns helpful error
  5. Confirmed normal slack create (no --app) works unchanged

Notes

  • Follow-up PR: fetch and write the remote manifest to the project after linking

Requirements

@srtaalej srtaalej self-assigned this May 28, 2026
@srtaalej srtaalej added enhancement M-T: A feature request for new functionality semver:minor Use on pull requests to describe the release version increment labels May 28, 2026
@codecov

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.66%. Comparing base (7563522) to head (8ef2b29).

Files with missing lines Patch % Lines
cmd/project/create.go 75.00% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #565      +/-   ##
==========================================
+ Coverage   71.64%   71.66%   +0.01%     
==========================================
  Files         226      226              
  Lines       19154    19178      +24     
==========================================
+ Hits        13723    13744      +21     
- Misses       4220     4222       +2     
- Partials     1211     1212       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@srtaalej srtaalej marked this pull request as ready for review May 28, 2026 15:26
@srtaalej srtaalej requested a review from a team as a code owner May 28, 2026 15:26

@zimeg zimeg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@srtaalej Awesome changes going on here! 🎁

I'm leaving a handful of comments around refactoring logic into adjacent places. Hoping that we can compose commands overall and avoid adding too much to create for ongoing iteration.

Two notable changes to the experience that I'll call out here include:

  • Accepting the environment flag alongside a default
  • Skipping the name prompt when an existing app ID is provided

Quite excited for what this hopes to unlock 🔏

Comment thread cmd/project/create_app.go Outdated
Comment on lines +74 to +82
// fetchRemoteManifest retrieves the app manifest from the platform via apps.manifest.export.
func fetchRemoteManifest(ctx context.Context, clients *shared.ClientFactory, token string, appID string) (types.SlackYaml, error) {
manifest, err := clients.AppClient().Manifest.GetManifestRemote(ctx, token, appID)
if err != nil {
return types.SlackYaml{}, slackerror.New(slackerror.ErrInvalidManifest).
WithMessage("Failed to fetch manifest for app %s", appID)
}
return manifest, nil
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// fetchRemoteManifest retrieves the app manifest from the platform via apps.manifest.export.
func fetchRemoteManifest(ctx context.Context, clients *shared.ClientFactory, token string, appID string) (types.SlackYaml, error) {
manifest, err := clients.AppClient().Manifest.GetManifestRemote(ctx, token, appID)
if err != nil {
return types.SlackYaml{}, slackerror.New(slackerror.ErrInvalidManifest).
WithMessage("Failed to fetch manifest for app %s", appID)
}
return manifest, nil
}

🪓 suggestion: Let's inline this! I think the error returned might sometimes be different from invalid manifest that we might want to surface

Comment thread cmd/project/create_app.go Outdated
Comment on lines +84 to +98
// writeManifestToProject writes the fetched manifest JSON to the project directory.
func writeManifestToProject(fs afero.Fs, projectPath string, manifest types.SlackYaml) error {
manifestData, err := json.MarshalIndent(manifest.AppManifest, "", " ")
if err != nil {
return slackerror.Wrap(err, slackerror.ErrProjectFileUpdate).
WithMessage("Failed to serialize app manifest")
}

manifestPath := filepath.Join(projectPath, "manifest.json")
if err := afero.WriteFile(fs, manifestPath, append(manifestData, '\n'), 0644); err != nil {
return slackerror.Wrap(err, slackerror.ErrProjectFileUpdate).
WithMessage("Failed to write manifest to project")
}
return nil
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛻 suggestion: Let's move this logic to internal/app/manifest for adjacent changes of #543

Comment thread cmd/project/create.go
Comment thread cmd/project/create_app.go Outdated
Comment on lines +100 to +116
// linkAppToProject saves the app to the project's apps JSON file.
// Defaults to local/dev unless the manifest explicitly uses a hosted runtime.
func linkAppToProject(ctx context.Context, clients *shared.ClientFactory, auth types.SlackAuth, appID string, manifest types.SlackYaml) error {
app := types.App{
AppID: appID,
TeamID: auth.TeamID,
TeamDomain: auth.TeamDomain,
EnterpriseID: auth.EnterpriseID,
}

if manifest.IsFunctionRuntimeSlackHosted() {
return clients.AppClient().SaveDeployed(ctx, app)
}
app.IsDev = true
app.UserID = auth.UserID
return clients.AppClient().SaveLocal(ctx, app)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔭 question: Can we reuse logic of the app link command? We perhaps might change outputs but I'm hoping we move toward focused and atomic commands that perhaps compose!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👾 issue: I'm concerned of the forced default local app here. The same CI example I share earlier is for a production app and I don't have immediate option to "deploy" the right app after using this:

$ slack create --app A0582JYKGB1 --template zimeg/slacks --branch snaek --name snaek --force

🌠 suggestion: We might want to use the --environment flag to decide this? I still think a default "local" makes sense - CI should be explicit!

Comment thread cmd/project/create.go Outdated
WithMessage("The --app flag requires the --template flag when used with create")
}

// Fail fast: resolve auth and fetch manifest before creating the project

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🪬 thought: Related to comments of logic moved to internal I'm curious if we can move this check too? I understand a mismatched app ID will cause error but I don't think we should prompt for name when the --app flag is used...

🐮 ramble: For example a minimal example seems excessive for CI use case:

$ slack create --app A0582JYKGB1 --template zimeg/slacks --branch snaek --name snaek --force

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true! 🫡

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧂 ramble: Similar comment as below I wonder if this is logic can be passed as createArgs or perhaps afterwards to the link command?

@srtaalej srtaalej requested a review from zimeg June 2, 2026 04:54

@zimeg zimeg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@srtaalej Leaving a few more comments around logic that we might want to separate between project create and app link command 🔭

To me it's seeming more that these commands should be sequenced so the --app and --environment flags are passed through the "link" command after the "create" command clones the template. I'm unsure of right scope for this but notice a few improvements happening:

  • Finding saved authentication for a provided link app 🎁
  • Copying the existing app manifest from upstream app settings ⚙️
  • Saving the provided app ID from create flags 🏁

Am requesting approval to hope we can reuse more command logic and think these enhancements might be alright to break into multiple changesets if the notes above seem right?

Comment thread cmd/project/create.go Outdated
WithMessage("The --subdir flag requires the --template flag")
}

// --app requires --template (Mode 2 deferred)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👁️‍🗨️question: Which mode 2 in this case?

Comment thread cmd/project/create.go
Comment thread internal/pkg/apps/link.go Outdated
Comment on lines +107 to +115
// FetchRemoteManifest retrieves the app manifest from the platform via apps.manifest.export.
func FetchRemoteManifest(ctx context.Context, clients *shared.ClientFactory, token string, appID string) (types.SlackYaml, error) {
manifest, err := clients.AppClient().Manifest.GetManifestRemote(ctx, token, appID)
if err != nil {
return types.SlackYaml{}, slackerror.Wrap(err, slackerror.ErrInvalidManifest).
WithMessage("Failed to fetch manifest for app %s", appID)
}
return manifest, nil
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🪓 suggestion: Let's inline this! I think the error returned might sometimes be different from invalid manifest that we might want to surface

Suggested change
// FetchRemoteManifest retrieves the app manifest from the platform via apps.manifest.export.
func FetchRemoteManifest(ctx context.Context, clients *shared.ClientFactory, token string, appID string) (types.SlackYaml, error) {
manifest, err := clients.AppClient().Manifest.GetManifestRemote(ctx, token, appID)
if err != nil {
return types.SlackYaml{}, slackerror.Wrap(err, slackerror.ErrInvalidManifest).
WithMessage("Failed to fetch manifest for app %s", appID)
}
return manifest, nil
}

Comment thread internal/pkg/apps/link.go Outdated
Comment on lines +133 to +157
// SaveAppToProject writes the linked app to the project's apps JSON file,
// checking for conflicts before saving unless --force is set.
func SaveAppToProject(ctx context.Context, clients *shared.ClientFactory, app types.App) error {
deploy, err := clients.AppClient().GetDeployed(ctx, app.TeamID)
if err != nil {
return err
}
local, err := clients.AppClient().GetLocal(ctx, app.TeamID)
if err != nil {
return err
}
switch app.IsDev {
case true:
if clients.Config.ForceFlag || (local.IsNew() && deploy.AppID != app.AppID) {
return clients.AppClient().SaveLocal(ctx, app)
}
case false:
if clients.Config.ForceFlag || (deploy.IsNew() && local.AppID != app.AppID) {
return clients.AppClient().SaveDeployed(ctx, app)
}
}
return slackerror.New(slackerror.ErrAppFound).
WithMessage("A saved app was found and cannot be overwritten").
WithRemediation("Remove the app from this project or try again with %s", style.Bold("--force"))
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦠 suggestion: This is duplicate to app link implementation we might not want to duplicate this?

slack-cli/cmd/app/link.go

Lines 283 to 307 in dc30dc3

// saveAppToJSON writes the linked app to file for later use while not writing
// app IDs that exist
func saveAppToJSON(ctx context.Context, clients *shared.ClientFactory, app types.App) error {
deploy, err := clients.AppClient().GetDeployed(ctx, app.TeamID)
if err != nil {
return err
}
local, err := clients.AppClient().GetLocal(ctx, app.TeamID)
if err != nil {
return err
}
switch app.IsDev {
case true:
if clients.Config.ForceFlag || (local.IsNew() && deploy.AppID != app.AppID) {
return clients.AppClient().SaveLocal(ctx, app)
}
case false:
if clients.Config.ForceFlag || (deploy.IsNew() && local.AppID != app.AppID) {
return clients.AppClient().SaveDeployed(ctx, app)
}
}
return slackerror.New(slackerror.ErrAppFound).
WithMessage("A saved app was found and cannot be overwritten").
WithRemediation("Remove the app from this project or try again with %s", style.Bold("--force"))
}

Comment thread internal/pkg/apps/link.go Outdated
Comment on lines +117 to +131
// WriteManifestToProject writes the fetched manifest JSON to the project directory.
func WriteManifestToProject(fs afero.Fs, projectPath string, manifest types.SlackYaml) error {
manifestData, err := json.MarshalIndent(manifest.AppManifest, "", " ")
if err != nil {
return slackerror.Wrap(err, slackerror.ErrProjectFileUpdate).
WithMessage("Failed to serialize app manifest")
}

manifestPath := filepath.Join(projectPath, "manifest.json")
if err := afero.WriteFile(fs, manifestPath, append(manifestData, '\n'), 0644); err != nil {
return slackerror.Wrap(err, slackerror.ErrProjectFileUpdate).
WithMessage("Failed to write manifest to project")
}
return nil
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗳️ suggestion: We might want to make this internal/app/manifest.go I think we want to move away from internal/pkg/....... ongoing

Comment thread internal/pkg/apps/link.go Outdated
Comment on lines +30 to +73
// ResolveAuthForApp finds an authenticated workspace that has access to the given app ID.
func ResolveAuthForApp(ctx context.Context, clients *shared.ClientFactory, appID string) (types.SlackAuth, error) {
if clients.Config.TokenFlag != "" {
auth, err := clients.Auth().AuthWithToken(ctx, clients.Config.TokenFlag)
if err != nil {
return types.SlackAuth{}, slackerror.Wrap(err, slackerror.ErrNotAuthed)
}
return auth, nil
}

allAuths, err := clients.Auth().Auths(ctx)
if err != nil {
return types.SlackAuth{}, slackerror.Wrap(err, slackerror.ErrNotAuthed)
}

if len(allAuths) == 0 {
return types.SlackAuth{}, slackerror.New(slackerror.ErrNotAuthed).
WithMessage("No workspaces connected").
WithRemediation("Run %s to sign in to a workspace that has access to app %s", style.Commandf("login", false), appID)
}

if clients.Config.TeamFlag != "" {
for i := range allAuths {
if allAuths[i].TeamID == clients.Config.TeamFlag || allAuths[i].TeamDomain == clients.Config.TeamFlag {
if _, err := clients.API().GetAppStatus(ctx, allAuths[i].Token, []string{appID}, allAuths[i].TeamID); err == nil {
return allAuths[i], nil
}
}
}
return types.SlackAuth{}, slackerror.New(slackerror.ErrTeamNotFound).
WithMessage("The specified team does not have access to app %s", appID).
WithRemediation("Run %s to sign in to the workspace that owns this app", style.Commandf("login", false))
}

for i := range allAuths {
if _, err := clients.API().GetAppStatus(ctx, allAuths[i].Token, []string{appID}, allAuths[i].TeamID); err == nil {
return allAuths[i], nil
}
}

return types.SlackAuth{}, slackerror.New(slackerror.ErrAppNotFound).
WithMessage("No authenticated workspace has access to app %s", appID).
WithRemediation("Run %s to sign in to the workspace that owns this app", style.Commandf("login", false))
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💾 thought: This might be something we use to replace the following:

slack-cli/cmd/app/link.go

Lines 211 to 240 in aceb7a4

// 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)
if err != nil {
return types.App{}, &types.SlackAuth{}, err
}
appID, err := promptAppID(ctx, clients)
if err != nil {
return types.App{}, &types.SlackAuth{}, err
}
isProduction, err := promptIsProduction(ctx, clients)
if err != nil {
return types.App{}, &types.SlackAuth{}, err
}
app := types.App{
AppID: appID,
EnterpriseID: slackAuth.EnterpriseID,
TeamDomain: slackAuth.TeamDomain,
TeamID: slackAuth.TeamID,
}
if !isProduction {
app.IsDev = true
app.UserID = slackAuth.UserID
}
apps, err := apps.FetchAppInstallStates(ctx, clients, []types.App{app})
if err != nil {
return app, slackAuth, nil
}
return apps[0], slackAuth, nil
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👾 ramble: Am leaving comments in hopes that these additions can be reused more as:

  • Run the create command
    • If "--app" flag then run the link command
    • No "--app" flag continues without change

Comment thread cmd/project/create.go Outdated
WithMessage("The --app flag requires the --template flag when used with create")
}

// Fail fast: resolve auth and fetch manifest before creating the project

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧂 ramble: Similar comment as below I wonder if this is logic can be passed as createArgs or perhaps afterwards to the link command?

@srtaalej

srtaalej commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

@zimeg thanks for re-review! i agree these should be separate changesets 🤔 your comments definitely clarified the command flow!

@srtaalej srtaalej requested review from mwbrooks and zimeg June 8, 2026 21:31
@srtaalej srtaalej added this to the Next Release milestone Jun 8, 2026
@srtaalej srtaalej added the changelog Use on updates to be included in the release notes label Jun 9, 2026

@zimeg zimeg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@srtaalej Thanks for breaking this into multiple changes 🪬 ✨

The comments I'm leaving are much more focused and I think we can land this in separated changes to build confidence throughout. Right now I comment on testing and handling edge cases more before building on this more 🔭

Overall an exciting experience to start with an existing app! 🎁

Comment thread cmd/project/create.go Outdated
var createAppNameFlag string
var createListFlag bool
var createSubdirFlag string
var createEnvironmentFlag string

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧮 quibble: For alphabetical order?

Comment thread cmd/project/create.go Outdated
{Command: "create my-project -t slack-samples/deno-hello-world", Meaning: "Start a new project from a specific template"},
{Command: "create --name my-project", Meaning: "Create a project named 'my-project'"},
{Command: "create my-project -t org/monorepo --subdir apps/my-app", Meaning: "Create from a subdirectory of a template"},
{Command: "create my-project -t slack-samples/bolt-js-starter-template --app A0123456789", Meaning: "Create from template and link to an existing app"},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🪬 suggestion: Including the --environment flag might be meaningful with this example to avoid retries in CI setup or experiments avoiding prompts?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Instead, how about we add a CI/Scripting example and allow the above example to stay focused on creating a project with an existing app ID.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed!

Comment thread cmd/project/create.go Outdated
Comment on lines +143 to +147
// --environment requires --app
if cmd.Flags().Changed("environment") && !appFlagProvided {
return slackerror.New(slackerror.ErrMismatchedFlags).
WithMessage("The --environment flag requires the --app flag when used with create")
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔬 thought: We might guard against unexpected --environment values with this check or perhaps reset all changes if link command errors? I find template remains in an incomplete state after this command:

$ slack create --template slack-samples/bolt-js-starter-template --app A0B93LJK24A --name asdf --environment deploy --team slackbox-ez

🦠 ramble: The issue here is "deploy" instead of "deployed" I believe!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true! 🫡

},
}, func(cf *shared.ClientFactory) *cobra.Command {
return NewCreateCommand(cf)
})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 suggestion: Adding cases to cover the expected happy paths might be meaningful to guarantee the create command keeps support for these paths as underlined commands change

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog Use on updates to be included in the release notes enhancement M-T: A feature request for new functionality semver:minor Use on pull requests to describe the release version increment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants