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.
Files changed (74) hide show
  1. package/.adspecs/feature.json +16 -16
  2. package/.adspecs/feature.yml +28 -28
  3. package/.adspecs/paths.json +17 -17
  4. 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
  5. 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
  6. 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
  7. package/.claude-plugin/marketplace.json +23 -23
  8. package/.claude-plugin/plugin.json +18 -18
  9. package/.qoder-plugin/plugin.json +31 -31
  10. package/CLAUDE.md +1 -5
  11. package/INSTALL.md +3 -3
  12. package/README.md +395 -395
  13. package/bin/adspecs.js +129 -129
  14. package/hooks/commit-queue.js +245 -245
  15. package/hooks/hooks.json +63 -63
  16. package/hooks/session-start.js +44 -44
  17. package/hooks/wiki-queue.js +127 -127
  18. package/package.json +61 -61
  19. package/references/ant6-front-standard/index.md +99 -99
  20. package/references/antd-front-demo/public/mockServiceWorker.js +361 -361
  21. package/references/ecp-end-standard/index.md +63 -63
  22. 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
  23. 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
  24. 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
  25. package/references/python-end-standard/04-Redis/344/275/277/347/224/250/350/247/204/350/214/203.md +231 -231
  26. package/scripts/postinstall.js +107 -107
  27. package/scripts/sync-version.js +105 -105
  28. package/skills/.claude/.wiki-update-queue +26 -26
  29. package/skills/adspecs-constitution/SKILL.md +157 -0
  30. package/skills/adspecs-export-word/SKILL.md +498 -498
  31. package/skills/adspecs-export-word/references/md-to-docx.js +862 -862
  32. package/skills/adspecs-export-word/references/package-lock.json +220 -220
  33. package/skills/adspecs-export-word/references/package.json +10 -10
  34. package/skills/adspecs-front-prototype/SKILL.md +405 -405
  35. package/skills/adspecs-front-spec/SKILL.md +4 -4
  36. package/skills/adspecs-front-tasks/SKILL.md +213 -173
  37. package/skills/adspecs-plan/SKILL.md +59 -69
  38. package/skills/adspecs-prd/SKILL.md +13 -5
  39. package/skills/adspecs-prd-to-demo/SKILL.md +532 -0
  40. package/skills/adspecs-tasks/SKILL.md +175 -204
  41. package/skills/adspecs-update-status/SKILL.md +382 -382
  42. package/skills/adspecs-utest/SKILL.md +107 -116
  43. package/skills/grill-me/SKILL.md +7 -0
  44. package/skills/grill-me/agents/openai.yaml +5 -0
  45. package/skills/playwright-cli/SKILL.md +420 -0
  46. package/skills/playwright-cli/references/element-attributes.md +23 -0
  47. package/skills/playwright-cli/references/playwright-tests.md +39 -0
  48. package/skills/playwright-cli/references/request-mocking.md +87 -0
  49. package/skills/playwright-cli/references/running-code.md +241 -0
  50. package/skills/playwright-cli/references/session-management.md +225 -0
  51. package/skills/playwright-cli/references/storage-state.md +275 -0
  52. package/skills/playwright-cli/references/test-generation.md +433 -0
  53. package/skills/playwright-cli/references/tracing.md +139 -0
  54. package/skills/playwright-cli/references/video-recording.md +143 -0
  55. package/skills/playwright-trace/SKILL.md +171 -0
  56. package/skills/project-init/SKILL.md +93 -22
  57. package/skills/project-init/references/front-demo/.claude/settings.local.json +9 -0
  58. package/skills/wiki-update/SKILL.md +232 -232
  59. package/src/commands/doctor.js +197 -197
  60. package/src/commands/init.js +83 -83
  61. package/src/commands/plugin.js +165 -165
  62. package/src/commands/update.js +87 -87
  63. package/src/lib/area-scanner.js +129 -129
  64. package/src/lib/copier.js +104 -104
  65. package/src/lib/dir-utils.js +161 -133
  66. package/src/lib/json-merge.js +114 -114
  67. package/src/lib/paths-defaults.js +37 -37
  68. package/src/lib/prompts.js +428 -347
  69. package/src/lib/readme-gen.js +143 -143
  70. package/src/lib/report.js +338 -327
  71. package/src/lib/scaffolder.js +551 -518
  72. package/src/lib/short-name.js +36 -36
  73. package/src/utils.js +80 -80
  74. package/references/antd-front-demo/.env +0 -15
@@ -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: 'skip' };
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
- if (fs.existsSync(tmplDir) && hasAnyFile(tmplDir)) {
246
- sub.templates = await askSubDecision('.adspecs/templates');
247
- }
248
- }
249
-
250
- return sub;
251
- }
252
-
253
- /**
254
- * 子目录更新决策(2 选项)
255
- *
256
- * @param {string} subDirName - 子目录显示名
257
- * @returns {Promise<'update'|'skip'>}
258
- */
259
- async function askSubDecision(subDirName) {
260
- return await select({
261
- message: `${subDirName}/ 目录已存在且包含文件,是否使用最新规范文件更新?`,
262
- choices: [
263
- { name: '更新 — 使用最新文件覆盖现有内容', value: 'update' },
264
- { name: '跳过保留现有内容,不做任何修改', value: 'skip' },
265
- ],
266
- });
267
- }
268
-
269
- // ============================================================
270
- // 模式升级检测
271
- // ============================================================
272
-
273
- /**
274
- * 检测模式升级场景(simple classic/full/demo)
275
- *
276
- * docs/ 已存在且 skip,且当前模式为 classic/full/demo,
277
- * 检查 40-project_conventions/frontend-standard/ 是否为空(简易模式产物特征)。
278
- * 如果是,则需要自动补充 frontend-standard/ 文件。
279
- *
280
- * 简易模式会在 40-project_conventions/ 下生成 README.md 并创建空子目录
281
- * frontend-standard/ end-standard/,因此不能检查整个 40 目录,而要检查
282
- * frontend-standard/ 子目录是否为空。
283
- *
284
- * @param {object} decisions - 区域决策
285
- * @param {string} mode - 当前模式
286
- * @param {string} targetDir - 目标目录
287
- * @returns {object} { detected, emptyDirs }
288
- */
289
- function detectModeUpgrade(decisions, mode, targetDir) {
290
- // classic/full/demo 模式需要检测
291
- if (mode === 'simple') {
292
- return { detected: false, emptyDirs: [] };
293
- }
294
-
295
- // 仅当 docs/ 为 skip 时才有意义
296
- if (decisions.docs !== 'skip') {
297
- return { detected: false, emptyDirs: [] };
298
- }
299
-
300
- const frontendStdDir = path.join(targetDir, 'docs/40-project_conventions/frontend-standard');
301
- if (!fs.existsSync(frontendStdDir)) {
302
- return { detected: false, emptyDirs: [] };
303
- }
304
-
305
- // 检查 frontend-standard/ 是否为空(无任何文件)
306
- // 注意:不使用 isEmptyDirectoryTree,因为我们要检查的是这个特定子目录
307
- if (!hasAnyFile(frontendStdDir)) {
308
- return {
309
- detected: true,
310
- emptyDirs: ['40-project_conventions/frontend-standard/'],
311
- };
312
- }
313
-
314
- return { detected: false, emptyDirs: [] };
315
- }
316
-
317
- // ============================================================
318
- // 执行前确认
319
- // ============================================================
320
-
321
- /**
322
- * 执行前确认
323
- *
324
- * @param {object} options - CLI 选项
325
- * @returns {Promise<boolean>}
326
- */
327
- async function confirmExecution(options) {
328
- // --force / --non-interactive / --yes 自动确认
329
- if (options.force || options.nonInteractive || options.yes) {
330
- return true;
331
- }
332
-
333
- return await confirm({
334
- message: '是否继续执行以上计划?',
335
- default: true,
336
- });
337
- }
338
-
339
- module.exports = {
340
- collectProjectInfo,
341
- collectDecisions,
342
- collectSubDecisions,
343
- detectModeUpgrade,
344
- confirmExecution,
345
- askAreaDecision,
346
- askSubDecision,
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
+ };