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