@opentiny/tiny-robot-cli 0.5.2-alpha.13 → 0.5.2-alpha.15
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 +589 -198
- package/bin/utils.js +139 -47
- 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 +23 -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,55 +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.15'
|
|
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
|
-
if (
|
|
63
|
+
if (
|
|
64
|
+
normalizedCwd === normalizedWorkspaceRoot &&
|
|
65
|
+
workspacePatterns === undefined &&
|
|
66
|
+
packageDirs.length === 1 &&
|
|
67
|
+
packageDirs[0] === path.resolve(workspaceRoot) &&
|
|
68
|
+
fs.existsSync(rootPackageJson)
|
|
69
|
+
)
|
|
47
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
|
+
)
|
|
48
76
|
}
|
|
49
77
|
|
|
50
|
-
|
|
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
|
+
}
|
|
51
95
|
|
|
52
96
|
return select({
|
|
53
97
|
message: 'Multi-package workspace detected, select a target package:',
|
|
54
98
|
choices: packageDirs.map((dir) => ({
|
|
55
|
-
name: path.
|
|
99
|
+
name: path.relative(workspaceRoot, dir).replaceAll('\\', '/') || '(workspace root)',
|
|
56
100
|
value: dir,
|
|
57
101
|
})),
|
|
58
102
|
})
|
|
59
103
|
}
|
|
60
104
|
|
|
61
105
|
const projectRoot = findProjectRoot(cwd)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return projectRoot
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
console.error('Error: no package.json found.')
|
|
68
|
-
process.exit(1)
|
|
106
|
+
if (projectRoot) return projectRoot
|
|
107
|
+
throw new Error('no package.json found.')
|
|
69
108
|
}
|
|
70
109
|
|
|
71
110
|
function getTemplateFile(...segments) {
|
|
@@ -73,267 +112,615 @@ function getTemplateFile(...segments) {
|
|
|
73
112
|
}
|
|
74
113
|
|
|
75
114
|
function readPackageJson(pkgPath) {
|
|
76
|
-
|
|
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
|
|
77
118
|
}
|
|
78
119
|
|
|
79
|
-
function
|
|
80
|
-
|
|
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
|
|
81
176
|
}
|
|
82
177
|
|
|
83
178
|
function insertDependencyOrdered(dependencies, name, version) {
|
|
84
179
|
const next = {}
|
|
85
180
|
let inserted = false
|
|
86
|
-
|
|
87
181
|
for (const [key, value] of Object.entries(dependencies)) {
|
|
88
182
|
if (!inserted && name < key) {
|
|
89
183
|
next[name] = version
|
|
90
184
|
inserted = true
|
|
91
185
|
}
|
|
92
|
-
|
|
93
186
|
next[key] = value
|
|
94
187
|
}
|
|
95
|
-
|
|
96
|
-
if (!inserted) {
|
|
97
|
-
next[name] = version
|
|
98
|
-
}
|
|
99
|
-
|
|
188
|
+
if (!inserted) next[name] = version
|
|
100
189
|
return next
|
|
101
190
|
}
|
|
102
191
|
|
|
103
192
|
function ensureDependency(pkg, name, targetVersion) {
|
|
104
|
-
|
|
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
|
+
}
|
|
105
199
|
|
|
106
|
-
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
|
+
}
|
|
107
204
|
|
|
108
|
-
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 ??= {}
|
|
109
213
|
pkg.dependencies = insertDependencyOrdered(pkg.dependencies, name, targetVersion)
|
|
214
|
+
return { type: 'added', to: targetVersion, section: 'dependencies' }
|
|
215
|
+
}
|
|
110
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
|
+
|
|
222
|
+
if (typeof dependency.version !== 'string' || !semver.validRange(dependency.version)) {
|
|
111
223
|
return {
|
|
112
|
-
type: '
|
|
113
|
-
|
|
224
|
+
type: 'conflict',
|
|
225
|
+
reason: `${name} has an unsupported version specifier (${String(dependency.version)})`,
|
|
114
226
|
}
|
|
115
227
|
}
|
|
116
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
|
|
117
234
|
if (
|
|
118
|
-
|
|
119
|
-
semver.
|
|
235
|
+
typeof dependency.version === 'string' &&
|
|
236
|
+
((semver.valid(dependency.version) && semver.gte(dependency.version, targetVersion)) ||
|
|
237
|
+
(minimum && semver.gte(minimum, targetVersion)))
|
|
120
238
|
) {
|
|
239
|
+
return { type: 'skipped', section: dependency.section, version: dependency.version }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (dependency.section !== 'dependencies') {
|
|
121
243
|
return {
|
|
122
|
-
type: '
|
|
244
|
+
type: 'conflict',
|
|
245
|
+
reason: `${name}@${dependency.version} in ${dependency.section} does not satisfy ${targetVersion}`,
|
|
123
246
|
}
|
|
124
247
|
}
|
|
125
248
|
|
|
249
|
+
pkg.dependencies[name] = targetVersion
|
|
126
250
|
return {
|
|
127
|
-
type: '
|
|
128
|
-
|
|
251
|
+
type: 'updated',
|
|
252
|
+
section: dependency.section,
|
|
253
|
+
from: dependency.version,
|
|
129
254
|
to: targetVersion,
|
|
130
255
|
}
|
|
131
256
|
}
|
|
132
257
|
|
|
133
258
|
function printDependencyResult(result, name) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
}
|
|
138
269
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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 })
|
|
142
277
|
}
|
|
278
|
+
return files
|
|
143
279
|
}
|
|
144
280
|
|
|
145
|
-
function
|
|
146
|
-
const
|
|
147
|
-
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) }))
|
|
148
286
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
source: path.join(templateSource, 'TinyRobotChat.vue'),
|
|
153
|
-
target: path.join(targetDir, 'src/TinyRobotChat.vue'),
|
|
154
|
-
type: 'file',
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
label: 'Chat UI components',
|
|
158
|
-
source: path.join(templateSource, 'tiny-robot-chat/components'),
|
|
159
|
-
target: path.join(supportTarget, 'components'),
|
|
160
|
-
type: 'directory',
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
label: 'Chat composables',
|
|
164
|
-
source: path.join(templateSource, 'tiny-robot-chat/composables'),
|
|
165
|
-
target: path.join(supportTarget, 'composables'),
|
|
166
|
-
type: 'directory',
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
label: 'Chat configuration',
|
|
170
|
-
source: path.join(templateSource, 'tiny-robot-chat/config'),
|
|
171
|
-
target: path.join(supportTarget, 'config'),
|
|
172
|
-
type: 'directory',
|
|
173
|
-
},
|
|
174
|
-
]
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function findCopyConflicts(source, target) {
|
|
178
|
-
if (!fs.existsSync(target)) {
|
|
179
|
-
return []
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const sourceStat = fs.statSync(source)
|
|
183
|
-
const targetStat = fs.statSync(target)
|
|
184
|
-
|
|
185
|
-
if (!sourceStat.isDirectory()) {
|
|
186
|
-
return [target]
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (!targetStat.isDirectory()) {
|
|
190
|
-
return [target]
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return fs.readdirSync(source, { withFileTypes: true }).flatMap((entry) => {
|
|
194
|
-
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'),
|
|
195
290
|
})
|
|
291
|
+
|
|
292
|
+
return featureFiles
|
|
196
293
|
}
|
|
197
294
|
|
|
198
|
-
|
|
199
|
-
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
|
+
}
|
|
200
300
|
|
|
201
|
-
|
|
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
|
+
}
|
|
202
309
|
|
|
203
|
-
|
|
204
|
-
|
|
310
|
+
return { type: 'parsed', block: parsed.descriptor.template, components: compiled.ast.components ?? [] }
|
|
311
|
+
}
|
|
205
312
|
|
|
206
|
-
|
|
207
|
-
|
|
313
|
+
function findTemplateClose(block) {
|
|
314
|
+
return block.loc.end.offset
|
|
315
|
+
}
|
|
208
316
|
|
|
209
|
-
|
|
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
|
+
}
|
|
350
|
+
|
|
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}`
|
|
210
359
|
}
|
|
211
360
|
|
|
212
|
-
const
|
|
213
|
-
|
|
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
|
+
}
|
|
214
371
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
}
|
|
218
380
|
|
|
219
|
-
|
|
381
|
+
function formatRelative(file, targetDir) {
|
|
382
|
+
return path.relative(targetDir, file).replaceAll('\\', '/')
|
|
383
|
+
}
|
|
220
384
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
+
}
|
|
225
395
|
|
|
226
|
-
|
|
227
|
-
|
|
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.`)
|
|
453
|
+
|
|
454
|
+
const next = fileContent(content)
|
|
455
|
+
if (fs.existsSync(target) && fs.readFileSync(target).equals(next)) return false
|
|
228
456
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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 })
|
|
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)
|
|
234
488
|
}
|
|
489
|
+
|
|
490
|
+
throw new Error(
|
|
491
|
+
`Failed to apply changes; all changes were rolled back: ${error instanceof Error ? error.message : String(error)}`,
|
|
492
|
+
)
|
|
235
493
|
}
|
|
494
|
+
}
|
|
236
495
|
|
|
237
|
-
|
|
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')
|
|
238
499
|
|
|
239
|
-
if (
|
|
240
|
-
|
|
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.')
|
|
241
502
|
}
|
|
242
503
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
envResult,
|
|
246
|
-
dependencyResults,
|
|
504
|
+
if (!featureFilesSelected && featureFilesMissing && isSelected(selectedFiles, 'App.vue mount')) {
|
|
505
|
+
throw new Error('Select Chat feature files before mounting TinyRobotChat in App.vue.')
|
|
247
506
|
}
|
|
248
|
-
}
|
|
249
507
|
|
|
250
|
-
|
|
251
|
-
|
|
508
|
+
if (isSelected(selectedFiles, 'App.vue mount') && mountPlan?.type === 'manual' && !allowManualMount) {
|
|
509
|
+
throw new Error(`cannot safely mount TinyRobotChat: ${mountPlan.reason}`)
|
|
510
|
+
}
|
|
252
511
|
|
|
253
|
-
|
|
254
|
-
message: `Add the complete TinyRobot chat feature to ${targetDir}?`,
|
|
255
|
-
default: true,
|
|
256
|
-
})
|
|
512
|
+
if (isSelected(selectedFiles, 'package.json')) return
|
|
257
513
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
)
|
|
261
519
|
}
|
|
520
|
+
}
|
|
262
521
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
+
}
|
|
266
544
|
|
|
267
|
-
|
|
268
|
-
|
|
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
|
+
}
|
|
269
556
|
}
|
|
270
557
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
+
}
|
|
275
565
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
+
}
|
|
279
576
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
+
)
|
|
283
586
|
}
|
|
284
587
|
|
|
285
|
-
|
|
286
|
-
|
|
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')
|
|
287
620
|
}
|
|
621
|
+
}
|
|
622
|
+
|
|
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
|
+
})
|
|
288
649
|
|
|
289
|
-
|
|
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
|
+
}
|
|
290
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.')
|
|
291
685
|
printNextSteps({
|
|
292
|
-
|
|
293
|
-
dependencyChanged:
|
|
294
|
-
|
|
295
|
-
|
|
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'),
|
|
296
691
|
})
|
|
297
692
|
}
|
|
298
693
|
|
|
299
|
-
function printNextSteps({
|
|
300
|
-
const steps = [
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
"Example ('src/App.vue'):",
|
|
305
|
-
'',
|
|
306
|
-
' <script setup>',
|
|
307
|
-
" import TinyRobotChat from './TinyRobotChat.vue'",
|
|
308
|
-
' </script>',
|
|
309
|
-
'',
|
|
310
|
-
' <template>',
|
|
311
|
-
' <YourAppComponent />',
|
|
312
|
-
' <TinyRobotChat />',
|
|
313
|
-
' </template>',
|
|
314
|
-
].join('\n'),
|
|
315
|
-
]
|
|
316
|
-
|
|
317
|
-
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) {
|
|
318
699
|
steps.push(
|
|
319
700
|
[
|
|
320
|
-
'
|
|
701
|
+
'Render <TinyRobotChat /> near your main application component.',
|
|
321
702
|
'',
|
|
322
|
-
'
|
|
703
|
+
"Example ('src/App.vue'):",
|
|
323
704
|
'',
|
|
324
|
-
'
|
|
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>',
|
|
325
713
|
].join('\n'),
|
|
326
714
|
)
|
|
327
715
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
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`)
|
|
337
724
|
}
|
|
338
725
|
}
|
|
339
726
|
|
|
@@ -342,20 +729,24 @@ export function registerAddCommand(program) {
|
|
|
342
729
|
.command('add')
|
|
343
730
|
.description('Add a feature to the project')
|
|
344
731
|
.addArgument(new Argument('<type>', 'type of feature to add').choices(['chat']))
|
|
345
|
-
.
|
|
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) => {
|
|
346
737
|
try {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
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 })
|
|
350
741
|
} catch (error) {
|
|
351
742
|
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
352
743
|
console.error('\nOperation cancelled.')
|
|
353
|
-
|
|
354
744
|
process.exit(1)
|
|
355
745
|
}
|
|
356
|
-
|
|
357
746
|
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
|
|
358
747
|
process.exit(1)
|
|
359
748
|
}
|
|
360
749
|
})
|
|
361
750
|
}
|
|
751
|
+
|
|
752
|
+
export { DEPENDENCIES, ensureDependency, ensureStyleImports, getChatFeatureFiles, planMount, resolveTargetPackage }
|