From 4104779f75d24dbe5069e0cbd76adfee6790d879 Mon Sep 17 00:00:00 2001 From: cpojer Date: Thu, 15 Jan 2026 13:33:58 +0900 Subject: [PATCH] feat: Add an `--agent` flag and interactive prompt. --- README.md | 3 +- packages/global/AGENTS.md | 3 + packages/global/package.json | 1 + .../snap.txt | 2 + .../snap-tests/migration-check/snap.txt | 25 +++ .../snap-tests/migration-check/steps.json | 3 + .../snap.txt | 4 + .../snap-tests/migration-from-tsdown/snap.txt | 4 + .../migration-from-vitest-config/snap.txt | 2 + .../migration-from-vitest-files/snap.txt | 2 + .../migration-lintstagedrc-json/snap.txt | 3 + .../snap.txt | 2 + .../migration-merge-vite-config-js/snap.txt | 2 + .../migration-merge-vite-config-ts/snap.txt | 2 + .../snap.txt | 2 + .../migration-monorepo-pnpm/snap.txt | 2 + .../snap.txt | 2 + .../migration-monorepo-yarn4/snap.txt | 2 + .../migration-rewrite-declare-module/snap.txt | 2 + .../migration-skip-vite-dependency/snap.txt | 2 + .../snap.txt | 2 + .../snap-tests/migration-subpath/snap.txt | 2 + packages/global/snap-tests/new-check/snap.txt | 1 + .../snap-tests/new-vite-monorepo/snap.txt | 2 + packages/global/src/migration/bin.ts | 19 ++ packages/global/src/new/bin.ts | 205 +++++++++++------- packages/global/src/new/discovery.ts | 5 +- packages/global/src/utils/agent.ts | 168 ++++++++++++++ packages/global/src/utils/index.ts | 1 + 29 files changed, 398 insertions(+), 77 deletions(-) create mode 100644 packages/global/AGENTS.md create mode 100644 packages/global/snap-tests/migration-check/snap.txt create mode 100644 packages/global/snap-tests/migration-check/steps.json create mode 100644 packages/global/src/utils/agent.ts diff --git a/README.md b/README.md index a4d389eab7..e4c7425f07 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ # Vite+ -The Unified Toolchain for the Web +**The Unified Toolchain for the Web** +_dev, build, test, lint, format, monorepo caching & more in a single dependency, built for scale, speed, and sanity_ diff --git a/packages/global/AGENTS.md b/packages/global/AGENTS.md new file mode 100644 index 0000000000..f7c6052dae --- /dev/null +++ b/packages/global/AGENTS.md @@ -0,0 +1,3 @@ +# Using Vite+, the Unified Toolchain for the Web + +*TODO* diff --git a/packages/global/package.json b/packages/global/package.json index 5193da198b..8cc0ae5ca4 100644 --- a/packages/global/package.json +++ b/packages/global/package.json @@ -12,6 +12,7 @@ "files": [ "bin", "dist", + "AGENTS.md", "templates", "rules" ], diff --git a/packages/global/snap-tests/migration-auto-create-vite-config/snap.txt b/packages/global/snap-tests/migration-auto-create-vite-config/snap.txt index 629bb75a76..15ec98651d 100644 --- a/packages/global/snap-tests/migration-auto-create-vite-config/snap.txt +++ b/packages/global/snap-tests/migration-auto-create-vite-config/snap.txt @@ -13,6 +13,8 @@ │ ◆ ✅ Merged .oxfmtrc.json into vite.config.ts │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-check/snap.txt b/packages/global/snap-tests/migration-check/snap.txt new file mode 100644 index 0000000000..1bf826d1de --- /dev/null +++ b/packages/global/snap-tests/migration-check/snap.txt @@ -0,0 +1,25 @@ +> vite migration --help # show help +Usage: vite migration [PATH] [OPTIONS] + +Migrate standalone vite, vitest, oxlint, and oxfmt to unified vite-plus. + +Arguments: + PATH Target directory to migrate (default: current directory) + +Options: + --agent NAME Write agent instructions file into the project (e.g. chatgpt, claude, opencode). + --no-interactive Run in non-interactive mode (skip prompts and use defaults) + -h, --help Show this help message + +Examples: + # Migrate current package + vite migration + + # Migrate specific directory + vite migration my-app + + # Non-interactive mode + vite migration --no-interactive + +Aliases: migrate + diff --git a/packages/global/snap-tests/migration-check/steps.json b/packages/global/snap-tests/migration-check/steps.json new file mode 100644 index 0000000000..b01817d4c3 --- /dev/null +++ b/packages/global/snap-tests/migration-check/steps.json @@ -0,0 +1,3 @@ +{ + "commands": ["vite migration --help # show help"] +} diff --git a/packages/global/snap-tests/migration-from-tsdown-json-config/snap.txt b/packages/global/snap-tests/migration-from-tsdown-json-config/snap.txt index ea5a6ecac3..ca31ce56af 100644 --- a/packages/global/snap-tests/migration-from-tsdown-json-config/snap.txt +++ b/packages/global/snap-tests/migration-from-tsdown-json-config/snap.txt @@ -9,6 +9,8 @@ │ ◆ ✅ Merged tsdown.config.json into vite.config.ts │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! @@ -61,6 +63,8 @@ export default defineConfig({ │ ● pnpm@ installed │ +● Skipped writing AGENTS.md (already exists) +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-from-tsdown/snap.txt b/packages/global/snap-tests/migration-from-tsdown/snap.txt index aa389c1398..9190725eac 100644 --- a/packages/global/snap-tests/migration-from-tsdown/snap.txt +++ b/packages/global/snap-tests/migration-from-tsdown/snap.txt @@ -17,6 +17,8 @@ │ ● tsdown.config.ts │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! @@ -70,6 +72,8 @@ export default defineConfig({ │ ● 📦 Please manually merge tsdown.config.ts into vite.config.ts, see https://viteplus.dev/migration/#tsdown │ +● Skipped writing AGENTS.md (already exists) +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-from-vitest-config/snap.txt b/packages/global/snap-tests/migration-from-vitest-config/snap.txt index edf5e9c9f0..73546c29f6 100644 --- a/packages/global/snap-tests/migration-from-vitest-config/snap.txt +++ b/packages/global/snap-tests/migration-from-vitest-config/snap.txt @@ -11,6 +11,8 @@ │ ● vitest.config.ts │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-from-vitest-files/snap.txt b/packages/global/snap-tests/migration-from-vitest-files/snap.txt index 4d0bed19cd..e80f0eb3d6 100644 --- a/packages/global/snap-tests/migration-from-vitest-files/snap.txt +++ b/packages/global/snap-tests/migration-from-vitest-files/snap.txt @@ -11,6 +11,8 @@ │ ● test/hello.ts │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-lintstagedrc-json/snap.txt b/packages/global/snap-tests/migration-lintstagedrc-json/snap.txt index 20b5199db4..6fb7285b47 100644 --- a/packages/global/snap-tests/migration-lintstagedrc-json/snap.txt +++ b/packages/global/snap-tests/migration-lintstagedrc-json/snap.txt @@ -7,6 +7,7 @@ Arguments: PATH Target directory to migrate (default: current directory) Options: + --agent NAME Write agent instructions file into the project (e.g. chatgpt, claude, opencode). --no-interactive Run in non-interactive mode (skip prompts and use defaults) -h, --help Show this help message @@ -34,6 +35,8 @@ Aliases: migrate │ ◆ ✅ Rewrote lint-staged config in .lintstagedrc.json │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-lintstagedrc-not-support/snap.txt b/packages/global/snap-tests/migration-lintstagedrc-not-support/snap.txt index 2c51a64109..1a89a43ef1 100644 --- a/packages/global/snap-tests/migration-lintstagedrc-not-support/snap.txt +++ b/packages/global/snap-tests/migration-lintstagedrc-not-support/snap.txt @@ -15,6 +15,8 @@ │ ▲ Please migrate the lint-staged config manually, see https://viteplus.dev/migration/#lint-staged for more details │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-merge-vite-config-js/snap.txt b/packages/global/snap-tests/migration-merge-vite-config-js/snap.txt index 1151efc1e4..5f37c55f02 100644 --- a/packages/global/snap-tests/migration-merge-vite-config-js/snap.txt +++ b/packages/global/snap-tests/migration-merge-vite-config-js/snap.txt @@ -9,6 +9,8 @@ │ ◆ ✅ Merged .oxlintrc.json into vite.config.js │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-merge-vite-config-ts/snap.txt b/packages/global/snap-tests/migration-merge-vite-config-ts/snap.txt index aaa0cce3ba..f2cab6c7d9 100644 --- a/packages/global/snap-tests/migration-merge-vite-config-ts/snap.txt +++ b/packages/global/snap-tests/migration-merge-vite-config-ts/snap.txt @@ -15,6 +15,8 @@ │ ● vite.config.ts │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/snap.txt b/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/snap.txt index 94c62f481d..218b6d4288 100644 --- a/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/snap.txt +++ b/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/snap.txt @@ -9,6 +9,8 @@ │ ● vite.config.ts │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-monorepo-pnpm/snap.txt b/packages/global/snap-tests/migration-monorepo-pnpm/snap.txt index b5183463a4..91fffb8948 100644 --- a/packages/global/snap-tests/migration-monorepo-pnpm/snap.txt +++ b/packages/global/snap-tests/migration-monorepo-pnpm/snap.txt @@ -17,6 +17,8 @@ │ ● vite.config.ts │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-monorepo-skip-vite-peer-dependency/snap.txt b/packages/global/snap-tests/migration-monorepo-skip-vite-peer-dependency/snap.txt index ed5da5db9d..799ea6f8f6 100644 --- a/packages/global/snap-tests/migration-monorepo-skip-vite-peer-dependency/snap.txt +++ b/packages/global/snap-tests/migration-monorepo-skip-vite-peer-dependency/snap.txt @@ -9,6 +9,8 @@ │ ● packages/vite-plugin/src/index.ts │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-monorepo-yarn4/snap.txt b/packages/global/snap-tests/migration-monorepo-yarn4/snap.txt index d34ccead5b..0c25cb719f 100644 --- a/packages/global/snap-tests/migration-monorepo-yarn4/snap.txt +++ b/packages/global/snap-tests/migration-monorepo-yarn4/snap.txt @@ -11,6 +11,8 @@ │ ● vite.config.ts │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-rewrite-declare-module/snap.txt b/packages/global/snap-tests/migration-rewrite-declare-module/snap.txt index 0812a4702d..77ab9113da 100644 --- a/packages/global/snap-tests/migration-rewrite-declare-module/snap.txt +++ b/packages/global/snap-tests/migration-rewrite-declare-module/snap.txt @@ -11,6 +11,8 @@ │ ● src/index.ts │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-skip-vite-dependency/snap.txt b/packages/global/snap-tests/migration-skip-vite-dependency/snap.txt index 97db0ced25..1a61f5543c 100644 --- a/packages/global/snap-tests/migration-skip-vite-dependency/snap.txt +++ b/packages/global/snap-tests/migration-skip-vite-dependency/snap.txt @@ -11,6 +11,8 @@ │ ● src/index.ts │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-skip-vite-peer-dependency/snap.txt b/packages/global/snap-tests/migration-skip-vite-peer-dependency/snap.txt index f8f25efcac..8de86ffb0f 100644 --- a/packages/global/snap-tests/migration-skip-vite-peer-dependency/snap.txt +++ b/packages/global/snap-tests/migration-skip-vite-peer-dependency/snap.txt @@ -11,6 +11,8 @@ │ ● src/index.ts │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-subpath/snap.txt b/packages/global/snap-tests/migration-subpath/snap.txt index 66e3ba6764..da477b4514 100644 --- a/packages/global/snap-tests/migration-subpath/snap.txt +++ b/packages/global/snap-tests/migration-subpath/snap.txt @@ -7,6 +7,8 @@ │ ● pnpm@ installed │ +◆ Wrote agent instructions to AGENTS.md +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/new-check/snap.txt b/packages/global/snap-tests/new-check/snap.txt index e1bfab8a20..a2d3d8d8ae 100644 --- a/packages/global/snap-tests/new-check/snap.txt +++ b/packages/global/snap-tests/new-check/snap.txt @@ -13,6 +13,7 @@ Arguments: Options (before --): --directory DIR Target directory for the generated project. Only works for built-in templates; auto-detected for remote templates. + --agent NAME Write agent instructions file into the generated project (e.g. chatgpt, claude, opencode). --no-interactive Run in non-interactive mode (skip prompts and use defaults) --list List all available templates -h, --help Show this help message diff --git a/packages/global/snap-tests/new-vite-monorepo/snap.txt b/packages/global/snap-tests/new-vite-monorepo/snap.txt index 736ef3baf8..3fbc99858b 100644 --- a/packages/global/snap-tests/new-vite-monorepo/snap.txt +++ b/packages/global/snap-tests/new-vite-monorepo/snap.txt @@ -1,5 +1,6 @@ > vite new vite:monorepo --no-interactive # create monorepo with default values > ls vite-plus-monorepo | LC_ALL=C sort # check files created +AGENTS.md README.md apps package.json @@ -108,6 +109,7 @@ vite-plus-generator > vite new vite:monorepo --no-interactive --directory my-vite-plus-monorepo # create monorepo with custom directory > ls my-vite-plus-monorepo | LC_ALL=C sort # check files created +AGENTS.md README.md apps package.json diff --git a/packages/global/src/migration/bin.ts b/packages/global/src/migration/bin.ts index c6b9f50127..75fab2d271 100644 --- a/packages/global/src/migration/bin.ts +++ b/packages/global/src/migration/bin.ts @@ -14,6 +14,8 @@ import { runViteInstall, upgradeYarn, cancelAndExit, + selectAgentTargetPath, + writeAgentInstructions, } from '../utils/index.js'; import { checkVitestVersion, @@ -34,6 +36,7 @@ Arguments: PATH Target directory to migrate (default: current directory) Options: + --agent NAME Write agent instructions file into the project (e.g. chatgpt, claude, opencode). --no-interactive Run in non-interactive mode (skip prompts and use defaults) -h, --help Show this help message @@ -53,6 +56,7 @@ Aliases: ${gray('migrate')} export interface MigrationOptions { interactive: boolean; help?: boolean; + agent?: string; } function parseArgs() { @@ -61,9 +65,11 @@ function parseArgs() { const parsed = mri<{ help?: boolean; interactive?: boolean; + agent?: string; }>(args, { alias: { h: 'help' }, boolean: ['help', 'interactive'], + string: ['agent'], default: { interactive: defaultInteractive() }, }); @@ -79,6 +85,7 @@ function parseArgs() { options: { interactive: parsed.interactive, help: parsed.help, + agent: parsed.agent, } as MigrationOptions, }; } @@ -154,6 +161,18 @@ async function main() { rewriteStandaloneProject(workspaceInfo.rootDir, workspaceInfo); } + const selectedAgentTargetPath = await selectAgentTargetPath({ + interactive: options.interactive, + agent: options.agent, + onCancel: () => cancelAndExit(), + }); + + await writeAgentInstructions({ + projectRoot: workspaceInfo.rootDir, + targetPath: selectedAgentTargetPath, + interactive: options.interactive, + }); + // reinstall after migration await runViteInstall(workspaceInfo.rootDir, options.interactive); prompts.outro(green('✨ Migration completed!')); diff --git a/packages/global/src/new/bin.ts b/packages/global/src/new/bin.ts index 5529fc7b69..288ae3a999 100644 --- a/packages/global/src/new/bin.ts +++ b/packages/global/src/new/bin.ts @@ -20,9 +20,12 @@ import { updateWorkspaceConfig, runViteInstall, templatesDir, + selectAgentTargetPath, + detectExistingAgentTargetPath, + writeAgentInstructions, } from '../utils/index.js'; import type { ExecutionResult } from './command.js'; -import { discoverTemplate } from './discovery.js'; +import { discoverTemplate, inferParentDir } from './discovery.js'; import { cancelAndExit, checkProjectDirExists, promptPackageNameAndTargetDir } from './prompts.js'; import { executeBuiltinTemplate, @@ -50,6 +53,7 @@ Arguments: Options (before --): --directory DIR Target directory for the generated project. Only works for built-in templates; auto-detected for remote templates. + --agent NAME Write agent instructions file into the generated project (e.g. chatgpt, claude, opencode). --no-interactive Run in non-interactive mode (skip prompts and use defaults) --list List all available templates -h, --help Show this help message @@ -96,6 +100,7 @@ export interface Options { interactive: boolean; list: boolean; help: boolean; + agent?: string; } // Parse CLI arguments: split on '--' separator @@ -114,10 +119,11 @@ function parseArgs() { interactive?: boolean; list?: boolean; help?: boolean; + agent?: string; }>(viteArgs, { alias: { h: 'help' }, boolean: ['help', 'list', 'all', 'interactive'], - string: ['directory'], + string: ['directory', 'agent'], default: { interactive: defaultInteractive() }, }); @@ -130,6 +136,7 @@ function parseArgs() { interactive: parsed.interactive, list: parsed.list || false, help: parsed.help || false, + agent: parsed.agent, } as Options, templateArgs, }; @@ -195,6 +202,8 @@ Use \`vite new --list\` to list all available templates, or run \`vite new --hel // Interactive mode: prompt for template if not provided let selectedTemplateName = templateName as string; let selectedTemplateArgs = [...templateArgs]; + let selectedAgentTargetPath: string | undefined; + let selectedParentDir: string | undefined; if (!selectedTemplateName) { const templates: { label: string; value: string; hint: string }[] = []; @@ -279,6 +288,99 @@ Use \`vite new --list\` to list all available templates, or run \`vite new --hel } } + const isBuiltinTemplate = selectedTemplateName.startsWith('vite:'); + if (targetDir && !isBuiltinTemplate) { + cancelAndExit('The --directory option is only available for builtin templates', 1); + } + if (selectedTemplateName === BuiltinTemplate.monorepo && isMonorepo) { + prompts.log.info( + 'You are already in a monorepo workspace.\nUse a different template or run this command outside the monorepo', + ); + cancelAndExit('Cannot create a monorepo inside an existing monorepo', 1); + } + + if (isMonorepo && options.interactive && !targetDir) { + let parentDir: string | undefined; + if (workspaceInfoOptional.parentDirs.length > 0) { + const defaultParentDir = + inferParentDir(selectedTemplateName, workspaceInfoOptional) ?? + workspaceInfoOptional.parentDirs[0]; + const selected = await prompts.select({ + message: 'Where should the new package be added to the monorepo:', + options: workspaceInfoOptional.parentDirs + .map((dir) => ({ + label: `${dir}/`, + value: dir, + hint: ``, + })) + .concat([ + { + label: 'other', + value: 'other', + hint: 'Enter a custom target directory', + }, + ]), + initialValue: defaultParentDir, + }); + + if (prompts.isCancel(selected)) { + cancelAndExit(); + } + + if (selected !== 'other') { + parentDir = selected; + } + } + + if (!parentDir) { + const customTargetDir = await prompts.text({ + message: 'Where should the new package be added to the monorepo:', + placeholder: 'e.g., packages/', + validate: (value) => { + return formatTargetDir(value).error; + }, + }); + + if (prompts.isCancel(customTargetDir)) { + cancelAndExit(); + } + + parentDir = customTargetDir; + } + + selectedParentDir = parentDir; + } + if (isMonorepo && !options.interactive && !targetDir) { + const inferredParentDir = + inferParentDir(selectedTemplateName, workspaceInfoOptional) ?? + workspaceInfoOptional.parentDirs[0]; + selectedParentDir = inferredParentDir; + } + + if (isBuiltinTemplate && !targetDir) { + if (selectedTemplateName === BuiltinTemplate.monorepo) { + const selected = await promptPackageNameAndTargetDir( + 'vite-plus-monorepo', + options.interactive, + ); + packageName = selected.packageName; + targetDir = selected.targetDir; + } else { + let defaultPackageName = `vite-plus-${selectedTemplateName.split(':')[1]}`; + if (workspaceInfoOptional.monorepoScope) { + defaultPackageName = `${workspaceInfoOptional.monorepoScope}/${defaultPackageName}`; + } + const selected = await promptPackageNameAndTargetDir( + defaultPackageName, + options.interactive, + ); + packageName = selected.packageName; + targetDir = selectedParentDir + ? path.join(selectedParentDir, selected.targetDir) + : selected.targetDir; + } + } + // Prompt for package manager or use default const packageManager = workspaceInfoOptional.packageManager ?? (await selectPackageManager(options.interactive)); @@ -294,6 +396,18 @@ Use \`vite new --list\` to list all available templates, or run \`vite new --hel downloadPackageManager: downloadResult, }; + const existingAgentTargetPath = + options.agent || !options.interactive + ? undefined + : detectExistingAgentTargetPath(workspaceInfoOptional.rootDir); + selectedAgentTargetPath = + existingAgentTargetPath ?? + (await selectAgentTargetPath({ + interactive: options.interactive, + agent: options.agent, + onCancel: () => cancelAndExit(), + })); + // Discover template const templateInfo = discoverTemplate( selectedTemplateName, @@ -302,11 +416,12 @@ Use \`vite new --list\` to list all available templates, or run \`vite new --hel options.interactive, ); + if (selectedParentDir) { + templateInfo.parentDir = selectedParentDir; + } + // only for builtin templates if (targetDir) { - if (templateInfo.type !== TemplateType.builtin) { - cancelAndExit('The --directory option is only available for builtin templates', 1); - } // reset auto detect parent directory templateInfo.parentDir = undefined; } @@ -315,23 +430,6 @@ Use \`vite new --list\` to list all available templates, or run \`vite new --hel // #region Handle monorepo template if (templateInfo.command === BuiltinTemplate.monorepo) { - // Validate: cannot create monorepo inside an existing monorepo - if (isMonorepo) { - prompts.log.info( - 'You are already in a monorepo workspace.\nUse a different template or run this command outside the monorepo', - ); - cancelAndExit('Cannot create a monorepo inside an existing monorepo', 1); - } - - if (!packageName) { - const selected = await promptPackageNameAndTargetDir( - 'vite-plus-monorepo', - options.interactive, - ); - packageName = selected.packageName; - targetDir = selected.targetDir; - } - prompts.log.info(`Target directory: ${cyan(targetDir)}`); await checkProjectDirExists(path.join(workspaceInfo.rootDir, targetDir), options.interactive); const result = await executeMonorepoTemplate( @@ -345,6 +443,11 @@ Use \`vite new --list\` to list all available templates, or run \`vite new --hel // rewrite monorepo to add vite-plus dependencies const fullPath = path.join(workspaceInfo.rootDir, result.projectDir!); + await writeAgentInstructions({ + projectRoot: fullPath, + targetPath: selectedAgentTargetPath, + interactive: options.interactive, + }); workspaceInfo.rootDir = fullPath; rewriteMonorepo(workspaceInfo); await runViteInstall(fullPath, options.interactive); @@ -356,59 +459,6 @@ Use \`vite new --list\` to list all available templates, or run \`vite new --hel // #region Handle single project template - if (isMonorepo && options.interactive) { - if (!targetDir) { - // no custom target directory provided, prompt for parent directory - let parentDir: string | undefined; - if (workspaceInfo.parentDirs.length > 0) { - const defaultParentDir = templateInfo.parentDir ?? workspaceInfo.parentDirs[0]; - const selected = await prompts.select({ - message: 'Where should the new package be added to the monorepo:', - options: workspaceInfo.parentDirs - .map((dir) => ({ - label: `${dir}/`, - value: dir, - hint: ``, - })) - .concat([ - { - label: 'other', - value: 'other', - hint: 'Enter a custom target directory', - }, - ]), - initialValue: defaultParentDir, - }); - - if (prompts.isCancel(selected)) { - cancelAndExit(); - } - - if (selected !== 'other') { - parentDir = selected; - } - } - - if (!parentDir) { - const customTargetDir = await prompts.text({ - message: 'Where should the new package be added to the monorepo:', - placeholder: 'e.g., packages/', - validate: (value) => { - return formatTargetDir(value).error; - }, - }); - - if (prompts.isCancel(customTargetDir)) { - cancelAndExit(); - } - - parentDir = customTargetDir; - } - - templateInfo.parentDir = parentDir; - } - } - let result: ExecutionResult; if (templateInfo.type === TemplateType.builtin) { // prompt for package name if not provided @@ -445,6 +495,11 @@ Use \`vite new --list\` to list all available templates, or run \`vite new --hel // Show detected project directory prompts.log.success(`Detected project directory: ${green(projectDir)}`); const fullPath = path.join(workspaceInfo.rootDir, projectDir); + await writeAgentInstructions({ + projectRoot: fullPath, + targetPath: selectedAgentTargetPath, + interactive: options.interactive, + }); // Monorepo integration if (isMonorepo) { diff --git a/packages/global/src/new/discovery.ts b/packages/global/src/new/discovery.ts index 892eaf56e9..33d50196b5 100644 --- a/packages/global/src/new/discovery.ts +++ b/packages/global/src/new/discovery.ts @@ -117,7 +117,10 @@ export function discoverTemplate( } // Infer the parent directory of the generated package based on the template name -function inferParentDir(templateName: string, workspaceInfo: WorkspaceInfo): string | undefined { +export function inferParentDir( + templateName: string, + workspaceInfo: WorkspaceInfo, +): string | undefined { if (workspaceInfo.parentDirs.length === 0) { return; } diff --git a/packages/global/src/utils/agent.ts b/packages/global/src/utils/agent.ts new file mode 100644 index 0000000000..b0cd32ccf8 --- /dev/null +++ b/packages/global/src/utils/agent.ts @@ -0,0 +1,168 @@ +import fs from 'node:fs'; +import fsPromises from 'node:fs/promises'; +import path from 'node:path'; + +import * as prompts from '@clack/prompts'; + +import { pkgRoot } from './path.js'; + +export const AGENTS = [ + { id: 'chatgpt-codex', label: 'ChatGPT (Codex)', targetPath: 'AGENTS.md' }, + { id: 'claude', label: 'Claude Code', targetPath: 'CLAUDE.md' }, + { + id: 'copilot', + label: 'GitHub Copilot', + targetPath: '.github/copilot-instructions.md', + }, + { id: 'cursor', label: 'Cursor', targetPath: '.cursor/rules/viteplus.mdc' }, + { + id: 'jetbrains', + label: 'JetBrains AI Assistant', + targetPath: '.aiassistant/rules/viteplus.md', + }, + { id: 'amp', label: 'Amp', targetPath: 'AGENTS.md' }, + { id: 'opencode', label: 'OpenCode', targetPath: 'AGENTS.md' }, + { id: 'other', label: 'Other', targetPath: 'AGENTS.md' }, +] as const; + +const AGENT_ALIASES: Record = { + chatgpt: 'chatgpt-codex', + codex: 'chatgpt-codex', +}; +export async function selectAgentTargetPath({ + interactive, + agent, + onCancel, +}: { + interactive: boolean; + agent?: string; + onCancel: () => void; +}) { + if (interactive && !agent) { + const selectedAgent = await prompts.select({ + message: 'Which agent are you using?', + options: [ + { + label: 'None', + value: null, + hint: 'Skip writing agent instructions', + }, + ...AGENTS.map((option) => ({ + label: option.label, + value: option.id, + hint: option.targetPath, + })), + ], + initialValue: 'chatgpt-codex', + }); + + if (prompts.isCancel(selectedAgent)) { + onCancel(); + return undefined; + } + + if (selectedAgent === null) { + return undefined; + } + return resolveAgentTargetPath(selectedAgent); + } + + return resolveAgentTargetPath(agent ?? 'other'); +} + +export function detectExistingAgentTargetPath(projectRoot: string) { + for (const option of AGENTS) { + const targetPath = path.join(projectRoot, option.targetPath); + if (fs.existsSync(targetPath)) { + return option.targetPath; + } + } + return undefined; +} + +export function resolveAgentTargetPath(agent?: string) { + if (!agent) { + return 'AGENTS.md'; + } + const normalized = normalizeAgentName(agent); + const alias = AGENT_ALIASES[normalized]; + const resolved = alias ? normalizeAgentName(alias) : normalized; + const match = AGENTS.find( + (option) => + normalizeAgentName(option.id) === resolved || + normalizeAgentName(option.label) === resolved, + ); + return match?.targetPath ?? 'AGENTS.md'; +} + +export async function writeAgentInstructions({ + projectRoot, + targetPath, + interactive, +}: { + projectRoot: string; + targetPath: string | undefined; + interactive: boolean; +}) { + if (!targetPath) { + return; + } + + const sourcePath = path.join(pkgRoot, 'AGENTS.md'); + if (!fs.existsSync(sourcePath)) { + prompts.log.warn('Agent instructions template not found; skipping.'); + return; + } + + const destinationPath = path.join(projectRoot, targetPath); + await fsPromises.mkdir(path.dirname(destinationPath), { recursive: true }); + if (fs.existsSync(destinationPath)) { + if (interactive) { + const action = await prompts.select({ + message: `Agent instructions already exist at ${targetPath}.`, + options: [ + { + label: 'Append', + value: 'append', + hint: 'Add template content to the end', + }, + { + label: 'Skip', + value: 'skip', + hint: 'Leave existing file unchanged', + }, + ], + initialValue: 'skip', + }); + if (prompts.isCancel(action) || action === 'skip') { + prompts.log.info(`Skipped writing ${targetPath}`); + return; + } + + const [existingContent, incomingContent] = await Promise.all([ + fsPromises.readFile(destinationPath, 'utf-8'), + fsPromises.readFile(sourcePath, 'utf-8'), + ]); + const separator = existingContent.endsWith('\n') ? '' : '\n'; + await fsPromises.appendFile( + destinationPath, + `${separator}\n${incomingContent}`, + ); + prompts.log.success(`Appended agent instructions to ${targetPath}`); + return; + } + + prompts.log.info(`Skipped writing ${targetPath} (already exists)`); + return; + } + + await fsPromises.copyFile(sourcePath, destinationPath); + prompts.log.success(`Wrote agent instructions to ${targetPath}`); +} + +function normalizeAgentName(value: string) { + return value + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, ''); +} diff --git a/packages/global/src/utils/index.ts b/packages/global/src/utils/index.ts index ffaa7080b5..773293a036 100644 --- a/packages/global/src/utils/index.ts +++ b/packages/global/src/utils/index.ts @@ -6,3 +6,4 @@ export * from './workspace.js'; export * from './yaml.js'; export * from './command.js'; export * from './constants.js'; +export * from './agent.js';