diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_application/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_application/snapshots.toml index 397834670d..27c73be16d 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_application/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_application/snapshots.toml @@ -4,3 +4,25 @@ vp = "global" steps = [ { argv = ["vp", "create", "vite:application", "--no-interactive"], comment = "verify next command suggests vp run", continue-on-failure = true }, ] + +[[case]] +name = "create_git_command_application" +vp = "global" +steps = [ + { argv = ["vp", "create", "vite:application", "--no-interactive", "--git"], comment = "standalone create: Git command must not change the current directory", continue-on-failure = true }, + { argv = ["vpt", "stat-file", "vite-plus-application/.git", "--assert", "dir"], comment = "Git repository initialized", continue-on-failure = true }, +] + +[[case]] +name = "create_git_command_application_with_special_directories" +vp = "global" +steps = [ + { argv = ["vp", "create", "vite:application", "--no-interactive", "--git", "--directory", "examples with spaces/my-app"], comment = "quote a nested target directory that contains spaces", continue-on-failure = true }, + { argv = ["vpt", "stat-file", "examples with spaces/my-app/.git", "--assert", "dir"], comment = "Git repository initialized in the target directory with spaces", continue-on-failure = true }, + { argv = ["vp", "create", "vite:application", "--no-interactive", "--git", "--directory", "examples;tools/my-app"], comment = "quote a nested target directory that contains a shell metacharacter", continue-on-failure = true }, + { argv = ["vpt", "stat-file", "examples;tools/my-app/.git", "--assert", "dir"], comment = "Git repository initialized in the target directory with a shell metacharacter", continue-on-failure = true }, + { argv = ["vp", "create", "vite:application", "--no-interactive", "--git", "--directory", "示例/my-app"], comment = "quote a nested target directory that contains Chinese characters", continue-on-failure = true }, + { argv = ["vpt", "stat-file", "示例/my-app/.git", "--assert", "dir"], comment = "Git repository initialized in the target directory with Chinese characters", continue-on-failure = true }, + { argv = ["vp", "create", "vite:application", "--no-interactive", "--git", "--directory", "サンプル/my-app"], comment = "quote a nested target directory that contains Japanese characters", continue-on-failure = true }, + { argv = ["vpt", "stat-file", "サンプル/my-app/.git", "--assert", "dir"], comment = "Git repository initialized in the target directory with Japanese characters", continue-on-failure = true }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_application/snapshots/create_git_command_application.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_application/snapshots/create_git_command_application.md new file mode 100644 index 0000000000..2e558fe0d4 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_application/snapshots/create_git_command_application.md @@ -0,0 +1,22 @@ +# create_git_command_application + +## `vp create vite:application --no-interactive --git` + +standalone create: Git command must not change the current directory + +``` + +Using default package name: vite-plus-application +◇ Scaffolded vite-plus-application with Vite application +• Node pnpm +→ Git (optional): git -C vite-plus-application add -A && git -C vite-plus-application commit -m "chore: initial commit" +→ Next: cd vite-plus-application && vp run +``` + +## `vpt stat-file vite-plus-application/.git --assert dir` + +Git repository initialized + +``` +vite-plus-application/.git: dir +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_application/snapshots/create_git_command_application_with_special_directories.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_application/snapshots/create_git_command_application_with_special_directories.md new file mode 100644 index 0000000000..85fa9eca1e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_application/snapshots/create_git_command_application_with_special_directories.md @@ -0,0 +1,77 @@ +# create_git_command_application_with_special_directories + +## `vp create vite:application --no-interactive --git --directory 'examples with spaces/my-app'` + +quote a nested target directory that contains spaces + +``` +◇ Scaffolded examples with spaces/my-app with Vite application +• Node pnpm +→ Git (optional): git -C "examples with spaces/my-app" add -A && git -C "examples with spaces/my-app" commit -m "chore: initial commit" +→ Next: cd "examples with spaces/my-app" && vp run +``` + +## `vpt stat-file 'examples with spaces/my-app/.git' --assert dir` + +Git repository initialized in the target directory with spaces + +``` +examples with spaces/my-app/.git: dir +``` + +## `vp create vite:application --no-interactive --git --directory examples;tools/my-app` + +quote a nested target directory that contains a shell metacharacter + +``` +◇ Scaffolded examples;tools/my-app with Vite application +• Node pnpm +→ Git (optional): git -C "examples;tools/my-app" add -A && git -C "examples;tools/my-app" commit -m "chore: initial commit" +→ Next: cd "examples;tools/my-app" && vp run +``` + +## `vpt stat-file examples;tools/my-app/.git --assert dir` + +Git repository initialized in the target directory with a shell metacharacter + +``` +examples;tools/my-app/.git: dir +``` + +## `vp create vite:application --no-interactive --git --directory 示例/my-app` + +quote a nested target directory that contains Chinese characters + +``` +◇ Scaffolded 示例/my-app with Vite application +• Node pnpm +→ Git (optional): git -C "示例/my-app" add -A && git -C "示例/my-app" commit -m "chore: initial commit" +→ Next: cd "示例/my-app" && vp run +``` + +## `vpt stat-file 示例/my-app/.git --assert dir` + +Git repository initialized in the target directory with Chinese characters + +``` +示例/my-app/.git: dir +``` + +## `vp create vite:application --no-interactive --git --directory サンプル/my-app` + +quote a nested target directory that contains Japanese characters + +``` +◇ Scaffolded サンプル/my-app with Vite application +• Node pnpm +→ Git (optional): git -C "サンプル/my-app" add -A && git -C "サンプル/my-app" commit -m "chore: initial commit" +→ Next: cd "サンプル/my-app" && vp run +``` + +## `vpt stat-file サンプル/my-app/.git --assert dir` + +Git repository initialized in the target directory with Japanese characters + +``` +サンプル/my-app/.git: dir +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/snapshots/create_org_bundled_monorepo.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/snapshots/create_org_bundled_monorepo.md index a5f8cc7d31..5c05d098d5 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/snapshots/create_org_bundled_monorepo.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/snapshots/create_org_bundled_monorepo.md @@ -7,6 +7,7 @@ bundled monorepo: extract tarball, scaffold, inject create.defaultTemplate ``` ◇ Scaffolded my-mono • Node pnpm +→ Git (optional): git -C my-mono add -A && git -C my-mono commit -m "chore: initial commit" → Next: cd my-mono && vp run ``` diff --git a/packages/cli/src/create/__tests__/utils.spec.ts b/packages/cli/src/create/__tests__/utils.spec.ts index 35b2378b47..d935aa240a 100644 --- a/packages/cli/src/create/__tests__/utils.spec.ts +++ b/packages/cli/src/create/__tests__/utils.spec.ts @@ -89,6 +89,17 @@ describe('formatTargetDir', () => { expect(formatTargetDir('my-package@').error).matchSnapshot(); expect(formatTargetDir('my-package@1.0.0').error).matchSnapshot(); }); + + it('keeps valid package names under parent directories that need shell quoting', () => { + expect(formatTargetDir('examples with spaces/my-app')).toEqual({ + directory: 'examples with spaces/my-app', + packageName: 'my-app', + }); + expect(formatTargetDir('examples;tools/my-app')).toEqual({ + directory: 'examples;tools/my-app', + packageName: 'my-app', + }); + }); }); describe('deriveDefaultPackageName', () => { diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index 2c36225a71..01bad1d372 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -242,7 +242,12 @@ function getNextCommand(projectDir: string, command: string) { if (!projectDir || projectDir === '.') { return command; } - return `cd ${projectDir} && ${command}`; + return `cd ${formatProjectDirArgument(projectDir)} && ${command}`; +} + +function formatProjectDirArgument(projectDir: string) { + const argument = projectDir.startsWith('-') ? `./${projectDir}` : projectDir; + return /^[A-Za-z0-9_@./\\-]+$/.test(argument) ? argument : JSON.stringify(argument); } function getCopilotSetupRoot(projectRoot: string, isExistingMonorepo: boolean) { @@ -254,6 +259,7 @@ function getCopilotSetupRoot(projectRoot: string, isExistingMonorepo: boolean) { function showCreateSummary(options: { description?: string; + gitInitialized: boolean; installSummary?: CommandRunSummary; nextCommand: string; packageManager: string; @@ -262,6 +268,7 @@ function showCreateSummary(options: { }) { const { description, + gitInitialized, installSummary, nextCommand, packageManager, @@ -284,6 +291,12 @@ function showCreateSummary(options: { )}`, ); } + if (gitInitialized) { + const git = + !projectDir || projectDir === '.' ? 'git' : `git -C ${formatProjectDirArgument(projectDir)}`; + const gitCommand = `${git} add -A && ${git} commit -m "chore: initial commit"`; + log(`${styleText('blue', '→')} Git (optional): ${accent(gitCommand)}`); + } log(`${styleText('blue', '→')} Next: ${accent(nextCommand)}`); } @@ -346,6 +359,7 @@ async function main() { let selectedEditors: Awaited>; let selectedParentDir: string | undefined; let remoteTargetDir: string | undefined; + let gitInitialized = false; let shouldSetupHooks = false; let bundled: Extract | undefined; let skipShorthandExpansion = false; @@ -883,7 +897,8 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h // matching the standalone path below. if (shouldSetupGit) { updateCreateProgress('Initializing git repository'); - if (await initGitRepository(fullPath)) { + gitInitialized = await initGitRepository(fullPath); + if (gitInitialized) { ensureDefaultGitignoreEntries(fullPath); } } @@ -953,6 +968,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h clearCreateProgress(); showCreateSummary({ description: describeScaffold(selectedTemplateName, selectedTemplateArgs), + gitInitialized, installSummary, nextCommand: getNextCommand(projectDir, 'vp run'), packageManager: workspaceInfo.packageManager, @@ -1256,7 +1272,8 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h // for a subdirectory project and its staged workflow is left half-set-up. if (shouldSetupGit) { updateCreateProgress('Initializing git repository'); - if (await initGitRepository(fullPath)) { + gitInitialized = await initGitRepository(fullPath); + if (gitInitialized) { ensureDefaultGitignoreEntries(fullPath); } } @@ -1293,6 +1310,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h clearCreateProgress(); showCreateSummary({ description: describeScaffold(selectedTemplateName, selectedTemplateArgs), + gitInitialized, installSummary, nextCommand: getNextCommand(projectDir, 'vp run'), packageManager: workspaceInfo.packageManager,