@opentiny/tiny-robot-cli 0.4.2-alpha.6 → 0.4.2-alpha.7
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/bin/cli.js +7 -168
- package/bin/commands/add.js +468 -0
- package/bin/commands/create.js +150 -0
- package/bin/utils.js +317 -0
- package/package.json +6 -3
- package/templates/chat/.env.example +1 -0
- package/templates/chat/src/TinyRobotChat.vue +35 -0
package/bin/cli.js
CHANGED
|
@@ -1,180 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import fs from 'node:fs'
|
|
4
|
-
import path from 'node:path'
|
|
5
3
|
import process from 'node:process'
|
|
6
|
-
import { fileURLToPath } from 'node:url'
|
|
7
|
-
import { input, select } from '@inquirer/prompts'
|
|
8
4
|
import { Command } from 'commander'
|
|
9
5
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const DEFAULT_PROJECT_NAME = 'tiny-robot-app'
|
|
13
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
14
|
-
const __dirname = path.dirname(__filename)
|
|
15
|
-
const templatesRoot = path.resolve(__dirname, '../templates')
|
|
16
|
-
|
|
17
|
-
function getAvailableTemplates() {
|
|
18
|
-
if (!fs.existsSync(templatesRoot)) {
|
|
19
|
-
return []
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return fs
|
|
23
|
-
.readdirSync(templatesRoot, { withFileTypes: true })
|
|
24
|
-
.filter((entry) => entry.isDirectory())
|
|
25
|
-
.map((entry) => entry.name)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function validateProjectName(name) {
|
|
29
|
-
// Keep project naming rules strict for npm package compatibility.
|
|
30
|
-
const npmSafePattern = /^[a-z0-9-]+$/
|
|
31
|
-
return npmSafePattern.test(name)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function getTemplateDir(templateName) {
|
|
35
|
-
return path.join(templatesRoot, templateName)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function resolveProjectName(initialProjectName, skipPrompts) {
|
|
39
|
-
if (initialProjectName) {
|
|
40
|
-
return initialProjectName
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (skipPrompts) {
|
|
44
|
-
return DEFAULT_PROJECT_NAME
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return input({
|
|
48
|
-
message: 'Project name:',
|
|
49
|
-
default: DEFAULT_PROJECT_NAME,
|
|
50
|
-
validate: (value) => {
|
|
51
|
-
if (!value) {
|
|
52
|
-
return 'Project name is required.'
|
|
53
|
-
}
|
|
54
|
-
if (!validateProjectName(value)) {
|
|
55
|
-
return 'Project name can only contain lowercase letters, numbers, and dashes.'
|
|
56
|
-
}
|
|
57
|
-
const targetDir = path.resolve(process.cwd(), value)
|
|
58
|
-
if (fs.existsSync(targetDir)) {
|
|
59
|
-
return `Target directory already exists: ${targetDir}`
|
|
60
|
-
}
|
|
61
|
-
return true
|
|
62
|
-
},
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function resolveTemplateName(initialTemplateName, availableTemplates, skipPrompts) {
|
|
67
|
-
if (initialTemplateName) {
|
|
68
|
-
return initialTemplateName
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (skipPrompts) {
|
|
72
|
-
return availableTemplates.includes(DEFAULT_TEMPLATE) ? DEFAULT_TEMPLATE : availableTemplates[0]
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return select({
|
|
76
|
-
message: 'Template:',
|
|
77
|
-
default: availableTemplates.includes(DEFAULT_TEMPLATE) ? DEFAULT_TEMPLATE : availableTemplates[0],
|
|
78
|
-
choices: availableTemplates.map((templateName) => ({
|
|
79
|
-
name: templateName,
|
|
80
|
-
value: templateName,
|
|
81
|
-
})),
|
|
82
|
-
})
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function copyTemplate(sourceDir, targetDir) {
|
|
86
|
-
fs.cpSync(sourceDir, targetDir, {
|
|
87
|
-
recursive: true,
|
|
88
|
-
filter: (source) => {
|
|
89
|
-
const name = path.basename(source)
|
|
90
|
-
// Ignore local build artifacts to keep generated projects clean.
|
|
91
|
-
return !['node_modules', '.git', 'dist', '.DS_Store', '.vite'].includes(name)
|
|
92
|
-
},
|
|
93
|
-
})
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function renameSpecialFiles(targetDir) {
|
|
97
|
-
const from = path.join(targetDir, '_gitignore')
|
|
98
|
-
const to = path.join(targetDir, '.gitignore')
|
|
99
|
-
|
|
100
|
-
if (fs.existsSync(from)) {
|
|
101
|
-
fs.renameSync(from, to)
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function replaceProjectName(targetDir, projectName) {
|
|
106
|
-
const filesToReplace = ['package.json', 'README.md']
|
|
107
|
-
|
|
108
|
-
for (const relativeFile of filesToReplace) {
|
|
109
|
-
const absoluteFile = path.join(targetDir, relativeFile)
|
|
110
|
-
|
|
111
|
-
if (!fs.existsSync(absoluteFile)) {
|
|
112
|
-
continue
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const content = fs.readFileSync(absoluteFile, 'utf-8')
|
|
116
|
-
const nextContent = content.replaceAll(TEMPLATE_PLACEHOLDER, projectName)
|
|
117
|
-
fs.writeFileSync(absoluteFile, nextContent, 'utf-8')
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function createProject(initialProjectName, initialTemplateName, skipPrompts) {
|
|
122
|
-
const availableTemplates = getAvailableTemplates()
|
|
123
|
-
if (availableTemplates.length === 0) {
|
|
124
|
-
console.error('Error: no templates found.')
|
|
125
|
-
process.exit(1)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const projectName = await resolveProjectName(initialProjectName, skipPrompts)
|
|
129
|
-
const templateName = await resolveTemplateName(initialTemplateName, availableTemplates, skipPrompts)
|
|
130
|
-
|
|
131
|
-
if (!validateProjectName(projectName)) {
|
|
132
|
-
console.error('Error: project name can only contain lowercase letters, numbers, and dashes.')
|
|
133
|
-
process.exit(1)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const templateDir = getTemplateDir(templateName)
|
|
137
|
-
const targetDir = path.resolve(process.cwd(), projectName)
|
|
138
|
-
|
|
139
|
-
if (!fs.existsSync(templateDir)) {
|
|
140
|
-
console.error(`Error: template "${templateName}" does not exist. Available: ${availableTemplates.join(', ')}`)
|
|
141
|
-
process.exit(1)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (fs.existsSync(targetDir)) {
|
|
145
|
-
console.error(`Error: target directory already exists: ${targetDir}`)
|
|
146
|
-
process.exit(1)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
copyTemplate(templateDir, targetDir)
|
|
150
|
-
renameSpecialFiles(targetDir)
|
|
151
|
-
replaceProjectName(targetDir, projectName)
|
|
152
|
-
|
|
153
|
-
console.log('\nProject created successfully!')
|
|
154
|
-
console.log(`\nNext steps:`)
|
|
155
|
-
console.log(` cd ${projectName}`)
|
|
156
|
-
console.log(' pnpm install')
|
|
157
|
-
console.log(' pnpm dev\n')
|
|
158
|
-
}
|
|
6
|
+
import { registerCreateCommand } from './commands/create.js'
|
|
7
|
+
import { registerAddCommand } from './commands/add.js'
|
|
159
8
|
|
|
160
9
|
function run() {
|
|
161
10
|
const program = new Command()
|
|
162
|
-
program
|
|
163
|
-
.name('tiny-robot-cli')
|
|
164
|
-
.description('CLI to scaffold TinyRobot product projects')
|
|
165
|
-
.showHelpAfterError()
|
|
166
11
|
|
|
167
|
-
program
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const skipPrompts = !process.stdout.isTTY
|
|
173
|
-
createProject(projectName ?? '', options.template ?? '', skipPrompts).catch((error) => {
|
|
174
|
-
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
|
|
175
|
-
process.exit(1)
|
|
176
|
-
})
|
|
177
|
-
})
|
|
12
|
+
program.name('tiny-robot-cli').description('CLI to scaffold TinyRobot product projects').showHelpAfterError()
|
|
13
|
+
|
|
14
|
+
registerCreateCommand(program)
|
|
15
|
+
|
|
16
|
+
registerAddCommand(program)
|
|
178
17
|
|
|
179
18
|
if (process.argv.length <= 2) {
|
|
180
19
|
program.outputHelp()
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { checkbox, select } from '@inquirer/prompts'
|
|
2
|
+
import { Argument } from 'commander'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
import semver from 'semver'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
copyFile,
|
|
10
|
+
findProjectRoot,
|
|
11
|
+
findSubPackageRoot,
|
|
12
|
+
findWorkspacePackages,
|
|
13
|
+
findWorkspaceRoot,
|
|
14
|
+
getTemplateDir,
|
|
15
|
+
invariant,
|
|
16
|
+
listPackages,
|
|
17
|
+
logSkip,
|
|
18
|
+
logSuccess,
|
|
19
|
+
mergeEnvFile,
|
|
20
|
+
} from '../utils.js'
|
|
21
|
+
|
|
22
|
+
const DEP_NAME = '@opentiny/tiny-robot'
|
|
23
|
+
const TARGET_VERSION = '0.4.2-alpha.6'
|
|
24
|
+
const STYLE_IMPORT = "import '@opentiny/tiny-robot/dist/style.css'"
|
|
25
|
+
|
|
26
|
+
function logUnavailable(label) {
|
|
27
|
+
logSkip(`${label} could not be applied`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function logSkippedSelection(label) {
|
|
31
|
+
logSkip(`${label} change was not selected`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function resolveTargetPackage(cwd) {
|
|
35
|
+
const workspaceRoot = findWorkspaceRoot(cwd)
|
|
36
|
+
|
|
37
|
+
if (workspaceRoot) {
|
|
38
|
+
const subPackageRoot = findSubPackageRoot(cwd, workspaceRoot)
|
|
39
|
+
|
|
40
|
+
if (subPackageRoot) {
|
|
41
|
+
return subPackageRoot
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const workspacePatterns = findWorkspacePackages(workspaceRoot)
|
|
45
|
+
|
|
46
|
+
const packageDirs = listPackages(workspaceRoot, workspacePatterns)
|
|
47
|
+
|
|
48
|
+
invariant(packageDirs.length > 0, 'no packages found in workspace.')
|
|
49
|
+
|
|
50
|
+
return select({
|
|
51
|
+
message: 'Multi-package workspace detected, select a target package:',
|
|
52
|
+
choices: packageDirs.map((dir) => ({
|
|
53
|
+
name: path.basename(dir),
|
|
54
|
+
value: dir,
|
|
55
|
+
})),
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const projectRoot = findProjectRoot(cwd)
|
|
60
|
+
|
|
61
|
+
if (projectRoot) {
|
|
62
|
+
return projectRoot
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.error('Error: no package.json found.')
|
|
66
|
+
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getTemplateFile(...segments) {
|
|
71
|
+
return path.join(getTemplateDir('chat'), ...segments)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readPackageJson(pkgPath) {
|
|
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`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function findMainEntry(targetDir) {
|
|
83
|
+
for (const file of ['src/main.ts', 'src/main.js']) {
|
|
84
|
+
const fullPath = path.join(targetDir, file)
|
|
85
|
+
|
|
86
|
+
if (fs.existsSync(fullPath)) {
|
|
87
|
+
return fullPath
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ensureStyleImport(mainFile) {
|
|
95
|
+
const content = fs.readFileSync(mainFile, 'utf-8')
|
|
96
|
+
|
|
97
|
+
if (content.includes(STYLE_IMPORT)) {
|
|
98
|
+
return {
|
|
99
|
+
type: 'skipped',
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const lines = content.split('\n')
|
|
104
|
+
|
|
105
|
+
let lastImportIndex = -1
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < lines.length; i++) {
|
|
108
|
+
if (/^\s*import\s/.test(lines[i])) {
|
|
109
|
+
lastImportIndex = i
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (lastImportIndex !== -1) {
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (lastImportIndex === -1) {
|
|
119
|
+
lines.unshift(STYLE_IMPORT)
|
|
120
|
+
} else {
|
|
121
|
+
lines.splice(lastImportIndex + 1, 0, STYLE_IMPORT)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fs.writeFileSync(mainFile, lines.join('\n'))
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
type: 'inserted',
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function insertDependencyOrdered(dependencies, name, version) {
|
|
132
|
+
const next = {}
|
|
133
|
+
|
|
134
|
+
let inserted = false
|
|
135
|
+
|
|
136
|
+
for (const [key, value] of Object.entries(dependencies)) {
|
|
137
|
+
if (!inserted && name < key) {
|
|
138
|
+
next[name] = version
|
|
139
|
+
inserted = true
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
next[key] = value
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!inserted) {
|
|
146
|
+
next[name] = version
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return next
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function ensureDependency(pkg, name, targetVersion) {
|
|
153
|
+
pkg.dependencies ??= {}
|
|
154
|
+
|
|
155
|
+
const currentVersion = pkg.dependencies[name]
|
|
156
|
+
|
|
157
|
+
// 1. 不存在 => 新增(使用 ordered insert)
|
|
158
|
+
if (!currentVersion) {
|
|
159
|
+
pkg.dependencies = insertDependencyOrdered(pkg.dependencies, name, targetVersion)
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
type: 'added',
|
|
163
|
+
to: targetVersion,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 2. 已存在 => 只更新版本,不调整顺序
|
|
168
|
+
const current = semver.valid(currentVersion)
|
|
169
|
+
|
|
170
|
+
if (current !== targetVersion) {
|
|
171
|
+
pkg.dependencies[name] = targetVersion
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
type: 'updated',
|
|
175
|
+
from: currentVersion,
|
|
176
|
+
to: targetVersion,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 3. 完全一致 => 跳过
|
|
181
|
+
return {
|
|
182
|
+
type: 'skipped',
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function printDependencyResult(result, name) {
|
|
187
|
+
switch (result.type) {
|
|
188
|
+
case 'added':
|
|
189
|
+
logSuccess(`Added ${name}@${TARGET_VERSION}`)
|
|
190
|
+
break
|
|
191
|
+
|
|
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
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getChatFeatureFiles(targetDir) {
|
|
203
|
+
const mainEntry = findMainEntry(targetDir)
|
|
204
|
+
|
|
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
|
|
245
|
+
|
|
246
|
+
const disabled = !file.target
|
|
247
|
+
|
|
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
|
+
}
|
|
254
|
+
|
|
255
|
+
const description = descriptions[file.label]
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
name: disabled
|
|
259
|
+
? `${file.action(false)} ${file.label} — ${description} (main.ts/js not found)`
|
|
260
|
+
: `${file.action(exists)} ${file.label} — ${description}`,
|
|
261
|
+
|
|
262
|
+
value: file.label,
|
|
263
|
+
|
|
264
|
+
checked: !disabled,
|
|
265
|
+
|
|
266
|
+
disabled,
|
|
267
|
+
}
|
|
268
|
+
}),
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function isSelected(selectedFiles, label) {
|
|
273
|
+
return selectedFiles.includes(label)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function addFeature(targetDir, type) {
|
|
277
|
+
invariant(type === 'chat', `unsupported feature: ${type}`)
|
|
278
|
+
|
|
279
|
+
const files = getChatFeatureFiles(targetDir)
|
|
280
|
+
|
|
281
|
+
const selectedFiles = await selectFileChanges(files)
|
|
282
|
+
|
|
283
|
+
if (selectedFiles.length === 0) {
|
|
284
|
+
logSkip('No changes selected.')
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const componentFile = files.find((f) => {
|
|
289
|
+
return f.label === 'TinyRobotChat.vue'
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
const envFile = files.find((f) => {
|
|
293
|
+
return f.label === '.env'
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
invariant(componentFile, 'TinyRobotChat.vue config missing.')
|
|
297
|
+
|
|
298
|
+
invariant(envFile, '.env config missing.')
|
|
299
|
+
|
|
300
|
+
console.log('\nChange Results\n')
|
|
301
|
+
|
|
302
|
+
let needsManualStyleImport = false
|
|
303
|
+
let envChanged = false
|
|
304
|
+
let dependencyChanged = false
|
|
305
|
+
|
|
306
|
+
if (isSelected(selectedFiles, 'TinyRobotChat.vue')) {
|
|
307
|
+
copyFile(componentFile.template, componentFile.target)
|
|
308
|
+
logSuccess(`Copied ${componentFile.label}`)
|
|
309
|
+
} else {
|
|
310
|
+
logSkippedSelection(componentFile.label)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const mainFile = files.find((f) => {
|
|
314
|
+
return f.label === 'main entry style import'
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
if (!mainFile?.target) {
|
|
318
|
+
needsManualStyleImport = true
|
|
319
|
+
|
|
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)
|
|
324
|
+
|
|
325
|
+
switch (result.type) {
|
|
326
|
+
case 'inserted':
|
|
327
|
+
logSuccess('Inserted TinyRobot style import')
|
|
328
|
+
break
|
|
329
|
+
|
|
330
|
+
case 'skipped':
|
|
331
|
+
logSkip('TinyRobot style import already exists')
|
|
332
|
+
break
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
needsManualStyleImport = true
|
|
336
|
+
|
|
337
|
+
logSkippedSelection(mainFile.label)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (isSelected(selectedFiles, '.env')) {
|
|
342
|
+
const envResult = mergeEnvFile(envFile.template, envFile.target)
|
|
343
|
+
|
|
344
|
+
switch (envResult.type) {
|
|
345
|
+
case 'created':
|
|
346
|
+
envChanged = true
|
|
347
|
+
|
|
348
|
+
logSuccess('Created .env')
|
|
349
|
+
break
|
|
350
|
+
|
|
351
|
+
case 'merged':
|
|
352
|
+
envChanged = true
|
|
353
|
+
|
|
354
|
+
logSuccess(`Added ${envResult.added} env variables`)
|
|
355
|
+
break
|
|
356
|
+
|
|
357
|
+
case 'skipped':
|
|
358
|
+
logSkip('.env already contains required variables')
|
|
359
|
+
break
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
logSkippedSelection('.env')
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const pkgPath = path.join(targetDir, 'package.json')
|
|
366
|
+
|
|
367
|
+
invariant(fs.existsSync(pkgPath), 'package.json not found.')
|
|
368
|
+
|
|
369
|
+
const pkg = readPackageJson(pkgPath)
|
|
370
|
+
|
|
371
|
+
if (isSelected(selectedFiles, 'package.json')) {
|
|
372
|
+
const result = ensureDependency(pkg, DEP_NAME, TARGET_VERSION)
|
|
373
|
+
|
|
374
|
+
if (result.type !== 'skipped') {
|
|
375
|
+
dependencyChanged = true
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
writePackageJson(pkgPath, pkg)
|
|
379
|
+
|
|
380
|
+
printDependencyResult(result, DEP_NAME)
|
|
381
|
+
} else {
|
|
382
|
+
logSkippedSelection('package.json')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
console.log(`\nSuccessfully added "${type}" feature to ${targetDir}`)
|
|
386
|
+
|
|
387
|
+
printNextSteps({
|
|
388
|
+
needsManualStyleImport,
|
|
389
|
+
envChanged,
|
|
390
|
+
dependencyChanged,
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function printNextSteps({ needsManualStyleImport, envChanged, dependencyChanged }) {
|
|
395
|
+
const steps = []
|
|
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) {
|
|
421
|
+
steps.push(
|
|
422
|
+
[
|
|
423
|
+
'Configure your AI provider API key in the .env file.',
|
|
424
|
+
'',
|
|
425
|
+
'Example:',
|
|
426
|
+
'',
|
|
427
|
+
' VITE_DEEPSEEK_API_KEY=your_api_key',
|
|
428
|
+
].join('\n'),
|
|
429
|
+
)
|
|
430
|
+
}
|
|
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`)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function registerAddCommand(program) {
|
|
448
|
+
program
|
|
449
|
+
.command('add')
|
|
450
|
+
.description('Add a feature to the project')
|
|
451
|
+
.addArgument(new Argument('<type>', 'type of feature to add').choices(['chat']))
|
|
452
|
+
.action(async (type) => {
|
|
453
|
+
try {
|
|
454
|
+
const targetDir = await resolveTargetPackage(process.cwd())
|
|
455
|
+
|
|
456
|
+
await addFeature(targetDir, type)
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
459
|
+
console.error('\nOperation cancelled.')
|
|
460
|
+
|
|
461
|
+
process.exit(1)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
|
|
465
|
+
process.exit(1)
|
|
466
|
+
}
|
|
467
|
+
})
|
|
468
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { input, select } from '@inquirer/prompts'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_PROJECT_NAME,
|
|
7
|
+
DEFAULT_TEMPLATE,
|
|
8
|
+
exists,
|
|
9
|
+
getAvailableTemplates,
|
|
10
|
+
getTemplateDir,
|
|
11
|
+
invariant,
|
|
12
|
+
scaffoldProject,
|
|
13
|
+
validateProjectName,
|
|
14
|
+
} from '../utils.js'
|
|
15
|
+
|
|
16
|
+
async function promptOrFallback(skipPrompt, fallbackValue, promptFactory) {
|
|
17
|
+
if (skipPrompt) {
|
|
18
|
+
return fallbackValue
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return promptFactory()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function resolveProjectName(initialValue, skipPrompt) {
|
|
25
|
+
if (initialValue) {
|
|
26
|
+
return initialValue
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return promptOrFallback(skipPrompt, DEFAULT_PROJECT_NAME, () => {
|
|
30
|
+
return input({
|
|
31
|
+
message: 'Project name:',
|
|
32
|
+
default: DEFAULT_PROJECT_NAME,
|
|
33
|
+
validate(value) {
|
|
34
|
+
if (!value) {
|
|
35
|
+
return 'Project name is required.'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!validateProjectName(value)) {
|
|
39
|
+
return 'Project name can only contain lowercase letters, numbers, and dashes.'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const targetDir = path.resolve(process.cwd(), value)
|
|
43
|
+
|
|
44
|
+
if (exists(targetDir)) {
|
|
45
|
+
return `Target directory already exists: ${targetDir}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return true
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function resolveTemplateName(initialValue, templates, skipPrompt) {
|
|
55
|
+
if (initialValue) {
|
|
56
|
+
return initialValue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const fallback = templates.includes(DEFAULT_TEMPLATE) ? DEFAULT_TEMPLATE : templates[0]
|
|
60
|
+
|
|
61
|
+
return promptOrFallback(skipPrompt, fallback, () => {
|
|
62
|
+
return select({
|
|
63
|
+
message: 'Template:',
|
|
64
|
+
default: fallback,
|
|
65
|
+
choices: templates.map((templateName) => ({
|
|
66
|
+
name: templateName,
|
|
67
|
+
value: templateName,
|
|
68
|
+
})),
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function resolveCreateOptions(initialProjectName, initialTemplateName, skipPrompt) {
|
|
74
|
+
const availableTemplates = getAvailableTemplates()
|
|
75
|
+
|
|
76
|
+
invariant(availableTemplates.length > 0, 'no templates found.')
|
|
77
|
+
|
|
78
|
+
const projectName = await resolveProjectName(initialProjectName, skipPrompt)
|
|
79
|
+
|
|
80
|
+
const templateName = await resolveTemplateName(initialTemplateName, availableTemplates, skipPrompt)
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
projectName,
|
|
84
|
+
templateName,
|
|
85
|
+
availableTemplates,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function validateCreateOptions(options) {
|
|
90
|
+
const { projectName, templateName, availableTemplates } = options
|
|
91
|
+
|
|
92
|
+
invariant(validateProjectName(projectName), 'project name can only contain lowercase letters, numbers, and dashes.')
|
|
93
|
+
|
|
94
|
+
invariant(
|
|
95
|
+
availableTemplates.includes(templateName),
|
|
96
|
+
`template "${templateName}" does not exist. Available: ${availableTemplates.join(', ')}`,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const templateDir = getTemplateDir(templateName)
|
|
100
|
+
|
|
101
|
+
invariant(exists(templateDir), `template directory missing: ${templateDir}`)
|
|
102
|
+
|
|
103
|
+
const targetDir = path.resolve(process.cwd(), projectName)
|
|
104
|
+
|
|
105
|
+
invariant(!exists(targetDir), `target directory already exists: ${targetDir}`)
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
templateDir,
|
|
109
|
+
targetDir,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function printCreateSuccess(projectName) {
|
|
114
|
+
console.log('\nProject created successfully!')
|
|
115
|
+
console.log('\nNext steps:')
|
|
116
|
+
console.log(` cd ${projectName}`)
|
|
117
|
+
console.log(' pnpm install')
|
|
118
|
+
console.log(' pnpm dev')
|
|
119
|
+
console.log()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function createProject(initialProjectName, initialTemplateName, skipPrompt) {
|
|
123
|
+
const options = await resolveCreateOptions(initialProjectName, initialTemplateName, skipPrompt)
|
|
124
|
+
|
|
125
|
+
const { templateDir, targetDir } = validateCreateOptions(options)
|
|
126
|
+
|
|
127
|
+
scaffoldProject(templateDir, targetDir, options.projectName)
|
|
128
|
+
|
|
129
|
+
printCreateSuccess(options.projectName)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function registerCreateCommand(program) {
|
|
133
|
+
program
|
|
134
|
+
.command('create [project-name]')
|
|
135
|
+
.description('Create a TinyRobot project from template')
|
|
136
|
+
.option('-t, --template <name>', 'template name')
|
|
137
|
+
.action((projectName, options) => {
|
|
138
|
+
const skipPrompt = !process.stdout.isTTY
|
|
139
|
+
|
|
140
|
+
createProject(projectName ?? '', options.template ?? '', skipPrompt).catch((error) => {
|
|
141
|
+
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
142
|
+
console.error('\nOperation cancelled.')
|
|
143
|
+
process.exit(1)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
|
|
147
|
+
process.exit(1)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
}
|
package/bin/utils.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import pc from 'picocolors'
|
|
5
|
+
import yaml from 'yaml'
|
|
6
|
+
|
|
7
|
+
const TEMPLATE_PLACEHOLDER = '__PROJECT_NAME__'
|
|
8
|
+
|
|
9
|
+
export const BUILTIN_TEMPLATES = ['basic']
|
|
10
|
+
export const DEFAULT_TEMPLATE = 'basic'
|
|
11
|
+
export const DEFAULT_PROJECT_NAME = 'tiny-robot-app'
|
|
12
|
+
|
|
13
|
+
const WORKSPACE_FILES = ['pnpm-workspace.yaml', 'pnpm-workspace.yml']
|
|
14
|
+
|
|
15
|
+
const IGNORE_COPY_FILES = ['node_modules', '.git', 'dist', '.DS_Store', '.vite']
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
18
|
+
const __dirname = path.dirname(__filename)
|
|
19
|
+
|
|
20
|
+
const templatesRoot = path.resolve(__dirname, '../templates')
|
|
21
|
+
|
|
22
|
+
function createStatusLabel(icon, label, color) {
|
|
23
|
+
return `${color(icon)} ${color(label)}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function logSuccess(message) {
|
|
27
|
+
console.log(`${createStatusLabel('✔', 'SUCCESS', pc.green)} ${message}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function logSkip(message) {
|
|
31
|
+
console.log(`${createStatusLabel('○', 'SKIPPED', pc.dim)} ${message}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function logError(message) {
|
|
35
|
+
console.log(`${createStatusLabel('✖', 'FAILED', pc.red)} ${message}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function invariant(condition, message) {
|
|
39
|
+
if (!condition) {
|
|
40
|
+
console.error(`Error: ${message}`)
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function exists(file) {
|
|
46
|
+
return fs.existsSync(file)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isPackageDir(dir) {
|
|
50
|
+
return exists(path.join(dir, 'package.json'))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function findUp(startDir, matcher) {
|
|
54
|
+
let dir = path.resolve(startDir)
|
|
55
|
+
|
|
56
|
+
for (;;) {
|
|
57
|
+
if (matcher(dir)) {
|
|
58
|
+
return dir
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const parent = path.dirname(dir)
|
|
62
|
+
|
|
63
|
+
if (parent === dir) {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
dir = parent
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getAvailableTemplates() {
|
|
72
|
+
if (!exists(templatesRoot)) {
|
|
73
|
+
return []
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return fs
|
|
77
|
+
.readdirSync(templatesRoot, { withFileTypes: true })
|
|
78
|
+
.filter((entry) => entry.isDirectory() && BUILTIN_TEMPLATES.includes(entry.name))
|
|
79
|
+
.map((entry) => entry.name)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getTemplateDir(templateName) {
|
|
83
|
+
return path.join(templatesRoot, templateName)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function validateProjectName(name) {
|
|
87
|
+
return /^[a-z0-9-]+$/.test(name)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function copyTemplate(sourceDir, targetDir) {
|
|
91
|
+
fs.cpSync(sourceDir, targetDir, {
|
|
92
|
+
recursive: true,
|
|
93
|
+
filter: (source) => {
|
|
94
|
+
return !IGNORE_COPY_FILES.includes(path.basename(source))
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renameSpecialFiles(targetDir) {
|
|
100
|
+
const files = [['_gitignore', '.gitignore']]
|
|
101
|
+
|
|
102
|
+
for (const [fromName, toName] of files) {
|
|
103
|
+
const from = path.join(targetDir, fromName)
|
|
104
|
+
const to = path.join(targetDir, toName)
|
|
105
|
+
|
|
106
|
+
if (exists(from)) {
|
|
107
|
+
fs.renameSync(from, to)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function replaceTemplateVariables(targetDir, variables) {
|
|
113
|
+
const replaceFiles = ['package.json', 'README.md']
|
|
114
|
+
|
|
115
|
+
for (const relativePath of replaceFiles) {
|
|
116
|
+
const file = path.join(targetDir, relativePath)
|
|
117
|
+
|
|
118
|
+
if (!exists(file)) {
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let content = fs.readFileSync(file, 'utf-8')
|
|
123
|
+
|
|
124
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
125
|
+
content = content.replaceAll(key, value)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fs.writeFileSync(file, content, 'utf-8')
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function scaffoldProject(templateDir, targetDir, projectName) {
|
|
133
|
+
copyTemplate(templateDir, targetDir)
|
|
134
|
+
|
|
135
|
+
renameSpecialFiles(targetDir)
|
|
136
|
+
|
|
137
|
+
replaceTemplateVariables(targetDir, {
|
|
138
|
+
[TEMPLATE_PLACEHOLDER]: projectName,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resolveWorkspaceFile(workspaceRoot) {
|
|
143
|
+
for (const name of WORKSPACE_FILES) {
|
|
144
|
+
const file = path.join(workspaceRoot, name)
|
|
145
|
+
|
|
146
|
+
if (exists(file)) {
|
|
147
|
+
return file
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function findWorkspaceRoot(cwd) {
|
|
155
|
+
return findUp(cwd, (dir) => {
|
|
156
|
+
return WORKSPACE_FILES.some((name) => {
|
|
157
|
+
return exists(path.join(dir, name))
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function findProjectRoot(cwd) {
|
|
163
|
+
return findUp(cwd, (dir) => {
|
|
164
|
+
return isPackageDir(dir)
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function findSubPackageRoot(cwd, workspaceRoot) {
|
|
169
|
+
return findUp(cwd, (dir) => {
|
|
170
|
+
if (dir === workspaceRoot) {
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return isPackageDir(dir)
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function findWorkspacePackages(workspaceRoot) {
|
|
179
|
+
const workspaceFile = resolveWorkspaceFile(workspaceRoot)
|
|
180
|
+
|
|
181
|
+
if (!workspaceFile) {
|
|
182
|
+
return []
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const content = fs.readFileSync(workspaceFile, 'utf8')
|
|
187
|
+
|
|
188
|
+
const config = yaml.parse(content)
|
|
189
|
+
|
|
190
|
+
return Array.isArray(config?.packages)
|
|
191
|
+
? config.packages.filter(
|
|
192
|
+
(pattern) => typeof pattern === 'string' && pattern.length > 0 && !pattern.startsWith('!'),
|
|
193
|
+
)
|
|
194
|
+
: []
|
|
195
|
+
} catch {
|
|
196
|
+
return []
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizeWorkspacePattern(pattern) {
|
|
201
|
+
return pattern.replace(/\*\*?/g, '').replace(/\/$/, '')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function listPackages(workspaceRoot, patterns) {
|
|
205
|
+
const packageDirs = []
|
|
206
|
+
|
|
207
|
+
const addPackage = (dir) => {
|
|
208
|
+
if (isPackageDir(dir)) {
|
|
209
|
+
packageDirs.push(dir)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const pattern of patterns) {
|
|
214
|
+
const base = normalizeWorkspacePattern(pattern)
|
|
215
|
+
|
|
216
|
+
const fullPath = path.join(workspaceRoot, base)
|
|
217
|
+
|
|
218
|
+
if (!exists(fullPath)) {
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (pattern.includes('*')) {
|
|
223
|
+
const entries = fs.readdirSync(fullPath, {
|
|
224
|
+
withFileTypes: true,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
for (const entry of entries) {
|
|
228
|
+
if (entry.isDirectory()) {
|
|
229
|
+
addPackage(path.join(fullPath, entry.name))
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
addPackage(fullPath)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return packageDirs
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function ensureDir(file) {
|
|
241
|
+
fs.mkdirSync(path.dirname(file), {
|
|
242
|
+
recursive: true,
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function copyFile(from, to) {
|
|
247
|
+
ensureDir(to)
|
|
248
|
+
|
|
249
|
+
fs.copyFileSync(from, to)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parseEnv(content) {
|
|
253
|
+
const map = new Map()
|
|
254
|
+
|
|
255
|
+
for (const line of content.split('\n')) {
|
|
256
|
+
const trimmed = line.trim()
|
|
257
|
+
|
|
258
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const index = trimmed.indexOf('=')
|
|
263
|
+
|
|
264
|
+
if (index === -1) {
|
|
265
|
+
continue
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const key = trimmed.slice(0, index).trim()
|
|
269
|
+
|
|
270
|
+
map.set(key, trimmed)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return map
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function mergeEnvFile(templateFile, targetFile) {
|
|
277
|
+
if (!fs.existsSync(targetFile)) {
|
|
278
|
+
copyFile(templateFile, targetFile)
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
type: 'created',
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const templateContent = fs.readFileSync(templateFile, 'utf-8')
|
|
286
|
+
|
|
287
|
+
const targetContent = fs.readFileSync(targetFile, 'utf-8')
|
|
288
|
+
|
|
289
|
+
const templateEnv = parseEnv(templateContent)
|
|
290
|
+
|
|
291
|
+
const targetEnv = parseEnv(targetContent)
|
|
292
|
+
|
|
293
|
+
const appendLines = []
|
|
294
|
+
|
|
295
|
+
for (const [key, line] of templateEnv) {
|
|
296
|
+
if (!targetEnv.has(key)) {
|
|
297
|
+
appendLines.push(line)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (appendLines.length === 0) {
|
|
302
|
+
return {
|
|
303
|
+
type: 'skipped',
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const targetTrimmed = targetContent.replace(/\s*$/, '')
|
|
308
|
+
|
|
309
|
+
const nextContent = targetTrimmed ? `${targetTrimmed}\n${appendLines.join('\n')}\n` : `${appendLines.join('\n')}\n`
|
|
310
|
+
|
|
311
|
+
fs.writeFileSync(targetFile, nextContent)
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
type: 'merged',
|
|
315
|
+
added: appendLines.length,
|
|
316
|
+
}
|
|
317
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opentiny/tiny-robot-cli",
|
|
3
|
-
"version": "0.4.2-alpha.
|
|
3
|
+
"version": "0.4.2-alpha.7",
|
|
4
4
|
"description": "CLI to scaffold TinyRobot product projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@inquirer/prompts": "^8.3.2",
|
|
23
|
-
"commander": "^14.0.3"
|
|
23
|
+
"commander": "^14.0.3",
|
|
24
|
+
"picocolors": "^1.1.1",
|
|
25
|
+
"semver": "^7.8.1",
|
|
26
|
+
"yaml": "^2.9.0"
|
|
24
27
|
},
|
|
25
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "6713e6fd0f9a7239d940e5402fec6b8d81bce365"
|
|
26
29
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VITE_DEEPSEEK_API_KEY=
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ChatMcpServerConfig, ChatModelOption } from '@opentiny/tiny-robot/experimental'
|
|
3
|
+
import { TrThemeProvider } from '@opentiny/tiny-robot'
|
|
4
|
+
import { TrChat } from '@opentiny/tiny-robot/experimental'
|
|
5
|
+
import { ref } from 'vue'
|
|
6
|
+
|
|
7
|
+
const show = ref(false)
|
|
8
|
+
const fullscreen = ref(false)
|
|
9
|
+
|
|
10
|
+
const chatModelOptions: ChatModelOption[] = [
|
|
11
|
+
{
|
|
12
|
+
id: 'deepseek-chat',
|
|
13
|
+
provider: 'deepseek',
|
|
14
|
+
name: 'DeepSeek Chat',
|
|
15
|
+
model: 'deepseek-chat',
|
|
16
|
+
apiUrl: 'https://api.deepseek.com/chat/completions',
|
|
17
|
+
apiKey: import.meta.env.VITE_DEEPSEEK_API_KEY || '',
|
|
18
|
+
},
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
const chatMcpServers: Record<string, ChatMcpServerConfig> = {}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<TrThemeProvider>
|
|
26
|
+
<TrChat
|
|
27
|
+
v-model:show="show"
|
|
28
|
+
v-model:fullscreen="fullscreen"
|
|
29
|
+
title="TrChat Preview"
|
|
30
|
+
system-prompt="You are a helpful assistant."
|
|
31
|
+
:model-options="chatModelOptions"
|
|
32
|
+
:mcp-servers="chatMcpServers"
|
|
33
|
+
/>
|
|
34
|
+
</TrThemeProvider>
|
|
35
|
+
</template>
|