adspecs 0.1.18 → 0.1.20

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 (88) hide show
  1. package/.adspecs/extensions/git/commands/adspecs.git.initialize.md +4 -4
  2. package/.adspecs/extensions/git/extension.yml +1 -1
  3. package/.adspecs/extensions/git/git-config.yml +17 -2
  4. package/.adspecs/extensions.yml +32 -0
  5. package/.adspecs/feature.json +16 -16
  6. package/.adspecs/feature.yml +28 -28
  7. package/.adspecs/paths.json +17 -0
  8. package/.adspecs/templates/02-/344/272/247/345/223/201/351/234/200/346/261/202/350/257/264/346/230/216/344/271/246/346/250/241/346/235/277.md +4 -0
  9. package/.adspecs/workflows/adspecs/workflow.yml +5 -4
  10. package/.claude-plugin/marketplace.json +23 -23
  11. package/.claude-plugin/plugin.json +18 -18
  12. package/.qoder-plugin/plugin.json +31 -31
  13. package/CLAUDE.md +1 -5
  14. package/README.md +395 -395
  15. package/bin/adspecs.js +129 -129
  16. package/hooks/commit-queue.js +245 -245
  17. package/hooks/hooks.json +63 -63
  18. package/hooks/session-start.js +44 -44
  19. package/hooks/wiki-queue.js +127 -127
  20. package/package.json +61 -61
  21. package/references/ant6-front-standard/index.md +99 -99
  22. package/references/antd-front-demo/.env.develop +12 -0
  23. package/references/antd-front-demo/.env.production +5 -0
  24. package/references/antd-front-demo/package-lock.json +2 -2
  25. package/references/antd-front-demo/package.json +1 -1
  26. package/references/antd-front-demo/public/mockServiceWorker.js +361 -361
  27. package/references/ecp-end-standard/index.md +63 -63
  28. 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
  29. 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
  30. 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
  31. package/references/python-end-standard/04-Redis/344/275/277/347/224/250/350/247/204/350/214/203.md +231 -231
  32. package/scripts/postinstall.js +107 -107
  33. package/scripts/sync-version.js +105 -105
  34. package/skills/.claude/.wiki-update-queue +26 -26
  35. package/skills/adspecs-constitution/SKILL.md +157 -0
  36. package/skills/adspecs-export-word/SKILL.md +498 -498
  37. package/skills/adspecs-export-word/references/md-to-docx.js +862 -862
  38. package/skills/adspecs-export-word/references/package-lock.json +220 -220
  39. package/skills/adspecs-export-word/references/package.json +10 -10
  40. package/skills/adspecs-front-prototype/SKILL.md +405 -0
  41. package/skills/adspecs-git-commit/SKILL.md +23 -8
  42. package/skills/adspecs-git-initialize/SKILL.md +54 -0
  43. package/skills/adspecs-plan/SKILL.md +59 -69
  44. package/skills/adspecs-prd/SKILL.md +13 -5
  45. package/skills/adspecs-prd-to-demo/SKILL.md +532 -0
  46. package/skills/adspecs-tasks/SKILL.md +175 -204
  47. package/skills/adspecs-update-status/SKILL.md +382 -382
  48. package/skills/adspecs-utest/SKILL.md +107 -116
  49. package/skills/grill-me/SKILL.md +7 -0
  50. package/skills/grill-me/agents/openai.yaml +5 -0
  51. package/skills/playwright-cli/SKILL.md +420 -0
  52. package/skills/playwright-cli/references/element-attributes.md +23 -0
  53. package/skills/playwright-cli/references/playwright-tests.md +39 -0
  54. package/skills/playwright-cli/references/request-mocking.md +87 -0
  55. package/skills/playwright-cli/references/running-code.md +241 -0
  56. package/skills/playwright-cli/references/session-management.md +225 -0
  57. package/skills/playwright-cli/references/storage-state.md +275 -0
  58. package/skills/playwright-cli/references/test-generation.md +433 -0
  59. package/skills/playwright-cli/references/tracing.md +139 -0
  60. package/skills/playwright-cli/references/video-recording.md +143 -0
  61. package/skills/playwright-trace/SKILL.md +171 -0
  62. package/skills/project-init/references/front-demo/.claude/settings.local.json +9 -0
  63. package/skills/wiki-update/SKILL.md +232 -232
  64. package/src/commands/doctor.js +197 -197
  65. package/src/commands/init.js +83 -83
  66. package/src/commands/plugin.js +165 -165
  67. package/src/commands/update.js +87 -87
  68. package/src/lib/area-scanner.js +129 -129
  69. package/src/lib/copier.js +104 -104
  70. package/src/lib/dir-utils.js +133 -133
  71. package/src/lib/json-merge.js +114 -114
  72. package/src/lib/paths-defaults.js +37 -37
  73. package/src/lib/prompts.js +347 -345
  74. package/src/lib/readme-gen.js +143 -143
  75. package/src/lib/report.js +327 -327
  76. package/src/lib/scaffolder.js +518 -518
  77. package/src/lib/short-name.js +36 -36
  78. package/src/utils.js +80 -80
  79. package/references/antd-front-demo/.env +0 -15
  80. package/references/antd-front-demo/src/pages/aps/coil/components/CoilDrawer.tsx +0 -141
  81. package/references/antd-front-demo/src/pages/aps/coil/index.tsx +0 -209
  82. package/references/antd-front-demo/src/pages/aps/optimization/index.tsx +0 -192
  83. package/references/antd-front-demo/src/pages/aps/order/components/OrderDrawer.tsx +0 -251
  84. package/references/antd-front-demo/src/pages/aps/order/index.tsx +0 -273
  85. package/references/antd-front-demo/src/pages/aps/param/index.tsx +0 -153
  86. package/references/antd-front-demo/src/pages/aps/plan/components/CuttingPlanVisualization.tsx +0 -821
  87. package/references/antd-front-demo/src/pages/aps/plan/index.tsx +0 -254
  88. package/references/antd-front-demo/src/pages/aps/task/index.tsx +0 -190
@@ -1,345 +1,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 } = 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 || 'vue';
59
- if ((mode === 'classic' || mode === 'full') && !options.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
-
73
- // === 第二次收集:补充信息 ===
74
- const companyName =
75
- options.company ||
76
- (nonInteractive
77
- ? 'XX有限公司'
78
- : await input({
79
- message: '请输入公司名称(不填默认"XX有限公司"):',
80
- default: 'XX有限公司',
81
- }));
82
-
83
- const confidentiality =
84
- options.confidentiality && options.confidentiality !== '内部资料'
85
- ? options.confidentiality
86
- : nonInteractive
87
- ? '内部资料'
88
- : await select({
89
- message: '请选择文件密级(不选默认"内部资料"):',
90
- choices: [
91
- { name: '内部资料(默认) — 仅限公司内部使用', value: '内部资料' },
92
- { name: '公开 — 可对外公开', value: '公开' },
93
- { name: '机密 — 限授权人员访问', value: '机密' },
94
- { name: '绝密 — 最高保密等级', value: '绝密' },
95
- ],
96
- });
97
-
98
- return {
99
- project_name: projectName,
100
- project_dir: projectDir,
101
- company_name: companyName,
102
- confidentiality,
103
- mode,
104
- frontDemo,
105
- modules: options.modules || '3100-crm-mdm:mdm,3103-crm-contract:contract',
106
- };
107
- }
108
-
109
- // ============================================================
110
- // 冲突决策(每区域 3 选项)
111
- // ============================================================
112
-
113
- /**
114
- * 收集每区域的冲突决策
115
- *
116
- * @param {object} areaStates - 区域状态映射(来自 scanAreaStates)
117
- * @param {object} options - CLI 选项
118
- * @returns {Promise<object>} 决策映射 { areaKey: 'create'|'recreate'|'update'|'skip' }
119
- */
120
- async function collectDecisions(areaStates, options) {
121
- const force = options.force || false;
122
- const nonInteractive = options.nonInteractive || false;
123
-
124
- // --force:所有已存在区域直接 recreate
125
- if (force) {
126
- const decisions = {};
127
- for (const [key, state] of Object.entries(areaStates)) {
128
- decisions[key] = state.exists ? 'recreate' : 'create';
129
- }
130
- return decisions;
131
- }
132
-
133
- // --non-interactive:所有已存在区域 skip(保留现有)
134
- if (nonInteractive) {
135
- const decisions = {};
136
- for (const [key, state] of Object.entries(areaStates)) {
137
- decisions[key] = state.exists ? 'skip' : 'create';
138
- }
139
- return decisions;
140
- }
141
-
142
- // 交互模式:对每个已存在区域调用 3 选项 prompt
143
- const decisions = {};
144
- const existingAreas = Object.entries(areaStates).filter(([, state]) => state.exists);
145
-
146
- if (existingAreas.length === 0) {
147
- // 所有区域都不存在 → 全部 create
148
- for (const key of Object.keys(areaStates)) {
149
- decisions[key] = 'create';
150
- }
151
- return decisions;
152
- }
153
-
154
- // 提示用户是否要逐个确认
155
- if (existingAreas.length >= 2) {
156
- const bulkChoice = await select({
157
- message: `检测到 ${existingAreas.length} 个区域已存在,如何统一处理?`,
158
- choices: [
159
- { name: '逐个确认 — 对每个区域分别选择处理方式', value: 'ask-each' },
160
- { name: '全部重新创建 — 删除并重建所有已存在区域', value: 'recreate-all' },
161
- { name: '全部跳过 — 保留所有已存在区域', value: 'skip-all' },
162
- ],
163
- });
164
-
165
- if (bulkChoice === 'recreate-all') {
166
- for (const [key, state] of Object.entries(areaStates)) {
167
- decisions[key] = state.exists ? 'recreate' : 'create';
168
- }
169
- return decisions;
170
- }
171
- if (bulkChoice === 'skip-all') {
172
- for (const [key, state] of Object.entries(areaStates)) {
173
- decisions[key] = state.exists ? 'skip' : 'create';
174
- }
175
- return decisions;
176
- }
177
- // ask-each: 继续逐个询问
178
- }
179
-
180
- // 逐个询问
181
- for (const [key, state] of Object.entries(areaStates)) {
182
- if (!state.exists) {
183
- decisions[key] = 'create';
184
- } else {
185
- decisions[key] = await askAreaDecision(areaDisplayName(key));
186
- }
187
- }
188
-
189
- return decisions;
190
- }
191
-
192
- /**
193
- * 3 选项冲突决策
194
- *
195
- * @param {string} areaName - 区域显示名(如 'docs')
196
- * @returns {Promise<'recreate'|'update'|'skip'>}
197
- */
198
- async function askAreaDecision(areaName) {
199
- return await select({
200
- message: `${areaName}/ 已存在,如何处理?`,
201
- choices: [
202
- { name: '重新创建 — 删除现有目录,使用最新模板重建', value: 'recreate' },
203
- { name: '更新 — 保留现有目录,按新内容覆盖(保留本地修改之外的文件)', value: 'update' },
204
- { name: '跳过 — 保留现有目录,不做任何修改', value: 'skip' },
205
- ],
206
- });
207
- }
208
-
209
- // ============================================================
210
- // 子目录决策
211
- // ============================================================
212
-
213
- /**
214
- * 收集子目录决策(仅当父目录 skip 时)
215
- *
216
- * @param {object} decisions - 区域决策
217
- * @param {string} targetDir - 目标目录
218
- * @param {object} options - CLI 选项
219
- * @returns {Promise<object>} 子决策 { conventions, templates }
220
- */
221
- async function collectSubDecisions(decisions, targetDir, options) {
222
- const sub = { conventions: 'skip', templates: 'skip' };
223
- const nonInteractive = options.nonInteractive || false;
224
- const force = options.force || false;
225
-
226
- // --force 模式:全部 skip(因为 recreate 已经完整重建了)
227
- if (force) return sub;
228
-
229
- // --non-interactive 模式:全部 skip
230
- if (nonInteractive) return sub;
231
-
232
- // conventions_decision:docs/ skip 且 40-project_conventions/ 有文件
233
- if (decisions.docs === 'skip') {
234
- const convDir = path.join(targetDir, 'docs/40-project_conventions');
235
- if (fs.existsSync(convDir) && hasAnyFile(convDir)) {
236
- sub.conventions = await askSubDecision('docs/40-project_conventions');
237
- }
238
- }
239
-
240
- // templates_decision:.adspecs/ skip 且 templates/ 有文件
241
- if (decisions.adspecs === 'skip') {
242
- const tmplDir = path.join(targetDir, '.adspecs/templates');
243
- if (fs.existsSync(tmplDir) && hasAnyFile(tmplDir)) {
244
- sub.templates = await askSubDecision('.adspecs/templates');
245
- }
246
- }
247
-
248
- return sub;
249
- }
250
-
251
- /**
252
- * 子目录更新决策(2 选项)
253
- *
254
- * @param {string} subDirName - 子目录显示名
255
- * @returns {Promise<'update'|'skip'>}
256
- */
257
- async function askSubDecision(subDirName) {
258
- return await select({
259
- message: `${subDirName}/ 目录已存在且包含文件,是否使用最新规范文件更新?`,
260
- choices: [
261
- { name: '更新 — 使用最新文件覆盖现有内容', value: 'update' },
262
- { name: '跳过 — 保留现有内容,不做任何修改', value: 'skip' },
263
- ],
264
- });
265
- }
266
-
267
- // ============================================================
268
- // 模式升级检测
269
- // ============================================================
270
-
271
- /**
272
- * 检测模式升级场景(simple → classic/full/demo)
273
- *
274
- * 当 docs/ 已存在且 skip,且当前模式为 classic/full/demo,
275
- * 检查 40-project_conventions/frontend-standard/ 是否为空(简易模式产物特征)。
276
- * 如果是,则需要自动补充 frontend-standard/ 文件。
277
- *
278
- * 简易模式会在 40-project_conventions/ 下生成 README.md 并创建空子目录
279
- * frontend-standard/ 和 end-standard/,因此不能检查整个 40 目录,而要检查
280
- * frontend-standard/ 子目录是否为空。
281
- *
282
- * @param {object} decisions - 区域决策
283
- * @param {string} mode - 当前模式
284
- * @param {string} targetDir - 目标目录
285
- * @returns {object} { detected, emptyDirs }
286
- */
287
- function detectModeUpgrade(decisions, mode, targetDir) {
288
- // 仅 classic/full/demo 模式需要检测
289
- if (mode === 'simple') {
290
- return { detected: false, emptyDirs: [] };
291
- }
292
-
293
- // 仅当 docs/ 为 skip 时才有意义
294
- if (decisions.docs !== 'skip') {
295
- return { detected: false, emptyDirs: [] };
296
- }
297
-
298
- const frontendStdDir = path.join(targetDir, 'docs/40-project_conventions/frontend-standard');
299
- if (!fs.existsSync(frontendStdDir)) {
300
- return { detected: false, emptyDirs: [] };
301
- }
302
-
303
- // 检查 frontend-standard/ 是否为空(无任何文件)
304
- // 注意:不使用 isEmptyDirectoryTree,因为我们要检查的是这个特定子目录
305
- if (!hasAnyFile(frontendStdDir)) {
306
- return {
307
- detected: true,
308
- emptyDirs: ['40-project_conventions/frontend-standard/'],
309
- };
310
- }
311
-
312
- return { detected: false, emptyDirs: [] };
313
- }
314
-
315
- // ============================================================
316
- // 执行前确认
317
- // ============================================================
318
-
319
- /**
320
- * 执行前确认
321
- *
322
- * @param {object} options - CLI 选项
323
- * @returns {Promise<boolean>}
324
- */
325
- async function confirmExecution(options) {
326
- // --force / --non-interactive / --yes 自动确认
327
- if (options.force || options.nonInteractive || options.yes) {
328
- return true;
329
- }
330
-
331
- return await confirm({
332
- message: '是否继续执行以上计划?',
333
- default: true,
334
- });
335
- }
336
-
337
- module.exports = {
338
- collectProjectInfo,
339
- collectDecisions,
340
- collectSubDecisions,
341
- detectModeUpgrade,
342
- confirmExecution,
343
- askAreaDecision,
344
- askSubDecision,
345
- };
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
+ };