Skip to content

Comments

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

Open
srtaalej wants to merge 8 commits intomainfrom
ale-feat-subdir-flag
Open

feat(create): add '--subdir <path>' flag to 'create' command#345
srtaalej wants to merge 8 commits 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 73.58491% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.77%. Comparing base (62770ba) to head (32ede49).

Files with missing lines Patch % Lines
internal/pkg/create/create.go 70.21% 12 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #345      +/-   ##
==========================================
+ Coverage   64.62%   64.77%   +0.14%     
==========================================
  Files         212      212              
  Lines       17820    17869      +49     
==========================================
+ Hits        11517    11575      +58     
+ Misses       5225     5213      -12     
- Partials     1078     1081       +3     

☔ 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.

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!

Copy link
Member

Choose a reason for hiding this comment

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

🪬 question(non-blocking): Would passing subdir to createApp in all instances - either a default "." or the normalized path - reduce this implementation more? I'm concerned that a change to the temporary directory logic might require repeated change if we keep separate setups for this.

// 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-")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ty for sharing - i didnt know we were using a package for temp directories 👍 this is a game changer 💯

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 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

MkdirTemp generates unique path names which we want for createApp. But we don't want the directory itself which is why its deleted right away. Its a workaround to extract the unique name creation part of the MkdirTemp function so we can use it in createApp.

@srtaalej srtaalej requested a review from zimeg February 23, 2026 18:37
Copy link
Member

@zimeg zimeg left a comment

Choose a reason for hiding this comment

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

@srtaalej Super nice changesets here! I'm leaving a few comments but this works well for me - it's a neat unlock! 🎉

Before merging we might want to revisit error messages and codes. Please correct me if sentence casings should be lowercased, but I find that this is confusing in most implementations for me... The error code itself had another note, but these aren't blocking changes!

I also resolved a few comments with recent commits but found this a bit confusing in review. If I can kind request these be marked resolved with matching commits before next review it'd be kind 💌

// --subdir requires --template
if cmd.Flags().Changed("subdir") && !templateFlagProvided {
return slackerror.New(slackerror.ErrMismatchedFlags).
WithMessage("the --subdir flag requires the --template flag")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
WithMessage("the --subdir flag requires the --template flag")
WithMessage("The --subdir flag requires the --template flag")

🏁 polish: To match surrounding error messages. Though I'm not confident in how consistent we are with this... Let's take note to include this in a Style Guide soon!

ErrSocketConnection = "socket_connection_error"
ErrScopesExceedAppConfig = "scopes_exceed_app_config"
ErrStreamingActivityLogs = "streaming_activity_logs_error"
ErrSubdirNotFound = "subdir_not_found"
Copy link
Member

Choose a reason for hiding this comment

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

💡 thought(non-blocking): Would it make sense to group this alongside other template errors? To me the referenced subdir wouldn't be clear at a glance. I'd suggest:

template_subdir_not_found

Which is so close to template_path_not_found!

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.

🪬 question(non-blocking): Would passing subdir to createApp in all instances - either a default "." or the normalized path - reduce this implementation more? I'm concerned that a change to the temporary directory logic might require repeated change if we keep separate setups for this.

}
if !filepath.IsLocal(cleaned) {
return "", slackerror.New(slackerror.ErrSubdirNotFound).
WithMessage("subdirectory path %q must be relative and within the template", subdir)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
WithMessage("subdirectory path %q must be relative and within the template", subdir)
WithMessage("Subdirectory path %q must be relative and within the template", subdir)

🧮 quibble: Casing to errors as a complete sentence!

if err != nil {
if os.IsNotExist(err) {
return slackerror.New(slackerror.ErrSubdirNotFound).
WithMessage("subdirectory %q was not found in the template", subdir).
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
WithMessage("subdirectory %q was not found in the template", subdir).
WithMessage("Subdirectory %q was not found in the template", subdir).

🚢 quibble: Casings to the sentence structure!

}
if !info.IsDir() {
return slackerror.New(slackerror.ErrSubdirNotFound).
WithMessage("path %q in the template is not a directory", subdir)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
WithMessage("path %q in the template is not a directory", subdir)
WithMessage("Path %q in the template is not a directory", subdir)

📠 quibble: Forbid this error be reached, but let's start with a capital path here.

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