feat(create): add '--subdir <path>' flag to 'create' command#345
feat(create): add '--subdir <path>' flag to 'create' command#345
Conversation
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
mwbrooks
left a comment
There was a problem hiding this comment.
🙌🏻 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
aferoyou may need to change your tests to not uset.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") |
There was a problem hiding this comment.
suggestion: An attempt to shorten the description so that it doesn't wrap on 80 character terminals
| 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") |
| // normalizeSubdir cleans the subdir path and returns "" if it resolves to root. | ||
| func normalizeSubdir(subdir string) (string, error) { |
| cleaned := filepath.Clean(subdir) | ||
| if cleaned == "." || cleaned == "/" { | ||
| return "", nil | ||
| } |
There was a problem hiding this comment.
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.
| 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) | ||
| } |
There was a problem hiding this comment.
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-*") |
There was a problem hiding this comment.
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-")| if err := createApp(ctx, tmpDir, template, gitBranch, log, fs); err != nil { | ||
| return err | ||
| } |
There was a problem hiding this comment.
praise: Clever and clean approach!
| 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()) |
| return goutils.CopyDirectory(goutils.CopyDirectoryOpts{ | ||
| Src: subdirPath, | ||
| Dst: dirPath, | ||
| IgnoreDirectories: []string{".git", ".venv", "node_modules"}, | ||
| IgnoreFiles: []string{".DS_Store"}, | ||
| }) |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
| // Remove so createApp can create it fresh (go-git requires non-existent target) | ||
| os.Remove(tmpDir) |
There was a problem hiding this comment.
question: Why do we make the temp directory on L377 and then immediately delete it?


Changelog
Added
--subdirflag 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--templateand--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 ignoredRequirements