Skip to content

Comments

feat(create): add '--subdir <path>' flag to 'create' command#345

Open
srtaalej wants to merge 1 commit intomainfrom
ale-feat-subdir-flag
Open

feat(create): add '--subdir <path>' flag to 'create' command#345
srtaalej wants to merge 1 commit intomainfrom
ale-feat-subdir-flag

Conversation

@srtaalej
Copy link
Contributor

@srtaalej srtaalej commented Feb 19, 2026

Changelog

Added --subdir flag to slack create for extracting a subdirectory from a template repository as the project root. This supports monorepo-style templates where multiple apps live in subdirectories (e.g. slack create my-app -t org/monorepo --subdir apps/my-app). 🦋

Summary

This PR adds a --subdir <path> flag to slack create that works alongside --template and --branch. When provided, the full template is cloned into a temporary directory, then only the specified subdirectory is copied to the final project path.

Behavior:

  • --subdir pydantic-ai/ → extracts the pydantic-ai directory from the template
  • --subdir . or --subdir / → equivalent to omitting the flag (full repo)
  • --subdir ../escape → rejected with an error (path traversal)
  • --subdir nonexistent → error with remediation hint pointing to the template
  • --list --subdir foo → --list returns early, --subdir is ignored

Requirements

@srtaalej srtaalej self-assigned this Feb 19, 2026
@srtaalej srtaalej requested a review from a team as a code owner February 19, 2026 21:01
@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 Feb 19, 2026
@codecov
Copy link

codecov bot commented Feb 19, 2026

Codecov Report

❌ Patch coverage is 70.21277% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.79%. Comparing base (113aa42) to head (e6cf706).

Files with missing lines Patch % Lines
internal/pkg/create/create.go 68.18% 12 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #345      +/-   ##
==========================================
+ Coverage   64.73%   64.79%   +0.06%     
==========================================
  Files         212      212              
  Lines       17809    17854      +45     
==========================================
+ Hits        11528    11569      +41     
+ Misses       5207     5202       -5     
- Partials     1074     1083       +9     

☔ View full report in Codecov by Sentry.
📢 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.

@mwbrooks mwbrooks added this to the Next Release milestone Feb 20, 2026
@mwbrooks mwbrooks changed the title feat(create): add 'subdir' flag to create command feat(create): add '--subdir <path>' flag to 'create' command Feb 20, 2026
@mwbrooks mwbrooks added the changelog Use on updates to be included in the release notes label Feb 20, 2026
Copy link
Member

@mwbrooks mwbrooks left a comment

Choose a reason for hiding this comment

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

🙌🏻 Amazing @srtaalej! Holy smokes, I can't believe you shared this PR today 👌🏻

🧪 Testing locally works great!

$ slack create -t https://github.com/slack-samples/bolt-python-examples --subdir block-kit

📝 I've left some feedback. For the afero feedback, think about it.

Generally we've put a huge amount of effort into ensuring the CLI uses afero whenever possible. In unit tests, Afero is a "virtual" memory-based file system - so there's no risk to the developer's machine and the tests run faster because it's not actually reading/writing to disk. However, sometimes it's difficult to use it certain areas. If you use afero you may need to change your tests to not use t.TempDir() and instead use Afero to create the temp directory.

cmd.Flags().StringVarP(&createGitBranchFlag, "branch", "b", "", "name of git branch to checkout")
cmd.Flags().StringVarP(&createAppNameFlag, "name", "n", "", "name for your app (overrides the name argument)")
cmd.Flags().BoolVar(&createListFlag, "list", false, "list available app templates")
cmd.Flags().StringVar(&createSubdirFlag, "subdir", "", "subdirectory within the template to use as project root")
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: An attempt to shorten the description so that it doesn't wrap on 80 character terminals

Suggested change
cmd.Flags().StringVar(&createSubdirFlag, "subdir", "", "subdirectory within the template to use as project root")
cmd.Flags().StringVar(&createSubdirFlag, "subdir", "", "subdirectory in the template to use as project")

Before:
Image

After:
Image

Comment on lines +358 to +359
// normalizeSubdir cleans the subdir path and returns "" if it resolves to root.
func normalizeSubdir(subdir string) (string, error) {
Copy link
Member

Choose a reason for hiding this comment

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

praise: very nice! 👌🏻

Comment on lines +363 to +366
cleaned := filepath.Clean(subdir)
if cleaned == "." || cleaned == "/" {
return "", nil
}
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: After filepath.Clean() maybe we should use filepath.isLocal(cleaned).

This function appears to check if the file path is within the subtree and prevent traversal attacks where the path tries to access files outside of the root directory (template directory).

I haven't used it personally, but it seems like a good security measure.

Image

Comment on lines +130 to +137
if subdir != "" {
if err := createAppFromSubdir(ctx, projectDirPath, createArgs.Template, createArgs.GitBranch, subdir, log, clients.Fs); err != nil {
return "", slackerror.Wrap(err, slackerror.ErrAppCreate)
}
} else {
if err := createApp(ctx, projectDirPath, createArgs.Template, createArgs.GitBranch, log, clients.Fs); err != nil {
return "", slackerror.Wrap(err, slackerror.ErrAppCreate)
}
Copy link
Member

Choose a reason for hiding this comment

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

praise: clean implementation choice!

// createAppFromSubdir clones the full template into a temp directory, then copies
// only the specified subdirectory to the final project path.
func createAppFromSubdir(ctx context.Context, dirPath string, template Template, gitBranch string, subdir string, log *logger.Logger, fs afero.Fs) error {
tmpDir, err := os.MkdirTemp("", "slack-create-*")
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: We should not use os. calls directly whenever possible. These calls are very hard to test because we can't mock it - it will create a temp directory during the unit test.

Instead, we should use our fs afero.Fs filesystem library. This uses os in production and a memory-based file system in tests.

I imagine we can use afero.GetTempDir() and afero.TempDir() to look something like:

tempDirRoot := afero.GetTempDir(fs, "")
tempDirPath, err := afero.TempDir(fs, tmpDirRoot, "slack-create-")

Comment on lines +385 to +387
if err := createApp(ctx, tmpDir, template, gitBranch, log, fs); err != nil {
return err
}
Copy link
Member

Choose a reason for hiding this comment

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

praise: Clever and clean approach!

Comment on lines +393 to +395
return slackerror.New(slackerror.ErrSubdirNotFound).
WithMessage("subdirectory %q was not found in the template", subdir).
WithRemediation("Check that the path exists in the template at %q", template.GetTemplatePath())
Copy link
Member

Choose a reason for hiding this comment

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

TIL: %q vs %s

Comment on lines +404 to +409
return goutils.CopyDirectory(goutils.CopyDirectoryOpts{
Src: subdirPath,
Dst: dirPath,
IgnoreDirectories: []string{".git", ".venv", "node_modules"},
IgnoreFiles: []string{".DS_Store"},
})
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: I noticed that we have the same IgnoreDirectories and IgnoreFiles on L337. Thoughts on pulling those two slices out to use in both places (less error prone if we need to ignore more files/directories). Alternatively, this could become a function used in both places.

AppName: appNameArg,
Template: template,
GitBranch: createGitBranchFlag,
Subdir: createSubdirFlag,
Copy link
Member

Choose a reason for hiding this comment

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

question: Right now --subdir without --template is silently accepted. For example, slack create --subdir src will use the src/ of whatever template is selected in the prompts.

Personally, I'd learn toward erroring if --subdir is used without --template because it avoids unexpected behaviour or exploits and we can "loosen" that contraint later if we decided we want that experience.

Comment on lines +381 to +382
// Remove so createApp can create it fresh (go-git requires non-existent target)
os.Remove(tmpDir)
Copy link
Member

Choose a reason for hiding this comment

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

question: Why do we make the temp directory on L377 and then immediately delete it?

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.

2 participants