@opentiny/tiny-robot-cli 0.5.2-alpha.12 → 0.5.2-alpha.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -5
- package/bin/commands/add.js +592 -197
- package/bin/utils.js +140 -44
- package/package.json +4 -3
- package/templates/basic/.env.example +8 -0
- package/templates/basic/README.md +53 -22
- package/templates/basic/index.html +2 -2
- package/templates/basic/package.json +12 -16
- package/templates/basic/public/favicon.svg +1 -0
- package/templates/basic/public/icons.svg +24 -0
- package/templates/basic/src/App.vue +112 -119
- package/templates/basic/src/main.ts +2 -2
- package/templates/basic/tsconfig.app.json +2 -5
- package/templates/basic/tsconfig.node.json +3 -6
- package/templates/basic/vite.config.ts +6 -1
- package/templates/chat/.env.example +8 -0
- package/templates/chat/README.md +21 -13
- package/templates/chat/package.json +5 -5
- package/templates/chat/src/TinyRobotChat.vue +52 -132
- package/templates/chat/src/components/ComposerTools.vue +99 -0
- package/templates/chat/src/{tiny-robot-chat/components → components}/WindowHeader.vue +1 -1
- package/templates/chat/src/{tiny-robot-chat/composables → composables}/useWindow.ts +8 -18
- package/templates/chat/src/config/chat-runtime.ts +82 -0
- package/templates/chat/src/{tiny-robot-chat/config → config}/chat-ui.ts +22 -4
- package/templates/chat/src/index.css +116 -0
- package/templates/chat/src/main.ts +3 -0
- package/templates/basic/public/modelcontextprotocol.png +0 -0
- package/templates/basic/src/components/ChatList.vue +0 -82
- package/templates/basic/src/components/ChatSender.vue +0 -125
- package/templates/basic/src/components/ConversationHistory.vue +0 -136
- package/templates/basic/src/components/HistoryDrawerButton.vue +0 -43
- package/templates/basic/src/components/McpServerPickerButton.vue +0 -278
- package/templates/basic/src/components/ThemeToggleButton.vue +0 -44
- package/templates/basic/src/components/icons/IconDeepThink.vue +0 -29
- package/templates/basic/src/components/icons/IconModelAliyunBailian.vue +0 -51
- package/templates/basic/src/components/icons/IconModelDeepseek.vue +0 -29
- package/templates/basic/src/components/icons/IconMoon.vue +0 -29
- package/templates/basic/src/components/icons/IconPlugin.vue +0 -29
- package/templates/basic/src/components/icons/IconSun.vue +0 -35
- package/templates/basic/src/components/icons/IconWebSearch.vue +0 -36
- package/templates/basic/src/components/icons/index.ts +0 -7
- package/templates/basic/src/composables/useChat.ts +0 -129
- package/templates/basic/src/composables/useMcp.ts +0 -170
- package/templates/basic/src/composables/useModel.ts +0 -82
- package/templates/basic/src/mcpServers.ts +0 -40
- package/templates/basic/src/models.ts +0 -81
- package/templates/chat/src/tiny-robot-chat/components/ComposerTools.vue +0 -72
- package/templates/chat/src/tiny-robot-chat/config/chat-runtime.ts +0 -26
package/bin/commands/add.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { checkbox, select } from '@inquirer/prompts'
|
|
2
|
+
import { compileTemplate, parse } from '@vue/compiler-sfc'
|
|
2
3
|
import { Argument } from 'commander'
|
|
3
4
|
import fs from 'node:fs'
|
|
4
5
|
import path from 'node:path'
|
|
@@ -6,8 +7,6 @@ import process from 'node:process'
|
|
|
6
7
|
import semver from 'semver'
|
|
7
8
|
|
|
8
9
|
import {
|
|
9
|
-
copyFile,
|
|
10
|
-
copyDirectory,
|
|
11
10
|
findProjectRoot,
|
|
12
11
|
findSubPackageRoot,
|
|
13
12
|
findWorkspacePackages,
|
|
@@ -17,51 +16,95 @@ import {
|
|
|
17
16
|
listPackages,
|
|
18
17
|
logSkip,
|
|
19
18
|
logSuccess,
|
|
20
|
-
|
|
19
|
+
mergeEnvContent,
|
|
21
20
|
} from '../utils.js'
|
|
22
21
|
|
|
23
|
-
const TARGET_VERSION = '0.5.2-alpha.
|
|
24
|
-
const
|
|
22
|
+
const TARGET_VERSION = '0.5.2-alpha.14'
|
|
23
|
+
const CHAT_ADD_FEATURE_DIR = 'src/tiny-robot-chat'
|
|
25
24
|
const DEPENDENCIES = {
|
|
26
25
|
'@opentiny/tiny-robot': TARGET_VERSION,
|
|
27
26
|
'@opentiny/tiny-robot-chat': TARGET_VERSION,
|
|
28
27
|
'@opentiny/tiny-robot-kit': TARGET_VERSION,
|
|
29
28
|
'@opentiny/tiny-robot-svgs': TARGET_VERSION,
|
|
30
|
-
'@vueuse/core':
|
|
29
|
+
'@vueuse/core': '13.1.0',
|
|
31
30
|
}
|
|
31
|
+
const PACKAGE_STYLE_IMPORTS = [
|
|
32
|
+
"import '@opentiny/tiny-robot/dist/style.css'",
|
|
33
|
+
"import '@opentiny/tiny-robot-chat/dist/style.css'",
|
|
34
|
+
]
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
function logUnavailable(label) {
|
|
37
|
+
logSkip(`${label} could not be applied`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function logSkippedSelection(label) {
|
|
41
|
+
logSkip(`${label} change was not selected`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function resolveTargetPackage(cwd, nonInteractive) {
|
|
34
45
|
const workspaceRoot = findWorkspaceRoot(cwd)
|
|
35
46
|
|
|
36
47
|
if (workspaceRoot) {
|
|
48
|
+
const workspacePatterns = findWorkspacePackages(workspaceRoot)
|
|
49
|
+
const packageDirs = listPackages(workspaceRoot, workspacePatterns)
|
|
37
50
|
const subPackageRoot = findSubPackageRoot(cwd, workspaceRoot)
|
|
38
51
|
|
|
39
52
|
if (subPackageRoot) {
|
|
40
|
-
return subPackageRoot
|
|
53
|
+
if (packageDirs.includes(subPackageRoot)) return subPackageRoot
|
|
54
|
+
throw new Error(
|
|
55
|
+
'The current package is not included by the workspace configuration. Run this command from an included package or update the workspace configuration.',
|
|
56
|
+
)
|
|
41
57
|
}
|
|
42
58
|
|
|
43
|
-
const
|
|
44
|
-
const
|
|
59
|
+
const normalizedCwd = path.resolve(cwd)
|
|
60
|
+
const normalizedWorkspaceRoot = path.resolve(workspaceRoot)
|
|
61
|
+
const rootPackageJson = path.join(workspaceRoot, 'package.json')
|
|
45
62
|
|
|
46
|
-
|
|
63
|
+
if (
|
|
64
|
+
normalizedCwd === normalizedWorkspaceRoot &&
|
|
65
|
+
workspacePatterns === undefined &&
|
|
66
|
+
packageDirs.length === 1 &&
|
|
67
|
+
packageDirs[0] === path.resolve(workspaceRoot) &&
|
|
68
|
+
fs.existsSync(rootPackageJson)
|
|
69
|
+
)
|
|
70
|
+
return workspaceRoot
|
|
71
|
+
|
|
72
|
+
if (normalizedCwd !== normalizedWorkspaceRoot) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
'No package.json found in the current package path. Run add from the workspace root or a package directory.',
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (packageDirs.length === 0) {
|
|
79
|
+
const detail =
|
|
80
|
+
workspacePatterns === undefined
|
|
81
|
+
? 'The workspace has no packages field and no package matched pnpm default workspace locations.'
|
|
82
|
+
: 'No packages matched the patterns in pnpm-workspace.yaml.'
|
|
83
|
+
throw new Error(`${detail} Run this command from an included package or update the workspace configuration.`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (packageDirs.length === 1) return packageDirs[0]
|
|
87
|
+
|
|
88
|
+
if (nonInteractive) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Multiple workspace packages found. Run add from the target package directory when using --yes or --dry-run. Candidates: ${packageDirs
|
|
91
|
+
.map((dir) => path.relative(workspaceRoot, dir).replaceAll('\\', '/') || '.')
|
|
92
|
+
.join(', ')}`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
47
95
|
|
|
48
96
|
return select({
|
|
49
97
|
message: 'Multi-package workspace detected, select a target package:',
|
|
50
98
|
choices: packageDirs.map((dir) => ({
|
|
51
|
-
name: path.
|
|
99
|
+
name: path.relative(workspaceRoot, dir).replaceAll('\\', '/') || '(workspace root)',
|
|
52
100
|
value: dir,
|
|
53
101
|
})),
|
|
54
102
|
})
|
|
55
103
|
}
|
|
56
104
|
|
|
57
105
|
const projectRoot = findProjectRoot(cwd)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return projectRoot
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
console.error('Error: no package.json found.')
|
|
64
|
-
process.exit(1)
|
|
106
|
+
if (projectRoot) return projectRoot
|
|
107
|
+
throw new Error('no package.json found.')
|
|
65
108
|
}
|
|
66
109
|
|
|
67
110
|
function getTemplateFile(...segments) {
|
|
@@ -69,267 +112,615 @@ function getTemplateFile(...segments) {
|
|
|
69
112
|
}
|
|
70
113
|
|
|
71
114
|
function readPackageJson(pkgPath) {
|
|
72
|
-
|
|
115
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
116
|
+
invariant(pkg && typeof pkg === 'object' && !Array.isArray(pkg), 'package.json must contain a JSON object.')
|
|
117
|
+
return pkg
|
|
73
118
|
}
|
|
74
119
|
|
|
75
|
-
function
|
|
76
|
-
|
|
120
|
+
function findMainEntry(targetDir) {
|
|
121
|
+
for (const file of ['src/main.ts', 'src/main.js']) {
|
|
122
|
+
const fullPath = path.join(targetDir, file)
|
|
123
|
+
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) return fullPath
|
|
124
|
+
}
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function importedModule(line) {
|
|
129
|
+
const match = /^\s*import(?:\s+[\s\S]*?\sfrom\s+)?\s*['"]([^'"]+)['"]\s*;?\s*$/.exec(line)
|
|
130
|
+
return match?.[1] ?? null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function insertImport(content, importStatement) {
|
|
134
|
+
const eol = content.includes('\r\n') ? '\r\n' : '\n'
|
|
135
|
+
const lines = content.replaceAll('\r\n', '\n').split('\n')
|
|
136
|
+
const moduleName = importedModule(importStatement)
|
|
137
|
+
if (moduleName && lines.some((line) => importedModule(line) === moduleName)) return content
|
|
138
|
+
|
|
139
|
+
let lastImportIndex = -1
|
|
140
|
+
for (let i = 0; i < lines.length; i++) {
|
|
141
|
+
if (/^\s*import\s/.test(lines[i])) {
|
|
142
|
+
lastImportIndex = i
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
if (lastImportIndex !== -1) break
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (lastImportIndex === -1) lines.unshift(importStatement)
|
|
149
|
+
else lines.splice(lastImportIndex + 1, 0, importStatement)
|
|
150
|
+
return lines.join(eol)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ensureStyleImportsContent(before, featureStyleImport) {
|
|
154
|
+
let after = before
|
|
155
|
+
for (const styleImport of [...PACKAGE_STYLE_IMPORTS, featureStyleImport]) {
|
|
156
|
+
after = insertImport(after, styleImport)
|
|
157
|
+
}
|
|
158
|
+
return { type: after === before ? 'skipped' : 'inserted', content: after }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function ensureStyleImports(mainFile, featureStyleImport) {
|
|
162
|
+
const before = fs.readFileSync(mainFile, 'utf-8')
|
|
163
|
+
const result = ensureStyleImportsContent(before, featureStyleImport)
|
|
164
|
+
if (result.type === 'inserted') fs.writeFileSync(mainFile, result.content)
|
|
165
|
+
return { type: result.type }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function findDependency(pkg, name) {
|
|
169
|
+
const matches = []
|
|
170
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
171
|
+
if (pkg[section] && Object.prototype.hasOwnProperty.call(pkg[section], name)) {
|
|
172
|
+
matches.push({ section, version: pkg[section][name] })
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return matches
|
|
77
176
|
}
|
|
78
177
|
|
|
79
178
|
function insertDependencyOrdered(dependencies, name, version) {
|
|
80
179
|
const next = {}
|
|
81
180
|
let inserted = false
|
|
82
|
-
|
|
83
181
|
for (const [key, value] of Object.entries(dependencies)) {
|
|
84
182
|
if (!inserted && name < key) {
|
|
85
183
|
next[name] = version
|
|
86
184
|
inserted = true
|
|
87
185
|
}
|
|
88
|
-
|
|
89
186
|
next[key] = value
|
|
90
187
|
}
|
|
91
|
-
|
|
92
|
-
if (!inserted) {
|
|
93
|
-
next[name] = version
|
|
94
|
-
}
|
|
95
|
-
|
|
188
|
+
if (!inserted) next[name] = version
|
|
96
189
|
return next
|
|
97
190
|
}
|
|
98
191
|
|
|
99
192
|
function ensureDependency(pkg, name, targetVersion) {
|
|
100
|
-
|
|
193
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
194
|
+
const value = pkg[section]
|
|
195
|
+
if (value !== undefined && (value === null || typeof value !== 'object' || Array.isArray(value))) {
|
|
196
|
+
return { type: 'conflict', reason: `${section} must be a JSON object` }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
101
199
|
|
|
102
|
-
const
|
|
200
|
+
const existing = findDependency(pkg, name)
|
|
201
|
+
if (existing.length > 1) {
|
|
202
|
+
return { type: 'conflict', reason: `${name} is declared in multiple dependency sections` }
|
|
203
|
+
}
|
|
103
204
|
|
|
104
|
-
if (
|
|
205
|
+
if (existing.length === 0) {
|
|
206
|
+
if (
|
|
207
|
+
pkg.dependencies !== undefined &&
|
|
208
|
+
(pkg.dependencies === null || typeof pkg.dependencies !== 'object' || Array.isArray(pkg.dependencies))
|
|
209
|
+
) {
|
|
210
|
+
return { type: 'conflict', reason: 'dependencies must be a JSON object' }
|
|
211
|
+
}
|
|
212
|
+
pkg.dependencies ??= {}
|
|
105
213
|
pkg.dependencies = insertDependencyOrdered(pkg.dependencies, name, targetVersion)
|
|
214
|
+
return { type: 'added', to: targetVersion, section: 'dependencies' }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const dependency = existing[0]
|
|
218
|
+
if (typeof dependency.version === 'string' && dependency.version.startsWith('workspace:')) {
|
|
219
|
+
return { type: 'skipped', section: dependency.section, version: dependency.version }
|
|
220
|
+
}
|
|
106
221
|
|
|
222
|
+
if (typeof dependency.version !== 'string' || !semver.validRange(dependency.version)) {
|
|
107
223
|
return {
|
|
108
|
-
type: '
|
|
109
|
-
|
|
224
|
+
type: 'conflict',
|
|
225
|
+
reason: `${name} has an unsupported version specifier (${String(dependency.version)})`,
|
|
110
226
|
}
|
|
111
227
|
}
|
|
112
228
|
|
|
229
|
+
const satisfies = semver.satisfies(targetVersion, dependency.version, { includePrerelease: true })
|
|
230
|
+
|
|
231
|
+
if (satisfies) return { type: 'skipped', section: dependency.section, version: dependency.version }
|
|
232
|
+
|
|
233
|
+
const minimum = typeof dependency.version === 'string' ? semver.minVersion(dependency.version) : null
|
|
113
234
|
if (
|
|
114
|
-
|
|
115
|
-
semver.
|
|
235
|
+
typeof dependency.version === 'string' &&
|
|
236
|
+
((semver.valid(dependency.version) && semver.gte(dependency.version, targetVersion)) ||
|
|
237
|
+
(minimum && semver.gte(minimum, targetVersion)))
|
|
116
238
|
) {
|
|
239
|
+
return { type: 'skipped', section: dependency.section, version: dependency.version }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (dependency.section !== 'dependencies') {
|
|
117
243
|
return {
|
|
118
|
-
type: '
|
|
244
|
+
type: 'conflict',
|
|
245
|
+
reason: `${name}@${dependency.version} in ${dependency.section} does not satisfy ${targetVersion}`,
|
|
119
246
|
}
|
|
120
247
|
}
|
|
121
248
|
|
|
249
|
+
pkg.dependencies[name] = targetVersion
|
|
122
250
|
return {
|
|
123
|
-
type: '
|
|
124
|
-
|
|
251
|
+
type: 'updated',
|
|
252
|
+
section: dependency.section,
|
|
253
|
+
from: dependency.version,
|
|
125
254
|
to: targetVersion,
|
|
126
255
|
}
|
|
127
256
|
}
|
|
128
257
|
|
|
129
258
|
function printDependencyResult(result, name) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
259
|
+
if (result.type === 'added') {
|
|
260
|
+
logSuccess(`Added ${name}@${result.to}`)
|
|
261
|
+
} else if (result.type === 'updated') {
|
|
262
|
+
logSuccess(`Updated ${name} from ${result.from} to ${result.to}`)
|
|
263
|
+
} else if (result.type === 'conflict') {
|
|
264
|
+
logSkip(`${name} requires manual dependency resolution (${result.reason})`)
|
|
265
|
+
} else {
|
|
266
|
+
logSkip(`${name} already satisfies required version (${result.version})`)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
134
269
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
270
|
+
function listFiles(sourceDir, relativeDir = '') {
|
|
271
|
+
const files = []
|
|
272
|
+
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
273
|
+
const relativePath = path.join(relativeDir, entry.name)
|
|
274
|
+
const sourcePath = path.join(sourceDir, entry.name)
|
|
275
|
+
if (entry.isDirectory()) files.push(...listFiles(sourcePath, relativePath))
|
|
276
|
+
else files.push({ source: sourcePath, relativePath })
|
|
138
277
|
}
|
|
278
|
+
return files
|
|
139
279
|
}
|
|
140
280
|
|
|
141
|
-
function
|
|
142
|
-
const
|
|
143
|
-
const
|
|
281
|
+
function getChatFeatureFiles(targetDir) {
|
|
282
|
+
const sourceRoot = getTemplateFile('src')
|
|
283
|
+
const featureFiles = listFiles(sourceRoot)
|
|
284
|
+
.filter(({ relativePath }) => relativePath !== 'main.ts' && relativePath !== 'index.css')
|
|
285
|
+
.map(({ source, relativePath }) => ({ source, target: path.join(targetDir, CHAT_ADD_FEATURE_DIR, relativePath) }))
|
|
144
286
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
source: path.join(templateSource, 'TinyRobotChat.vue'),
|
|
149
|
-
target: path.join(targetDir, 'src/TinyRobotChat.vue'),
|
|
150
|
-
type: 'file',
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
label: 'Chat UI components',
|
|
154
|
-
source: path.join(templateSource, 'tiny-robot-chat/components'),
|
|
155
|
-
target: path.join(supportTarget, 'components'),
|
|
156
|
-
type: 'directory',
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
label: 'Chat composables',
|
|
160
|
-
source: path.join(templateSource, 'tiny-robot-chat/composables'),
|
|
161
|
-
target: path.join(supportTarget, 'composables'),
|
|
162
|
-
type: 'directory',
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
label: 'Chat configuration',
|
|
166
|
-
source: path.join(templateSource, 'tiny-robot-chat/config'),
|
|
167
|
-
target: path.join(supportTarget, 'config'),
|
|
168
|
-
type: 'directory',
|
|
169
|
-
},
|
|
170
|
-
]
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function findCopyConflicts(source, target) {
|
|
174
|
-
if (!fs.existsSync(target)) {
|
|
175
|
-
return []
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const sourceStat = fs.statSync(source)
|
|
179
|
-
const targetStat = fs.statSync(target)
|
|
180
|
-
|
|
181
|
-
if (!sourceStat.isDirectory()) {
|
|
182
|
-
return [target]
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (!targetStat.isDirectory()) {
|
|
186
|
-
return [target]
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return fs.readdirSync(source, { withFileTypes: true }).flatMap((entry) => {
|
|
190
|
-
return findCopyConflicts(path.join(source, entry.name), path.join(target, entry.name))
|
|
287
|
+
featureFiles.push({
|
|
288
|
+
source: getTemplateFile('src', 'index.css'),
|
|
289
|
+
target: path.join(targetDir, CHAT_ADD_FEATURE_DIR, 'index.css'),
|
|
191
290
|
})
|
|
291
|
+
|
|
292
|
+
return featureFiles
|
|
192
293
|
}
|
|
193
294
|
|
|
194
|
-
|
|
195
|
-
const
|
|
295
|
+
function parseMountTemplate(source, filename) {
|
|
296
|
+
const parsed = parse(source, { filename })
|
|
297
|
+
if (parsed.errors.length > 0 || !parsed.descriptor.template) {
|
|
298
|
+
return { type: 'manual', reason: 'App.vue template could not be parsed safely' }
|
|
299
|
+
}
|
|
196
300
|
|
|
197
|
-
|
|
301
|
+
const compiled = compileTemplate({
|
|
302
|
+
source: parsed.descriptor.template.content,
|
|
303
|
+
filename,
|
|
304
|
+
id: 'tiny-robot-cli-mount',
|
|
305
|
+
})
|
|
306
|
+
if (compiled.errors.length > 0 || !compiled.ast) {
|
|
307
|
+
return { type: 'manual', reason: 'App.vue template could not be parsed safely' }
|
|
308
|
+
}
|
|
198
309
|
|
|
199
|
-
|
|
200
|
-
|
|
310
|
+
return { type: 'parsed', block: parsed.descriptor.template, components: compiled.ast.components ?? [] }
|
|
311
|
+
}
|
|
201
312
|
|
|
202
|
-
|
|
203
|
-
|
|
313
|
+
function findTemplateClose(block) {
|
|
314
|
+
return block.loc.end.offset
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function planMount(targetDir) {
|
|
318
|
+
const appFile = path.join(targetDir, 'src/App.vue')
|
|
319
|
+
if (!fs.existsSync(appFile)) return { type: 'manual', reason: 'src/App.vue was not found' }
|
|
320
|
+
if (!fs.statSync(appFile).isFile()) return { type: 'manual', reason: 'src/App.vue is not a file' }
|
|
321
|
+
const before = fs.readFileSync(appFile, 'utf-8')
|
|
322
|
+
|
|
323
|
+
const templatePlan = parseMountTemplate(before, appFile)
|
|
324
|
+
if (templatePlan.type === 'manual') return templatePlan
|
|
325
|
+
if (templatePlan.components.includes('TinyRobotChat')) return { type: 'skipped', content: before }
|
|
326
|
+
|
|
327
|
+
const importStatement = "import TinyRobotChat from './tiny-robot-chat/TinyRobotChat.vue'"
|
|
328
|
+
let after = before
|
|
329
|
+
const setupMatch = /<script\s+setup(?:\s[^>]*)?>([\s\S]*?)<\/script>/i.exec(before)
|
|
330
|
+
if (setupMatch) {
|
|
331
|
+
const setupContent = setupMatch[1]
|
|
332
|
+
const defaultImport = /^\s*import\s+TinyRobotChat\s+from\s+['"]([^'"]+)['"]\s*;?\s*$/m.exec(setupContent)
|
|
333
|
+
const featureImport =
|
|
334
|
+
/^\s*import\s+([A-Za-z_$][\w$]*)\s+from\s+['"]\.\/tiny-robot-chat\/TinyRobotChat\.vue['"]\s*;?\s*$/m.exec(
|
|
335
|
+
setupContent,
|
|
336
|
+
)
|
|
337
|
+
const namedImport = /^\s*import\s*\{[^}]*\bTinyRobotChat\b[^}]*\}\s*from\s+['"][^'"]+['"]\s*;?\s*$/m.test(
|
|
338
|
+
setupContent,
|
|
339
|
+
)
|
|
340
|
+
const localBinding = /^\s*(?:const|let|var|function|class)\s+TinyRobotChat\b/m.test(setupContent)
|
|
341
|
+
|
|
342
|
+
if (
|
|
343
|
+
namedImport ||
|
|
344
|
+
localBinding ||
|
|
345
|
+
(defaultImport && defaultImport[1] !== './tiny-robot-chat/TinyRobotChat.vue') ||
|
|
346
|
+
(featureImport && featureImport[1] !== 'TinyRobotChat')
|
|
347
|
+
) {
|
|
348
|
+
return { type: 'manual', reason: 'App.vue already declares TinyRobotChat with a different binding' }
|
|
349
|
+
}
|
|
204
350
|
|
|
205
|
-
|
|
351
|
+
if (!defaultImport) {
|
|
352
|
+
const insertionIndex = setupMatch.index + setupMatch[0].indexOf('>') + 1
|
|
353
|
+
after = `${before.slice(0, insertionIndex)}\n${importStatement}${before.slice(insertionIndex)}`
|
|
354
|
+
}
|
|
355
|
+
} else if (/<script(?:\s[^>]*)?>/.test(before)) {
|
|
356
|
+
return { type: 'manual', reason: 'App.vue has a non-setup script block' }
|
|
357
|
+
} else {
|
|
358
|
+
after = `<script setup lang="ts">\n${importStatement}\n</script>\n\n${before}`
|
|
206
359
|
}
|
|
207
360
|
|
|
208
|
-
const
|
|
209
|
-
|
|
361
|
+
const updatedTemplate = parseMountTemplate(after, appFile)
|
|
362
|
+
if (updatedTemplate.type === 'manual') return updatedTemplate
|
|
363
|
+
const templateClose = findTemplateClose(updatedTemplate.block)
|
|
364
|
+
if (templateClose === -1) return { type: 'manual', reason: 'App.vue template closing tag was not found' }
|
|
365
|
+
const beforeClose = after.slice(0, templateClose).trimEnd()
|
|
366
|
+
const lineIndent = /^\s*/.exec(beforeClose.slice(beforeClose.lastIndexOf('\n') + 1))?.[0] ?? ''
|
|
367
|
+
const indent = lineIndent || ' '
|
|
368
|
+
after = `${beforeClose}\n${indent}<TinyRobotChat />\n${after.slice(templateClose)}`
|
|
369
|
+
return { type: 'merge', content: after }
|
|
370
|
+
}
|
|
210
371
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
372
|
+
function inspectFeatureFiles(files) {
|
|
373
|
+
return files.map((file) => {
|
|
374
|
+
if (!fs.existsSync(file.target)) return { ...file, type: 'create' }
|
|
375
|
+
if (!fs.statSync(file.target).isFile()) return { ...file, type: 'conflict', reason: 'target is not a file' }
|
|
376
|
+
const same = fs.readFileSync(file.source).equals(fs.readFileSync(file.target))
|
|
377
|
+
return { ...file, type: same ? 'skipped' : 'conflict' }
|
|
378
|
+
})
|
|
379
|
+
}
|
|
214
380
|
|
|
215
|
-
|
|
381
|
+
function formatRelative(file, targetDir) {
|
|
382
|
+
return path.relative(targetDir, file).replaceAll('\\', '/')
|
|
383
|
+
}
|
|
216
384
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
385
|
+
function getChatFeatureChoices(targetDir, options) {
|
|
386
|
+
const mainEntry = findMainEntry(targetDir)
|
|
387
|
+
return [
|
|
388
|
+
{ label: 'Chat feature files', enabled: true },
|
|
389
|
+
{ label: 'main entry style imports', enabled: Boolean(mainEntry) },
|
|
390
|
+
{ label: '.env.example', enabled: true },
|
|
391
|
+
{ label: 'package.json', enabled: true },
|
|
392
|
+
...(options.mount ? [{ label: 'App.vue mount', enabled: fs.existsSync(path.join(targetDir, 'src/App.vue')) }] : []),
|
|
393
|
+
].map((item) => ({ ...item, mainEntry }))
|
|
394
|
+
}
|
|
221
395
|
|
|
222
|
-
|
|
223
|
-
|
|
396
|
+
async function selectFileChanges(targetDir, options) {
|
|
397
|
+
const files = getChatFeatureChoices(targetDir, options)
|
|
398
|
+
if (options.yes || options.dryRun || options.nonInteractive)
|
|
399
|
+
return files.filter((file) => file.enabled).map((file) => file.label)
|
|
400
|
+
return checkbox({
|
|
401
|
+
message: 'Select which file changes to apply (all selected by default):',
|
|
402
|
+
choices: files.map((file) => ({
|
|
403
|
+
name: file.enabled ? file.label : `${file.label} (not available)`,
|
|
404
|
+
value: file.label,
|
|
405
|
+
checked: file.enabled,
|
|
406
|
+
disabled: !file.enabled,
|
|
407
|
+
})),
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function isSelected(selectedFiles, label) {
|
|
412
|
+
return selectedFiles.includes(label)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function getDependencyPlan(pkg) {
|
|
416
|
+
const preview = JSON.parse(JSON.stringify(pkg))
|
|
417
|
+
|
|
418
|
+
return Object.entries(DEPENDENCIES).map(([name, version]) => ({
|
|
419
|
+
name,
|
|
420
|
+
result: ensureDependency(preview, name, version),
|
|
421
|
+
}))
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function getEnvPlan(targetDir) {
|
|
425
|
+
const templateFile = getTemplateFile('.env.example')
|
|
426
|
+
const targetFile = path.join(targetDir, '.env.example')
|
|
427
|
+
|
|
428
|
+
if (!fs.existsSync(templateFile)) return { type: 'unavailable' }
|
|
429
|
+
if (!fs.existsSync(targetFile)) return { type: 'create', targetFile, content: fs.readFileSync(templateFile) }
|
|
430
|
+
invariant(fs.statSync(targetFile).isFile(), '.env.example exists and is not a file.')
|
|
431
|
+
|
|
432
|
+
const result = mergeEnvContent(fs.readFileSync(templateFile, 'utf-8'), fs.readFileSync(targetFile, 'utf-8'))
|
|
433
|
+
return { ...result, targetFile }
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function formatPlanStatus(type) {
|
|
437
|
+
if (type === 'create') return '+'
|
|
438
|
+
if (type === 'merge') return '~'
|
|
439
|
+
if (type === 'unavailable' || type === 'conflict') return '!'
|
|
440
|
+
return '○'
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function printManualMcpSetup() {
|
|
444
|
+
console.log(' ! Manual: add the Model Context MCP proxy to vite.config.* under server.proxy')
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function fileContent(content) {
|
|
448
|
+
return Buffer.isBuffer(content) ? content : Buffer.from(content)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function addFileChange(changes, target, content, message) {
|
|
452
|
+
if (fs.existsSync(target)) invariant(fs.statSync(target).isFile(), `${target} exists and is not a file.`)
|
|
224
453
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
454
|
+
const next = fileContent(content)
|
|
455
|
+
if (fs.existsSync(target) && fs.readFileSync(target).equals(next)) return false
|
|
456
|
+
|
|
457
|
+
changes.push({ target, content, message })
|
|
458
|
+
return true
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function applyChanges(changes) {
|
|
462
|
+
const snapshots = changes.map(({ target }) => ({
|
|
463
|
+
target,
|
|
464
|
+
existed: fs.existsSync(target),
|
|
465
|
+
content: fs.existsSync(target) ? fs.readFileSync(target) : null,
|
|
466
|
+
}))
|
|
467
|
+
const createdDirectories = new Set()
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
for (const change of changes) {
|
|
471
|
+
let directory = path.dirname(change.target)
|
|
472
|
+
const missingDirectories = []
|
|
473
|
+
while (!fs.existsSync(directory)) {
|
|
474
|
+
missingDirectories.push(directory)
|
|
475
|
+
directory = path.dirname(directory)
|
|
476
|
+
}
|
|
477
|
+
fs.mkdirSync(path.dirname(change.target), { recursive: true })
|
|
478
|
+
for (const missingDirectory of missingDirectories) createdDirectories.add(missingDirectory)
|
|
479
|
+
fs.writeFileSync(change.target, change.content)
|
|
480
|
+
}
|
|
481
|
+
} catch (error) {
|
|
482
|
+
for (const snapshot of snapshots.reverse()) {
|
|
483
|
+
if (snapshot.existed) fs.writeFileSync(snapshot.target, snapshot.content)
|
|
484
|
+
else if (fs.existsSync(snapshot.target)) fs.rmSync(snapshot.target, { force: true })
|
|
230
485
|
}
|
|
486
|
+
for (const directory of [...createdDirectories].sort((left, right) => right.length - left.length)) {
|
|
487
|
+
if (fs.existsSync(directory) && fs.readdirSync(directory).length === 0) fs.rmdirSync(directory)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
throw new Error(
|
|
491
|
+
`Failed to apply changes; all changes were rolled back: ${error instanceof Error ? error.message : String(error)}`,
|
|
492
|
+
)
|
|
231
493
|
}
|
|
494
|
+
}
|
|
232
495
|
|
|
233
|
-
|
|
496
|
+
function validateSelection(selectedFiles, pkg, featureInspection, mountPlan, allowManualMount) {
|
|
497
|
+
const featureFilesSelected = isSelected(selectedFiles, 'Chat feature files')
|
|
498
|
+
const featureFilesMissing = featureInspection.some((file) => file.type === 'create')
|
|
234
499
|
|
|
235
|
-
if (
|
|
236
|
-
|
|
500
|
+
if (!featureFilesSelected && featureFilesMissing && isSelected(selectedFiles, 'main entry style imports')) {
|
|
501
|
+
throw new Error('Select Chat feature files before adding the local feature style import.')
|
|
237
502
|
}
|
|
238
503
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
envResult,
|
|
242
|
-
dependencyResults,
|
|
504
|
+
if (!featureFilesSelected && featureFilesMissing && isSelected(selectedFiles, 'App.vue mount')) {
|
|
505
|
+
throw new Error('Select Chat feature files before mounting TinyRobotChat in App.vue.')
|
|
243
506
|
}
|
|
244
|
-
}
|
|
245
507
|
|
|
246
|
-
|
|
247
|
-
|
|
508
|
+
if (isSelected(selectedFiles, 'App.vue mount') && mountPlan?.type === 'manual' && !allowManualMount) {
|
|
509
|
+
throw new Error(`cannot safely mount TinyRobotChat: ${mountPlan.reason}`)
|
|
510
|
+
}
|
|
248
511
|
|
|
249
|
-
|
|
250
|
-
message: `Add the complete TinyRobot chat feature to ${targetDir}?`,
|
|
251
|
-
default: true,
|
|
252
|
-
})
|
|
512
|
+
if (isSelected(selectedFiles, 'package.json')) return
|
|
253
513
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
514
|
+
const dependencyChanges = getDependencyPlan(pkg).filter(({ result }) => result.type !== 'skipped')
|
|
515
|
+
if (dependencyChanges.length > 0 && (featureFilesSelected || isSelected(selectedFiles, 'main entry style imports'))) {
|
|
516
|
+
throw new Error(
|
|
517
|
+
'Select package.json when adding the chat feature or its style imports so required dependencies can be checked.',
|
|
518
|
+
)
|
|
257
519
|
}
|
|
520
|
+
}
|
|
258
521
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
522
|
+
function prepareChanges(targetDir, selectedFiles, context) {
|
|
523
|
+
const { featureInspection, mainFile, mountPlan, pkgPath, pkg, allowConflicts } = context
|
|
524
|
+
const changes = []
|
|
525
|
+
const results = { featureInspection, style: null, env: null, dependencies: [], mount: null, dependencyChanged: false }
|
|
526
|
+
|
|
527
|
+
if (isSelected(selectedFiles, 'Chat feature files')) {
|
|
528
|
+
const conflicts = featureInspection.filter((file) => file.type === 'conflict')
|
|
529
|
+
if (conflicts.length > 0 && !allowConflicts) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
`file conflicts detected:\n${conflicts.map((file) => ` - ${formatRelative(file.target, targetDir)}`).join('\n')}\nResolve the conflicts and run add chat again.`,
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
for (const file of featureInspection) {
|
|
535
|
+
if (file.type === 'create')
|
|
536
|
+
addFileChange(
|
|
537
|
+
changes,
|
|
538
|
+
file.target,
|
|
539
|
+
fs.readFileSync(file.source),
|
|
540
|
+
`Created ${formatRelative(file.target, targetDir)}`,
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
262
544
|
|
|
263
|
-
|
|
264
|
-
|
|
545
|
+
if (isSelected(selectedFiles, 'main entry style imports')) {
|
|
546
|
+
if (!mainFile) results.style = { type: 'unavailable' }
|
|
547
|
+
else {
|
|
548
|
+
const styleResult = ensureStyleImportsContent(
|
|
549
|
+
fs.readFileSync(mainFile, 'utf-8'),
|
|
550
|
+
"import './tiny-robot-chat/index.css'",
|
|
551
|
+
)
|
|
552
|
+
results.style = styleResult
|
|
553
|
+
if (styleResult.type === 'inserted')
|
|
554
|
+
addFileChange(changes, mainFile, styleResult.content, 'Inserted TinyRobot and Chat feature style imports')
|
|
555
|
+
}
|
|
265
556
|
}
|
|
266
557
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
558
|
+
if (isSelected(selectedFiles, '.env.example')) {
|
|
559
|
+
results.env = getEnvPlan(targetDir)
|
|
560
|
+
if (results.env.type === 'create')
|
|
561
|
+
addFileChange(changes, results.env.targetFile, results.env.content, 'Created .env.example')
|
|
562
|
+
if (results.env.type === 'merged')
|
|
563
|
+
addFileChange(changes, results.env.targetFile, results.env.content, `Added ${results.env.added} env variables`)
|
|
564
|
+
}
|
|
271
565
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
566
|
+
if (isSelected(selectedFiles, 'package.json')) {
|
|
567
|
+
for (const [name, version] of Object.entries(DEPENDENCIES)) {
|
|
568
|
+
const result = ensureDependency(pkg, name, version)
|
|
569
|
+
if (result.type === 'conflict' && !allowConflicts) throw new Error(`${name}: ${result.reason}`)
|
|
570
|
+
results.dependencies.push({ name, result })
|
|
571
|
+
results.dependencyChanged ||= result.type !== 'skipped'
|
|
572
|
+
}
|
|
573
|
+
if (results.dependencyChanged)
|
|
574
|
+
addFileChange(changes, pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, 'Updated package.json')
|
|
575
|
+
}
|
|
275
576
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
577
|
+
if (isSelected(selectedFiles, 'App.vue mount')) {
|
|
578
|
+
results.mount = mountPlan
|
|
579
|
+
if (mountPlan.type === 'merge')
|
|
580
|
+
addFileChange(
|
|
581
|
+
changes,
|
|
582
|
+
path.join(targetDir, 'src/App.vue'),
|
|
583
|
+
mountPlan.content,
|
|
584
|
+
'Mounted TinyRobotChat in src/App.vue',
|
|
585
|
+
)
|
|
279
586
|
}
|
|
280
587
|
|
|
281
|
-
|
|
282
|
-
|
|
588
|
+
return { changes, results }
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function printChangeResults(targetDir, selectedFiles, results) {
|
|
592
|
+
if (isSelected(selectedFiles, 'Chat feature files')) {
|
|
593
|
+
for (const file of results.featureInspection) {
|
|
594
|
+
if (file.type === 'create') logSuccess(`Created ${formatRelative(file.target, targetDir)}`)
|
|
595
|
+
else if (file.type === 'skipped') logSkip(`${formatRelative(file.target, targetDir)} already exists`)
|
|
596
|
+
}
|
|
597
|
+
} else logSkippedSelection('Chat feature files')
|
|
598
|
+
|
|
599
|
+
if (isSelected(selectedFiles, 'main entry style imports')) {
|
|
600
|
+
if (results.style?.type === 'unavailable') logUnavailable('main entry style imports (main.ts/js not found)')
|
|
601
|
+
else if (results.style?.type === 'inserted') logSuccess('Inserted TinyRobot and Chat feature style imports')
|
|
602
|
+
else logSkip('TinyRobot and Chat feature style imports already exist')
|
|
603
|
+
} else logSkippedSelection('main entry style imports')
|
|
604
|
+
|
|
605
|
+
if (isSelected(selectedFiles, '.env.example')) {
|
|
606
|
+
if (results.env?.type === 'unavailable') logUnavailable('.env.example (template .env.example not found)')
|
|
607
|
+
else if (results.env?.type === 'create') logSuccess('Created .env.example')
|
|
608
|
+
else if (results.env?.type === 'merged') logSuccess(`Added ${results.env.added} env variables`)
|
|
609
|
+
else logSkip('.env.example already contains required variables')
|
|
610
|
+
} else logSkippedSelection('.env.example')
|
|
611
|
+
|
|
612
|
+
if (isSelected(selectedFiles, 'package.json')) {
|
|
613
|
+
for (const { name, result } of results.dependencies) printDependencyResult(result, name)
|
|
614
|
+
if (!results.dependencyChanged) logSkip('package.json already contains required dependencies')
|
|
615
|
+
} else logSkippedSelection('package.json')
|
|
616
|
+
|
|
617
|
+
if (isSelected(selectedFiles, 'App.vue mount')) {
|
|
618
|
+
if (results.mount?.type === 'merge') logSuccess('Mounted TinyRobotChat in src/App.vue')
|
|
619
|
+
else logSkip('App.vue already mounts TinyRobotChat')
|
|
283
620
|
}
|
|
621
|
+
}
|
|
284
622
|
|
|
285
|
-
|
|
623
|
+
async function addFeature(targetDir, type, options) {
|
|
624
|
+
invariant(type === 'chat', `unsupported feature: ${type}`)
|
|
625
|
+
const mountRequested = options.mount !== false
|
|
626
|
+
const featureFiles = getChatFeatureFiles(targetDir)
|
|
627
|
+
const mainFile = findMainEntry(targetDir)
|
|
628
|
+
const mountPlan = mountRequested ? planMount(targetDir) : null
|
|
629
|
+
const pkgPath = path.join(targetDir, 'package.json')
|
|
630
|
+
invariant(fs.existsSync(pkgPath), 'package.json not found.')
|
|
631
|
+
invariant(fs.statSync(pkgPath).isFile(), 'package.json is not a file.')
|
|
632
|
+
const pkg = readPackageJson(pkgPath)
|
|
633
|
+
const featureInspection = inspectFeatureFiles(featureFiles)
|
|
634
|
+
const selectedFiles = await selectFileChanges(targetDir, {
|
|
635
|
+
...options,
|
|
636
|
+
mount: mountRequested,
|
|
637
|
+
nonInteractive: options.nonInteractive,
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
validateSelection(selectedFiles, pkg, featureInspection, mountPlan, options.dryRun)
|
|
641
|
+
const prepared = prepareChanges(targetDir, selectedFiles, {
|
|
642
|
+
featureInspection,
|
|
643
|
+
mainFile,
|
|
644
|
+
mountPlan,
|
|
645
|
+
pkgPath,
|
|
646
|
+
pkg,
|
|
647
|
+
allowConflicts: options.dryRun,
|
|
648
|
+
})
|
|
286
649
|
|
|
650
|
+
if (options.dryRun) {
|
|
651
|
+
console.log('\nChange Plan\n')
|
|
652
|
+
for (const file of featureInspection)
|
|
653
|
+
console.log(
|
|
654
|
+
` ${file.type === 'create' ? '+' : file.type === 'conflict' ? '!' : '○'} ${formatRelative(file.target, targetDir)}`,
|
|
655
|
+
)
|
|
656
|
+
console.log(
|
|
657
|
+
` ${mainFile ? formatPlanStatus(prepared.results.style?.type) : '!'} ${mainFile ? formatRelative(mainFile, targetDir) : 'src/main.ts or src/main.js'} (style imports)`,
|
|
658
|
+
)
|
|
659
|
+
console.log(
|
|
660
|
+
` ${formatPlanStatus(prepared.results.env?.type ?? 'unavailable')} .env.example (${prepared.results.env?.type ?? 'not selected'})`,
|
|
661
|
+
)
|
|
662
|
+
const dependencyPlan =
|
|
663
|
+
prepared.results.dependencies.length > 0 ? prepared.results.dependencies : getDependencyPlan(pkg)
|
|
664
|
+
for (const { name, result } of dependencyPlan) {
|
|
665
|
+
const status = result.type === 'added' || result.type === 'updated' ? '~' : result.type === 'conflict' ? '!' : '○'
|
|
666
|
+
console.log(` ${status} package.json (${result.type}: ${name})`)
|
|
667
|
+
}
|
|
668
|
+
if (mountPlan)
|
|
669
|
+
console.log(
|
|
670
|
+
` ${mountPlan.type === 'merge' ? '~' : mountPlan.type === 'manual' ? '!' : '○'} src/App.vue (mount: ${mountPlan.type})`,
|
|
671
|
+
)
|
|
672
|
+
printManualMcpSetup()
|
|
673
|
+
return
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (selectedFiles.length === 0) {
|
|
677
|
+
logSkip('No changes selected.')
|
|
678
|
+
return
|
|
679
|
+
}
|
|
680
|
+
console.log('\nChange Results\n')
|
|
681
|
+
applyChanges(prepared.changes)
|
|
682
|
+
printChangeResults(targetDir, selectedFiles, prepared.results)
|
|
683
|
+
if (prepared.changes.length > 0) console.log(`\nSuccessfully added "${type}" feature to ${targetDir}`)
|
|
684
|
+
else logSkip('No changes were necessary.')
|
|
287
685
|
printNextSteps({
|
|
288
|
-
|
|
289
|
-
dependencyChanged:
|
|
290
|
-
|
|
291
|
-
|
|
686
|
+
mainFile,
|
|
687
|
+
dependencyChanged: prepared.results.dependencyChanged,
|
|
688
|
+
mounted: isSelected(selectedFiles, 'App.vue mount') && mountPlan?.type !== 'manual',
|
|
689
|
+
selectedFiles,
|
|
690
|
+
featureFilesSelected: isSelected(selectedFiles, 'Chat feature files'),
|
|
292
691
|
})
|
|
293
692
|
}
|
|
294
693
|
|
|
295
|
-
function printNextSteps({
|
|
296
|
-
const steps = [
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
"Example ('src/App.vue'):",
|
|
301
|
-
'',
|
|
302
|
-
' <script setup>',
|
|
303
|
-
" import TinyRobotChat from './TinyRobotChat.vue'",
|
|
304
|
-
' </script>',
|
|
305
|
-
'',
|
|
306
|
-
' <template>',
|
|
307
|
-
' <YourAppComponent />',
|
|
308
|
-
' <TinyRobotChat />',
|
|
309
|
-
' </template>',
|
|
310
|
-
].join('\n'),
|
|
311
|
-
]
|
|
312
|
-
|
|
313
|
-
if (envChanged) {
|
|
694
|
+
function printNextSteps({ mainFile, dependencyChanged, mounted, selectedFiles, featureFilesSelected }) {
|
|
695
|
+
const steps = []
|
|
696
|
+
if (featureFilesSelected && !mainFile)
|
|
697
|
+
steps.push("Import the package styles and './tiny-robot-chat/index.css' in your application entry file.")
|
|
698
|
+
if (featureFilesSelected && !mounted) {
|
|
314
699
|
steps.push(
|
|
315
700
|
[
|
|
316
|
-
'
|
|
701
|
+
'Render <TinyRobotChat /> near your main application component.',
|
|
317
702
|
'',
|
|
318
|
-
'
|
|
703
|
+
"Example ('src/App.vue'):",
|
|
319
704
|
'',
|
|
320
|
-
'
|
|
705
|
+
' <script setup lang="ts">',
|
|
706
|
+
" import TinyRobotChat from './tiny-robot-chat/TinyRobotChat.vue'",
|
|
707
|
+
' </script>',
|
|
708
|
+
'',
|
|
709
|
+
' <template>',
|
|
710
|
+
' <YourAppComponent />',
|
|
711
|
+
' <TinyRobotChat />',
|
|
712
|
+
' </template>',
|
|
321
713
|
].join('\n'),
|
|
322
714
|
)
|
|
323
715
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
console.log(`${index + 1}. ${step}\n`)
|
|
716
|
+
if (isSelected(selectedFiles, '.env.example'))
|
|
717
|
+
steps.push('Copy .env.example to .env.local and configure your AI provider API keys.')
|
|
718
|
+
if (featureFilesSelected)
|
|
719
|
+
steps.push('Add the Model Context MCP proxy to vite.config.* under server.proxy, then restart Vite.')
|
|
720
|
+
if (dependencyChanged) steps.push('Install or update project dependencies: pnpm install')
|
|
721
|
+
if (steps.length > 0) {
|
|
722
|
+
console.log('\nNext Steps\n')
|
|
723
|
+
for (const [index, step] of steps.entries()) console.log(`${index + 1}. ${step}\n`)
|
|
333
724
|
}
|
|
334
725
|
}
|
|
335
726
|
|
|
@@ -338,20 +729,24 @@ export function registerAddCommand(program) {
|
|
|
338
729
|
.command('add')
|
|
339
730
|
.description('Add a feature to the project')
|
|
340
731
|
.addArgument(new Argument('<type>', 'type of feature to add').choices(['chat']))
|
|
341
|
-
.
|
|
732
|
+
.option('--yes', 'apply all safe changes without prompts')
|
|
733
|
+
.option('--dry-run', 'print the change plan without modifying files')
|
|
734
|
+
.option('--mount', 'safely mount TinyRobotChat in src/App.vue (default)')
|
|
735
|
+
.option('--no-mount', 'keep App.vue unchanged and print the mount snippet')
|
|
736
|
+
.action(async (type, options) => {
|
|
342
737
|
try {
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
await addFeature(targetDir, type)
|
|
738
|
+
const nonInteractive = Boolean(options.yes || options.dryRun || !process.stdout.isTTY)
|
|
739
|
+
const targetDir = await resolveTargetPackage(process.cwd(), nonInteractive)
|
|
740
|
+
await addFeature(targetDir, type, { ...options, nonInteractive })
|
|
346
741
|
} catch (error) {
|
|
347
742
|
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
348
743
|
console.error('\nOperation cancelled.')
|
|
349
|
-
|
|
350
744
|
process.exit(1)
|
|
351
745
|
}
|
|
352
|
-
|
|
353
746
|
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
|
|
354
747
|
process.exit(1)
|
|
355
748
|
}
|
|
356
749
|
})
|
|
357
750
|
}
|
|
751
|
+
|
|
752
|
+
export { DEPENDENCIES, ensureDependency, ensureStyleImports, getChatFeatureFiles, planMount, resolveTargetPackage }
|