Skip to content

feat: azd init -t auto-creates project directory like git clone#7290

Open
spboyer wants to merge 9 commits intomainfrom
feat/init-auto-create-directory
Open

feat: azd init -t auto-creates project directory like git clone#7290
spboyer wants to merge 9 commits intomainfrom
feat/init-auto-create-directory

Conversation

@spboyer
Copy link
Member

@spboyer spboyer commented Mar 24, 2026

Summary

When using azd init -t <template>, automatically create a project directory named after the template and initialize inside it — similar to how git clone creates a directory.

This addresses user feedback from a getting-started study (#4032):

"Would have preferred if it went into a folder kind of like how GitHub does when you clone things."

Fixes #7289

Changes

  • Add optional [directory] positional argument to azd init
  • Auto-derive folder name from template path following git clone conventions
  • Create directory, os.Chdir into it, run full init pipeline inside
  • Pass . as directory to use current directory (preserves existing behavior)
  • Show cd hint after init so users know how to enter the project
  • Add DeriveDirectoryName() helper with path traversal protection
  • Validate target directory: prompt if non-empty, error with --no-prompt

Usage

# Auto-creates folder, initializes inside it
azd init -t todo-nodejs-mongo
# → creates todo-nodejs-mongo/, inits everything inside

# Explicit folder name
azd init -t todo-nodejs-mongo my-project
# → creates my-project/

# Opt-in to current directory (preserves existing behavior)
azd init -t todo-nodejs-mongo .
# → initializes in current directory

Testing

  • 11 new unit tests for resolve, validate, and create directory logic
  • 16 test cases for DeriveDirectoryName() including edge cases (traversal, .git suffix, various URL formats)
  • All existing init tests continue to pass
  • Snapshots updated (usage + fig spec)

Files Changed

File Change
pkg/templates/path.go New DeriveDirectoryName() with traversal protection
cmd/init.go Positional arg, auto-create + chdir, cd hint
cmd/init_test.go 11 new unit tests
pkg/templates/path_test.go 16 new test cases
cmd/testdata/*.snap Updated snapshots

Copilot AI review requested due to automatic review settings March 24, 2026 18:41
@spboyer spboyer requested a review from JeffreyCA as a code owner March 24, 2026 18:41
@spboyer spboyer self-assigned this Mar 24, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates azd init -t to behave more like git clone by defaulting template-based initialization into an auto-created project directory, with an optional positional [directory] override and updated outputs/specs/tests.

Changes:

  • Add optional [directory] positional arg to azd init and implement auto-create + chdir flow for template init.
  • Introduce templates.DeriveDirectoryName() helper (with intended traversal protection) and add unit tests.
  • Extend auth status contract/output to include token expiry (expiresOn) and improve login guidance for a specific auth error.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
cli/azd/pkg/templates/path.go Adds DeriveDirectoryName() helper used to derive the auto-created project folder name.
cli/azd/pkg/templates/path_test.go Adds test cases for DeriveDirectoryName() across URL/path formats and edge cases.
cli/azd/cmd/init.go Adds [directory] positional arg, directory validation/creation, chdir, and a post-init cd hint.
cli/azd/cmd/init_test.go Adds tests for resolving/validating/creating the target directory for template init.
cli/azd/cmd/testdata/TestUsage-azd-init.snap Updates usage snapshot to include [directory].
cli/azd/cmd/testdata/TestFigSpec.ts Updates fig completion spec to include optional directory arg for init.
cli/azd/pkg/contracts/auth.go Adds expiresOn field to auth status result contract.
cli/azd/pkg/contracts/auth_token_test.go Adds JSON roundtrip tests for StatusResult with/without expiresOn.
cli/azd/cmd/auth_status.go Populates StatusResult.ExpiresOn from acquired access token.
cli/azd/cmd/middleware/login_guard.go Wraps ErrNoCurrentUser with a suggestion to run azd auth login.

Comment on lines +999 to +1009
entries, err := os.ReadDir(targetDir)
if errors.Is(err, os.ErrNotExist) {
return nil // Directory doesn't exist yet — will be created
}
if err != nil {
return fmt.Errorf("reading directory '%s': %w", filepath.Base(targetDir), err)
}

if len(entries) == 0 {
return nil // Empty directory is fine
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

validateTargetDirectory uses os.ReadDir(targetDir), which reads the entire directory listing into memory just to check whether it’s empty. For large existing directories this can be unnecessarily expensive. Consider opening the directory and reading a single entry (e.g., f.ReadDir(1) / Readdirnames(1)) to determine emptiness.

Copilot uses AI. Check for mistakes.
Comment on lines 55 to 65
func newInitCmd() *cobra.Command {
return &cobra.Command{
Use: "init",
Use: "init [directory]",
Short: "Initialize a new application.",
Long: `Initialize a new application.

When used with --template, a new directory is created (named after the template)
and the project is initialized inside it — similar to git clone.
Pass "." as the directory to initialize in the current directory instead.`,
Args: cobra.MaximumNArgs(1),
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

The command’s Long help now states that --template creates a new directory, but elsewhere the init help text still describes initializing in the current directory (e.g., the generated help description used by docs/snapshots). Please update the remaining help description/footer text for consistency with the new default behavior when using templates.

Copilot uses AI. Check for mistakes.
Comment on lines 5 to +8
• To view all available sample templates, including those submitted by the azd community, visit: https://azure.github.io/awesome-azd.

Usage
azd init [flags]
azd init [directory] [flags]
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

The usage snapshot still says “Initialize a new application in your current directory.” With the new behavior (template init auto-creates a subdirectory by default), this description is now misleading. Update the snapshot/help text to reflect the template directory creation behavior (while still noting that . preserves current-directory init).

Copilot uses AI. Check for mistakes.
ClientID string `json:"clientId,omitempty"`

// When authenticated, the time at which the current access token expires.
ExpiresOn *RFC3339Time `json:"expiresOn,omitempty"`
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

StatusResult.ExpiresOn is typed as *RFC3339Time while LoginResult.ExpiresOn uses *time.Time. This can lead to inconsistent JSON timestamp formatting across auth commands. Consider aligning the types/formatting (or explicitly documenting the difference) so consumers don’t have to special-case parsing.

Suggested change
ExpiresOn *RFC3339Time `json:"expiresOn,omitempty"`
ExpiresOn *time.Time `json:"expiresOn,omitempty"`

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +156
func DeriveDirectoryName(templatePath string) string {
path := strings.TrimSpace(templatePath)
path = strings.TrimRight(path, "/")

// Strip .git suffix (like git clone does)
path = strings.TrimSuffix(path, ".git")

var name string

// For remote URIs, extract the last path segment from the URL
if isRemoteURI(path) {
// Handle git@host:owner/repo format
if strings.HasPrefix(path, "git@") {
if idx := strings.LastIndex(path, ":"); idx >= 0 {
path = path[idx+1:]
}
}

// Take the last path segment
if idx := strings.LastIndex(path, "/"); idx >= 0 {
name = path[idx+1:]
} else {
name = path
}
} else {
// For local paths and bare names, use the last path component
name = filepath.Base(path)
}

// Reject unsafe directory names that could cause path traversal
if name == "." || name == ".." || name == "" {
// Fall back to a sanitized version of the full path
name = strings.NewReplacer("/", "-", "\\", "-", ":", "-").Replace(
strings.TrimRight(templatePath, "/"))
}

return name
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

DeriveDirectoryName() can still return "." or ".." when templatePath itself is "."/".." (the fallback sanitizer doesn’t change those values). When used by azd init this can resolve to the current/parent directory (e.g., Join(wd, "..")), which contradicts the path-traversal protection intent and can lead to initializing in an unintended location. Ensure the function never returns "."/".."/"" (even after fallback sanitization) by choosing a safe default name (and consider trimming both "/" and "\" plus trimming spaces consistently).

Copilot uses AI. Check for mistakes.
{
name: "BareDotDotIsSanitized",
input: "..",
expected: "..",
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

This test case currently encodes the behavior that DeriveDirectoryName("..") returns "..". If DeriveDirectoryName is meant to provide path-traversal protection for derived project directories, returning ".." is unsafe (it can resolve to the parent directory). Update this expectation once the helper is fixed to return a non-traversing directory name for "."/".." inputs.

Suggested change
expected: "..",
expected: "dotdot",

Copilot uses AI. Check for mistakes.
Comment on lines +334 to +335
followUp += fmt.Sprintf("\n\nChange to the project directory:\n %s",
output.WithHighLightFormat("cd %s", cdPath))
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

The post-init cd hint is emitted as cd %s without quoting. If the derived/explicit directory contains spaces (e.g., azd init -t ... "my project"), the suggested command won’t work as-is. Consider conditionally quoting/escaping cdPath (e.g., wrap in double quotes) so the hint is copy/paste-safe across common shells.

Suggested change
followUp += fmt.Sprintf("\n\nChange to the project directory:\n %s",
output.WithHighLightFormat("cd %s", cdPath))
// Quote the cd path when it contains whitespace so the hint is copy/paste-safe.
cdPathDisplay := cdPath
if strings.ContainsAny(cdPath, " \t") {
cdPathDisplay = fmt.Sprintf("%q", cdPath)
}
followUp += fmt.Sprintf("\n\nChange to the project directory:\n %s",
output.WithHighLightFormat("cd %s", cdPathDisplay))

Copilot uses AI. Check for mistakes.
@spboyer spboyer force-pushed the feat/init-auto-create-directory branch from 6f0cbae to e625f6a Compare March 24, 2026 19:21
@spboyer
Copy link
Member Author

spboyer commented Mar 25, 2026

/azp run

@azure-pipelines
Copy link

You have several pipelines (over 10) configured to build pull requests in this repository. Specify which pipelines you would like to run by using /azp run [pipelines] command. You can specify multiple pipelines using a comma separated list.

spboyer and others added 9 commits March 24, 2026 17:32
Add --check flag to 'azd auth token' for lightweight auth validation.
Agents can call 'azd auth token --check' to validate authentication
state with exit code 0 (valid) or non-zero (invalid) without producing
standard output. This prevents costly retry loops where agents
speculatively call auth token and parse errors.

Enhance 'azd auth status --output json' to include expiresOn field,
giving agents machine-readable token expiry information for proactive
re-authentication.

Improve LoginGuardMiddleware to wrap ErrNoCurrentUser with actionable
ErrorWithSuggestion guidance, while preserving original error types for
cancellations and transient failures.

Changes:
- cmd/auth_token.go: Add --check flag with early-exit validation
- cmd/auth_token_test.go: Add 3 test cases (check success/failure/not-logged-in)
- cmd/auth_status.go: Populate ExpiresOn from token validation
- pkg/contracts/auth.go: Add ExpiresOn field to StatusResult
- cmd/middleware/login_guard.go: Wrap ErrNoCurrentUser with suggestion

Fixes #7234

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove redundant 'if a.flags.check' branches in auth_token.go that
  duplicated the same return (Copilot review comment #2)
- Add StatusResult JSON serialization tests verifying expiresOn is
  present when authenticated and omitted when unauthenticated
  (Copilot review comment #3)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of adding a --check flag to the hidden 'auth token' command,
make the existing 'auth status --output json' command agent-friendly:

- Exit non-zero when unauthenticated in machine-readable mode, so
  agents can rely on exit code without parsing output
- expiresOn field already added to StatusResult in this PR
- Remove --check flag and its tests (net -90 lines)

Agents can now validate auth with:
  azd auth status --output json
  # exit 0 + JSON with expiresOn = valid
  # exit 1 + JSON with status:unauthenticated = invalid

This is more discoverable than a hidden flag on a hidden command.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per @JeffreyCA feedback:
- Return auth.ErrNoCurrentUser when unauthenticated in both JSON and
  interactive modes (exit non-zero in all cases)
- In JSON mode, format output before returning error to avoid double-print
- In interactive mode, show status UX then exit non-zero

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per @vhvb1989 feedback: unauthenticated is a valid result, not a
command failure. Non-zero exit should only be for unexpected errors.
The expiresOn and LoginGuardMiddleware improvements remain.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When using azd init -t <template>, automatically create a project
directory named after the template and initialize inside it, similar
to how git clone creates a directory.

Changes:
- Add optional [directory] positional argument to azd init
- Auto-derive folder name from template path (git clone conventions)
- Create directory, os.Chdir into it, run full init pipeline inside
- Pass "." to use current directory (preserves existing behavior)
- Show cd hint after init so users know how to enter the project
- Add DeriveDirectoryName() helper with path traversal protection
- Validate target directory: prompt if non-empty, error with --no-prompt

Fixes #7289
Related to #4032

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@spboyer spboyer force-pushed the feat/init-auto-create-directory branch from e625f6a to 584d3b2 Compare March 25, 2026 00:32
spboyer added a commit that referenced this pull request Mar 25, 2026
Add lessons learned from recent PR reviews (#7290, #7251, #7250,
#7247, #7236, #7235, #7202, #7039) as agent instructions to prevent
recurring review findings.

New sections:
- Error handling: ErrorWithSuggestion completeness, telemetry service
  attribution, scope-agnostic messages
- Architecture boundaries: pkg/project target-agnostic, extension docs
- Output formatting: shell-safe paths, consistent JSON contracts
- Path safety: traversal validation, quoted paths in messages
- Testing best practices: test actual rules, extract shared helpers,
  correct env vars, TypeScript patterns, efficient dir checks
- CI/GitHub Actions: permissions, PATH handling, artifact downloads,
  prefer ADO for secrets

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
spboyer added a commit that referenced this pull request Mar 25, 2026
Add lessons learned from team and Copilot reviews across PRs #7290,
#7251, #7250, #7247, #7236, #7235, #7202, #7039 as agent instructions
to prevent recurring review findings.

New/expanded sections:
- Error handling: ErrorWithSuggestion field completeness, telemetry
  service attribution, scope-agnostic messages, link/suggestion parity,
  stale data in polling loops
- Architecture boundaries: pkg/project target-agnostic, extension docs
  separation, env var verification against source code
- Output formatting: shell-safe quoted paths, consistent JSON types
- Path safety: traversal validation, quoted paths in messages
- Code organization: extract shared logic across scopes
- Documentation standards: help text consistency, no dead references,
  PR description accuracy
- Testing best practices: test YAML rules e2e, extract shared helpers,
  correct env vars (AZD_FORCE_TTY, NO_COLOR), TypeScript patterns,
  reasonable timeouts, cross-platform paths, test new JSON fields
- CI / GitHub Actions: permissions blocks, PATH handling, cross-workflow
  artifacts, prefer ADO for secrets, no placeholder steps

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@spboyer
Copy link
Member Author

spboyer commented Mar 25, 2026

/azp run azure-dev - cli

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

azd init -t should auto-create a project folder like git clone

2 participants