feat(cli): add chat feature with template and commands#354
Conversation
WalkthroughThis PR refactors the CLI to delegate command registration to separate modules, establishes shared scaffolding utilities, extracts the ChangesCLI Commands and Feature Scaffolding
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📦 Package Previewpnpm add https://pkg.pr.new/@opentiny/tiny-robot@8bd46c7 pnpm add https://pkg.pr.new/@opentiny/tiny-robot-kit@8bd46c7 pnpm add https://pkg.pr.new/@opentiny/tiny-robot-svgs@8bd46c7 commit: 8bd46c7 |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/cli/bin/commands/add.js`:
- Around line 168-183: The code currently uses
semver.coerce(currentVersion)?.version which strips prerelease and makes semver
comparisons wrong; change the logic in the block that computes and compares
currentVersion/targetVersion (the variables currentVersion, targetVersion and
the branch that sets pkg.dependencies[name] and returns
{type:'updated'|'skipped'}) to first try semver.valid(currentVersion) (so
prerelease parts are preserved), then fall back to
semver.coerce(currentVersion)?.version if valid is null, and if both are null
treat the comparison as a raw string comparison of currentVersion vs
targetVersion (so non-semver specs aren't silently skipped); update the
conditional that decides to modify pkg.dependencies[name] accordingly.
In `@packages/cli/bin/commands/create.js`:
- Around line 89-99: The validateCreateOptions function currently builds a path
with getTemplateDir(templateName) and checks exists(templateDir), which allows
arbitrary paths; before calling getTemplateDir or exists, add an explicit
allowlist check that templateName is one of availableTemplates (e.g., if
(!availableTemplates.includes(templateName)) invariant(false, `template
"${templateName}" is not one of the available templates:
${availableTemplates.join(', ')}`)); keep the rest of the existing validation
(validateProjectName, exists(templateDir)) after this check to prevent
scaffolding from outside the intended templates directory.
- Around line 135-142: The current rejection handler for the createProject call
in the .action callback prints raw errors; modify that catch block to detect
inquirer Ctrl+C by inspecting error.name === 'ExitPromptError' and handle it as
a user cancellation (log the same friendly cancellation message and exit with
code 0) instead of printing the library error, otherwise keep the existing
error.message logging and exit with code 1; update the catch attached to
createProject(...) in create.js to perform this conditional branching.
In `@packages/cli/templates/chat/src/TinyRobotChat.vue`:
- Line 16: TinyRobotChat.vue currently sets apiUrl to the full endpoint
"https://api.deepseek.com/chat/completions" but the code in
packages/kit/src/providers/openai.ts treats apiUrl as a baseURL and appends
"/chat/completions", causing a doubled path; update the apiUrl value in
TinyRobotChat.vue to the base "https://api.deepseek.com/v1" (match
packages/kit/src/client.ts and packages/cli/templates/basic/src/models.ts) so
that the provider's append produces the correct final endpoint.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 03e3cc02-1ac7-4631-ae35-e3e03a0877fc
📒 Files selected for processing (7)
packages/cli/bin/cli.jspackages/cli/bin/commands/add.jspackages/cli/bin/commands/create.jspackages/cli/bin/utils.jspackages/cli/package.jsonpackages/cli/templates/chat/.env.examplepackages/cli/templates/chat/src/TinyRobotChat.vue
| const current = semver.valid(semver.coerce(currentVersion)?.version) | ||
|
|
||
| if (current && current !== targetVersion) { | ||
| pkg.dependencies[name] = targetVersion | ||
|
|
||
| return { | ||
| type: 'updated', | ||
| from: currentVersion, | ||
| to: targetVersion, | ||
| } | ||
| } | ||
|
|
||
| // 3. 完全一致 => 跳过 | ||
| return { | ||
| type: 'skipped', | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
`For semver@7.8.1, what are the return values of:
- semver.coerce('0.4.2-alpha.6')?.version
- semver.valid('0.4.2-alpha.6')
- semver.valid(semver.coerce('0.4.2-alpha.6')?.version)
Does coerce preserve prerelease identifiers?`
💡 Result:
In semver@7.8.1, the semver.coerce function by default does not preserve prerelease identifiers [1][2]. The function is designed to extract only the major, minor, and patch components [3]. For the specified inputs: 1) semver.coerce('0.4.2-alpha.6')?.version Returns: '0.4.2' [3][4][5]. (Note: The .version property of the resulting object returns the string representation, which in default mode strips the prerelease information [3][5].) 2) semver.valid('0.4.2-alpha.6') Returns: '0.4.2-alpha.6' [6][7]. (The valid function checks if the provided string is a valid semver string; since '0.4.2-alpha.6' is a valid semver string, it is returned as-is [7].) 3) semver.valid(semver.coerce('0.4.2-alpha.6')?.version) Returns: '0.4.2' [5]. (This takes the result of the coerce operation—'0.4.2'—and validates it [5].) Does coerce preserve prerelease identifiers? No, it does not preserve them by default [3][4]. However, semver.coerce supports an options object, and if the { includePrerelease: true } flag is provided, it will preserve both prerelease and build identifiers [1][5].
Citations:
- 1: https://npmx.dev/package/semver/v/7.8.1
- 2: https://github.com/npm/node-semver
- 3:
coercedrops build and prerelease information npm/node-semver#592 - 4: Coerce does not preserve valid versions with prerelease identifiers semver/semver#866
- 5: npm/node-semver@a7ab13a
- 6: https://docs.npmjs.com/cli/v6/using-npm/semver/
- 7: https://courses.cs.washington.edu/courses/cse481v/21sp/projects/team1/node-v14.17.0-linux-x64/lib/node_modules/npm/docs/public/using-npm/semver/
Fix ensureDependency prerelease/non-semver comparison in packages/cli/bin/commands/add.js
semver.coerce(currentVersion)?.versionstrips prerelease by default (e.g.,0.4.2-alpha.6→0.4.2), so an exact prerelease match can be reported asupdated.- If
semver.coerce(currentVersion)can’t parse,currentis falsy and the code returnsskipped, potentially treating non-semver specs as satisfied.
Suggested patch
function ensureDependency(pkg, name, targetVersion) {
pkg.dependencies ??= {}
const currentVersion = pkg.dependencies[name]
// 1. 不存在 => 新增(使用 ordered insert)
if (!currentVersion) {
pkg.dependencies = insertDependencyOrdered(pkg.dependencies, name, targetVersion)
return {
type: 'added',
to: targetVersion,
}
}
- // 2. 已存在 => 只更新版本,不调整顺序
- const current = semver.valid(semver.coerce(currentVersion)?.version)
-
- if (current && current !== targetVersion) {
+ // 2. 已存在 => 完全一致直接跳过
+ if (currentVersion === targetVersion) {
+ return { type: 'skipped' }
+ }
+
+ // 3. 已存在但不一致 => 更新版本,不调整顺序
+ // semver.coerce 会丢失 prerelease(如 alpha 标签),不要用它判等
+ if (semver.valid(currentVersion) !== targetVersion) {
pkg.dependencies[name] = targetVersion
return {
type: 'updated',
from: currentVersion,
to: targetVersion,
}
}
- // 3. 完全一致 => 跳过
+ // 4. 兜底
return {
type: 'skipped',
}
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/cli/bin/commands/add.js` around lines 168 - 183, The code currently
uses semver.coerce(currentVersion)?.version which strips prerelease and makes
semver comparisons wrong; change the logic in the block that computes and
compares currentVersion/targetVersion (the variables currentVersion,
targetVersion and the branch that sets pkg.dependencies[name] and returns
{type:'updated'|'skipped'}) to first try semver.valid(currentVersion) (so
prerelease parts are preserved), then fall back to
semver.coerce(currentVersion)?.version if valid is null, and if both are null
treat the comparison as a raw string comparison of currentVersion vs
targetVersion (so non-semver specs aren't silently skipped); update the
conditional that decides to modify pkg.dependencies[name] accordingly.
| function validateCreateOptions(options) { | ||
| const { projectName, templateName, availableTemplates } = options | ||
|
|
||
| invariant(validateProjectName(projectName), 'project name can only contain lowercase letters, numbers, and dashes.') | ||
|
|
||
| const templateDir = getTemplateDir(templateName) | ||
|
|
||
| invariant( | ||
| exists(templateDir), | ||
| `template "${templateName}" does not exist. Available: ${availableTemplates.join(', ')}`, | ||
| ) |
There was a problem hiding this comment.
Reject template names outside the built-in allowlist.
getTemplateDir(templateName) plus exists(templateDir) will accept values like --template ../some-dir as long as that directory exists, so create can scaffold from outside packages/cli/templates. Validate templateName against availableTemplates before joining the path.
Suggested fix
function validateCreateOptions(options) {
const { projectName, templateName, availableTemplates } = options
invariant(validateProjectName(projectName), 'project name can only contain lowercase letters, numbers, and dashes.')
+ invariant(
+ availableTemplates.includes(templateName),
+ `template "${templateName}" does not exist. Available: ${availableTemplates.join(', ')}`,
+ )
const templateDir = getTemplateDir(templateName)
- invariant(
- exists(templateDir),
- `template "${templateName}" does not exist. Available: ${availableTemplates.join(', ')}`,
- )
+ invariant(exists(templateDir), `template directory missing: ${templateDir}`)
const targetDir = path.resolve(process.cwd(), projectName)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/cli/bin/commands/create.js` around lines 89 - 99, The
validateCreateOptions function currently builds a path with
getTemplateDir(templateName) and checks exists(templateDir), which allows
arbitrary paths; before calling getTemplateDir or exists, add an explicit
allowlist check that templateName is one of availableTemplates (e.g., if
(!availableTemplates.includes(templateName)) invariant(false, `template
"${templateName}" is not one of the available templates:
${availableTemplates.join(', ')}`)); keep the rest of the existing validation
(validateProjectName, exists(templateDir)) after this check to prevent
scaffolding from outside the intended templates directory.
| .action((projectName, options) => { | ||
| const skipPrompt = !process.stdout.isTTY | ||
|
|
||
| createProject(projectName ?? '', options.template ?? '', skipPrompt).catch((error) => { | ||
| console.error(`Error: ${error instanceof Error ? error.message : String(error)}`) | ||
|
|
||
| process.exit(1) | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In @inquirer/prompts 8.3.2, what error is thrown when a prompt is cancelled with Ctrl+C?
💡 Result:
In @inquirer/prompts version 8.3.2, when a user cancels a prompt using Ctrl+C, the library throws an error named ExitPromptError [1][2][3]. The error message typically reads: ExitPromptError: User force closed the prompt with 0 null [1][2][4] Because this error is thrown, the promise returned by the prompt function rejects [2][4][3]. To handle this gracefully and prevent your application from crashing, you should wrap the prompt call in a try/catch block and check for this specific error name [1][2][3]. Example of graceful handling: try { const answer = await input({ message: 'Enter something:' }); } catch (error) { if (error instanceof Error && error.name === 'ExitPromptError') { // Handle the cancellation (e.g., exit silently or perform cleanup) process.exit(0); } else { // Handle other unexpected errors throw error; } } [1][3]
Citations:
- 1: https://stackoverflow.com/questions/79564039/inquirer-prompts-is-terminating-program
- 2: Problems with exiting from CLI SBoudrias/Inquirer.js#1050
- 3: Handle ctrl+c gracefully SBoudrias/Inquirer.js#1502
- 4:
@inquirer/select: prompt closes after timeout onCTRL+CorCTRL+DSBoudrias/Inquirer.js#1454
Handle Ctrl+C prompt cancellation (ExitPromptError) in create
@inquirer/prompts (v8.3.2) rejects with an error where error.name === 'ExitPromptError' on Ctrl+C; catch that and route it through the same friendly cancellation path instead of printing the raw library error.
Suggested fix
.option('-t, --template <name>', 'template name')
.action((projectName, options) => {
const skipPrompt = !process.stdout.isTTY
createProject(projectName ?? '', options.template ?? '', skipPrompt).catch((error) => {
+ if (error instanceof Error && error.name === 'ExitPromptError') {
+ console.error('\nOperation cancelled.')
+ process.exit(1)
+ }
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
})
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .action((projectName, options) => { | |
| const skipPrompt = !process.stdout.isTTY | |
| createProject(projectName ?? '', options.template ?? '', skipPrompt).catch((error) => { | |
| console.error(`Error: ${error instanceof Error ? error.message : String(error)}`) | |
| process.exit(1) | |
| }) | |
| .action((projectName, options) => { | |
| const skipPrompt = !process.stdout.isTTY | |
| createProject(projectName ?? '', options.template ?? '', skipPrompt).catch((error) => { | |
| if (error instanceof Error && error.name === 'ExitPromptError') { | |
| console.error('\nOperation cancelled.') | |
| process.exit(1) | |
| } | |
| console.error(`Error: ${error instanceof Error ? error.message : String(error)}`) | |
| process.exit(1) | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/cli/bin/commands/create.js` around lines 135 - 142, The current
rejection handler for the createProject call in the .action callback prints raw
errors; modify that catch block to detect inquirer Ctrl+C by inspecting
error.name === 'ExitPromptError' and handle it as a user cancellation (log the
same friendly cancellation message and exit with code 0) instead of printing the
library error, otherwise keep the existing error.message logging and exit with
code 1; update the catch attached to createProject(...) in create.js to perform
this conditional branching.
| export function findWorkspacePackages(workspaceRoot) { | ||
| const workspaceFile = resolveWorkspaceFile(workspaceRoot) | ||
|
|
||
| if (!workspaceFile) { | ||
| return [] | ||
| } | ||
|
|
||
| const content = fs.readFileSync(workspaceFile, 'utf-8') | ||
|
|
||
| const packages = [] | ||
|
|
||
| let inPackages = false | ||
|
|
||
| for (const line of content.split('\n')) { | ||
| const trimmed = line.trim() | ||
|
|
||
| if (trimmed === 'packages:') { | ||
| inPackages = true | ||
| continue | ||
| } | ||
|
|
||
| if (!inPackages) { | ||
| continue | ||
| } | ||
|
|
||
| const match = trimmed.match(/^-\s+(.+)$/) | ||
|
|
||
| if (match) { | ||
| const pattern = match[1] | ||
|
|
||
| if (!pattern.startsWith('!')) { | ||
| packages.push(pattern) | ||
| } | ||
|
|
||
| continue | ||
| } | ||
|
|
||
| if (trimmed && !trimmed.startsWith('#')) { | ||
| break | ||
| } | ||
| } | ||
|
|
||
| return packages | ||
| } | ||
|
|
||
| function normalizeWorkspacePattern(pattern) { | ||
| return pattern.replace(/\*\*?/g, '').replace(/\/$/, '') | ||
| } | ||
|
|
||
| export function listPackages(workspaceRoot, patterns) { | ||
| const packageDirs = [] | ||
|
|
||
| const addPackage = (dir) => { | ||
| if (isPackageDir(dir)) { | ||
| packageDirs.push(dir) | ||
| } | ||
| } | ||
|
|
||
| for (const pattern of patterns) { | ||
| const base = normalizeWorkspacePattern(pattern) | ||
|
|
||
| const fullPath = path.join(workspaceRoot, base) | ||
|
|
||
| if (!exists(fullPath)) { | ||
| continue | ||
| } | ||
|
|
||
| if (pattern.includes('*')) { | ||
| const entries = fs.readdirSync(fullPath, { | ||
| withFileTypes: true, | ||
| }) | ||
|
|
||
| for (const entry of entries) { | ||
| if (entry.isDirectory()) { | ||
| addPackage(path.join(fullPath, entry.name)) | ||
| } | ||
| } | ||
| } else { | ||
| addPackage(fullPath) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does pnpm-workspace.yaml allow quoted package glob entries, and what glob patterns are supported for workspace package discovery?
💡 Result:
Yes. In pnpm-workspace.yaml, the examples for the packages field use quoted strings (single quotes or double quotes) around glob patterns, e.g. 'packages/', 'components/', and '!/test/' [1][2]. pnpm’s documentation for pnpm-workspace.yaml shows the packages list as an array of include/exclude glob entries, using '!' for exclusions [1][2]. Workspace package discovery glob patterns supported (as shown in the official docs and pnpm’s own workspace config examples): - Include a specific relative directory name (example: 'my-app') [1][2] - Include all packages in a direct subdirectory via a single-segment wildcard (example: 'packages/') [1][2] - Include all packages in nested subdirectories via recursive wildcard (example: 'components/') [1][2] - Exclude patterns using a leading exclamation mark '!' (examples: '!/test/' [1][2] and '!utils/build-artifacts' [3]). So, pnpm-workspace.yaml supports quoted glob entries, and the supported discovery patterns include non-recursive '*', recursive '', and negated exclusions with '!'. [1][2]
Citations:
- 1: https://pnpm.io/next/pnpm-workspace_yaml
- 2: https://pnpm.io/pnpm-workspace_yaml
- 3: https://github.com/pnpm/pnpm/blob/2b14c742/pnpm-workspace.yaml
Workspace package discovery doesn’t handle pnpm-workspace.yaml quoted globs and recursive patterns.
In packages/cli/bin/utils.js (lines 177-257), findWorkspacePackages() returns YAML list entries verbatim, so pnpm’s documented quoted patterns (e.g. 'packages/*', 'components/') and quoted excludes (e.g. '!/test/') retain the surrounding quotes; listPackages() then builds paths that won’t exist and exclusions won’t match, causing packages/cli/bin/commands/add.js to report no packages found in workspace.. Additionally, normalizeWorkspacePattern() strips */** and listPackages() only expands a single directory level, so pnpm’s recursive workspace discovery (e.g. components/) isn’t resolved correctly.
| console.log(' pnpm install') | ||
| console.log(' pnpm dev\n') | ||
| } | ||
| import { registerCreateCommand } from './commands/create.js' |
| } | ||
|
|
||
| // 2. 已存在 => 只更新版本,不调整顺序 | ||
| const current = semver.valid(semver.coerce(currentVersion)?.version) |
| // 2. 已存在 => 只更新版本,不调整顺序 | ||
| const current = semver.valid(semver.coerce(currentVersion)?.version) | ||
|
|
||
| if (current && current !== targetVersion) { |
There was a problem hiding this comment.
部分场景版本可能用semver不能很好的得到,比如 'latest'/'alpha' 之类的,这种判断不了的情况建议是可以提示出来让用户自己处理
| for (const line of content.split('\n')) { | ||
| const trimmed = line.trim() | ||
|
|
||
| if (trimmed === 'packages:') { |
There was a problem hiding this comment.
建议不要自己实现yml文件逐行解析,会漏掉一些情况,比如跟在同一行:packages: ['packages/*']
可以直接用yaml库


Try using the CLI with:
to experience it.
Summary by CodeRabbit
Release Notes
New Features
createcommand to scaffold new projects from built-in templatesaddcommand to integrate features into existing projects with automatic configuration and dependency managementChores