Add interactive prompt for bun init in non-empty directories

When running `bun init` in a non-empty directory in TTY mode, users are now presented with an interactive prompt asking whether to:
1. Create in a new subdirectory (default)
2. Use the current directory (may overwrite files)
3. Cancel

This prevents accidental file creation in wrong directories and provides a better UX, similar to tools like `create-next-app`.

The prompt is only shown when:
- No folder was explicitly specified as an argument
- stdin is a TTY (not piped input)
- Not using auto-yes flags (-y, --react, etc.)
- The directory is not empty (ignores .DS_Store and Thumbs.db)

Fixes #24555

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2025-11-11 03:32:33 +00:00
parent 0a307ed880
commit b4493db088
2 changed files with 173 additions and 0 deletions

View File

@@ -339,6 +339,72 @@ pub const InitCommand = struct {
private: bool = true,
};
fn isDirectoryEmpty() bool {
var dir = std.fs.cwd().openDir(".", .{ .iterate = true }) catch return true;
defer dir.close();
var it = bun.DirIterator.iterate(.fromStdDir(dir), .u8);
while (it.next().unwrap() catch return true) |entry| {
const name = entry.name.slice();
// Ignore common hidden files that don't count as "project files"
if (strings.eqlComptime(name, ".") or
strings.eqlComptime(name, "..") or
strings.eqlComptime(name, ".DS_Store") or
strings.eqlComptime(name, "Thumbs.db"))
{
continue;
}
return false;
}
return true;
}
fn promptForNonEmptyDirectory(alloc: std.mem.Allocator) !?[]const u8 {
Output.prettyln("<r><yellow>⚠<r> The current directory is not empty.", .{});
Output.flush();
const selected = try radio("What would you like to do?", enum {
create_subdirectory,
use_current,
cancel,
pub const default: @This() = .create_subdirectory;
pub fn fmt(self: @This()) []const u8 {
return switch (self) {
.create_subdirectory => "<cyan>Create in a new subdirectory<r>",
.use_current => "<yellow>Use current directory (may overwrite files)<r>",
.cancel => "<red>Cancel<r>",
};
}
});
switch (selected) {
.create_subdirectory => {
const folder_name = prompt(
alloc,
"<r><cyan>subdirectory name<r> ",
"my-app",
) catch |err| {
if (err == error.EndOfStream) return null;
return err;
};
if (folder_name.len == 0) {
Output.prettyErrorln("Subdirectory name cannot be empty", .{});
return null;
}
return folder_name;
},
.use_current => {
return "";
},
.cancel => {
return null;
},
}
}
pub fn exec(alloc: std.mem.Allocator, init_args: [][:0]const u8) !void {
// --minimal is a special preset to create only empty package.json + tsconfig.json
var minimal = false;
@@ -387,6 +453,34 @@ pub const InitCommand = struct {
}
}
// Check if directory is non-empty and we're in a TTY environment
// Only prompt if no folder was explicitly specified and stdin is a TTY
const stdin_is_tty = std.posix.isatty(bun.FD.stdin().native());
if (initialize_in_folder == null and !auto_yes and Output.enable_ansi_colors_stderr and stdin_is_tty) {
if (!isDirectoryEmpty()) {
const result = promptForNonEmptyDirectory(alloc) catch |err| {
if (err == error.EndOfStream) {
Output.prettyln("<r><d>Cancelled.<r>", .{});
Global.exit(0);
}
return err;
};
if (result) |folder| {
if (folder.len > 0) {
// User wants to create a subdirectory
initialize_in_folder = folder;
}
// else: folder.len == 0 means use current directory
} else {
// User cancelled
Output.prettyln("<r><d>Cancelled.<r>", .{});
Global.exit(0);
}
}
}
if (initialize_in_folder) |ifdir| {
std.fs.cwd().makePath(ifdir) catch |err| {
Output.prettyErrorln("Failed to create directory {s}: {s}", .{ ifdir, @errorName(err) });