@llryiop/avatar-boot-cli 1.0.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 +309 -0
- package/bin/cli.js +3 -0
- package/docs/plans/2026-03-12-avatar-boot-cli-design.md +73 -0
- package/docs/plans/2026-03-12-avatar-boot-cli-plan.md +681 -0
- package/package.json +28 -0
- package/src/index.js +78 -0
- package/src/prompts.js +78 -0
- package/src/template.js +37 -0
- package/src/transform.js +172 -0
- package/src/utils.js +34 -0
- package/templates/.claude/rules/architecture-redlines.md +146 -0
- package/templates/.claude/rules/code-review-standards.md +137 -0
- package/templates/.claude/rules/coding-standards.md +56 -0
- package/templates/.claude/rules/git-commit.md +59 -0
- package/templates/.claude/rules/layered-architecture.md +201 -0
- package/templates/.claude/rules/mybatis-plus.md +263 -0
- package/templates/.claude/rules/tech-stack.md +41 -0
- package/templates/.claude/rules/version.md +467 -0
- package/templates/.claude/settings.local.json +18 -0
- package/templates/.claude/skills/ai-tool-guide/SKILL.md +314 -0
- package/templates/.claude/skills/api-design/SKILL.md +200 -0
- package/templates/.claude/skills/api-doc-generator/SKILL.md +380 -0
- package/templates/.claude/skills/api-service-module-creator/SKILL.md +1114 -0
- package/templates/.claude/skills/avatar-boot-starter-feign/SKILL.md +243 -0
- package/templates/.claude/skills/avatar-boot-starter-job/SKILL.md +437 -0
- package/templates/.claude/skills/avatar-boot-starter-kafka/SKILL.md +580 -0
- package/templates/.claude/skills/avatar-boot-starter-mysql/SKILL.md +572 -0
- package/templates/.claude/skills/avatar-boot-starter-nacos/SKILL.md +901 -0
- package/templates/.claude/skills/avatar-boot-starter-oss/SKILL.md +594 -0
- package/templates/.claude/skills/avatar-boot-starter-redis/SKILL.md +586 -0
- package/templates/.claude/skills/avatar-boot-starter-rocketmq/SKILL.md +662 -0
- package/templates/.claude/skills/avatar-boot-starter-web/SKILL.md +1007 -0
- package/templates/.claude/skills/changelog-generator/SKILL.md +114 -0
- package/templates/.claude/skills/code-review/SKILL.md +239 -0
- package/templates/.claude/skills/crud-generator/SKILL.md +824 -0
- package/templates/.claude/skills/database-design/SKILL.md +377 -0
- package/templates/.claude/skills/deployment-config/SKILL.md +277 -0
- package/templates/.claude/skills/incident-analysis/SKILL.md +241 -0
- package/templates/.claude/skills/integration-test-generator/SKILL.md +496 -0
- package/templates/.claude/skills/prompt-engineering/SKILL.md +249 -0
- package/templates/.claude/skills/requirement-management/SKILL.md +244 -0
- package/templates/.claude/skills/security-audit/SKILL.md +330 -0
- package/templates/.claude/skills/test-case-design/SKILL.md +257 -0
- package/templates/.claude/skills/testing-workflow/SKILL.md +68 -0
- package/templates/.claude/skills/troubleshooting/SKILL.md +240 -0
- package/templates/CLAUDE.md +173 -0
- package/templates/README.md +303 -0
- package/templates/avatar-scaffold-api/pom.xml +41 -0
- package/templates/avatar-scaffold-api/src/main/java/com/iflytek/avatar/login/api/LoginFeignClient.java +40 -0
- package/templates/avatar-scaffold-api/src/main/java/com/iflytek/avatar/login/constant/LoginConstant.java +21 -0
- package/templates/avatar-scaffold-api/src/main/java/com/iflytek/avatar/login/dto/request/LoginRequest.java +17 -0
- package/templates/avatar-scaffold-api/src/main/java/com/iflytek/avatar/login/dto/request/RefreshTokenRequest.java +14 -0
- package/templates/avatar-scaffold-api/src/main/java/com/iflytek/avatar/login/dto/response/LoginResponse.java +31 -0
- package/templates/avatar-scaffold-api/src/main/java/com/iflytek/avatar/login/dto/response/TokenInfoResponse.java +25 -0
- package/templates/avatar-scaffold-api/src/main/java/com/iflytek/avatar/login/enums/LoginTypeEnum.java +23 -0
- package/templates/avatar-scaffold-api/src/main/java/com/iflytek/avatar/login/exception/LoginException.java +23 -0
- package/templates/avatar-scaffold-service/k8s-app/Dockerfile +14 -0
- package/templates/avatar-scaffold-service/k8s-app/Dockerfile-arm64 +14 -0
- package/templates/avatar-scaffold-service/packaging/assembly.xml +16 -0
- package/templates/avatar-scaffold-service/pom.xml +150 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/Application.java +21 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/config/LoginConfig.java +20 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/controller/LoginController.java +37 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/converter/LoginConverter.java +54 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/feign/DemoFeign.java +21 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/repository/entity/UserLoginEntity.java +33 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/repository/entity/UserTokenEntity.java +39 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/repository/mapper/UserLoginMapper.java +20 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/service/LoginService.java +22 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/service/impl/LoginServiceImpl.java +43 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/utils/LoginUtils.java +31 -0
- package/templates/avatar-scaffold-service/src/main/resources/application-dev.yaml +29 -0
- package/templates/avatar-scaffold-service/src/main/resources/application-local.yaml +61 -0
- package/templates/avatar-scaffold-service/src/main/resources/application-prod.yaml +28 -0
- package/templates/avatar-scaffold-service/src/main/resources/application-test.yaml +28 -0
- package/templates/avatar-scaffold-service/src/main/resources/application.yaml +12 -0
- package/templates/pom.xml +98 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
# Avatar Boot CLI Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Build an npx CLI tool that interactively scaffolds AvatarBoot Java microservice projects from a Git template.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Node.js CLI using commander for commands, inquirer for prompts, simple string replacement on cloned Git template. Template sourced from `https://code.iflytek.com/YHZ_VoicePlatform/avatar-scaffold-project.git`.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js, commander, inquirer, chalk, ora, glob, child_process (git)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Task 1: Initialize npm project and create CLI entry point
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
- Create: `package.json`
|
|
17
|
+
- Create: `bin/cli.js`
|
|
18
|
+
- Create: `src/index.js`
|
|
19
|
+
|
|
20
|
+
**Step 1: Create package.json**
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"name": "avatar-boot-cli",
|
|
25
|
+
"version": "1.0.0",
|
|
26
|
+
"description": "CLI tool for scaffolding AvatarBoot Java microservice projects",
|
|
27
|
+
"main": "src/index.js",
|
|
28
|
+
"bin": {
|
|
29
|
+
"avatar-boot-cli": "./bin/cli.js"
|
|
30
|
+
},
|
|
31
|
+
"type": "module",
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "node --test src/__tests__/*.test.js"
|
|
34
|
+
},
|
|
35
|
+
"keywords": ["avatar", "boot", "scaffold", "java", "microservice"],
|
|
36
|
+
"license": "ISC"
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Step 2: Install dependencies**
|
|
41
|
+
|
|
42
|
+
Run: `npm install commander inquirer chalk ora glob`
|
|
43
|
+
Expected: node_modules created, dependencies added to package.json
|
|
44
|
+
|
|
45
|
+
**Step 3: Create bin/cli.js**
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
#!/usr/bin/env node
|
|
49
|
+
import { run } from '../src/index.js';
|
|
50
|
+
run();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Step 4: Create src/index.js (skeleton)**
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
import { Command } from 'commander';
|
|
57
|
+
import chalk from 'chalk';
|
|
58
|
+
|
|
59
|
+
export function run() {
|
|
60
|
+
const program = new Command();
|
|
61
|
+
|
|
62
|
+
program
|
|
63
|
+
.name('avatar-boot-cli')
|
|
64
|
+
.description('CLI tool for scaffolding AvatarBoot Java microservice projects')
|
|
65
|
+
.version('1.0.0');
|
|
66
|
+
|
|
67
|
+
program
|
|
68
|
+
.command('init [project-name]')
|
|
69
|
+
.description('Initialize a new AvatarBoot project')
|
|
70
|
+
.action(async (projectName) => {
|
|
71
|
+
console.log(chalk.blue('🚀 Avatar Boot CLI'));
|
|
72
|
+
console.log('Project:', projectName || 'my-avatar-service');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
program.parse();
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Step 5: Test CLI entry point**
|
|
80
|
+
|
|
81
|
+
Run: `chmod +x bin/cli.js && node bin/cli.js init test-project`
|
|
82
|
+
Expected: Output showing "Avatar Boot CLI" and "Project: test-project"
|
|
83
|
+
|
|
84
|
+
**Step 6: Commit**
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
git init
|
|
88
|
+
git add package.json bin/cli.js src/index.js
|
|
89
|
+
git commit -m "feat: initialize avatar-boot-cli project with commander setup"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### Task 2: Implement Git clone logic
|
|
95
|
+
|
|
96
|
+
**Files:**
|
|
97
|
+
- Create: `src/clone.js`
|
|
98
|
+
|
|
99
|
+
**Step 1: Create src/clone.js**
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
import { execSync } from 'child_process';
|
|
103
|
+
import fs from 'fs';
|
|
104
|
+
import path from 'path';
|
|
105
|
+
|
|
106
|
+
const REPO_URL = 'https://code.iflytek.com/YHZ_VoicePlatform/avatar-scaffold-project.git';
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Fetch available tags from remote repository
|
|
110
|
+
* @returns {string[]} List of tag names
|
|
111
|
+
*/
|
|
112
|
+
export function fetchTags() {
|
|
113
|
+
try {
|
|
114
|
+
const output = execSync(`git ls-remote --tags ${REPO_URL}`, {
|
|
115
|
+
encoding: 'utf-8',
|
|
116
|
+
timeout: 30000,
|
|
117
|
+
});
|
|
118
|
+
const tags = output
|
|
119
|
+
.split('\n')
|
|
120
|
+
.filter((line) => line.includes('refs/tags/'))
|
|
121
|
+
.map((line) => line.replace(/.*refs\/tags\//, '').replace(/\^{}$/, ''))
|
|
122
|
+
.filter((tag, index, arr) => arr.indexOf(tag) === index)
|
|
123
|
+
.filter((tag) => tag.length > 0)
|
|
124
|
+
.reverse();
|
|
125
|
+
return tags;
|
|
126
|
+
} catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Clone the scaffold template repository
|
|
133
|
+
* @param {string} targetDir - Directory to clone into
|
|
134
|
+
* @param {string} [tag] - Specific tag/branch to clone
|
|
135
|
+
*/
|
|
136
|
+
export function cloneTemplate(targetDir, tag) {
|
|
137
|
+
const absoluteTarget = path.resolve(targetDir);
|
|
138
|
+
|
|
139
|
+
if (fs.existsSync(absoluteTarget)) {
|
|
140
|
+
throw new Error(`Directory "${targetDir}" already exists`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const branchArg = tag ? `--branch ${tag}` : '';
|
|
144
|
+
execSync(`git clone --depth 1 ${branchArg} ${REPO_URL} "${absoluteTarget}"`, {
|
|
145
|
+
stdio: 'pipe',
|
|
146
|
+
timeout: 120000,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Remove .git directory
|
|
150
|
+
fs.rmSync(path.join(absoluteTarget, '.git'), { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Step 2: Test clone module**
|
|
155
|
+
|
|
156
|
+
Run: `node -e "import('./src/clone.js').then(m => console.log('Tags:', m.fetchTags()))"`
|
|
157
|
+
Expected: List of tags (or empty array if no tags yet)
|
|
158
|
+
|
|
159
|
+
**Step 3: Commit**
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
git add src/clone.js
|
|
163
|
+
git commit -m "feat: add git clone and tag fetching logic"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
### Task 3: Implement interactive prompts
|
|
169
|
+
|
|
170
|
+
**Files:**
|
|
171
|
+
- Create: `src/prompts.js`
|
|
172
|
+
|
|
173
|
+
**Step 1: Create src/prompts.js**
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
import inquirer from 'inquirer';
|
|
177
|
+
import { fetchTags } from './clone.js';
|
|
178
|
+
|
|
179
|
+
const STARTER_MODULES = [
|
|
180
|
+
{ name: 'avatar-boot-starter-web (Web 基础设施)', value: 'web', checked: true },
|
|
181
|
+
{ name: 'avatar-boot-starter-nacos (服务注册与配置)', value: 'nacos', checked: true },
|
|
182
|
+
{ name: 'avatar-boot-starter-feign (服务间调用)', value: 'feign', checked: false },
|
|
183
|
+
{ name: 'avatar-boot-starter-mysql (MySQL 数据库)', value: 'mysql', checked: false },
|
|
184
|
+
{ name: 'avatar-boot-starter-redis (Redis 缓存)', value: 'redis', checked: false },
|
|
185
|
+
{ name: 'avatar-boot-starter-kafka (Kafka 消息队列)', value: 'kafka', checked: false },
|
|
186
|
+
{ name: 'avatar-boot-starter-rocketmq (RocketMQ 消息队列)', value: 'rocketmq', checked: false },
|
|
187
|
+
{ name: 'avatar-boot-starter-job (XXL-Job 任务调度)', value: 'job', checked: false },
|
|
188
|
+
{ name: 'avatar-boot-starter-oss (对象存储)', value: 'oss', checked: false },
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Run interactive prompts to collect project configuration
|
|
193
|
+
* @param {string} [initialName] - Project name from CLI argument
|
|
194
|
+
* @returns {Promise<object>} User configuration
|
|
195
|
+
*/
|
|
196
|
+
export async function collectConfig(initialName) {
|
|
197
|
+
const tags = fetchTags();
|
|
198
|
+
|
|
199
|
+
const versionChoices = tags.length > 0
|
|
200
|
+
? [...tags.map((t) => ({ name: t, value: t })), { name: 'latest (main branch)', value: '' }]
|
|
201
|
+
: [{ name: 'latest (main branch)', value: '' }];
|
|
202
|
+
|
|
203
|
+
const questions = [
|
|
204
|
+
{
|
|
205
|
+
type: 'input',
|
|
206
|
+
name: 'projectName',
|
|
207
|
+
message: '项目名称:',
|
|
208
|
+
default: initialName || 'my-avatar-service',
|
|
209
|
+
validate: (input) => /^[a-z][a-z0-9-]*$/.test(input) || '项目名称只能包含小写字母、数字和连字符,且以字母开头',
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
type: 'input',
|
|
213
|
+
name: 'groupId',
|
|
214
|
+
message: 'Maven groupId:',
|
|
215
|
+
default: 'com.iflytek.avatar',
|
|
216
|
+
validate: (input) => /^[a-z][a-z0-9.]*$/.test(input) || 'groupId 格式不正确',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
type: 'list',
|
|
220
|
+
name: 'version',
|
|
221
|
+
message: 'Avatar Boot 版本:',
|
|
222
|
+
choices: versionChoices,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
type: 'checkbox',
|
|
226
|
+
name: 'starters',
|
|
227
|
+
message: '选择 Starter 模块:',
|
|
228
|
+
choices: STARTER_MODULES,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
type: 'confirm',
|
|
232
|
+
name: 'includeExample',
|
|
233
|
+
message: '是否生成示例代码 (login 模块):',
|
|
234
|
+
default: true,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
type: 'input',
|
|
238
|
+
name: 'nacosAddr',
|
|
239
|
+
message: 'Nacos 地址:',
|
|
240
|
+
default: '127.0.0.1:8848',
|
|
241
|
+
when: (answers) => answers.starters.includes('nacos'),
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
type: 'input',
|
|
245
|
+
name: 'dbUrl',
|
|
246
|
+
message: '数据库地址 (host:port/dbname):',
|
|
247
|
+
default: 'localhost:3306/avatar',
|
|
248
|
+
when: (answers) => answers.starters.includes('mysql'),
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
type: 'confirm',
|
|
252
|
+
name: 'includeAiConfig',
|
|
253
|
+
message: '是否生成 .claude/ AI Agent 配置:',
|
|
254
|
+
default: true,
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
type: 'input',
|
|
258
|
+
name: 'serverPort',
|
|
259
|
+
message: '服务端口:',
|
|
260
|
+
default: '8888',
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
return inquirer.prompt(questions);
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Step 2: Test prompts module**
|
|
269
|
+
|
|
270
|
+
Run: `node -e "import('./src/prompts.js').then(m => m.collectConfig('test').then(c => console.log(JSON.stringify(c, null, 2))))"`
|
|
271
|
+
Expected: Interactive prompts appear, config object printed after completion
|
|
272
|
+
|
|
273
|
+
**Step 3: Commit**
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
git add src/prompts.js
|
|
277
|
+
git commit -m "feat: add interactive prompts for project configuration"
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
### Task 4: Implement template transformation logic
|
|
283
|
+
|
|
284
|
+
**Files:**
|
|
285
|
+
- Create: `src/transform.js`
|
|
286
|
+
- Create: `src/utils.js`
|
|
287
|
+
|
|
288
|
+
**Step 1: Create src/utils.js**
|
|
289
|
+
|
|
290
|
+
```javascript
|
|
291
|
+
import fs from 'fs';
|
|
292
|
+
import path from 'path';
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Convert groupId to directory path (e.g., "com.iflytek.avatar" → "com/iflytek/avatar")
|
|
296
|
+
*/
|
|
297
|
+
export function groupIdToPath(groupId) {
|
|
298
|
+
return groupId.replace(/\./g, '/');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Recursively get all files in a directory
|
|
303
|
+
*/
|
|
304
|
+
export function getAllFiles(dir, fileList = []) {
|
|
305
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
306
|
+
for (const entry of entries) {
|
|
307
|
+
const fullPath = path.join(dir, entry.name);
|
|
308
|
+
if (entry.isDirectory()) {
|
|
309
|
+
getAllFiles(fullPath, fileList);
|
|
310
|
+
} else {
|
|
311
|
+
fileList.push(fullPath);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return fileList;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Replace content in a file
|
|
319
|
+
*/
|
|
320
|
+
export function replaceInFile(filePath, replacements) {
|
|
321
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
322
|
+
for (const [search, replace] of replacements) {
|
|
323
|
+
content = content.replaceAll(search, replace);
|
|
324
|
+
}
|
|
325
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Rename directory
|
|
330
|
+
*/
|
|
331
|
+
export function renameDir(oldPath, newPath) {
|
|
332
|
+
if (fs.existsSync(oldPath)) {
|
|
333
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
334
|
+
fs.renameSync(oldPath, newPath);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**Step 2: Create src/transform.js**
|
|
340
|
+
|
|
341
|
+
```javascript
|
|
342
|
+
import fs from 'fs';
|
|
343
|
+
import path from 'path';
|
|
344
|
+
import { groupIdToPath, getAllFiles, replaceInFile, renameDir } from './utils.js';
|
|
345
|
+
|
|
346
|
+
const DEFAULT_GROUP_ID = 'com.iflytek.avatar';
|
|
347
|
+
const DEFAULT_GROUP_PATH = 'com/iflytek/avatar';
|
|
348
|
+
const DEFAULT_ARTIFACT_ID = 'avatar-template';
|
|
349
|
+
const DEFAULT_APP_NAME = 'scaffold-template';
|
|
350
|
+
const DEFAULT_NACOS_ADDR = '172.29.242.247:8848';
|
|
351
|
+
|
|
352
|
+
// All starters that can be managed in service pom.xml
|
|
353
|
+
const ALL_STARTERS = ['web', 'nacos', 'feign', 'mysql', 'redis', 'kafka', 'rocketmq', 'job', 'oss'];
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Transform cloned template based on user configuration
|
|
357
|
+
* @param {string} projectDir - Root directory of cloned template
|
|
358
|
+
* @param {object} config - User configuration from prompts
|
|
359
|
+
*/
|
|
360
|
+
export function transformProject(projectDir, config) {
|
|
361
|
+
const {
|
|
362
|
+
projectName,
|
|
363
|
+
groupId = DEFAULT_GROUP_ID,
|
|
364
|
+
starters = ['web', 'nacos'],
|
|
365
|
+
includeExample = true,
|
|
366
|
+
nacosAddr = '127.0.0.1:8848',
|
|
367
|
+
dbUrl,
|
|
368
|
+
includeAiConfig = true,
|
|
369
|
+
serverPort = '8888',
|
|
370
|
+
} = config;
|
|
371
|
+
|
|
372
|
+
const newGroupPath = groupIdToPath(groupId);
|
|
373
|
+
const apiModule = `${projectName}-api`;
|
|
374
|
+
const serviceModule = `${projectName}-service`;
|
|
375
|
+
|
|
376
|
+
// 1. Rename module directories
|
|
377
|
+
renameDir(
|
|
378
|
+
path.join(projectDir, 'avatar-scaffold-api'),
|
|
379
|
+
path.join(projectDir, apiModule)
|
|
380
|
+
);
|
|
381
|
+
renameDir(
|
|
382
|
+
path.join(projectDir, 'avatar-scaffold-service'),
|
|
383
|
+
path.join(projectDir, serviceModule)
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// 2. Rename Java package directories if groupId changed
|
|
387
|
+
if (groupId !== DEFAULT_GROUP_ID) {
|
|
388
|
+
// API module
|
|
389
|
+
const apiOldPkg = path.join(projectDir, apiModule, 'src/main/java', DEFAULT_GROUP_PATH);
|
|
390
|
+
const apiNewPkg = path.join(projectDir, apiModule, 'src/main/java', newGroupPath);
|
|
391
|
+
if (fs.existsSync(apiOldPkg)) {
|
|
392
|
+
renameDir(apiOldPkg, apiNewPkg);
|
|
393
|
+
// Clean up empty parent directories
|
|
394
|
+
cleanEmptyDirs(path.join(projectDir, apiModule, 'src/main/java'));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Service module
|
|
398
|
+
const svcOldPkg = path.join(projectDir, serviceModule, 'src/main/java', DEFAULT_GROUP_PATH);
|
|
399
|
+
const svcNewPkg = path.join(projectDir, serviceModule, 'src/main/java', newGroupPath);
|
|
400
|
+
if (fs.existsSync(svcOldPkg)) {
|
|
401
|
+
renameDir(svcOldPkg, svcNewPkg);
|
|
402
|
+
cleanEmptyDirs(path.join(projectDir, serviceModule, 'src/main/java'));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 3. Remove example code if not needed
|
|
407
|
+
if (!includeExample) {
|
|
408
|
+
const apiLoginDir = path.join(projectDir, apiModule, 'src/main/java', newGroupPath, 'login');
|
|
409
|
+
const svcLoginDir = path.join(projectDir, serviceModule, 'src/main/java', newGroupPath, 'login');
|
|
410
|
+
if (fs.existsSync(apiLoginDir)) fs.rmSync(apiLoginDir, { recursive: true });
|
|
411
|
+
if (fs.existsSync(svcLoginDir)) fs.rmSync(svcLoginDir, { recursive: true });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 4. Remove .claude/ if not needed
|
|
415
|
+
if (!includeAiConfig) {
|
|
416
|
+
const claudeDir = path.join(projectDir, '.claude');
|
|
417
|
+
if (fs.existsSync(claudeDir)) fs.rmSync(claudeDir, { recursive: true });
|
|
418
|
+
const claudeMd = path.join(projectDir, 'CLAUDE.md');
|
|
419
|
+
if (fs.existsSync(claudeMd)) fs.unlinkSync(claudeMd);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 5. Replace content in all text files
|
|
423
|
+
const textExtensions = ['.xml', '.java', '.yaml', '.yml', '.properties', '.md', '.txt'];
|
|
424
|
+
const allFiles = getAllFiles(projectDir);
|
|
425
|
+
const textFiles = allFiles.filter((f) =>
|
|
426
|
+
textExtensions.some((ext) => f.endsWith(ext))
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
const replacements = [
|
|
430
|
+
// Module names in pom.xml
|
|
431
|
+
['avatar-scaffold-api', apiModule],
|
|
432
|
+
['avatar-scaffold-service', serviceModule],
|
|
433
|
+
// Artifact IDs
|
|
434
|
+
['avatar-template-api', `${projectName}-api`],
|
|
435
|
+
['avatar-template-service', `${projectName}-service`],
|
|
436
|
+
['avatar-template', projectName],
|
|
437
|
+
// Group ID
|
|
438
|
+
[DEFAULT_GROUP_ID, groupId],
|
|
439
|
+
// Package path in Java files
|
|
440
|
+
[`package ${DEFAULT_GROUP_ID}`, `package ${groupId}`],
|
|
441
|
+
[`import ${DEFAULT_GROUP_ID}`, `import ${groupId}`],
|
|
442
|
+
// Application name
|
|
443
|
+
[DEFAULT_APP_NAME, projectName],
|
|
444
|
+
// Nacos address
|
|
445
|
+
[DEFAULT_NACOS_ADDR, nacosAddr],
|
|
446
|
+
// Server port
|
|
447
|
+
['port: 8888', `port: ${serverPort}`],
|
|
448
|
+
// Main class reference
|
|
449
|
+
[`${DEFAULT_GROUP_ID}.Application`, `${groupId}.Application`],
|
|
450
|
+
];
|
|
451
|
+
|
|
452
|
+
for (const file of textFiles) {
|
|
453
|
+
replaceInFile(file, replacements);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 6. Handle starter dependencies in service pom.xml
|
|
457
|
+
transformServicePom(path.join(projectDir, serviceModule, 'pom.xml'), starters);
|
|
458
|
+
|
|
459
|
+
// 7. Handle database URL in config
|
|
460
|
+
if (dbUrl && starters.includes('mysql')) {
|
|
461
|
+
const devYaml = path.join(projectDir, serviceModule, 'src/main/resources/application-dev.yaml');
|
|
462
|
+
if (fs.existsSync(devYaml)) {
|
|
463
|
+
replaceInFile(devYaml, [['localhost:3306/avatar', dbUrl]]);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 8. Remove .DS_Store files
|
|
468
|
+
for (const file of allFiles) {
|
|
469
|
+
if (path.basename(file) === '.DS_Store') {
|
|
470
|
+
fs.unlinkSync(file);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Manage starter dependencies in service pom.xml
|
|
477
|
+
* - Selected starters: uncomment if commented, add if missing
|
|
478
|
+
* - Unselected starters: comment out if present
|
|
479
|
+
*/
|
|
480
|
+
function transformServicePom(pomPath, selectedStarters) {
|
|
481
|
+
if (!fs.existsSync(pomPath)) return;
|
|
482
|
+
|
|
483
|
+
let content = fs.readFileSync(pomPath, 'utf-8');
|
|
484
|
+
|
|
485
|
+
for (const starter of ALL_STARTERS) {
|
|
486
|
+
const artifactId = `avatar-boot-starter-${starter}`;
|
|
487
|
+
const depBlock = ` <dependency>\n <groupId>com.iflytek.avatar.boot</groupId>\n <artifactId>${artifactId}</artifactId>\n </dependency>`;
|
|
488
|
+
const commentedPattern = new RegExp(
|
|
489
|
+
`<!--\\s*<dependency>\\s*\\n\\s*<groupId>com\\.iflytek\\.avatar\\.boot</groupId>\\s*\\n\\s*<artifactId>${artifactId}</artifactId>\\s*\\n\\s*</dependency>\\s*-->`,
|
|
490
|
+
's'
|
|
491
|
+
);
|
|
492
|
+
const uncommentedPattern = new RegExp(
|
|
493
|
+
`<dependency>\\s*\\n\\s*<groupId>com\\.iflytek\\.avatar\\.boot</groupId>\\s*\\n\\s*<artifactId>${artifactId}</artifactId>\\s*\\n\\s*</dependency>`,
|
|
494
|
+
's'
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
if (selectedStarters.includes(starter)) {
|
|
498
|
+
// Uncomment if commented
|
|
499
|
+
const commentedMatch = content.match(commentedPattern);
|
|
500
|
+
if (commentedMatch) {
|
|
501
|
+
content = content.replace(commentedMatch[0], depBlock);
|
|
502
|
+
} else if (!uncommentedPattern.test(content)) {
|
|
503
|
+
// Add dependency if not present at all
|
|
504
|
+
const insertPoint = content.indexOf('</dependencies>');
|
|
505
|
+
if (insertPoint !== -1) {
|
|
506
|
+
content = content.slice(0, insertPoint) + depBlock + '\n' + content.slice(insertPoint);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
// Comment out if uncommented
|
|
511
|
+
const uncommentedMatch = content.match(uncommentedPattern);
|
|
512
|
+
if (uncommentedMatch) {
|
|
513
|
+
content = content.replace(
|
|
514
|
+
uncommentedMatch[0],
|
|
515
|
+
`<!--${uncommentedMatch[0]}-->`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
fs.writeFileSync(pomPath, content, 'utf-8');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Remove empty directories recursively (bottom-up)
|
|
526
|
+
*/
|
|
527
|
+
function cleanEmptyDirs(dir) {
|
|
528
|
+
if (!fs.existsSync(dir)) return;
|
|
529
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
530
|
+
for (const entry of entries) {
|
|
531
|
+
if (entry.isDirectory()) {
|
|
532
|
+
cleanEmptyDirs(path.join(dir, entry.name));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Re-read after potential child cleanup
|
|
536
|
+
const remaining = fs.readdirSync(dir);
|
|
537
|
+
if (remaining.length === 0) {
|
|
538
|
+
fs.rmdirSync(dir);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
**Step 3: Commit**
|
|
544
|
+
|
|
545
|
+
```bash
|
|
546
|
+
git add src/utils.js src/transform.js
|
|
547
|
+
git commit -m "feat: add template transformation and string replacement logic"
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
---
|
|
551
|
+
|
|
552
|
+
### Task 5: Wire everything together in src/index.js
|
|
553
|
+
|
|
554
|
+
**Files:**
|
|
555
|
+
- Modify: `src/index.js`
|
|
556
|
+
|
|
557
|
+
**Step 1: Update src/index.js with full init flow**
|
|
558
|
+
|
|
559
|
+
```javascript
|
|
560
|
+
import { Command } from 'commander';
|
|
561
|
+
import chalk from 'chalk';
|
|
562
|
+
import ora from 'ora';
|
|
563
|
+
import { collectConfig } from './prompts.js';
|
|
564
|
+
import { cloneTemplate } from './clone.js';
|
|
565
|
+
import { transformProject } from './transform.js';
|
|
566
|
+
|
|
567
|
+
export function run() {
|
|
568
|
+
const program = new Command();
|
|
569
|
+
|
|
570
|
+
program
|
|
571
|
+
.name('avatar-boot-cli')
|
|
572
|
+
.description('CLI tool for scaffolding AvatarBoot Java microservice projects')
|
|
573
|
+
.version('1.0.0');
|
|
574
|
+
|
|
575
|
+
program
|
|
576
|
+
.command('init [project-name]')
|
|
577
|
+
.description('Initialize a new AvatarBoot project')
|
|
578
|
+
.action(async (projectName) => {
|
|
579
|
+
console.log('');
|
|
580
|
+
console.log(chalk.blue.bold(' Avatar Boot CLI'));
|
|
581
|
+
console.log(chalk.gray(' AvatarBoot Java 微服务项目脚手架工具'));
|
|
582
|
+
console.log('');
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
// 1. Collect configuration
|
|
586
|
+
const config = await collectConfig(projectName);
|
|
587
|
+
|
|
588
|
+
console.log('');
|
|
589
|
+
|
|
590
|
+
// 2. Clone template
|
|
591
|
+
const spinner = ora('正在克隆模板仓库...').start();
|
|
592
|
+
try {
|
|
593
|
+
cloneTemplate(config.projectName, config.version || undefined);
|
|
594
|
+
spinner.succeed('模板仓库克隆完成');
|
|
595
|
+
} catch (err) {
|
|
596
|
+
spinner.fail('模板仓库克隆失败');
|
|
597
|
+
console.error(chalk.red(err.message));
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// 3. Transform project
|
|
602
|
+
const transformSpinner = ora('正在生成项目...').start();
|
|
603
|
+
try {
|
|
604
|
+
transformProject(config.projectName, config);
|
|
605
|
+
transformSpinner.succeed('项目生成完成');
|
|
606
|
+
} catch (err) {
|
|
607
|
+
transformSpinner.fail('项目生成失败');
|
|
608
|
+
console.error(chalk.red(err.message));
|
|
609
|
+
process.exit(1);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// 4. Print success message
|
|
613
|
+
console.log('');
|
|
614
|
+
console.log(chalk.green.bold(' ✅ 项目创建成功!'));
|
|
615
|
+
console.log('');
|
|
616
|
+
console.log(chalk.white(' 开始开发:'));
|
|
617
|
+
console.log(chalk.cyan(` cd ${config.projectName}`));
|
|
618
|
+
console.log(chalk.cyan(' mvn clean compile'));
|
|
619
|
+
console.log('');
|
|
620
|
+
console.log(chalk.white(' 项目结构:'));
|
|
621
|
+
console.log(chalk.gray(` ${config.projectName}/`));
|
|
622
|
+
console.log(chalk.gray(` ├── ${config.projectName}-api/ # API 模块 (接口定义)`));
|
|
623
|
+
console.log(chalk.gray(` ├── ${config.projectName}-service/ # Service 模块 (业务实现)`));
|
|
624
|
+
console.log(chalk.gray(` └── pom.xml`));
|
|
625
|
+
console.log('');
|
|
626
|
+
} catch (err) {
|
|
627
|
+
if (err.name === 'ExitPromptError') {
|
|
628
|
+
console.log(chalk.yellow('\n 已取消'));
|
|
629
|
+
} else {
|
|
630
|
+
console.error(chalk.red(`\n 错误: ${err.message}`));
|
|
631
|
+
}
|
|
632
|
+
process.exit(1);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
program.parse();
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
**Step 2: Test full flow**
|
|
641
|
+
|
|
642
|
+
Run: `node bin/cli.js init`
|
|
643
|
+
Expected: Full interactive flow works — prompts → clone → transform → success message
|
|
644
|
+
|
|
645
|
+
**Step 3: Commit**
|
|
646
|
+
|
|
647
|
+
```bash
|
|
648
|
+
git add src/index.js
|
|
649
|
+
git commit -m "feat: wire init command with prompts, clone, and transform"
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
### Task 6: Add .gitignore and finalize
|
|
655
|
+
|
|
656
|
+
**Files:**
|
|
657
|
+
- Create: `.gitignore`
|
|
658
|
+
|
|
659
|
+
**Step 1: Create .gitignore**
|
|
660
|
+
|
|
661
|
+
```
|
|
662
|
+
node_modules/
|
|
663
|
+
.DS_Store
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
**Step 2: Final commit**
|
|
667
|
+
|
|
668
|
+
```bash
|
|
669
|
+
git add .gitignore
|
|
670
|
+
git commit -m "chore: add .gitignore"
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## Task Dependencies
|
|
676
|
+
|
|
677
|
+
```
|
|
678
|
+
Task 1 (npm + CLI entry) → Task 2 (clone) → Task 3 (prompts) → Task 4 (transform) → Task 5 (wire together) → Task 6 (finalize)
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
All tasks are sequential — each builds on the previous.
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@llryiop/avatar-boot-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool for scaffolding AvatarBoot Java microservice projects",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"avatar-boot-cli": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test src/__tests__/*.test.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"avatar",
|
|
15
|
+
"boot",
|
|
16
|
+
"scaffold",
|
|
17
|
+
"java",
|
|
18
|
+
"microservice"
|
|
19
|
+
],
|
|
20
|
+
"license": "ISC",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"chalk": "^5.6.2",
|
|
23
|
+
"commander": "^14.0.3",
|
|
24
|
+
"glob": "^13.0.6",
|
|
25
|
+
"inquirer": "^13.3.0",
|
|
26
|
+
"ora": "^9.3.0"
|
|
27
|
+
}
|
|
28
|
+
}
|