@opentiny/tiny-robot-cli 0.5.2-alpha.15 → 0.5.2-alpha.17
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 +1 -38
- package/bin/commands/add.js +268 -552
- package/bin/utils.js +44 -144
- package/package.json +3 -5
- package/templates/basic/.env.example +0 -8
- package/templates/basic/README.md +22 -53
- package/templates/basic/index.html +2 -2
- package/templates/basic/package.json +16 -12
- package/templates/basic/public/modelcontextprotocol.png +0 -0
- package/templates/basic/src/App.vue +119 -112
- package/templates/basic/src/components/ChatList.vue +82 -0
- package/templates/basic/src/components/ChatSender.vue +125 -0
- package/templates/basic/src/components/ConversationHistory.vue +136 -0
- package/templates/basic/src/components/HistoryDrawerButton.vue +43 -0
- package/templates/basic/src/components/McpServerPickerButton.vue +278 -0
- package/templates/basic/src/components/ThemeToggleButton.vue +44 -0
- package/templates/basic/src/components/icons/IconDeepThink.vue +29 -0
- package/templates/basic/src/components/icons/IconModelAliyunBailian.vue +51 -0
- package/templates/basic/src/components/icons/IconModelDeepseek.vue +29 -0
- package/templates/basic/src/components/icons/IconMoon.vue +29 -0
- package/templates/basic/src/components/icons/IconPlugin.vue +29 -0
- package/templates/basic/src/components/icons/IconSun.vue +35 -0
- package/templates/basic/src/components/icons/IconWebSearch.vue +36 -0
- package/templates/basic/src/components/icons/index.ts +7 -0
- package/templates/basic/src/composables/useChat.ts +129 -0
- package/templates/basic/src/composables/useMcp.ts +170 -0
- package/templates/basic/src/composables/useModel.ts +82 -0
- package/templates/basic/src/main.ts +2 -2
- package/templates/basic/src/mcpServers.ts +40 -0
- package/templates/basic/src/models.ts +81 -0
- package/templates/basic/tsconfig.app.json +5 -2
- package/templates/basic/tsconfig.node.json +6 -3
- package/templates/basic/vite.config.ts +1 -6
- package/templates/chat/.env.example +0 -9
- package/templates/chat/src/TinyRobotChat.vue +27 -88
- package/templates/basic/public/favicon.svg +0 -1
- package/templates/basic/public/icons.svg +0 -24
- package/templates/chat/README.md +0 -46
- package/templates/chat/index.html +0 -12
- package/templates/chat/package.json +0 -28
- package/templates/chat/src/components/ComposerTools.vue +0 -99
- package/templates/chat/src/components/WindowHeader.vue +0 -192
- package/templates/chat/src/composables/useWindow.ts +0 -83
- package/templates/chat/src/config/chat-runtime.ts +0 -82
- package/templates/chat/src/config/chat-ui.ts +0 -290
- package/templates/chat/src/index.css +0 -116
- package/templates/chat/src/main.ts +0 -7
- package/templates/chat/tsconfig.app.json +0 -13
- package/templates/chat/tsconfig.json +0 -7
- package/templates/chat/tsconfig.node.json +0 -19
- package/templates/chat/vite.config.ts +0 -16
package/bin/commands/add.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { checkbox, select } from '@inquirer/prompts'
|
|
2
|
-
import { compileTemplate, parse } from '@vue/compiler-sfc'
|
|
3
2
|
import { Argument } from 'commander'
|
|
4
3
|
import fs from 'node:fs'
|
|
5
4
|
import path from 'node:path'
|
|
@@ -7,6 +6,7 @@ import process from 'node:process'
|
|
|
7
6
|
import semver from 'semver'
|
|
8
7
|
|
|
9
8
|
import {
|
|
9
|
+
copyFile,
|
|
10
10
|
findProjectRoot,
|
|
11
11
|
findSubPackageRoot,
|
|
12
12
|
findWorkspacePackages,
|
|
@@ -16,22 +16,12 @@ import {
|
|
|
16
16
|
listPackages,
|
|
17
17
|
logSkip,
|
|
18
18
|
logSuccess,
|
|
19
|
-
|
|
19
|
+
mergeEnvFile,
|
|
20
20
|
} from '../utils.js'
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
'@opentiny/tiny-robot': TARGET_VERSION,
|
|
26
|
-
'@opentiny/tiny-robot-chat': TARGET_VERSION,
|
|
27
|
-
'@opentiny/tiny-robot-kit': TARGET_VERSION,
|
|
28
|
-
'@opentiny/tiny-robot-svgs': TARGET_VERSION,
|
|
29
|
-
'@vueuse/core': '13.1.0',
|
|
30
|
-
}
|
|
31
|
-
const PACKAGE_STYLE_IMPORTS = [
|
|
32
|
-
"import '@opentiny/tiny-robot/dist/style.css'",
|
|
33
|
-
"import '@opentiny/tiny-robot-chat/dist/style.css'",
|
|
34
|
-
]
|
|
22
|
+
const DEP_NAME = '@opentiny/tiny-robot'
|
|
23
|
+
const TARGET_VERSION = '0.5.1-alpha.2'
|
|
24
|
+
const STYLE_IMPORT = "import '@opentiny/tiny-robot/dist/style.css'"
|
|
35
25
|
|
|
36
26
|
function logUnavailable(label) {
|
|
37
27
|
logSkip(`${label} could not be applied`)
|
|
@@ -41,70 +31,40 @@ function logSkippedSelection(label) {
|
|
|
41
31
|
logSkip(`${label} change was not selected`)
|
|
42
32
|
}
|
|
43
33
|
|
|
44
|
-
async function resolveTargetPackage(cwd
|
|
34
|
+
async function resolveTargetPackage(cwd) {
|
|
45
35
|
const workspaceRoot = findWorkspaceRoot(cwd)
|
|
46
36
|
|
|
47
37
|
if (workspaceRoot) {
|
|
48
|
-
const workspacePatterns = findWorkspacePackages(workspaceRoot)
|
|
49
|
-
const packageDirs = listPackages(workspaceRoot, workspacePatterns)
|
|
50
38
|
const subPackageRoot = findSubPackageRoot(cwd, workspaceRoot)
|
|
51
39
|
|
|
52
40
|
if (subPackageRoot) {
|
|
53
|
-
|
|
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
|
+
return subPackageRoot
|
|
57
42
|
}
|
|
58
43
|
|
|
59
|
-
const
|
|
60
|
-
const normalizedWorkspaceRoot = path.resolve(workspaceRoot)
|
|
61
|
-
const rootPackageJson = path.join(workspaceRoot, 'package.json')
|
|
62
|
-
|
|
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
|
-
}
|
|
44
|
+
const workspacePatterns = findWorkspacePackages(workspaceRoot)
|
|
85
45
|
|
|
86
|
-
|
|
46
|
+
const packageDirs = listPackages(workspaceRoot, workspacePatterns)
|
|
87
47
|
|
|
88
|
-
|
|
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
|
-
}
|
|
48
|
+
invariant(packageDirs.length > 0, 'no packages found in workspace.')
|
|
95
49
|
|
|
96
50
|
return select({
|
|
97
51
|
message: 'Multi-package workspace detected, select a target package:',
|
|
98
52
|
choices: packageDirs.map((dir) => ({
|
|
99
|
-
name: path.
|
|
53
|
+
name: path.basename(dir),
|
|
100
54
|
value: dir,
|
|
101
55
|
})),
|
|
102
56
|
})
|
|
103
57
|
}
|
|
104
58
|
|
|
105
59
|
const projectRoot = findProjectRoot(cwd)
|
|
106
|
-
|
|
107
|
-
|
|
60
|
+
|
|
61
|
+
if (projectRoot) {
|
|
62
|
+
return projectRoot
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.error('Error: no package.json found.')
|
|
66
|
+
|
|
67
|
+
process.exit(1)
|
|
108
68
|
}
|
|
109
69
|
|
|
110
70
|
function getTemplateFile(...segments) {
|
|
@@ -112,299 +72,200 @@ function getTemplateFile(...segments) {
|
|
|
112
72
|
}
|
|
113
73
|
|
|
114
74
|
function readPackageJson(pkgPath) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
75
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function writePackageJson(pkgPath, pkg) {
|
|
79
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
|
|
118
80
|
}
|
|
119
81
|
|
|
120
82
|
function findMainEntry(targetDir) {
|
|
121
83
|
for (const file of ['src/main.ts', 'src/main.js']) {
|
|
122
84
|
const fullPath = path.join(targetDir, file)
|
|
123
|
-
|
|
85
|
+
|
|
86
|
+
if (fs.existsSync(fullPath)) {
|
|
87
|
+
return fullPath
|
|
88
|
+
}
|
|
124
89
|
}
|
|
90
|
+
|
|
125
91
|
return null
|
|
126
92
|
}
|
|
127
93
|
|
|
128
|
-
function
|
|
129
|
-
const
|
|
130
|
-
return match?.[1] ?? null
|
|
131
|
-
}
|
|
94
|
+
function ensureStyleImport(mainFile) {
|
|
95
|
+
const content = fs.readFileSync(mainFile, 'utf-8')
|
|
132
96
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
97
|
+
if (content.includes(STYLE_IMPORT)) {
|
|
98
|
+
return {
|
|
99
|
+
type: 'skipped',
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const lines = content.split('\n')
|
|
138
104
|
|
|
139
105
|
let lastImportIndex = -1
|
|
106
|
+
|
|
140
107
|
for (let i = 0; i < lines.length; i++) {
|
|
141
108
|
if (/^\s*import\s/.test(lines[i])) {
|
|
142
109
|
lastImportIndex = i
|
|
143
110
|
continue
|
|
144
111
|
}
|
|
145
|
-
if (lastImportIndex !== -1) break
|
|
146
|
-
}
|
|
147
112
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
113
|
+
if (lastImportIndex !== -1) {
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
}
|
|
152
117
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
118
|
+
if (lastImportIndex === -1) {
|
|
119
|
+
lines.unshift(STYLE_IMPORT)
|
|
120
|
+
} else {
|
|
121
|
+
lines.splice(lastImportIndex + 1, 0, STYLE_IMPORT)
|
|
157
122
|
}
|
|
158
|
-
return { type: after === before ? 'skipped' : 'inserted', content: after }
|
|
159
|
-
}
|
|
160
123
|
|
|
161
|
-
|
|
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
|
-
}
|
|
124
|
+
fs.writeFileSync(mainFile, lines.join('\n'))
|
|
167
125
|
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
}
|
|
126
|
+
return {
|
|
127
|
+
type: 'inserted',
|
|
174
128
|
}
|
|
175
|
-
return matches
|
|
176
129
|
}
|
|
177
130
|
|
|
178
131
|
function insertDependencyOrdered(dependencies, name, version) {
|
|
179
132
|
const next = {}
|
|
133
|
+
|
|
180
134
|
let inserted = false
|
|
135
|
+
|
|
181
136
|
for (const [key, value] of Object.entries(dependencies)) {
|
|
182
137
|
if (!inserted && name < key) {
|
|
183
138
|
next[name] = version
|
|
184
139
|
inserted = true
|
|
185
140
|
}
|
|
141
|
+
|
|
186
142
|
next[key] = value
|
|
187
143
|
}
|
|
188
|
-
|
|
144
|
+
|
|
145
|
+
if (!inserted) {
|
|
146
|
+
next[name] = version
|
|
147
|
+
}
|
|
148
|
+
|
|
189
149
|
return next
|
|
190
150
|
}
|
|
191
151
|
|
|
192
152
|
function ensureDependency(pkg, name, targetVersion) {
|
|
193
|
-
|
|
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
|
-
}
|
|
153
|
+
pkg.dependencies ??= {}
|
|
199
154
|
|
|
200
|
-
const
|
|
201
|
-
if (existing.length > 1) {
|
|
202
|
-
return { type: 'conflict', reason: `${name} is declared in multiple dependency sections` }
|
|
203
|
-
}
|
|
155
|
+
const currentVersion = pkg.dependencies[name]
|
|
204
156
|
|
|
205
|
-
|
|
206
|
-
|
|
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 ??= {}
|
|
157
|
+
// 1. 不存在 => 新增(使用 ordered insert)
|
|
158
|
+
if (!currentVersion) {
|
|
213
159
|
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
|
-
}
|
|
221
160
|
|
|
222
|
-
if (typeof dependency.version !== 'string' || !semver.validRange(dependency.version)) {
|
|
223
161
|
return {
|
|
224
|
-
type: '
|
|
225
|
-
|
|
162
|
+
type: 'added',
|
|
163
|
+
to: targetVersion,
|
|
226
164
|
}
|
|
227
165
|
}
|
|
228
166
|
|
|
229
|
-
|
|
167
|
+
// 2. 已存在 => 只更新版本,不调整顺序
|
|
168
|
+
const current = semver.valid(currentVersion)
|
|
230
169
|
|
|
231
|
-
if (
|
|
170
|
+
if (current !== targetVersion) {
|
|
171
|
+
pkg.dependencies[name] = targetVersion
|
|
232
172
|
|
|
233
|
-
const minimum = typeof dependency.version === 'string' ? semver.minVersion(dependency.version) : null
|
|
234
|
-
if (
|
|
235
|
-
typeof dependency.version === 'string' &&
|
|
236
|
-
((semver.valid(dependency.version) && semver.gte(dependency.version, targetVersion)) ||
|
|
237
|
-
(minimum && semver.gte(minimum, targetVersion)))
|
|
238
|
-
) {
|
|
239
|
-
return { type: 'skipped', section: dependency.section, version: dependency.version }
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (dependency.section !== 'dependencies') {
|
|
243
173
|
return {
|
|
244
|
-
type: '
|
|
245
|
-
|
|
174
|
+
type: 'updated',
|
|
175
|
+
from: currentVersion,
|
|
176
|
+
to: targetVersion,
|
|
246
177
|
}
|
|
247
178
|
}
|
|
248
179
|
|
|
249
|
-
|
|
180
|
+
// 3. 完全一致 => 跳过
|
|
250
181
|
return {
|
|
251
|
-
type: '
|
|
252
|
-
section: dependency.section,
|
|
253
|
-
from: dependency.version,
|
|
254
|
-
to: targetVersion,
|
|
182
|
+
type: 'skipped',
|
|
255
183
|
}
|
|
256
184
|
}
|
|
257
185
|
|
|
258
186
|
function printDependencyResult(result, name) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
}
|
|
187
|
+
switch (result.type) {
|
|
188
|
+
case 'added':
|
|
189
|
+
logSuccess(`Added ${name}@${TARGET_VERSION}`)
|
|
190
|
+
break
|
|
269
191
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
192
|
+
case 'updated':
|
|
193
|
+
logSuccess(`Updated ${name} from ${result.from} to ${result.to}`)
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
case 'skipped':
|
|
197
|
+
logSkip(`${name} already satisfies required version`)
|
|
198
|
+
break
|
|
277
199
|
}
|
|
278
|
-
return files
|
|
279
200
|
}
|
|
280
201
|
|
|
281
202
|
function getChatFeatureFiles(targetDir) {
|
|
282
|
-
const
|
|
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) }))
|
|
286
|
-
|
|
287
|
-
featureFiles.push({
|
|
288
|
-
source: getTemplateFile('src', 'index.css'),
|
|
289
|
-
target: path.join(targetDir, CHAT_ADD_FEATURE_DIR, 'index.css'),
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
return featureFiles
|
|
293
|
-
}
|
|
294
|
-
|
|
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
|
-
}
|
|
300
|
-
|
|
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
|
-
}
|
|
309
|
-
|
|
310
|
-
return { type: 'parsed', block: parsed.descriptor.template, components: compiled.ast.components ?? [] }
|
|
311
|
-
}
|
|
203
|
+
const mainEntry = findMainEntry(targetDir)
|
|
312
204
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
205
|
+
return [
|
|
206
|
+
{
|
|
207
|
+
label: 'TinyRobotChat.vue',
|
|
208
|
+
target: path.join(targetDir, 'src/TinyRobotChat.vue'),
|
|
209
|
+
template: getTemplateFile('src', 'TinyRobotChat.vue'),
|
|
210
|
+
action(fileExists) {
|
|
211
|
+
return fileExists ? 'overwrite' : 'create'
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
label: 'main entry style import',
|
|
216
|
+
target: mainEntry,
|
|
217
|
+
action() {
|
|
218
|
+
return 'modify'
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
label: '.env',
|
|
223
|
+
target: path.join(targetDir, '.env'),
|
|
224
|
+
template: getTemplateFile('.env.example'),
|
|
225
|
+
action(fileExists) {
|
|
226
|
+
return fileExists ? 'modify' : 'create'
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
{
|
|
231
|
+
label: 'package.json',
|
|
232
|
+
target: path.join(targetDir, 'package.json'),
|
|
233
|
+
action() {
|
|
234
|
+
return 'modify'
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function selectFileChanges(files) {
|
|
241
|
+
return checkbox({
|
|
242
|
+
message: 'Select which file changes to apply (all selected by default):',
|
|
243
|
+
choices: files.map((file) => {
|
|
244
|
+
const exists = file.target ? fs.existsSync(file.target) : false
|
|
316
245
|
|
|
317
|
-
|
|
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
|
-
}
|
|
246
|
+
const disabled = !file.target
|
|
350
247
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
} else {
|
|
358
|
-
after = `<script setup lang="ts">\n${importStatement}\n</script>\n\n${before}`
|
|
359
|
-
}
|
|
248
|
+
const descriptions = {
|
|
249
|
+
'TinyRobotChat.vue': 'integrate TinyRobot chat component',
|
|
250
|
+
'main entry style import': 'import TinyRobot styles',
|
|
251
|
+
'.env': 'add environment variables',
|
|
252
|
+
'package.json': 'add TinyRobot dependencies',
|
|
253
|
+
}
|
|
360
254
|
|
|
361
|
-
|
|
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
|
-
}
|
|
255
|
+
const description = descriptions[file.label]
|
|
371
256
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const same = fs.readFileSync(file.source).equals(fs.readFileSync(file.target))
|
|
377
|
-
return { ...file, type: same ? 'skipped' : 'conflict' }
|
|
378
|
-
})
|
|
379
|
-
}
|
|
257
|
+
return {
|
|
258
|
+
name: disabled
|
|
259
|
+
? `${file.action(false)} ${file.label} — ${description} (main.ts/js not found)`
|
|
260
|
+
: `${file.action(exists)} ${file.label} — ${description}`,
|
|
380
261
|
|
|
381
|
-
|
|
382
|
-
return path.relative(targetDir, file).replaceAll('\\', '/')
|
|
383
|
-
}
|
|
262
|
+
value: file.label,
|
|
384
263
|
|
|
385
|
-
|
|
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
|
-
}
|
|
264
|
+
checked: !disabled,
|
|
395
265
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
})),
|
|
266
|
+
disabled,
|
|
267
|
+
}
|
|
268
|
+
}),
|
|
408
269
|
})
|
|
409
270
|
}
|
|
410
271
|
|
|
@@ -412,315 +273,174 @@ function isSelected(selectedFiles, label) {
|
|
|
412
273
|
return selectedFiles.includes(label)
|
|
413
274
|
}
|
|
414
275
|
|
|
415
|
-
function
|
|
416
|
-
|
|
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')
|
|
276
|
+
async function addFeature(targetDir, type) {
|
|
277
|
+
invariant(type === 'chat', `unsupported feature: ${type}`)
|
|
427
278
|
|
|
428
|
-
|
|
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.')
|
|
279
|
+
const files = getChatFeatureFiles(targetDir)
|
|
431
280
|
|
|
432
|
-
const
|
|
433
|
-
return { ...result, targetFile }
|
|
434
|
-
}
|
|
281
|
+
const selectedFiles = await selectFileChanges(files)
|
|
435
282
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
return '○'
|
|
441
|
-
}
|
|
283
|
+
if (selectedFiles.length === 0) {
|
|
284
|
+
logSkip('No changes selected.')
|
|
285
|
+
return
|
|
286
|
+
}
|
|
442
287
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
288
|
+
const componentFile = files.find((f) => {
|
|
289
|
+
return f.label === 'TinyRobotChat.vue'
|
|
290
|
+
})
|
|
446
291
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
292
|
+
const envFile = files.find((f) => {
|
|
293
|
+
return f.label === '.env'
|
|
294
|
+
})
|
|
450
295
|
|
|
451
|
-
|
|
452
|
-
if (fs.existsSync(target)) invariant(fs.statSync(target).isFile(), `${target} exists and is not a file.`)
|
|
296
|
+
invariant(componentFile, 'TinyRobotChat.vue config missing.')
|
|
453
297
|
|
|
454
|
-
|
|
455
|
-
if (fs.existsSync(target) && fs.readFileSync(target).equals(next)) return false
|
|
298
|
+
invariant(envFile, '.env config missing.')
|
|
456
299
|
|
|
457
|
-
|
|
458
|
-
return true
|
|
459
|
-
}
|
|
300
|
+
console.log('\nChange Results\n')
|
|
460
301
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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 })
|
|
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
|
-
}
|
|
302
|
+
let needsManualStyleImport = false
|
|
303
|
+
let envChanged = false
|
|
304
|
+
let dependencyChanged = false
|
|
489
305
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
)
|
|
306
|
+
if (isSelected(selectedFiles, 'TinyRobotChat.vue')) {
|
|
307
|
+
copyFile(componentFile.template, componentFile.target)
|
|
308
|
+
logSuccess(`Copied ${componentFile.label}`)
|
|
309
|
+
} else {
|
|
310
|
+
logSkippedSelection(componentFile.label)
|
|
493
311
|
}
|
|
494
|
-
}
|
|
495
|
-
|
|
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')
|
|
499
312
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
313
|
+
const mainFile = files.find((f) => {
|
|
314
|
+
return f.label === 'main entry style import'
|
|
315
|
+
})
|
|
503
316
|
|
|
504
|
-
if (!
|
|
505
|
-
|
|
506
|
-
}
|
|
317
|
+
if (!mainFile?.target) {
|
|
318
|
+
needsManualStyleImport = true
|
|
507
319
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
320
|
+
logUnavailable('main entry style import (main.ts/js not found)')
|
|
321
|
+
} else {
|
|
322
|
+
if (isSelected(selectedFiles, mainFile.label)) {
|
|
323
|
+
const result = ensureStyleImport(mainFile.target)
|
|
511
324
|
|
|
512
|
-
|
|
325
|
+
switch (result.type) {
|
|
326
|
+
case 'inserted':
|
|
327
|
+
logSuccess('Inserted TinyRobot style import')
|
|
328
|
+
break
|
|
513
329
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
330
|
+
case 'skipped':
|
|
331
|
+
logSkip('TinyRobot style import already exists')
|
|
332
|
+
break
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
needsManualStyleImport = true
|
|
521
336
|
|
|
522
|
-
|
|
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
|
-
)
|
|
337
|
+
logSkippedSelection(mainFile.label)
|
|
542
338
|
}
|
|
543
339
|
}
|
|
544
340
|
|
|
545
|
-
if (isSelected(selectedFiles, '
|
|
546
|
-
|
|
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
|
-
}
|
|
556
|
-
}
|
|
341
|
+
if (isSelected(selectedFiles, '.env')) {
|
|
342
|
+
const envResult = mergeEnvFile(envFile.template, envFile.target)
|
|
557
343
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
}
|
|
344
|
+
switch (envResult.type) {
|
|
345
|
+
case 'created':
|
|
346
|
+
envChanged = true
|
|
565
347
|
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
}
|
|
348
|
+
logSuccess('Created .env')
|
|
349
|
+
break
|
|
576
350
|
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
)
|
|
586
|
-
}
|
|
351
|
+
case 'merged':
|
|
352
|
+
envChanged = true
|
|
587
353
|
|
|
588
|
-
|
|
589
|
-
|
|
354
|
+
logSuccess(`Added ${envResult.added} env variables`)
|
|
355
|
+
break
|
|
590
356
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (file.type === 'create') logSuccess(`Created ${formatRelative(file.target, targetDir)}`)
|
|
595
|
-
else if (file.type === 'skipped') logSkip(`${formatRelative(file.target, targetDir)} already exists`)
|
|
357
|
+
case 'skipped':
|
|
358
|
+
logSkip('.env already contains required variables')
|
|
359
|
+
break
|
|
596
360
|
}
|
|
597
|
-
} else
|
|
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')
|
|
361
|
+
} else {
|
|
362
|
+
logSkippedSelection('.env')
|
|
620
363
|
}
|
|
621
|
-
}
|
|
622
364
|
|
|
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
365
|
const pkgPath = path.join(targetDir, 'package.json')
|
|
366
|
+
|
|
630
367
|
invariant(fs.existsSync(pkgPath), 'package.json not found.')
|
|
631
|
-
|
|
368
|
+
|
|
632
369
|
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
370
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
featureInspection,
|
|
643
|
-
mainFile,
|
|
644
|
-
mountPlan,
|
|
645
|
-
pkgPath,
|
|
646
|
-
pkg,
|
|
647
|
-
allowConflicts: options.dryRun,
|
|
648
|
-
})
|
|
371
|
+
if (isSelected(selectedFiles, 'package.json')) {
|
|
372
|
+
const result = ensureDependency(pkg, DEP_NAME, TARGET_VERSION)
|
|
649
373
|
|
|
650
|
-
|
|
651
|
-
|
|
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})`)
|
|
374
|
+
if (result.type !== 'skipped') {
|
|
375
|
+
dependencyChanged = true
|
|
667
376
|
}
|
|
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
377
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
378
|
+
writePackageJson(pkgPath, pkg)
|
|
379
|
+
|
|
380
|
+
printDependencyResult(result, DEP_NAME)
|
|
381
|
+
} else {
|
|
382
|
+
logSkippedSelection('package.json')
|
|
679
383
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
if (prepared.changes.length > 0) console.log(`\nSuccessfully added "${type}" feature to ${targetDir}`)
|
|
684
|
-
else logSkip('No changes were necessary.')
|
|
384
|
+
|
|
385
|
+
console.log(`\nSuccessfully added "${type}" feature to ${targetDir}`)
|
|
386
|
+
|
|
685
387
|
printNextSteps({
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
selectedFiles,
|
|
690
|
-
featureFilesSelected: isSelected(selectedFiles, 'Chat feature files'),
|
|
388
|
+
needsManualStyleImport,
|
|
389
|
+
envChanged,
|
|
390
|
+
dependencyChanged,
|
|
691
391
|
})
|
|
692
392
|
}
|
|
693
393
|
|
|
694
|
-
function printNextSteps({
|
|
394
|
+
function printNextSteps({ needsManualStyleImport, envChanged, dependencyChanged }) {
|
|
695
395
|
const steps = []
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
396
|
+
|
|
397
|
+
if (needsManualStyleImport) {
|
|
398
|
+
steps.push(
|
|
399
|
+
['Import TinyRobot styles in your application entry file.', '', 'Example:', '', ` ${STYLE_IMPORT}`].join('\n'),
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
steps.push(
|
|
404
|
+
[
|
|
405
|
+
'Render <TinyRobotChat /> near your main application component.',
|
|
406
|
+
'',
|
|
407
|
+
"Example ('src/App.vue'):",
|
|
408
|
+
'',
|
|
409
|
+
' <script setup>',
|
|
410
|
+
" import TinyRobotChat from './TinyRobotChat.vue'",
|
|
411
|
+
' </script>',
|
|
412
|
+
'',
|
|
413
|
+
' <template>',
|
|
414
|
+
' <YourAppComponent />',
|
|
415
|
+
' <TinyRobotChat />',
|
|
416
|
+
' </template>',
|
|
417
|
+
].join('\n'),
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
if (envChanged) {
|
|
699
421
|
steps.push(
|
|
700
422
|
[
|
|
701
|
-
'
|
|
423
|
+
'Configure your AI provider API key in the .env file.',
|
|
702
424
|
'',
|
|
703
|
-
|
|
425
|
+
'Example:',
|
|
704
426
|
'',
|
|
705
|
-
'
|
|
706
|
-
" import TinyRobotChat from './tiny-robot-chat/TinyRobotChat.vue'",
|
|
707
|
-
' </script>',
|
|
708
|
-
'',
|
|
709
|
-
' <template>',
|
|
710
|
-
' <YourAppComponent />',
|
|
711
|
-
' <TinyRobotChat />',
|
|
712
|
-
' </template>',
|
|
427
|
+
' VITE_DEEPSEEK_API_KEY=your_api_key',
|
|
713
428
|
].join('\n'),
|
|
714
429
|
)
|
|
715
430
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
if (steps.length
|
|
722
|
-
|
|
723
|
-
|
|
431
|
+
|
|
432
|
+
if (dependencyChanged) {
|
|
433
|
+
steps.push(['Install or update project dependencies.', '', 'Example:', '', ' pnpm install'].join('\n'))
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (steps.length === 0) {
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
console.log('\nNext Steps\n')
|
|
441
|
+
|
|
442
|
+
for (const [index, step] of steps.entries()) {
|
|
443
|
+
console.log(`${index + 1}. ${step}\n`)
|
|
724
444
|
}
|
|
725
445
|
}
|
|
726
446
|
|
|
@@ -729,24 +449,20 @@ export function registerAddCommand(program) {
|
|
|
729
449
|
.command('add')
|
|
730
450
|
.description('Add a feature to the project')
|
|
731
451
|
.addArgument(new Argument('<type>', 'type of feature to add').choices(['chat']))
|
|
732
|
-
.
|
|
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) => {
|
|
452
|
+
.action(async (type) => {
|
|
737
453
|
try {
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
await addFeature(targetDir, type
|
|
454
|
+
const targetDir = await resolveTargetPackage(process.cwd())
|
|
455
|
+
|
|
456
|
+
await addFeature(targetDir, type)
|
|
741
457
|
} catch (error) {
|
|
742
458
|
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
743
459
|
console.error('\nOperation cancelled.')
|
|
460
|
+
|
|
744
461
|
process.exit(1)
|
|
745
462
|
}
|
|
463
|
+
|
|
746
464
|
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
|
|
747
465
|
process.exit(1)
|
|
748
466
|
}
|
|
749
467
|
})
|
|
750
468
|
}
|
|
751
|
-
|
|
752
|
-
export { DEPENDENCIES, ensureDependency, ensureStyleImports, getChatFeatureFiles, planMount, resolveTargetPackage }
|