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,518 +1,518 @@
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
+
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 };