Skip to content

feat(cli): add chat feature with template and commands#354

Open
gene9831 wants to merge 3 commits into
opentiny:developfrom
gene9831:cli/add-chat
Open

feat(cli): add chat feature with template and commands#354
gene9831 wants to merge 3 commits into
opentiny:developfrom
gene9831:cli/add-chat

Conversation

@gene9831
Copy link
Copy Markdown
Collaborator

@gene9831 gene9831 commented May 29, 2026

Try using the CLI with:

npx @opentiny/tiny-robot-cli@0.4.1-alpha.2 add chat
pnpm dlx @opentiny/tiny-robot-cli@0.4.1-alpha.2 add chat

to experience it.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added create command to scaffold new projects from built-in templates
    • Added add command to integrate features into existing projects with automatic configuration and dependency management
    • Introduced chat feature template with AI model integration support
  • Chores

    • Updated CLI dependencies to support new functionality

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

Walkthrough

This PR refactors the CLI to delegate command registration to separate modules, establishes shared scaffolding utilities, extracts the create command into a dedicated module, introduces a new add command for feature scaffolding, and defines chat template resources. The entry point now acts as a router that initializes program metadata and invokes both command registrations.

Changes

CLI Commands and Feature Scaffolding

Layer / File(s) Summary
CLI entry point refactoring
packages/cli/bin/cli.js
The CLI bootstrap now imports and delegates create and add command registration to external modules, reducing the entry point to program configuration and registration invocations.
Shared utilities and scaffolding infrastructure
packages/cli/bin/utils.js
Utilities module exports logging, filesystem checks, template discovery, project scaffolding with placeholder substitution, workspace and package root discovery, wildcard pattern expansion, and environment file merging. Supports both new project creation and feature addition into existing projects.
Create command for new projects
packages/cli/bin/commands/create.js
Implements the create command: resolves project name and template via prompts or defaults (respecting TTY/skip flags), validates inputs, invokes scaffoldProject, and prints completion status with next-step instructions or normalized errors.
Add command: target resolution and constants
packages/cli/bin/commands/add.js (lines 1–93)
Establishes the add command: defines TinyRobot dependency and style import constants, and implements resolveTargetPackage to locate the correct package directory, supporting monorepos by prompting when multiple packages exist.
Add command: style import and dependency management
packages/cli/bin/commands/add.js (lines 94–201)
Implements core modifications: ensureStyleImport inserts CSS imports after existing imports, insertDependencyOrdered maintains lexicographic order in package.json, and ensureDependency adds or updates the TinyRobot dependency with semver validation.
Add command: feature selection and execution
packages/cli/bin/commands/add.js (lines 202–467)
Renders an interactive checkbox prompt to select feature modifications (component, style import, .env, dependency), applies all selected changes, tracks what occurred, and prints conditional next-step guidance based on which modifications were applied or require manual follow-up.
Chat template component and dependencies
packages/cli/package.json, packages/cli/templates/chat/.env.example, packages/cli/templates/chat/src/TinyRobotChat.vue
Adds picocolors and semver dependencies, defines the VITE_DEEPSEEK_API_KEY environment variable in the chat template, and provides the TinyRobotChat.vue component that wraps TrChat with theme provider and reactive visibility/fullscreen state.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Whiskers twitch with glee—commands now modular and free!
Create new worlds, add features with ease,
Templates dance, dependencies please!
A chat awaits with DeepSeek's embrace,
Hopping forward at a faster pace. 🚀

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a new chat feature to the CLI with templates and commands. It directly matches the changeset which introduces the add command, chat template, utilities, and refactored CLI bootstrap.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

✅ Preview build completed successfully!

Click the image above to preview.
Preview will be automatically removed when this PR is closed.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

@gene9831 gene9831 marked this pull request as ready for review May 29, 2026 11:14
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4f01301 and 8bd46c7.

📒 Files selected for processing (7)
  • packages/cli/bin/cli.js
  • packages/cli/bin/commands/add.js
  • packages/cli/bin/commands/create.js
  • packages/cli/bin/utils.js
  • packages/cli/package.json
  • packages/cli/templates/chat/.env.example
  • packages/cli/templates/chat/src/TinyRobotChat.vue

Comment on lines +168 to +183
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',
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

`For semver@7.8.1, what are the return values of:

  1. semver.coerce('0.4.2-alpha.6')?.version
  2. semver.valid('0.4.2-alpha.6')
  3. 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:


Fix ensureDependency prerelease/non-semver comparison in packages/cli/bin/commands/add.js

  • semver.coerce(currentVersion)?.version strips prerelease by default (e.g., 0.4.2-alpha.60.4.2), so an exact prerelease match can be reported as updated.
  • If semver.coerce(currentVersion) can’t parse, current is falsy and the code returns skipped, 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.

Comment on lines +89 to +99
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(', ')}`,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +135 to +142
.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)
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 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:


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.

Suggested change
.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.

Comment thread packages/cli/bin/utils.js
Comment on lines +177 to +257
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)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 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:


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.

Comment thread packages/cli/templates/chat/src/TinyRobotChat.vue
Comment thread packages/cli/bin/cli.js
console.log(' pnpm install')
console.log(' pnpm dev\n')
}
import { registerCreateCommand } from './commands/create.js'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

建议CLI项目都使用TS

}

// 2. 已存在 => 只更新版本,不调整顺序
const current = semver.valid(semver.coerce(currentVersion)?.version)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

image semver.coerce会丢失prerelease信息,例如xxx-alpha.x后面的alpha.x会丢失,看下是否符合预期

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

比如当版本是0.4.2,可能会造成跳过

// 2. 已存在 => 只更新版本,不调整顺序
const current = semver.valid(semver.coerce(currentVersion)?.version)

if (current && current !== targetVersion) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

部分场景版本可能用semver不能很好的得到,比如 'latest'/'alpha' 之类的,这种判断不了的情况建议是可以提示出来让用户自己处理

Comment thread packages/cli/bin/utils.js
for (const line of content.split('\n')) {
const trimmed = line.trim()

if (trimmed === 'packages:') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

建议不要自己实现yml文件逐行解析,会漏掉一些情况,比如跟在同一行:packages: ['packages/*']
可以直接用yaml库

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants