Skip to content

fix(create-tldraw): create subdirectory from project name#8161

Merged
AniKrisn merged 3 commits intomainfrom
ani/cli-fix
Mar 10, 2026
Merged

fix(create-tldraw): create subdirectory from project name#8161
AniKrisn merged 3 commits intomainfrom
ani/cli-fix

Conversation

@AniKrisn
Copy link
Copy Markdown
Contributor

@AniKrisn AniKrisn commented Mar 5, 2026

Closes #8148

Summary

  • npm create tldraw now always creates a subdirectory from the project name instead of installing into the current directory
  • If a directory with that name already exists, appends -1, -2, etc. — never overwrites
  • Removes the --overwrite flag and "directory is not empty" prompt, since they're no longer needed

This one change fixes both bugs from the issue:

  1. Files in wrong directory — the name now determines the directory
  2. Ctrl+C writing files — the prompt where this happened no longer exists

Test plan

  • Run cli.cjs from a non-empty directory, type a name → verify files go into a new subdirectory
  • Run it again with the same name → verify it creates <name>-1 instead of overwriting
  • Press Ctrl+C at any prompt → verify no files are created

Change type

  • bugfix
  • improvement
  • feature
  • api
  • other

🤖 Generated with Claude Code


Note

Medium Risk
Behavioral change to CLI output location and directory selection could affect existing user workflows/scripts, but it’s limited to project scaffolding and reduces risk of accidental overwrites.

Overview
create-tldraw no longer scaffolds into the current directory or overwrites an existing target. It now derives a project directory from the provided/entered app name, and if that directory already exists it automatically picks the next available -1, -2, etc. suffix.

This removes the --overwrite flag and the interactive “directory not empty” prompt/cleanup path, replaces it with findAvailableDir, and hardens isDirEmpty to treat non-directories as non-empty (with new unit tests covering file paths and .git-only dirs).

Written by Cursor Bugbot for commit 846d76e. This will update automatically on new commits. Configure here.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
examples Ready Ready Preview Mar 10, 2026 5:39pm
5 Skipped Deployments
Project Deployment Actions Updated (UTC)
analytics Ignored Ignored Preview Mar 10, 2026 5:39pm
tldraw-docs Ignored Ignored Preview Mar 10, 2026 5:39pm
chat-template Skipped Skipped Mar 10, 2026 5:39pm
tldraw-shader Skipped Skipped Mar 10, 2026 5:39pm
workflow-template Skipped Skipped Mar 10, 2026 5:39pm

Request Review

@AniKrisn AniKrisn requested a review from mimecuvalo March 5, 2026 14:28
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: findAvailableDir crashes when candidate path is a file
    • Updated isDirEmpty to treat existing non-directory paths as unavailable by checking statSync(path).isDirectory() before reading entries, preventing ENOTDIR crashes for file candidates.

Create PR

Or push these changes by commenting:

@cursor push ffa6159acb
Preview (ffa6159acb)
diff --git a/packages/create-tldraw/src/utils.ts b/packages/create-tldraw/src/utils.ts
--- a/packages/create-tldraw/src/utils.ts
+++ b/packages/create-tldraw/src/utils.ts
@@ -1,5 +1,5 @@
 import { isCancel, outro } from '@clack/prompts'
-import { existsSync, readdirSync } from 'node:fs'
+import { existsSync, readdirSync, statSync } from 'node:fs'
 import { basename, resolve } from 'node:path'
 
 export function nicelog(...args: unknown[]) {
@@ -12,6 +12,10 @@
 		return true
 	}
 
+	if (!statSync(path).isDirectory()) {
+		return false
+	}
+
 	const files = readdirSync(path)
 	return files.length === 0 || (files.length === 1 && files[0] === '.git')
 }
This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

@huppy-bot huppy-bot bot added the feature New feature label Mar 10, 2026
@mimecuvalo
Copy link
Copy Markdown
Member

I'm down with this change! @steveruizok would you like to weigh in?

@AniKrisn AniKrisn added this pull request to the merge queue Mar 10, 2026
@mimecuvalo mimecuvalo removed this pull request from the merge queue due to a manual request Mar 10, 2026
Copy link
Copy Markdown
Member

@mimecuvalo mimecuvalo left a comment

Choose a reason for hiding this comment

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

LGTM pre-approved module the one cursor comment

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Silent directory rename changes package name without notification
    • The CLI now preserves the user-selected app name for package.json and logs when it switches to a suffixed directory because the requested one already exists.
  • ✅ Fixed: Unbounded loop in findAvailableDir lacks safety limit
    • findAvailableDir now enforces a 1000-attempt cap and throws a clear error if no available directory is found within that limit.

Create PR

Or push these changes by commenting:

@cursor push b903f05838
Preview (b903f05838)
diff --git a/packages/create-tldraw/src/main.ts b/packages/create-tldraw/src/main.ts
--- a/packages/create-tldraw/src/main.ts
+++ b/packages/create-tldraw/src/main.ts
@@ -43,11 +43,20 @@
 	const template = await templatePicker(args.template, args['no-telemetry'])
 	const name = await namePicker(maybeTargetDir)
 
-	const targetDir = findAvailableDir(maybeTargetDir ?? resolve(process.cwd(), name))
+	const requestedTargetDir = maybeTargetDir ?? resolve(process.cwd(), name)
+	const targetDir = findAvailableDir(requestedTargetDir)
+	if (targetDir !== requestedTargetDir) {
+		nicelog(
+			`${relative(process.cwd(), requestedTargetDir)} already exists, using ${relative(
+				process.cwd(),
+				targetDir
+			)} instead.`
+		)
+	}
 	mkdirSync(targetDir, { recursive: true })
 
 	await downloadTemplate(template, targetDir)
-	await renameTemplate(pathToName(targetDir), targetDir)
+	await renameTemplate(name, targetDir)
 
 	const manager = getPackageManager()
 	const doneMessage = ['Done! Now run:', '']
@@ -142,10 +151,15 @@
 function findAvailableDir(targetDir: string): string {
 	if (isDirEmpty(targetDir)) return targetDir
 
-	for (let i = 1; ; i++) {
+	const maxAttempts = 1000
+	for (let i = 1; i <= maxAttempts; i++) {
 		const candidate = `${targetDir}-${i}`
 		if (isDirEmpty(candidate)) return candidate
 	}
+
+	throw new Error(
+		`Could not find an available directory for ${targetDir} after ${maxAttempts} attempts.`
+	)
 }
 
 async function downloadTemplate(template: Template, targetDir: string) {

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

if (isDirEmpty(targetDir)) {
mkdirSync(targetDir, { recursive: true })
return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unbounded loop in findAvailableDir lacks safety limit

Low Severity

The for (let i = 1; ; i++) loop in findAvailableDir has no upper bound. While it will practically always terminate quickly, there's no safety valve — if many similarly-named directories exist (e.g., from scripted usage), the loop spins indefinitely with no timeout or maximum attempts. Adding a reasonable cap (and a clear error message if exceeded) would make this more robust.

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

lol

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

await ensureDirectoryEmpty(targetDir, args.overwrite)
const requestedDir = maybeTargetDir ?? resolve(process.cwd(), name)
const targetDir = findAvailableDir(requestedDir)
mkdirSync(targetDir, { recursive: true })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Silent directory rename without notifying the user

Medium Severity

When the requested directory already exists, findAvailableDir silently changes the target to a suffixed name (e.g., my-app-1) without ever telling the user. The only hint is the cd path in the outro message, which is easy to miss. A user running npm create tldraw my-app would naturally try cd my-app afterward and not find their project. There needs to be an explicit message when the directory is changed from what the user requested.

Additional Locations (1)
Fix in Cursor Fix in Web

@AniKrisn AniKrisn added this pull request to the merge queue Mar 10, 2026
Merged via the queue into main with commit 581b927 Mar 10, 2026
24 checks passed
@AniKrisn AniKrisn deleted the ani/cli-fix branch March 10, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

create-tldraw CLI installs in current directory instead of creating named subdirectory

2 participants