adspecs 0.1.19 → 0.1.21
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/.adspecs/feature.json +16 -16
- package/.adspecs/feature.yml +28 -28
- package/.adspecs/paths.json +17 -17
- package/.adspecs/templates/04-/345/211/215/347/253/257/345/212/237/350/203/275/350/256/276/350/256/241/346/250/241/346/235/277.md +1 -1
- package/.adspecs/templates/05-/345/220/216/347/253/257/344/273/273/345/212/241/346/270/205/345/215/225/346/250/241/346/235/277.md +613 -724
- package/.adspecs/templates/05b-/345/211/215/347/253/257/344/273/273/345/212/241/346/270/205/345/215/225/346/250/241/346/235/277.md +51 -51
- package/.claude-plugin/marketplace.json +23 -23
- package/.claude-plugin/plugin.json +18 -18
- package/.qoder-plugin/plugin.json +31 -31
- package/CLAUDE.md +1 -5
- package/INSTALL.md +3 -3
- package/README.md +395 -395
- package/bin/adspecs.js +129 -129
- package/hooks/commit-queue.js +245 -245
- package/hooks/hooks.json +63 -63
- package/hooks/session-start.js +44 -44
- package/hooks/wiki-queue.js +127 -127
- package/package.json +61 -61
- package/references/ant6-front-standard/index.md +99 -99
- package/references/antd-front-demo/public/mockServiceWorker.js +361 -361
- package/references/ecp-end-standard/index.md +63 -63
- package/references/python-end-standard/01-Python/345/220/216/347/253/257/347/274/226/347/240/201/350/247/204/350/214/203.md +372 -372
- package/references/python-end-standard/02-/346/225/260/346/215/256/345/272/223/350/256/276/350/256/241/344/270/216/344/275/277/347/224/250/350/247/204/350/214/203.md +226 -226
- package/references/python-end-standard/03-Celery/345/274/202/346/255/245/344/273/273/345/212/241/350/247/204/350/214/203.md +237 -237
- package/references/python-end-standard/04-Redis/344/275/277/347/224/250/350/247/204/350/214/203.md +231 -231
- package/scripts/postinstall.js +107 -107
- package/scripts/sync-version.js +105 -105
- package/skills/.claude/.wiki-update-queue +26 -26
- package/skills/adspecs-constitution/SKILL.md +157 -0
- package/skills/adspecs-export-word/SKILL.md +498 -498
- package/skills/adspecs-export-word/references/md-to-docx.js +862 -862
- package/skills/adspecs-export-word/references/package-lock.json +220 -220
- package/skills/adspecs-export-word/references/package.json +10 -10
- package/skills/adspecs-front-prototype/SKILL.md +405 -405
- package/skills/adspecs-front-spec/SKILL.md +4 -4
- package/skills/adspecs-front-tasks/SKILL.md +213 -173
- package/skills/adspecs-plan/SKILL.md +59 -69
- package/skills/adspecs-prd/SKILL.md +13 -5
- package/skills/adspecs-prd-to-demo/SKILL.md +532 -0
- package/skills/adspecs-tasks/SKILL.md +175 -204
- package/skills/adspecs-update-status/SKILL.md +382 -382
- package/skills/adspecs-utest/SKILL.md +107 -116
- package/skills/grill-me/SKILL.md +7 -0
- package/skills/grill-me/agents/openai.yaml +5 -0
- package/skills/playwright-cli/SKILL.md +420 -0
- package/skills/playwright-cli/references/element-attributes.md +23 -0
- package/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/skills/playwright-cli/references/request-mocking.md +87 -0
- package/skills/playwright-cli/references/running-code.md +241 -0
- package/skills/playwright-cli/references/session-management.md +225 -0
- package/skills/playwright-cli/references/storage-state.md +275 -0
- package/skills/playwright-cli/references/test-generation.md +433 -0
- package/skills/playwright-cli/references/tracing.md +139 -0
- package/skills/playwright-cli/references/video-recording.md +143 -0
- package/skills/playwright-trace/SKILL.md +171 -0
- package/skills/project-init/SKILL.md +93 -22
- package/skills/project-init/references/front-demo/.claude/settings.local.json +9 -0
- package/skills/wiki-update/SKILL.md +232 -232
- package/src/commands/doctor.js +197 -197
- package/src/commands/init.js +83 -83
- package/src/commands/plugin.js +165 -165
- package/src/commands/update.js +87 -87
- package/src/lib/area-scanner.js +129 -129
- package/src/lib/copier.js +104 -104
- package/src/lib/dir-utils.js +161 -133
- package/src/lib/json-merge.js +114 -114
- package/src/lib/paths-defaults.js +37 -37
- package/src/lib/prompts.js +428 -347
- package/src/lib/readme-gen.js +143 -143
- package/src/lib/report.js +338 -327
- package/src/lib/scaffolder.js +551 -518
- package/src/lib/short-name.js +36 -36
- package/src/utils.js +80 -80
- package/references/antd-front-demo/.env +0 -15
package/src/lib/prompts.js
CHANGED
|
@@ -1,347 +1,428 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { input, select, confirm } = require('@inquirer/prompts');
|
|
6
|
-
const { hasAnyFile, isEmptyDirectoryTree } = require('./dir-utils');
|
|
7
|
-
const { areaDisplayName } = require('./area-scanner');
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* 交互式提示封装
|
|
11
|
-
*
|
|
12
|
-
* 处理三种模式:
|
|
13
|
-
* - 交互模式(默认):通过 inquirer 提示用户输入
|
|
14
|
-
* - 非交互模式(--non-interactive):所有未提供的参数使用默认值
|
|
15
|
-
* - 强制模式(--force):所有已存在区域直接 recreate
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
// ============================================================
|
|
19
|
-
// 项目信息收集
|
|
20
|
-
// ============================================================
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* 收集项目信息(5 个字段)
|
|
24
|
-
*
|
|
25
|
-
* @param {object} options - CLI 传入的选项
|
|
26
|
-
* @returns {Promise<object>} 完整的项目信息对象
|
|
27
|
-
*/
|
|
28
|
-
async function collectProjectInfo(options) {
|
|
29
|
-
const nonInteractive = options.nonInteractive || false;
|
|
30
|
-
|
|
31
|
-
// === 第一次收集:核心信息 ===
|
|
32
|
-
const projectName =
|
|
33
|
-
options.projectName ||
|
|
34
|
-
(nonInteractive ? 'my-project' : await input({ message: '请输入项目名称:' }));
|
|
35
|
-
|
|
36
|
-
const projectDir =
|
|
37
|
-
options.projectDir ??
|
|
38
|
-
(nonInteractive
|
|
39
|
-
? ''
|
|
40
|
-
: await input({ message: '请输入项目目录名(留空=当前目录):', default: '' }));
|
|
41
|
-
|
|
42
|
-
// === 初始化模式 ===
|
|
43
|
-
const mode =
|
|
44
|
-
options.mode ||
|
|
45
|
-
(nonInteractive
|
|
46
|
-
? 'simple'
|
|
47
|
-
: await select({
|
|
48
|
-
message: '请选择初始化模式:',
|
|
49
|
-
choices: [
|
|
50
|
-
{ name: '简易模式 — 仅文档目录 (docs + wiki)', value: 'simple' },
|
|
51
|
-
{ name: '经典模式 — 文档 + 前端演示 (+ front-demo)', value: 'classic' },
|
|
52
|
-
{ name: '完整模式 — 所有目录 (+ web-ui + service)', value: 'full' },
|
|
53
|
-
{ name: '轻量模式 — Python+React 全栈 demo', value: 'demo' },
|
|
54
|
-
],
|
|
55
|
-
}));
|
|
56
|
-
|
|
57
|
-
// === 前端技术栈(仅 classic/full 模式需要选择) ===
|
|
58
|
-
let frontDemo = options.frontDemo;
|
|
59
|
-
if ((mode === 'classic' || mode === 'full') && !frontDemo && !nonInteractive) {
|
|
60
|
-
frontDemo = await select({
|
|
61
|
-
message: '请选择前端演示技术栈:',
|
|
62
|
-
choices: [
|
|
63
|
-
{ name: 'Vue(Vue 3 + Vite + TypeScript + Element Plus)', value: 'vue' },
|
|
64
|
-
{ name: 'Ant Design React(React + Ant Design 6)', value: 'antd' },
|
|
65
|
-
{ name: '跳过 — 不创建前端演示项目', value: 'skip' },
|
|
66
|
-
],
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
if (mode === 'demo') {
|
|
70
|
-
frontDemo = 'antd'; // 轻量模式固定 Ant Design React
|
|
71
|
-
}
|
|
72
|
-
// 兜底默认:非交互模式 / simple 模式 / 用户未选时 → vue
|
|
73
|
-
frontDemo = frontDemo || 'vue';
|
|
74
|
-
|
|
75
|
-
// === 第二次收集:补充信息 ===
|
|
76
|
-
const companyName =
|
|
77
|
-
options.company ||
|
|
78
|
-
(nonInteractive
|
|
79
|
-
? 'XX有限公司'
|
|
80
|
-
: await input({
|
|
81
|
-
message: '请输入公司名称(不填默认"XX有限公司"):',
|
|
82
|
-
default: 'XX有限公司',
|
|
83
|
-
}));
|
|
84
|
-
|
|
85
|
-
const confidentiality =
|
|
86
|
-
options.confidentiality && options.confidentiality !== '内部资料'
|
|
87
|
-
? options.confidentiality
|
|
88
|
-
: nonInteractive
|
|
89
|
-
? '内部资料'
|
|
90
|
-
: await select({
|
|
91
|
-
message: '请选择文件密级(不选默认"内部资料"):',
|
|
92
|
-
choices: [
|
|
93
|
-
{ name: '内部资料(默认) — 仅限公司内部使用', value: '内部资料' },
|
|
94
|
-
{ name: '公开 — 可对外公开', value: '公开' },
|
|
95
|
-
{ name: '机密 — 限授权人员访问', value: '机密' },
|
|
96
|
-
{ name: '绝密 — 最高保密等级', value: '绝密' },
|
|
97
|
-
],
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
project_name: projectName,
|
|
102
|
-
project_dir: projectDir,
|
|
103
|
-
company_name: companyName,
|
|
104
|
-
confidentiality,
|
|
105
|
-
mode,
|
|
106
|
-
frontDemo,
|
|
107
|
-
modules: options.modules || '3100-crm-mdm:mdm,3103-crm-contract:contract',
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ============================================================
|
|
112
|
-
// 冲突决策(每区域 3 选项)
|
|
113
|
-
// ============================================================
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* 收集每区域的冲突决策
|
|
117
|
-
*
|
|
118
|
-
* @param {object} areaStates - 区域状态映射(来自 scanAreaStates)
|
|
119
|
-
* @param {object} options - CLI 选项
|
|
120
|
-
* @returns {Promise<object>} 决策映射 { areaKey: 'create'|'recreate'|'update'|'skip' }
|
|
121
|
-
*/
|
|
122
|
-
async function collectDecisions(areaStates, options) {
|
|
123
|
-
const force = options.force || false;
|
|
124
|
-
const nonInteractive = options.nonInteractive || false;
|
|
125
|
-
|
|
126
|
-
// --force:所有已存在区域直接 recreate
|
|
127
|
-
if (force) {
|
|
128
|
-
const decisions = {};
|
|
129
|
-
for (const [key, state] of Object.entries(areaStates)) {
|
|
130
|
-
decisions[key] = state.exists ? 'recreate' : 'create';
|
|
131
|
-
}
|
|
132
|
-
return decisions;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// --non-interactive:所有已存在区域 skip(保留现有)
|
|
136
|
-
if (nonInteractive) {
|
|
137
|
-
const decisions = {};
|
|
138
|
-
for (const [key, state] of Object.entries(areaStates)) {
|
|
139
|
-
decisions[key] = state.exists ? 'skip' : 'create';
|
|
140
|
-
}
|
|
141
|
-
return decisions;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// 交互模式:对每个已存在区域调用 3 选项 prompt
|
|
145
|
-
const decisions = {};
|
|
146
|
-
const existingAreas = Object.entries(areaStates).filter(([, state]) => state.exists);
|
|
147
|
-
|
|
148
|
-
if (existingAreas.length === 0) {
|
|
149
|
-
// 所有区域都不存在 → 全部 create
|
|
150
|
-
for (const key of Object.keys(areaStates)) {
|
|
151
|
-
decisions[key] = 'create';
|
|
152
|
-
}
|
|
153
|
-
return decisions;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// 提示用户是否要逐个确认
|
|
157
|
-
if (existingAreas.length >= 2) {
|
|
158
|
-
const bulkChoice = await select({
|
|
159
|
-
message: `检测到 ${existingAreas.length} 个区域已存在,如何统一处理?`,
|
|
160
|
-
choices: [
|
|
161
|
-
{ name: '逐个确认 — 对每个区域分别选择处理方式', value: 'ask-each' },
|
|
162
|
-
{ name: '全部重新创建 — 删除并重建所有已存在区域', value: 'recreate-all' },
|
|
163
|
-
{ name: '全部跳过 — 保留所有已存在区域', value: 'skip-all' },
|
|
164
|
-
],
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
if (bulkChoice === 'recreate-all') {
|
|
168
|
-
for (const [key, state] of Object.entries(areaStates)) {
|
|
169
|
-
decisions[key] = state.exists ? 'recreate' : 'create';
|
|
170
|
-
}
|
|
171
|
-
return decisions;
|
|
172
|
-
}
|
|
173
|
-
if (bulkChoice === 'skip-all') {
|
|
174
|
-
for (const [key, state] of Object.entries(areaStates)) {
|
|
175
|
-
decisions[key] = state.exists ? 'skip' : 'create';
|
|
176
|
-
}
|
|
177
|
-
return decisions;
|
|
178
|
-
}
|
|
179
|
-
// ask-each: 继续逐个询问
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// 逐个询问
|
|
183
|
-
for (const [key, state] of Object.entries(areaStates)) {
|
|
184
|
-
if (!state.exists) {
|
|
185
|
-
decisions[key] = 'create';
|
|
186
|
-
} else {
|
|
187
|
-
decisions[key] = await askAreaDecision(areaDisplayName(key));
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return decisions;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* 3 选项冲突决策
|
|
196
|
-
*
|
|
197
|
-
* @param {string} areaName - 区域显示名(如 'docs')
|
|
198
|
-
* @returns {Promise<'recreate'|'update'|'skip'>}
|
|
199
|
-
*/
|
|
200
|
-
async function askAreaDecision(areaName) {
|
|
201
|
-
return await select({
|
|
202
|
-
message: `${areaName}/ 已存在,如何处理?`,
|
|
203
|
-
choices: [
|
|
204
|
-
{ name: '重新创建 — 删除现有目录,使用最新模板重建', value: 'recreate' },
|
|
205
|
-
{ name: '更新 — 保留现有目录,按新内容覆盖(保留本地修改之外的文件)', value: 'update' },
|
|
206
|
-
{ name: '跳过 — 保留现有目录,不做任何修改', value: 'skip' },
|
|
207
|
-
],
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// ============================================================
|
|
212
|
-
// 子目录决策
|
|
213
|
-
// ============================================================
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* 收集子目录决策(仅当父目录 skip 时)
|
|
217
|
-
*
|
|
218
|
-
* @param {object} decisions - 区域决策
|
|
219
|
-
* @param {string} targetDir - 目标目录
|
|
220
|
-
* @param {object} options - CLI 选项
|
|
221
|
-
* @returns {Promise<object>} 子决策 { conventions, templates }
|
|
222
|
-
*/
|
|
223
|
-
async function collectSubDecisions(decisions, targetDir, options) {
|
|
224
|
-
const sub = { conventions: 'skip', templates:
|
|
225
|
-
const nonInteractive = options.nonInteractive || false;
|
|
226
|
-
const force = options.force || false;
|
|
227
|
-
|
|
228
|
-
// --force 模式:全部 skip(因为 recreate 已经完整重建了)
|
|
229
|
-
if (force) return sub;
|
|
230
|
-
|
|
231
|
-
// --non-interactive 模式:全部 skip
|
|
232
|
-
if (nonInteractive) return sub;
|
|
233
|
-
|
|
234
|
-
// conventions_decision:docs/ skip 且 40-project_conventions/ 有文件
|
|
235
|
-
if (decisions.docs === 'skip') {
|
|
236
|
-
const convDir = path.join(targetDir, 'docs/40-project_conventions');
|
|
237
|
-
if (fs.existsSync(convDir) && hasAnyFile(convDir)) {
|
|
238
|
-
sub.conventions = await askSubDecision('docs/40-project_conventions');
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// templates_decision:.adspecs/ skip 且 templates/ 有文件
|
|
243
|
-
if (decisions.adspecs === 'skip') {
|
|
244
|
-
const tmplDir = path.join(targetDir, '.adspecs/templates');
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* @
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
{ name: '
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { input, select, confirm } = require('@inquirer/prompts');
|
|
6
|
+
const { hasAnyFile, isEmptyDirectoryTree, listFiles } = require('./dir-utils');
|
|
7
|
+
const { areaDisplayName } = require('./area-scanner');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 交互式提示封装
|
|
11
|
+
*
|
|
12
|
+
* 处理三种模式:
|
|
13
|
+
* - 交互模式(默认):通过 inquirer 提示用户输入
|
|
14
|
+
* - 非交互模式(--non-interactive):所有未提供的参数使用默认值
|
|
15
|
+
* - 强制模式(--force):所有已存在区域直接 recreate
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ============================================================
|
|
19
|
+
// 项目信息收集
|
|
20
|
+
// ============================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 收集项目信息(5 个字段)
|
|
24
|
+
*
|
|
25
|
+
* @param {object} options - CLI 传入的选项
|
|
26
|
+
* @returns {Promise<object>} 完整的项目信息对象
|
|
27
|
+
*/
|
|
28
|
+
async function collectProjectInfo(options) {
|
|
29
|
+
const nonInteractive = options.nonInteractive || false;
|
|
30
|
+
|
|
31
|
+
// === 第一次收集:核心信息 ===
|
|
32
|
+
const projectName =
|
|
33
|
+
options.projectName ||
|
|
34
|
+
(nonInteractive ? 'my-project' : await input({ message: '请输入项目名称:' }));
|
|
35
|
+
|
|
36
|
+
const projectDir =
|
|
37
|
+
options.projectDir ??
|
|
38
|
+
(nonInteractive
|
|
39
|
+
? ''
|
|
40
|
+
: await input({ message: '请输入项目目录名(留空=当前目录):', default: '' }));
|
|
41
|
+
|
|
42
|
+
// === 初始化模式 ===
|
|
43
|
+
const mode =
|
|
44
|
+
options.mode ||
|
|
45
|
+
(nonInteractive
|
|
46
|
+
? 'simple'
|
|
47
|
+
: await select({
|
|
48
|
+
message: '请选择初始化模式:',
|
|
49
|
+
choices: [
|
|
50
|
+
{ name: '简易模式 — 仅文档目录 (docs + wiki)', value: 'simple' },
|
|
51
|
+
{ name: '经典模式 — 文档 + 前端演示 (+ front-demo)', value: 'classic' },
|
|
52
|
+
{ name: '完整模式 — 所有目录 (+ web-ui + service)', value: 'full' },
|
|
53
|
+
{ name: '轻量模式 — Python+React 全栈 demo', value: 'demo' },
|
|
54
|
+
],
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
// === 前端技术栈(仅 classic/full 模式需要选择) ===
|
|
58
|
+
let frontDemo = options.frontDemo;
|
|
59
|
+
if ((mode === 'classic' || mode === 'full') && !frontDemo && !nonInteractive) {
|
|
60
|
+
frontDemo = await select({
|
|
61
|
+
message: '请选择前端演示技术栈:',
|
|
62
|
+
choices: [
|
|
63
|
+
{ name: 'Vue(Vue 3 + Vite + TypeScript + Element Plus)', value: 'vue' },
|
|
64
|
+
{ name: 'Ant Design React(React + Ant Design 6)', value: 'antd' },
|
|
65
|
+
{ name: '跳过 — 不创建前端演示项目', value: 'skip' },
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (mode === 'demo') {
|
|
70
|
+
frontDemo = 'antd'; // 轻量模式固定 Ant Design React
|
|
71
|
+
}
|
|
72
|
+
// 兜底默认:非交互模式 / simple 模式 / 用户未选时 → vue
|
|
73
|
+
frontDemo = frontDemo || 'vue';
|
|
74
|
+
|
|
75
|
+
// === 第二次收集:补充信息 ===
|
|
76
|
+
const companyName =
|
|
77
|
+
options.company ||
|
|
78
|
+
(nonInteractive
|
|
79
|
+
? 'XX有限公司'
|
|
80
|
+
: await input({
|
|
81
|
+
message: '请输入公司名称(不填默认"XX有限公司"):',
|
|
82
|
+
default: 'XX有限公司',
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
const confidentiality =
|
|
86
|
+
options.confidentiality && options.confidentiality !== '内部资料'
|
|
87
|
+
? options.confidentiality
|
|
88
|
+
: nonInteractive
|
|
89
|
+
? '内部资料'
|
|
90
|
+
: await select({
|
|
91
|
+
message: '请选择文件密级(不选默认"内部资料"):',
|
|
92
|
+
choices: [
|
|
93
|
+
{ name: '内部资料(默认) — 仅限公司内部使用', value: '内部资料' },
|
|
94
|
+
{ name: '公开 — 可对外公开', value: '公开' },
|
|
95
|
+
{ name: '机密 — 限授权人员访问', value: '机密' },
|
|
96
|
+
{ name: '绝密 — 最高保密等级', value: '绝密' },
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
project_name: projectName,
|
|
102
|
+
project_dir: projectDir,
|
|
103
|
+
company_name: companyName,
|
|
104
|
+
confidentiality,
|
|
105
|
+
mode,
|
|
106
|
+
frontDemo,
|
|
107
|
+
modules: options.modules || '3100-crm-mdm:mdm,3103-crm-contract:contract',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============================================================
|
|
112
|
+
// 冲突决策(每区域 3 选项)
|
|
113
|
+
// ============================================================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 收集每区域的冲突决策
|
|
117
|
+
*
|
|
118
|
+
* @param {object} areaStates - 区域状态映射(来自 scanAreaStates)
|
|
119
|
+
* @param {object} options - CLI 选项
|
|
120
|
+
* @returns {Promise<object>} 决策映射 { areaKey: 'create'|'recreate'|'update'|'skip' }
|
|
121
|
+
*/
|
|
122
|
+
async function collectDecisions(areaStates, options) {
|
|
123
|
+
const force = options.force || false;
|
|
124
|
+
const nonInteractive = options.nonInteractive || false;
|
|
125
|
+
|
|
126
|
+
// --force:所有已存在区域直接 recreate
|
|
127
|
+
if (force) {
|
|
128
|
+
const decisions = {};
|
|
129
|
+
for (const [key, state] of Object.entries(areaStates)) {
|
|
130
|
+
decisions[key] = state.exists ? 'recreate' : 'create';
|
|
131
|
+
}
|
|
132
|
+
return decisions;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --non-interactive:所有已存在区域 skip(保留现有)
|
|
136
|
+
if (nonInteractive) {
|
|
137
|
+
const decisions = {};
|
|
138
|
+
for (const [key, state] of Object.entries(areaStates)) {
|
|
139
|
+
decisions[key] = state.exists ? 'skip' : 'create';
|
|
140
|
+
}
|
|
141
|
+
return decisions;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 交互模式:对每个已存在区域调用 3 选项 prompt
|
|
145
|
+
const decisions = {};
|
|
146
|
+
const existingAreas = Object.entries(areaStates).filter(([, state]) => state.exists);
|
|
147
|
+
|
|
148
|
+
if (existingAreas.length === 0) {
|
|
149
|
+
// 所有区域都不存在 → 全部 create
|
|
150
|
+
for (const key of Object.keys(areaStates)) {
|
|
151
|
+
decisions[key] = 'create';
|
|
152
|
+
}
|
|
153
|
+
return decisions;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 提示用户是否要逐个确认
|
|
157
|
+
if (existingAreas.length >= 2) {
|
|
158
|
+
const bulkChoice = await select({
|
|
159
|
+
message: `检测到 ${existingAreas.length} 个区域已存在,如何统一处理?`,
|
|
160
|
+
choices: [
|
|
161
|
+
{ name: '逐个确认 — 对每个区域分别选择处理方式', value: 'ask-each' },
|
|
162
|
+
{ name: '全部重新创建 — 删除并重建所有已存在区域', value: 'recreate-all' },
|
|
163
|
+
{ name: '全部跳过 — 保留所有已存在区域', value: 'skip-all' },
|
|
164
|
+
],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (bulkChoice === 'recreate-all') {
|
|
168
|
+
for (const [key, state] of Object.entries(areaStates)) {
|
|
169
|
+
decisions[key] = state.exists ? 'recreate' : 'create';
|
|
170
|
+
}
|
|
171
|
+
return decisions;
|
|
172
|
+
}
|
|
173
|
+
if (bulkChoice === 'skip-all') {
|
|
174
|
+
for (const [key, state] of Object.entries(areaStates)) {
|
|
175
|
+
decisions[key] = state.exists ? 'skip' : 'create';
|
|
176
|
+
}
|
|
177
|
+
return decisions;
|
|
178
|
+
}
|
|
179
|
+
// ask-each: 继续逐个询问
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 逐个询问
|
|
183
|
+
for (const [key, state] of Object.entries(areaStates)) {
|
|
184
|
+
if (!state.exists) {
|
|
185
|
+
decisions[key] = 'create';
|
|
186
|
+
} else {
|
|
187
|
+
decisions[key] = await askAreaDecision(areaDisplayName(key));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return decisions;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 3 选项冲突决策
|
|
196
|
+
*
|
|
197
|
+
* @param {string} areaName - 区域显示名(如 'docs')
|
|
198
|
+
* @returns {Promise<'recreate'|'update'|'skip'>}
|
|
199
|
+
*/
|
|
200
|
+
async function askAreaDecision(areaName) {
|
|
201
|
+
return await select({
|
|
202
|
+
message: `${areaName}/ 已存在,如何处理?`,
|
|
203
|
+
choices: [
|
|
204
|
+
{ name: '重新创建 — 删除现有目录,使用最新模板重建', value: 'recreate' },
|
|
205
|
+
{ name: '更新 — 保留现有目录,按新内容覆盖(保留本地修改之外的文件)', value: 'update' },
|
|
206
|
+
{ name: '跳过 — 保留现有目录,不做任何修改', value: 'skip' },
|
|
207
|
+
],
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ============================================================
|
|
212
|
+
// 子目录决策
|
|
213
|
+
// ============================================================
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 收集子目录决策(仅当父目录 skip 时)
|
|
217
|
+
*
|
|
218
|
+
* @param {object} decisions - 区域决策
|
|
219
|
+
* @param {string} targetDir - 目标目录
|
|
220
|
+
* @param {object} options - CLI 选项
|
|
221
|
+
* @returns {Promise<object>} 子决策 { conventions, templates }
|
|
222
|
+
*/
|
|
223
|
+
async function collectSubDecisions(decisions, targetDir, pluginDir, options) {
|
|
224
|
+
const sub = { conventions: 'skip', templates: {} };
|
|
225
|
+
const nonInteractive = options.nonInteractive || false;
|
|
226
|
+
const force = options.force || false;
|
|
227
|
+
|
|
228
|
+
// --force 模式:全部 skip(因为 recreate 已经完整重建了)
|
|
229
|
+
if (force) return sub;
|
|
230
|
+
|
|
231
|
+
// --non-interactive 模式:全部 skip
|
|
232
|
+
if (nonInteractive) return sub;
|
|
233
|
+
|
|
234
|
+
// conventions_decision:docs/ skip 且 40-project_conventions/ 有文件
|
|
235
|
+
if (decisions.docs === 'skip') {
|
|
236
|
+
const convDir = path.join(targetDir, 'docs/40-project_conventions');
|
|
237
|
+
if (fs.existsSync(convDir) && hasAnyFile(convDir)) {
|
|
238
|
+
sub.conventions = await askSubDecision('docs/40-project_conventions');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// templates_decision:.adspecs/ skip 且 templates/ 有文件 → 逐文件确认
|
|
243
|
+
if (decisions.adspecs === 'skip') {
|
|
244
|
+
const tmplDir = path.join(targetDir, '.adspecs/templates');
|
|
245
|
+
const pluginTmplDir = path.join(pluginDir, '.adspecs/templates');
|
|
246
|
+
if (fs.existsSync(tmplDir) && hasAnyFile(tmplDir)) {
|
|
247
|
+
sub.templates = await collectTemplatesDecision(tmplDir, pluginTmplDir);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return sub;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 子目录更新决策(2 选项)
|
|
256
|
+
*
|
|
257
|
+
* @param {string} subDirName - 子目录显示名
|
|
258
|
+
* @returns {Promise<'update'|'skip'>}
|
|
259
|
+
*/
|
|
260
|
+
async function askSubDecision(subDirName) {
|
|
261
|
+
return await select({
|
|
262
|
+
message: `${subDirName}/ 目录已存在且包含文件,是否使用最新规范文件更新?`,
|
|
263
|
+
choices: [
|
|
264
|
+
{ name: '更新 — 使用最新文件覆盖现有内容', value: 'update' },
|
|
265
|
+
{ name: '跳过 — 保留现有内容,不做任何修改', value: 'skip' },
|
|
266
|
+
],
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 逐文件确认模板文件是否替换
|
|
272
|
+
*
|
|
273
|
+
* 扫描目标目录和插件根目录的模板文件,自动识别新增文件(直接复制),
|
|
274
|
+
* 对共有文件提供批量快捷选项 + 逐文件确认。
|
|
275
|
+
*
|
|
276
|
+
* @param {string} targetTmplDir - 目标项目 templates 目录
|
|
277
|
+
* @param {string} pluginTmplDir - 插件根目录 templates 目录
|
|
278
|
+
* @returns {Promise<Object<string, 'replace'|'keep'>>} 逐文件决策映射
|
|
279
|
+
*/
|
|
280
|
+
async function collectTemplatesDecision(targetTmplDir, pluginTmplDir) {
|
|
281
|
+
const targetFiles = listFiles(targetTmplDir).filter((f) => f.endsWith('.md'));
|
|
282
|
+
const pluginFiles = listFiles(pluginTmplDir).filter((f) => f.endsWith('.md'));
|
|
283
|
+
|
|
284
|
+
const targetSet = new Set(targetFiles);
|
|
285
|
+
const pluginSet = new Set(pluginFiles);
|
|
286
|
+
|
|
287
|
+
// 新增文件(插件有,目标没有)→ 直接复制,无需确认
|
|
288
|
+
const newFiles = pluginFiles.filter((f) => !targetSet.has(f));
|
|
289
|
+
// 共有文件 → 需要逐文件确认
|
|
290
|
+
const commonFiles = pluginFiles.filter((f) => targetSet.has(f));
|
|
291
|
+
|
|
292
|
+
if (commonFiles.length === 0) {
|
|
293
|
+
// 没有共有文件,无需确认
|
|
294
|
+
return {};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 批量快捷选项
|
|
298
|
+
const bulkChoice = await select({
|
|
299
|
+
message: `.adspecs/templates/ 目录已存在且包含 ${targetFiles.length} 个模板文件(其中 ${commonFiles.length} 个为已有,${newFiles.length} 个为新增),如何处理已有文件?`,
|
|
300
|
+
choices: [
|
|
301
|
+
{ name: '逐文件确认 — 对每个已有模板文件逐一询问是否替换(推荐)', value: 'ask-each' },
|
|
302
|
+
{ name: '全部保留现有 — 保留所有已有模板文件不做修改', value: 'keep-all' },
|
|
303
|
+
{ name: '全部替换为最新 — 使用插件根目录中的最新版本覆盖所有已有模板文件', value: 'replace-all' },
|
|
304
|
+
],
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (bulkChoice === 'keep-all') {
|
|
308
|
+
const decision = {};
|
|
309
|
+
for (const f of commonFiles) {
|
|
310
|
+
decision[f] = 'keep';
|
|
311
|
+
}
|
|
312
|
+
return decision;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (bulkChoice === 'replace-all') {
|
|
316
|
+
const decision = {};
|
|
317
|
+
for (const f of commonFiles) {
|
|
318
|
+
decision[f] = 'replace';
|
|
319
|
+
}
|
|
320
|
+
return decision;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 逐文件询问(每批最多 4 个文件,避免 inquirer 单次问题过多)
|
|
324
|
+
const decision = {};
|
|
325
|
+
const BATCH_SIZE = 4;
|
|
326
|
+
for (let i = 0; i < commonFiles.length; i += BATCH_SIZE) {
|
|
327
|
+
const batch = commonFiles.slice(i, i + BATCH_SIZE);
|
|
328
|
+
const batchNum = Math.floor(i / BATCH_SIZE) + 1;
|
|
329
|
+
const totalBatches = Math.ceil(commonFiles.length / BATCH_SIZE);
|
|
330
|
+
|
|
331
|
+
console.log('');
|
|
332
|
+
console.log(`--- 模板文件确认(第 ${batchNum}/${totalBatches} 批,共 ${commonFiles.length} 个) ---`);
|
|
333
|
+
|
|
334
|
+
for (const file of batch) {
|
|
335
|
+
const choice = await select({
|
|
336
|
+
message: `模板文件 ${file} 已存在,是否使用最新版本替换?`,
|
|
337
|
+
choices: [
|
|
338
|
+
{ name: '保留现有 — 保留当前项目中的版本(用户可能已按项目实际情况调整过)', value: 'keep' },
|
|
339
|
+
{ name: '替换为最新 — 使用插件根目录中的最新版本覆盖现有文件', value: 'replace' },
|
|
340
|
+
],
|
|
341
|
+
});
|
|
342
|
+
decision[file] = choice;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return decision;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ============================================================
|
|
350
|
+
// 模式升级检测
|
|
351
|
+
// ============================================================
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* 检测模式升级场景(simple → classic/full/demo)
|
|
355
|
+
*
|
|
356
|
+
* 当 docs/ 已存在且 skip,且当前模式为 classic/full/demo,
|
|
357
|
+
* 检查 40-project_conventions/frontend-standard/ 是否为空(简易模式产物特征)。
|
|
358
|
+
* 如果是,则需要自动补充 frontend-standard/ 文件。
|
|
359
|
+
*
|
|
360
|
+
* 简易模式会在 40-project_conventions/ 下生成 README.md 并创建空子目录
|
|
361
|
+
* frontend-standard/ 和 end-standard/,因此不能检查整个 40 目录,而要检查
|
|
362
|
+
* frontend-standard/ 子目录是否为空。
|
|
363
|
+
*
|
|
364
|
+
* @param {object} decisions - 区域决策
|
|
365
|
+
* @param {string} mode - 当前模式
|
|
366
|
+
* @param {string} targetDir - 目标目录
|
|
367
|
+
* @returns {object} { detected, emptyDirs }
|
|
368
|
+
*/
|
|
369
|
+
function detectModeUpgrade(decisions, mode, targetDir) {
|
|
370
|
+
// 仅 classic/full/demo 模式需要检测
|
|
371
|
+
if (mode === 'simple') {
|
|
372
|
+
return { detected: false, emptyDirs: [] };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 仅当 docs/ 为 skip 时才有意义
|
|
376
|
+
if (decisions.docs !== 'skip') {
|
|
377
|
+
return { detected: false, emptyDirs: [] };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const frontendStdDir = path.join(targetDir, 'docs/40-project_conventions/frontend-standard');
|
|
381
|
+
if (!fs.existsSync(frontendStdDir)) {
|
|
382
|
+
return { detected: false, emptyDirs: [] };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 检查 frontend-standard/ 是否为空(无任何文件)
|
|
386
|
+
// 注意:不使用 isEmptyDirectoryTree,因为我们要检查的是这个特定子目录
|
|
387
|
+
if (!hasAnyFile(frontendStdDir)) {
|
|
388
|
+
return {
|
|
389
|
+
detected: true,
|
|
390
|
+
emptyDirs: ['40-project_conventions/frontend-standard/'],
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return { detected: false, emptyDirs: [] };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ============================================================
|
|
398
|
+
// 执行前确认
|
|
399
|
+
// ============================================================
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 执行前确认
|
|
403
|
+
*
|
|
404
|
+
* @param {object} options - CLI 选项
|
|
405
|
+
* @returns {Promise<boolean>}
|
|
406
|
+
*/
|
|
407
|
+
async function confirmExecution(options) {
|
|
408
|
+
// --force / --non-interactive / --yes 自动确认
|
|
409
|
+
if (options.force || options.nonInteractive || options.yes) {
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return await confirm({
|
|
414
|
+
message: '是否继续执行以上计划?',
|
|
415
|
+
default: true,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
module.exports = {
|
|
420
|
+
collectProjectInfo,
|
|
421
|
+
collectDecisions,
|
|
422
|
+
collectSubDecisions,
|
|
423
|
+
collectTemplatesDecision,
|
|
424
|
+
detectModeUpgrade,
|
|
425
|
+
confirmExecution,
|
|
426
|
+
askAreaDecision,
|
|
427
|
+
askSubDecision,
|
|
428
|
+
};
|