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