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,518 +1,551 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { copyDirFiltered, removeDir, countFiles } = require('./copier');
6
- const { generateReadme, generateWikiReadme, generateServiceReadme, DOCS_READMES } = require('./readme-gen');
7
- const { mergeJsonFile, mergeProjectJson } = require('./json-merge');
8
- const { generatePathsConfig } = require('./paths-defaults');
9
-
10
- /**
11
- * 脚手架核心
12
- *
13
- * 接收 per-zone decisions 对象,按决策分支执行(create/recreate/update/skip)。
14
- * 通过 logger 回调实时输出每阶段状态。
15
- * 支持子目录决策、模式升级自动补充、skip 场景下 feature.json 属性补全。
16
- */
17
-
18
- // ============================================================
19
- // 主入口
20
- // ============================================================
21
-
22
- /**
23
- * 执行脚手架
24
- *
25
- * @param {object} opts
26
- * @param {string} opts.targetDir - 目标项目根目录
27
- * @param {string} opts.pluginDir - 插件根目录
28
- * @param {string} opts.mode - 初始化模式
29
- * @param {string} opts.frontDemo - 前端技术栈(vue/antd/skip)
30
- * @param {string} opts.project_name
31
- * @param {string} opts.project_short_name
32
- * @param {string} opts.project_dir
33
- * @param {string} opts.company_name
34
- * @param {string} opts.confidentiality
35
- * @param {string} opts.modules
36
- * @param {object} opts.decisions - 每区域决策 { areaKey: 'create'|'recreate'|'update'|'skip' }
37
- * @param {object} opts.subDecisions - 子目录决策 { conventions, templates }
38
- * @param {object} opts.modeUpgrade - 模式升级检测 { detected, emptyDirs }
39
- * @param {function} opts.logger - 实时状态回调 (msg: string) => void
40
- * @returns {Promise<object>} 执行报告
41
- */
42
- async function scaffold(opts) {
43
- const {
44
- targetDir,
45
- pluginDir,
46
- mode,
47
- frontDemo,
48
- project_name,
49
- project_short_name,
50
- project_dir,
51
- company_name,
52
- confidentiality,
53
- modules,
54
- decisions,
55
- subDecisions = { conventions: 'skip', templates: 'skip' },
56
- modeUpgrade = { detected: false, emptyDirs: [] },
57
- logger = () => {},
58
- } = opts;
59
-
60
- const report = {
61
- targetDir,
62
- mode,
63
- projectName: project_name,
64
- projectShortName: project_short_name,
65
- companyName: company_name,
66
- confidentiality,
67
- stages: [],
68
- startedAt: new Date().toISOString(),
69
- };
70
-
71
- const FRONT_DEMO_SOURCE = frontDemo === 'antd' ? 'antd-front-demo' : 'front-demo';
72
-
73
- fs.mkdirSync(targetDir, { recursive: true });
74
-
75
- // === 阶段 1: docs/ ===
76
- report.stages.push(
77
- await scaffoldDocs(targetDir, pluginDir, mode, FRONT_DEMO_SOURCE, decisions.docs, subDecisions, modeUpgrade, logger)
78
- );
79
-
80
- // === 阶段 2: wiki/ ===
81
- report.stages.push(scaffoldWiki(targetDir, decisions.wiki, logger));
82
-
83
- // === 阶段 3: web-ui/ (full/demo) ===
84
- if (mode === 'full' || mode === 'demo') {
85
- report.stages.push(await scaffoldWebUi(targetDir, pluginDir, decisions.webUi, logger));
86
- }
87
-
88
- // === 阶段 4: service/ (full/demo) ===
89
- if (mode === 'full' || mode === 'demo') {
90
- report.stages.push(scaffoldService(targetDir, modules, decisions.service, logger));
91
- }
92
-
93
- // === 阶段 5: front-demo/ (classic/full/demo,且 frontDemo !== 'skip') ===
94
- if (mode !== 'simple' && frontDemo !== 'skip') {
95
- report.stages.push(await scaffoldFrontDemo(targetDir, pluginDir, FRONT_DEMO_SOURCE, decisions.frontDemo, logger));
96
- } else if (mode !== 'simple' && frontDemo === 'skip') {
97
- logger(`⏭ 阶段 5 跳过:用户选择不创建 front-demo/`);
98
- report.stages.push({ name: 'front-demo/', status: 'skipped', reason: '用户选择跳过' });
99
- }
100
-
101
- // === 阶段 6: .adspecs/ ===
102
- report.stages.push(
103
- await scaffoldAdspecs(targetDir, pluginDir, decisions.adspecs, subDecisions, logger)
104
- );
105
-
106
- // === 阶段 6.5: project.json ===
107
- const projectJsonResult = writeProjectJson(targetDir, pluginDir, {
108
- project_name,
109
- project_short_name,
110
- project_dir,
111
- company_name,
112
- confidentiality,
113
- });
114
- logger(
115
- projectJsonResult.created
116
- ? `✅ 阶段 6.5 完成:项目元数据已生成 .adspecs/project.json`
117
- : `🔄 阶段 6.5 完成:项目元数据已更新 .adspecs/project.json`
118
- );
119
- report.stages.push({
120
- name: '.adspecs/project.json',
121
- status: projectJsonResult.created ? 'created' : 'updated',
122
- detail: projectJsonResult,
123
- });
124
-
125
- // === 阶段 7: paths.json ===
126
- const pathsResult = writePathsJson(targetDir);
127
- logger(
128
- pathsResult.created
129
- ? `✅ 阶段 7 完成:路径配置文件已生成 .adspecs/paths.json`
130
- : `⏭ 阶段 7 跳过:.adspecs/paths.json 已存在`
131
- );
132
- report.stages.push({
133
- name: '.adspecs/paths.json',
134
- status: pathsResult.created ? 'created' : 'exists',
135
- });
136
-
137
- report.completedAt = new Date().toISOString();
138
- return report;
139
- }
140
-
141
- // ============================================================
142
- // 各阶段实现
143
- // ============================================================
144
-
145
- /**
146
- * 决策前处理:根据 decision 决定目录状态
147
- *
148
- * @returns {'created'|'recreated'|'skipped'|'updated'} action
149
- */
150
- function prepareDirByDecision(dirPath, decision, logger, areaName) {
151
- if (decision === 'skip') {
152
- return 'skip';
153
- }
154
- if (decision === 'recreate') {
155
- if (fs.existsSync(dirPath)) {
156
- removeDir(dirPath);
157
- logger(`🔄 删除旧 ${areaName}/ 并重建...`);
158
- }
159
- fs.mkdirSync(dirPath, { recursive: true });
160
- return 'recreate';
161
- }
162
- if (decision === 'update') {
163
- // update:保留目录,后续操作会覆盖文件
164
- fs.mkdirSync(dirPath, { recursive: true });
165
- return 'update';
166
- }
167
- // create:目录原本就不存在
168
- fs.mkdirSync(dirPath, { recursive: true });
169
- return 'create';
170
- }
171
-
172
- /**
173
- * 阶段 1: docs/ 目录结构
174
- */
175
- async function scaffoldDocs(targetDir, pluginDir, mode, frontDemoSource, decision, subDecisions, modeUpgrade, logger) {
176
- const docsDir = path.join(targetDir, 'docs');
177
- const stage = { name: 'docs/', dirs: [], files: 0 };
178
-
179
- if (decision === 'skip') {
180
- logger(`⏭ 阶段 1 跳过:docs/ 已存在,保留现有内容`);
181
-
182
- // 子决策:conventions
183
- if (subDecisions.conventions === 'update') {
184
- const frontendStandard = resolveFrontendStandard(frontDemoSource);
185
- copyDirFiltered(
186
- path.join(pluginDir, 'references', frontendStandard),
187
- path.join(docsDir, '40-project_conventions/frontend-standard')
188
- );
189
- logger(`🔄 阶段 1 补充更新:docs/40-project_conventions/frontend-standard/ 已按前端技术栈更新`);
190
- stage.subDecisions = { conventions: 'update' };
191
- }
192
-
193
- // 模式升级自动补充
194
- if (modeUpgrade.detected) {
195
- const frontendStandard = resolveFrontendStandard(frontDemoSource);
196
- fs.mkdirSync(path.join(docsDir, '40-project_conventions/frontend-standard'), { recursive: true });
197
- copyDirFiltered(
198
- path.join(pluginDir, 'references', frontendStandard),
199
- path.join(docsDir, '40-project_conventions/frontend-standard')
200
- );
201
- logger(`📦 补充:检测到 40-project_conventions/ 为空目录(简易模式产物),已按前端技术栈补充 frontend-standard/`);
202
- stage.modeUpgrade = modeUpgrade;
203
- }
204
-
205
- stage.status = 'skipped';
206
- return stage;
207
- }
208
-
209
- const action = prepareDirByDecision(docsDir, decision, logger, 'docs');
210
-
211
- // 11 个一级子目录
212
- const subdirs = [
213
- '00-planning',
214
- '00-customer-requirements',
215
- '10-architecture',
216
- '20-prd',
217
- '30-system-design',
218
- '40-project_conventions',
219
- '70-scripts/depoly',
220
- '80-development/800-log',
221
- '80-development/801-dev',
222
- '80-development/802-test',
223
- '100-help',
224
- '300-specs',
225
- ];
226
- for (const sub of subdirs) {
227
- fs.mkdirSync(path.join(docsDir, sub), { recursive: true });
228
- stage.dirs.push(sub);
229
- }
230
-
231
- // 40-project_conventions 按模式处理
232
- const convDir = path.join(docsDir, '40-project_conventions');
233
- fs.mkdirSync(path.join(convDir, 'frontend-standard'), { recursive: true });
234
- fs.mkdirSync(path.join(convDir, 'end-standard'), { recursive: true });
235
-
236
- if (mode === 'demo') {
237
- copyDirFiltered(
238
- path.join(pluginDir, 'references/ant6-front-standard'),
239
- path.join(convDir, 'frontend-standard')
240
- );
241
- copyDirFiltered(
242
- path.join(pluginDir, 'references/python-end-standard'),
243
- path.join(convDir, 'end-standard')
244
- );
245
- } else if (mode === 'classic' || mode === 'full') {
246
- const frontendStandard = resolveFrontendStandard(frontDemoSource);
247
- copyDirFiltered(
248
- path.join(pluginDir, 'references', frontendStandard),
249
- path.join(convDir, 'frontend-standard')
250
- );
251
- }
252
-
253
- // 生成各子目录的 README.md(create/recreate/update 都重新生成)
254
- if (decision !== 'skip') {
255
- for (const [subdirKey, meta] of Object.entries(DOCS_READMES)) {
256
- const readmePath = path.join(docsDir, subdirKey, 'README.md');
257
- fs.writeFileSync(readmePath, generateReadme(meta), 'utf8');
258
- stage.files++;
259
- }
260
- }
261
-
262
- stage.fileCount = countFiles(docsDir);
263
- stage.status = action === 'recreate' ? 'recreated' : 'created';
264
-
265
- const verb = action === 'recreate' ? '重新创建' : '创建';
266
- const convNote =
267
- mode === 'demo'
268
- ? ',40 按 Python+React 技术栈复制前后端规范'
269
- : mode === 'simple'
270
- ? ',40 仅创建空目录'
271
- : ',其中 40 从 references 复制';
272
- logger(`✅ 阶段 1 完成:docs/ 目录结构已${verb}(11 个一级子目录${convNote})`);
273
-
274
- return stage;
275
- }
276
-
277
- /**
278
- * 阶段 2: wiki/ 基线文件目录
279
- */
280
- function scaffoldWiki(targetDir, decision, logger) {
281
- const wikiDir = path.join(targetDir, 'wiki');
282
- const stage = { name: 'wiki/' };
283
-
284
- if (decision === 'skip') {
285
- logger(`⏭ 阶段 2 跳过:wiki/ 已存在,保留现有内容`);
286
- stage.status = 'skipped';
287
- return stage;
288
- }
289
-
290
- const action = prepareDirByDecision(wikiDir, decision, logger, 'wiki');
291
- fs.writeFileSync(path.join(wikiDir, 'README.md'), generateWikiReadme(), 'utf8');
292
-
293
- stage.status = action === 'recreate' ? 'recreated' : 'created';
294
- stage.fileCount = 1;
295
-
296
- const verb = action === 'recreate' ? '重新创建' : '创建';
297
- logger(`✅ 阶段 2 完成:wiki/ 基线文件目录已${verb}`);
298
-
299
- return stage;
300
- }
301
-
302
- /**
303
- * 阶段 3: web-ui/ 前端项目(完整/轻量模式)
304
- */
305
- async function scaffoldWebUi(targetDir, pluginDir, decision, logger) {
306
- const webUiDir = path.join(targetDir, 'web-ui');
307
- const stage = { name: 'web-ui/' };
308
-
309
- if (decision === 'skip') {
310
- logger(`⏭ 阶段 3 跳过:web-ui/ 已存在,保留现有内容`);
311
- stage.status = 'skipped';
312
- return stage;
313
- }
314
-
315
- const action = prepareDirByDecision(webUiDir, decision, logger, 'web-ui');
316
-
317
- // update / create / recreate 都从 references 复制
318
- const result = copyDirFiltered(path.join(pluginDir, 'references/web-ui'), webUiDir);
319
-
320
- stage.fileCount = result.fileCount;
321
- stage.skippedDirs = result.skippedDirs;
322
- stage.status = action === 'recreate' ? 'recreated' : 'created';
323
-
324
- const verb = action === 'recreate' ? '重新复制' : '复制';
325
- logger(`✅ 阶段 3 完成:web-ui/ 前端项目已${verb}(${result.fileCount} 个文件)`);
326
-
327
- return stage;
328
- }
329
-
330
- /**
331
- * 阶段 4: service/ 后端服务目录(完整/轻量模式)
332
- */
333
- function scaffoldService(targetDir, modules, decision, logger) {
334
- const serviceDir = path.join(targetDir, 'service');
335
- const stage = { name: 'service/' };
336
-
337
- if (decision === 'skip') {
338
- logger(`⏭ 阶段 4 跳过:service/ 已存在,保留现有内容`);
339
- stage.status = 'skipped';
340
- return stage;
341
- }
342
-
343
- const action = prepareDirByDecision(serviceDir, decision, logger, 'service');
344
- fs.writeFileSync(path.join(serviceDir, 'README.md'), generateServiceReadme(), 'utf8');
345
-
346
- // 按模块列表创建子目录
347
- if (modules) {
348
- const moduleList = modules.split(',').map((s) => s.trim()).filter(Boolean);
349
- for (const mod of moduleList) {
350
- // 格式:{port}-crm-{name}:{package}
351
- const match = mod.match(/^(\d+)-crm-([^:]+):(.+)$/);
352
- if (match) {
353
- const [, port, name] = match;
354
- const modPath = path.join(serviceDir, `${port}-crm-${name}`, 'service');
355
- fs.mkdirSync(modPath, { recursive: true });
356
- fs.writeFileSync(path.join(modPath, 'README.md'), generateServiceReadme(), 'utf8');
357
- }
358
- }
359
- }
360
-
361
- stage.fileCount = countFiles(serviceDir);
362
- stage.status = action === 'recreate' ? 'recreated' : 'created';
363
-
364
- const verb = action === 'recreate' ? '重新创建' : '创建';
365
- logger(`✅ 阶段 4 完成:service 目录已${verb}`);
366
-
367
- return stage;
368
- }
369
-
370
- /**
371
- * 阶段 5: front-demo/ 前端演示项目(经典/完整/轻量模式)
372
- */
373
- async function scaffoldFrontDemo(targetDir, pluginDir, frontDemoSource, decision, logger) {
374
- const frontDemoDir = path.join(targetDir, 'front-demo');
375
- const stage = { name: 'front-demo/', frontDemoSource };
376
-
377
- if (decision === 'skip') {
378
- logger(`⏭ 阶段 5 跳过:front-demo/ 已存在,保留现有内容`);
379
- stage.status = 'skipped';
380
- return stage;
381
- }
382
-
383
- const action = prepareDirByDecision(frontDemoDir, decision, logger, 'front-demo');
384
-
385
- const result = copyDirFiltered(
386
- path.join(pluginDir, 'references', frontDemoSource),
387
- frontDemoDir
388
- );
389
-
390
- stage.fileCount = result.fileCount;
391
- stage.skippedDirs = result.skippedDirs;
392
- stage.status = action === 'recreate' ? 'recreated' : 'created';
393
-
394
- const techName = frontDemoSource === 'antd-front-demo' ? 'Ant Design React' : 'Vue';
395
- const verb = action === 'recreate' ? '重新复制' : '复制';
396
- logger(`✅ 阶段 5 完成:front-demo/ 已${verb}(${techName},${result.fileCount} 个文件)`);
397
-
398
- return stage;
399
- }
400
-
401
- /**
402
- * 阶段 6: .adspecs/ 配置与脚本目录
403
- */
404
- async function scaffoldAdspecs(targetDir, pluginDir, decision, subDecisions, logger) {
405
- const adspecsDir = path.join(targetDir, '.adspecs');
406
- const stage = { name: '.adspecs/' };
407
-
408
- if (decision === 'skip') {
409
- logger(`⏭ 阶段 6 跳过:.adspecs/ 已存在,保留现有内容`);
410
-
411
- // 子决策:templates
412
- if (subDecisions.templates === 'update') {
413
- copyDirFiltered(
414
- path.join(pluginDir, '.adspecs/templates'),
415
- path.join(adspecsDir, 'templates')
416
- );
417
- logger(`🔄 阶段 6 补充更新:.adspecs/templates/ 已从插件根目录更新`);
418
- stage.subDecisions = { templates: 'update' };
419
- }
420
-
421
- // 属性补全(无论是否更新 templates 都执行)
422
- const featureResult = mergeJsonFile(
423
- path.join(adspecsDir, 'feature.json'),
424
- path.join(pluginDir, '.adspecs/feature.json')
425
- );
426
- if (featureResult.addedKeys.length > 0) {
427
- logger(`🔧 阶段 6 属性补全:feature.json 已补充缺失属性 [${featureResult.addedKeys.join(', ')}]`);
428
- stage.featureResult = featureResult;
429
- } else {
430
- logger(`✅ 阶段 6:feature.json 属性完整,无需补全`);
431
- }
432
-
433
- stage.status = 'skipped';
434
- return stage;
435
- }
436
-
437
- // recreate / update / create
438
- let backupFeature = null;
439
- let backupProject = null;
440
-
441
- if (decision === 'recreate' && fs.existsSync(adspecsDir)) {
442
- // 备份 feature.json / project.json
443
- const featurePath = path.join(adspecsDir, 'feature.json');
444
- const projectPath = path.join(adspecsDir, 'project.json');
445
- if (fs.existsSync(featurePath)) backupFeature = fs.readFileSync(featurePath, 'utf8');
446
- if (fs.existsSync(projectPath)) backupProject = fs.readFileSync(projectPath, 'utf8');
447
- removeDir(adspecsDir);
448
- logger(`🔄 删除旧 .adspecs/ 并重建...`);
449
- } else if (decision === 'update') {
450
- // update:保留目录,后续覆盖复制
451
- fs.mkdirSync(adspecsDir, { recursive: true });
452
- }
453
-
454
- // 从插件根目录复制 .adspecs/
455
- const result = copyDirFiltered(path.join(pluginDir, '.adspecs'), adspecsDir);
456
- stage.fileCount = result.fileCount;
457
-
458
- // recreate 场景:恢复备份并合并
459
- if (decision === 'recreate' && (backupFeature || backupProject)) {
460
- if (backupFeature) {
461
- const featureResult = mergeJsonFile(
462
- path.join(adspecsDir, 'feature.json'),
463
- path.join(pluginDir, '.adspecs/feature.json')
464
- );
465
- stage.featureResult = featureResult;
466
- }
467
- stage.projectJsonBacked = !!backupProject;
468
- logger(`🔧 feature.json/project.json 已恢复并补全属性`);
469
- }
470
-
471
- stage.status = decision === 'recreate' ? 'recreated' : 'created';
472
- const verb = decision === 'recreate' ? '重新复制' : '复制';
473
- logger(`✅ 阶段 6 完成:.adspecs/ 配置与脚本目录已${verb}(${result.fileCount} 个文件)`);
474
-
475
- return stage;
476
- }
477
-
478
- /**
479
- * 阶段 6.5: 生成 .adspecs/project.json
480
- */
481
- function writeProjectJson(targetDir, pluginDir, values) {
482
- const targetPath = path.join(targetDir, '.adspecs/project.json');
483
- const referencePath = path.join(pluginDir, '.adspecs/project.json');
484
- const created = !fs.existsSync(targetPath);
485
-
486
- const merged = mergeProjectJson(
487
- targetPath,
488
- { ...values, created_at: new Date().toISOString().slice(0, 10) },
489
- referencePath
490
- );
491
-
492
- return { created, merged };
493
- }
494
-
495
- /**
496
- * 阶段 7: 生成 .adspecs/paths.json
497
- */
498
- function writePathsJson(targetDir) {
499
- const targetPath = path.join(targetDir, '.adspecs/paths.json');
500
- const created = !fs.existsSync(targetPath);
501
-
502
- if (created) {
503
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
504
- const config = generatePathsConfig();
505
- fs.writeFileSync(targetPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
506
- }
507
-
508
- return { created };
509
- }
510
-
511
- /**
512
- * 根据前端演示源解析前端规范目录名
513
- */
514
- function resolveFrontendStandard(frontDemoSource) {
515
- return frontDemoSource === 'antd-front-demo' ? 'ant6-front-standard' : 'ecp-front-standard';
516
- }
517
-
518
- module.exports = { scaffold };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { copyDirFiltered, removeDir, countFiles } = require('./copier');
6
+ const { generateReadme, generateWikiReadme, generateServiceReadme, DOCS_READMES } = require('./readme-gen');
7
+ const { mergeJsonFile, mergeProjectJson } = require('./json-merge');
8
+ const { generatePathsConfig } = require('./paths-defaults');
9
+ const { listFiles } = require('./dir-utils');
10
+
11
+ /**
12
+ * 脚手架核心
13
+ *
14
+ * 接收 per-zone decisions 对象,按决策分支执行(create/recreate/update/skip)。
15
+ * 通过 logger 回调实时输出每阶段状态。
16
+ * 支持子目录决策、模式升级自动补充、skip 场景下 feature.json 属性补全。
17
+ */
18
+
19
+ // ============================================================
20
+ // 主入口
21
+ // ============================================================
22
+
23
+ /**
24
+ * 执行脚手架
25
+ *
26
+ * @param {object} opts
27
+ * @param {string} opts.targetDir - 目标项目根目录
28
+ * @param {string} opts.pluginDir - 插件根目录
29
+ * @param {string} opts.mode - 初始化模式
30
+ * @param {string} opts.frontDemo - 前端技术栈(vue/antd/skip)
31
+ * @param {string} opts.project_name
32
+ * @param {string} opts.project_short_name
33
+ * @param {string} opts.project_dir
34
+ * @param {string} opts.company_name
35
+ * @param {string} opts.confidentiality
36
+ * @param {string} opts.modules
37
+ * @param {object} opts.decisions - 每区域决策 { areaKey: 'create'|'recreate'|'update'|'skip' }
38
+ * @param {object} opts.subDecisions - 子目录决策 { conventions, templates }
39
+ * @param {object} opts.modeUpgrade - 模式升级检测 { detected, emptyDirs }
40
+ * @param {function} opts.logger - 实时状态回调 (msg: string) => void
41
+ * @returns {Promise<object>} 执行报告
42
+ */
43
+ async function scaffold(opts) {
44
+ const {
45
+ targetDir,
46
+ pluginDir,
47
+ mode,
48
+ frontDemo,
49
+ project_name,
50
+ project_short_name,
51
+ project_dir,
52
+ company_name,
53
+ confidentiality,
54
+ modules,
55
+ decisions,
56
+ subDecisions = { conventions: 'skip', templates: {} },
57
+ modeUpgrade = { detected: false, emptyDirs: [] },
58
+ logger = () => {},
59
+ } = opts;
60
+
61
+ const report = {
62
+ targetDir,
63
+ mode,
64
+ projectName: project_name,
65
+ projectShortName: project_short_name,
66
+ companyName: company_name,
67
+ confidentiality,
68
+ stages: [],
69
+ startedAt: new Date().toISOString(),
70
+ };
71
+
72
+ const FRONT_DEMO_SOURCE = frontDemo === 'antd' ? 'antd-front-demo' : 'front-demo';
73
+
74
+ fs.mkdirSync(targetDir, { recursive: true });
75
+
76
+ // === 阶段 1: docs/ ===
77
+ report.stages.push(
78
+ await scaffoldDocs(targetDir, pluginDir, mode, FRONT_DEMO_SOURCE, decisions.docs, subDecisions, modeUpgrade, logger)
79
+ );
80
+
81
+ // === 阶段 2: wiki/ ===
82
+ report.stages.push(scaffoldWiki(targetDir, decisions.wiki, logger));
83
+
84
+ // === 阶段 3: web-ui/ (full/demo) ===
85
+ if (mode === 'full' || mode === 'demo') {
86
+ report.stages.push(await scaffoldWebUi(targetDir, pluginDir, decisions.webUi, logger));
87
+ }
88
+
89
+ // === 阶段 4: service/ (full/demo) ===
90
+ if (mode === 'full' || mode === 'demo') {
91
+ report.stages.push(scaffoldService(targetDir, modules, decisions.service, logger));
92
+ }
93
+
94
+ // === 阶段 5: front-demo/ (classic/full/demo,且 frontDemo !== 'skip') ===
95
+ if (mode !== 'simple' && frontDemo !== 'skip') {
96
+ report.stages.push(await scaffoldFrontDemo(targetDir, pluginDir, FRONT_DEMO_SOURCE, decisions.frontDemo, logger));
97
+ } else if (mode !== 'simple' && frontDemo === 'skip') {
98
+ logger(`⏭ 阶段 5 跳过:用户选择不创建 front-demo/`);
99
+ report.stages.push({ name: 'front-demo/', status: 'skipped', reason: '用户选择跳过' });
100
+ }
101
+
102
+ // === 阶段 6: .adspecs/ ===
103
+ report.stages.push(
104
+ await scaffoldAdspecs(targetDir, pluginDir, decisions.adspecs, subDecisions, logger)
105
+ );
106
+
107
+ // === 阶段 6.5: project.json ===
108
+ const projectJsonResult = writeProjectJson(targetDir, pluginDir, {
109
+ project_name,
110
+ project_short_name,
111
+ project_dir,
112
+ company_name,
113
+ confidentiality,
114
+ });
115
+ logger(
116
+ projectJsonResult.created
117
+ ? `✅ 阶段 6.5 完成:项目元数据已生成 .adspecs/project.json`
118
+ : `🔄 阶段 6.5 完成:项目元数据已更新 .adspecs/project.json`
119
+ );
120
+ report.stages.push({
121
+ name: '.adspecs/project.json',
122
+ status: projectJsonResult.created ? 'created' : 'updated',
123
+ detail: projectJsonResult,
124
+ });
125
+
126
+ // === 阶段 7: paths.json ===
127
+ const pathsResult = writePathsJson(targetDir);
128
+ logger(
129
+ pathsResult.created
130
+ ? `✅ 阶段 7 完成:路径配置文件已生成 .adspecs/paths.json`
131
+ : `⏭ 阶段 7 跳过:.adspecs/paths.json 已存在`
132
+ );
133
+ report.stages.push({
134
+ name: '.adspecs/paths.json',
135
+ status: pathsResult.created ? 'created' : 'exists',
136
+ });
137
+
138
+ report.completedAt = new Date().toISOString();
139
+ return report;
140
+ }
141
+
142
+ // ============================================================
143
+ // 各阶段实现
144
+ // ============================================================
145
+
146
+ /**
147
+ * 决策前处理:根据 decision 决定目录状态
148
+ *
149
+ * @returns {'created'|'recreated'|'skipped'|'updated'} action
150
+ */
151
+ function prepareDirByDecision(dirPath, decision, logger, areaName) {
152
+ if (decision === 'skip') {
153
+ return 'skip';
154
+ }
155
+ if (decision === 'recreate') {
156
+ if (fs.existsSync(dirPath)) {
157
+ removeDir(dirPath);
158
+ logger(`🔄 删除旧 ${areaName}/ 并重建...`);
159
+ }
160
+ fs.mkdirSync(dirPath, { recursive: true });
161
+ return 'recreate';
162
+ }
163
+ if (decision === 'update') {
164
+ // update:保留目录,后续操作会覆盖文件
165
+ fs.mkdirSync(dirPath, { recursive: true });
166
+ return 'update';
167
+ }
168
+ // create:目录原本就不存在
169
+ fs.mkdirSync(dirPath, { recursive: true });
170
+ return 'create';
171
+ }
172
+
173
+ /**
174
+ * 阶段 1: docs/ 目录结构
175
+ */
176
+ async function scaffoldDocs(targetDir, pluginDir, mode, frontDemoSource, decision, subDecisions, modeUpgrade, logger) {
177
+ const docsDir = path.join(targetDir, 'docs');
178
+ const stage = { name: 'docs/', dirs: [], files: 0 };
179
+
180
+ if (decision === 'skip') {
181
+ logger(`⏭ 阶段 1 跳过:docs/ 已存在,保留现有内容`);
182
+
183
+ // 子决策:conventions
184
+ if (subDecisions.conventions === 'update') {
185
+ const frontendStandard = resolveFrontendStandard(frontDemoSource);
186
+ copyDirFiltered(
187
+ path.join(pluginDir, 'references', frontendStandard),
188
+ path.join(docsDir, '40-project_conventions/frontend-standard')
189
+ );
190
+ logger(`🔄 阶段 1 补充更新:docs/40-project_conventions/frontend-standard/ 已按前端技术栈更新`);
191
+ stage.subDecisions = { conventions: 'update' };
192
+ }
193
+
194
+ // 模式升级自动补充
195
+ if (modeUpgrade.detected) {
196
+ const frontendStandard = resolveFrontendStandard(frontDemoSource);
197
+ fs.mkdirSync(path.join(docsDir, '40-project_conventions/frontend-standard'), { recursive: true });
198
+ copyDirFiltered(
199
+ path.join(pluginDir, 'references', frontendStandard),
200
+ path.join(docsDir, '40-project_conventions/frontend-standard')
201
+ );
202
+ logger(`📦 补充:检测到 40-project_conventions/ 为空目录(简易模式产物),已按前端技术栈补充 frontend-standard/`);
203
+ stage.modeUpgrade = modeUpgrade;
204
+ }
205
+
206
+ stage.status = 'skipped';
207
+ return stage;
208
+ }
209
+
210
+ const action = prepareDirByDecision(docsDir, decision, logger, 'docs');
211
+
212
+ // 11 个一级子目录
213
+ const subdirs = [
214
+ '00-planning',
215
+ '00-customer-requirements',
216
+ '10-architecture',
217
+ '20-prd',
218
+ '30-system-design',
219
+ '40-project_conventions',
220
+ '70-scripts/depoly',
221
+ '80-development/800-log',
222
+ '80-development/801-dev',
223
+ '80-development/802-test',
224
+ '100-help',
225
+ '300-specs',
226
+ ];
227
+ for (const sub of subdirs) {
228
+ fs.mkdirSync(path.join(docsDir, sub), { recursive: true });
229
+ stage.dirs.push(sub);
230
+ }
231
+
232
+ // 40-project_conventions 按模式处理
233
+ const convDir = path.join(docsDir, '40-project_conventions');
234
+ fs.mkdirSync(path.join(convDir, 'frontend-standard'), { recursive: true });
235
+ fs.mkdirSync(path.join(convDir, 'end-standard'), { recursive: true });
236
+
237
+ if (mode === 'demo') {
238
+ copyDirFiltered(
239
+ path.join(pluginDir, 'references/ant6-front-standard'),
240
+ path.join(convDir, 'frontend-standard')
241
+ );
242
+ copyDirFiltered(
243
+ path.join(pluginDir, 'references/python-end-standard'),
244
+ path.join(convDir, 'end-standard')
245
+ );
246
+ } else if (mode === 'classic' || mode === 'full') {
247
+ const frontendStandard = resolveFrontendStandard(frontDemoSource);
248
+ copyDirFiltered(
249
+ path.join(pluginDir, 'references', frontendStandard),
250
+ path.join(convDir, 'frontend-standard')
251
+ );
252
+ }
253
+
254
+ // 生成各子目录的 README.md(create/recreate/update 都重新生成)
255
+ if (decision !== 'skip') {
256
+ for (const [subdirKey, meta] of Object.entries(DOCS_READMES)) {
257
+ const readmePath = path.join(docsDir, subdirKey, 'README.md');
258
+ fs.writeFileSync(readmePath, generateReadme(meta), 'utf8');
259
+ stage.files++;
260
+ }
261
+ }
262
+
263
+ stage.fileCount = countFiles(docsDir);
264
+ stage.status = action === 'recreate' ? 'recreated' : 'created';
265
+
266
+ const verb = action === 'recreate' ? '重新创建' : '创建';
267
+ const convNote =
268
+ mode === 'demo'
269
+ ? ',40 Python+React 技术栈复制前后端规范'
270
+ : mode === 'simple'
271
+ ? '40 仅创建空目录'
272
+ : ',其中 40 references 复制';
273
+ logger(`✅ 阶段 1 完成:docs/ 目录结构已${verb}(11 个一级子目录${convNote})`);
274
+
275
+ return stage;
276
+ }
277
+
278
+ /**
279
+ * 阶段 2: wiki/ 基线文件目录
280
+ */
281
+ function scaffoldWiki(targetDir, decision, logger) {
282
+ const wikiDir = path.join(targetDir, 'wiki');
283
+ const stage = { name: 'wiki/' };
284
+
285
+ if (decision === 'skip') {
286
+ logger(`⏭ 阶段 2 跳过:wiki/ 已存在,保留现有内容`);
287
+ stage.status = 'skipped';
288
+ return stage;
289
+ }
290
+
291
+ const action = prepareDirByDecision(wikiDir, decision, logger, 'wiki');
292
+ fs.writeFileSync(path.join(wikiDir, 'README.md'), generateWikiReadme(), 'utf8');
293
+
294
+ stage.status = action === 'recreate' ? 'recreated' : 'created';
295
+ stage.fileCount = 1;
296
+
297
+ const verb = action === 'recreate' ? '重新创建' : '创建';
298
+ logger(`✅ 阶段 2 完成:wiki/ 基线文件目录已${verb}`);
299
+
300
+ return stage;
301
+ }
302
+
303
+ /**
304
+ * 阶段 3: web-ui/ 前端项目(完整/轻量模式)
305
+ */
306
+ async function scaffoldWebUi(targetDir, pluginDir, decision, logger) {
307
+ const webUiDir = path.join(targetDir, 'web-ui');
308
+ const stage = { name: 'web-ui/' };
309
+
310
+ if (decision === 'skip') {
311
+ logger(`⏭ 阶段 3 跳过:web-ui/ 已存在,保留现有内容`);
312
+ stage.status = 'skipped';
313
+ return stage;
314
+ }
315
+
316
+ const action = prepareDirByDecision(webUiDir, decision, logger, 'web-ui');
317
+
318
+ // update / create / recreate 都从 references 复制
319
+ const result = copyDirFiltered(path.join(pluginDir, 'references/web-ui'), webUiDir);
320
+
321
+ stage.fileCount = result.fileCount;
322
+ stage.skippedDirs = result.skippedDirs;
323
+ stage.status = action === 'recreate' ? 'recreated' : 'created';
324
+
325
+ const verb = action === 'recreate' ? '重新复制' : '复制';
326
+ logger(`✅ 阶段 3 完成:web-ui/ 前端项目已${verb}(${result.fileCount} 个文件)`);
327
+
328
+ return stage;
329
+ }
330
+
331
+ /**
332
+ * 阶段 4: service/ 后端服务目录(完整/轻量模式)
333
+ */
334
+ function scaffoldService(targetDir, modules, decision, logger) {
335
+ const serviceDir = path.join(targetDir, 'service');
336
+ const stage = { name: 'service/' };
337
+
338
+ if (decision === 'skip') {
339
+ logger(`⏭ 阶段 4 跳过:service/ 已存在,保留现有内容`);
340
+ stage.status = 'skipped';
341
+ return stage;
342
+ }
343
+
344
+ const action = prepareDirByDecision(serviceDir, decision, logger, 'service');
345
+ fs.writeFileSync(path.join(serviceDir, 'README.md'), generateServiceReadme(), 'utf8');
346
+
347
+ // 按模块列表创建子目录
348
+ if (modules) {
349
+ const moduleList = modules.split(',').map((s) => s.trim()).filter(Boolean);
350
+ for (const mod of moduleList) {
351
+ // 格式:{port}-crm-{name}:{package}
352
+ const match = mod.match(/^(\d+)-crm-([^:]+):(.+)$/);
353
+ if (match) {
354
+ const [, port, name] = match;
355
+ const modPath = path.join(serviceDir, `${port}-crm-${name}`, 'service');
356
+ fs.mkdirSync(modPath, { recursive: true });
357
+ fs.writeFileSync(path.join(modPath, 'README.md'), generateServiceReadme(), 'utf8');
358
+ }
359
+ }
360
+ }
361
+
362
+ stage.fileCount = countFiles(serviceDir);
363
+ stage.status = action === 'recreate' ? 'recreated' : 'created';
364
+
365
+ const verb = action === 'recreate' ? '重新创建' : '创建';
366
+ logger(`✅ 阶段 4 完成:service 目录已${verb}`);
367
+
368
+ return stage;
369
+ }
370
+
371
+ /**
372
+ * 阶段 5: front-demo/ 前端演示项目(经典/完整/轻量模式)
373
+ */
374
+ async function scaffoldFrontDemo(targetDir, pluginDir, frontDemoSource, decision, logger) {
375
+ const frontDemoDir = path.join(targetDir, 'front-demo');
376
+ const stage = { name: 'front-demo/', frontDemoSource };
377
+
378
+ if (decision === 'skip') {
379
+ logger(`⏭ 阶段 5 跳过:front-demo/ 已存在,保留现有内容`);
380
+ stage.status = 'skipped';
381
+ return stage;
382
+ }
383
+
384
+ const action = prepareDirByDecision(frontDemoDir, decision, logger, 'front-demo');
385
+
386
+ const result = copyDirFiltered(
387
+ path.join(pluginDir, 'references', frontDemoSource),
388
+ frontDemoDir
389
+ );
390
+
391
+ stage.fileCount = result.fileCount;
392
+ stage.skippedDirs = result.skippedDirs;
393
+ stage.status = action === 'recreate' ? 'recreated' : 'created';
394
+
395
+ const techName = frontDemoSource === 'antd-front-demo' ? 'Ant Design React' : 'Vue';
396
+ const verb = action === 'recreate' ? '重新复制' : '复制';
397
+ logger(`✅ 阶段 5 完成:front-demo/ 已${verb}(${techName},${result.fileCount} 个文件)`);
398
+
399
+ return stage;
400
+ }
401
+
402
+ /**
403
+ * 阶段 6: .adspecs/ 配置与脚本目录
404
+ */
405
+ async function scaffoldAdspecs(targetDir, pluginDir, decision, subDecisions, logger) {
406
+ const adspecsDir = path.join(targetDir, '.adspecs');
407
+ const stage = { name: '.adspecs/' };
408
+
409
+ if (decision === 'skip') {
410
+ logger(`⏭ 阶段 6 跳过:.adspecs/ 已存在,保留现有内容`);
411
+
412
+ // 子决策:templates(逐文件确认)
413
+ const templatesDecision = subDecisions.templates || {};
414
+ const templatesMap = typeof templatesDecision === 'object' ? templatesDecision : {};
415
+ const replaceFiles = Object.entries(templatesMap)
416
+ .filter(([, v]) => v === 'replace')
417
+ .map(([k]) => k);
418
+ const keepFiles = Object.entries(templatesMap)
419
+ .filter(([, v]) => v === 'keep')
420
+ .map(([k]) => k);
421
+
422
+ // 新增文件(插件有但目标没有)→ 直接复制
423
+ const pluginTmplDir = path.join(pluginDir, '.adspecs/templates');
424
+ const targetTmplDir = path.join(adspecsDir, 'templates');
425
+ const targetFiles = new Set(listFiles(targetTmplDir));
426
+ const pluginFiles = listFiles(pluginTmplDir);
427
+ const newFiles = pluginFiles.filter((f) => !targetFiles.has(f));
428
+
429
+ let addedCount = 0;
430
+ for (const f of newFiles) {
431
+ fs.copyFileSync(path.join(pluginTmplDir, f), path.join(targetTmplDir, f));
432
+ addedCount++;
433
+ }
434
+ if (addedCount > 0) {
435
+ logger(`📦 模板新增:已添加 ${addedCount} 个新模板文件`);
436
+ }
437
+
438
+ // 逐文件替换
439
+ let replacedCount = 0;
440
+ for (const f of replaceFiles) {
441
+ const srcPath = path.join(pluginTmplDir, f);
442
+ const dstPath = path.join(targetTmplDir, f);
443
+ if (fs.existsSync(srcPath)) {
444
+ fs.copyFileSync(srcPath, dstPath);
445
+ replacedCount++;
446
+ }
447
+ }
448
+
449
+ if (replacedCount > 0 || keepFiles.length > 0) {
450
+ logger(`🔄 阶段 6 补充更新:.adspecs/templates/ 已部分更新(${replacedCount} 个文件替换,${keepFiles.length} 个文件保留)`);
451
+ stage.subDecisions = { templates: { replaced: replacedCount, kept: keepFiles.length, added: addedCount } };
452
+ }
453
+
454
+ // 属性补全(无论是否更新 templates 都执行)
455
+ const featureResult = mergeJsonFile(
456
+ path.join(adspecsDir, 'feature.json'),
457
+ path.join(pluginDir, '.adspecs/feature.json')
458
+ );
459
+ if (featureResult.addedKeys.length > 0) {
460
+ logger(`🔧 阶段 6 属性补全:feature.json 已补充缺失属性 [${featureResult.addedKeys.join(', ')}]`);
461
+ stage.featureResult = featureResult;
462
+ } else {
463
+ logger(`✅ 阶段 6:feature.json 属性完整,无需补全`);
464
+ }
465
+
466
+ stage.status = 'skipped';
467
+ return stage;
468
+ }
469
+
470
+ // recreate / update / create
471
+ let backupFeature = null;
472
+ let backupProject = null;
473
+
474
+ if (decision === 'recreate' && fs.existsSync(adspecsDir)) {
475
+ // 备份 feature.json / project.json
476
+ const featurePath = path.join(adspecsDir, 'feature.json');
477
+ const projectPath = path.join(adspecsDir, 'project.json');
478
+ if (fs.existsSync(featurePath)) backupFeature = fs.readFileSync(featurePath, 'utf8');
479
+ if (fs.existsSync(projectPath)) backupProject = fs.readFileSync(projectPath, 'utf8');
480
+ removeDir(adspecsDir);
481
+ logger(`🔄 删除旧 .adspecs/ 并重建...`);
482
+ } else if (decision === 'update') {
483
+ // update:保留目录,后续覆盖复制
484
+ fs.mkdirSync(adspecsDir, { recursive: true });
485
+ }
486
+
487
+ // 从插件根目录复制 .adspecs/
488
+ const result = copyDirFiltered(path.join(pluginDir, '.adspecs'), adspecsDir);
489
+ stage.fileCount = result.fileCount;
490
+
491
+ // recreate 场景:恢复备份并合并
492
+ if (decision === 'recreate' && (backupFeature || backupProject)) {
493
+ if (backupFeature) {
494
+ const featureResult = mergeJsonFile(
495
+ path.join(adspecsDir, 'feature.json'),
496
+ path.join(pluginDir, '.adspecs/feature.json')
497
+ );
498
+ stage.featureResult = featureResult;
499
+ }
500
+ stage.projectJsonBacked = !!backupProject;
501
+ logger(`🔧 feature.json/project.json 已恢复并补全属性`);
502
+ }
503
+
504
+ stage.status = decision === 'recreate' ? 'recreated' : 'created';
505
+ const verb = decision === 'recreate' ? '重新复制' : '复制';
506
+ logger(`✅ 阶段 6 完成:.adspecs/ 配置与脚本目录已${verb}(${result.fileCount} 个文件)`);
507
+
508
+ return stage;
509
+ }
510
+
511
+ /**
512
+ * 阶段 6.5: 生成 .adspecs/project.json
513
+ */
514
+ function writeProjectJson(targetDir, pluginDir, values) {
515
+ const targetPath = path.join(targetDir, '.adspecs/project.json');
516
+ const referencePath = path.join(pluginDir, '.adspecs/project.json');
517
+ const created = !fs.existsSync(targetPath);
518
+
519
+ const merged = mergeProjectJson(
520
+ targetPath,
521
+ { ...values, created_at: new Date().toISOString().slice(0, 10) },
522
+ referencePath
523
+ );
524
+
525
+ return { created, merged };
526
+ }
527
+
528
+ /**
529
+ * 阶段 7: 生成 .adspecs/paths.json
530
+ */
531
+ function writePathsJson(targetDir) {
532
+ const targetPath = path.join(targetDir, '.adspecs/paths.json');
533
+ const created = !fs.existsSync(targetPath);
534
+
535
+ if (created) {
536
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
537
+ const config = generatePathsConfig();
538
+ fs.writeFileSync(targetPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
539
+ }
540
+
541
+ return { created };
542
+ }
543
+
544
+ /**
545
+ * 根据前端演示源解析前端规范目录名
546
+ */
547
+ function resolveFrontendStandard(frontDemoSource) {
548
+ return frontDemoSource === 'antd-front-demo' ? 'ant6-front-standard' : 'ecp-front-standard';
549
+ }
550
+
551
+ module.exports = { scaffold };