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.
- package/.adspecs/extensions/git/commands/adspecs.git.initialize.md +4 -4
- package/.adspecs/extensions/git/extension.yml +1 -1
- package/.adspecs/extensions/git/git-config.yml +17 -2
- package/.adspecs/extensions.yml +32 -0
- package/.adspecs/feature.json +16 -16
- package/.adspecs/feature.yml +28 -28
- package/.adspecs/paths.json +17 -0
- 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
- package/.adspecs/workflows/adspecs/workflow.yml +5 -4
- package/.claude-plugin/marketplace.json +23 -23
- package/.claude-plugin/plugin.json +18 -18
- package/.qoder-plugin/plugin.json +31 -31
- package/CLAUDE.md +1 -5
- package/README.md +395 -395
- package/bin/adspecs.js +129 -129
- package/hooks/commit-queue.js +245 -245
- package/hooks/hooks.json +63 -63
- package/hooks/session-start.js +44 -44
- package/hooks/wiki-queue.js +127 -127
- package/package.json +61 -61
- package/references/ant6-front-standard/index.md +99 -99
- package/references/antd-front-demo/.env.develop +12 -0
- package/references/antd-front-demo/.env.production +5 -0
- package/references/antd-front-demo/package-lock.json +2 -2
- package/references/antd-front-demo/package.json +1 -1
- package/references/antd-front-demo/public/mockServiceWorker.js +361 -361
- package/references/ecp-end-standard/index.md +63 -63
- package/references/python-end-standard/01-Python/345/220/216/347/253/257/347/274/226/347/240/201/350/247/204/350/214/203.md +372 -372
- package/references/python-end-standard/02-/346/225/260/346/215/256/345/272/223/350/256/276/350/256/241/344/270/216/344/275/277/347/224/250/350/247/204/350/214/203.md +226 -226
- package/references/python-end-standard/03-Celery/345/274/202/346/255/245/344/273/273/345/212/241/350/247/204/350/214/203.md +237 -237
- package/references/python-end-standard/04-Redis/344/275/277/347/224/250/350/247/204/350/214/203.md +231 -231
- package/scripts/postinstall.js +107 -107
- package/scripts/sync-version.js +105 -105
- package/skills/.claude/.wiki-update-queue +26 -26
- package/skills/adspecs-constitution/SKILL.md +157 -0
- package/skills/adspecs-export-word/SKILL.md +498 -498
- package/skills/adspecs-export-word/references/md-to-docx.js +862 -862
- package/skills/adspecs-export-word/references/package-lock.json +220 -220
- package/skills/adspecs-export-word/references/package.json +10 -10
- package/skills/adspecs-front-prototype/SKILL.md +405 -0
- package/skills/adspecs-git-commit/SKILL.md +23 -8
- package/skills/adspecs-git-initialize/SKILL.md +54 -0
- package/skills/adspecs-plan/SKILL.md +59 -69
- package/skills/adspecs-prd/SKILL.md +13 -5
- package/skills/adspecs-prd-to-demo/SKILL.md +532 -0
- package/skills/adspecs-tasks/SKILL.md +175 -204
- package/skills/adspecs-update-status/SKILL.md +382 -382
- package/skills/adspecs-utest/SKILL.md +107 -116
- package/skills/grill-me/SKILL.md +7 -0
- package/skills/grill-me/agents/openai.yaml +5 -0
- package/skills/playwright-cli/SKILL.md +420 -0
- package/skills/playwright-cli/references/element-attributes.md +23 -0
- package/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/skills/playwright-cli/references/request-mocking.md +87 -0
- package/skills/playwright-cli/references/running-code.md +241 -0
- package/skills/playwright-cli/references/session-management.md +225 -0
- package/skills/playwright-cli/references/storage-state.md +275 -0
- package/skills/playwright-cli/references/test-generation.md +433 -0
- package/skills/playwright-cli/references/tracing.md +139 -0
- package/skills/playwright-cli/references/video-recording.md +143 -0
- package/skills/playwright-trace/SKILL.md +171 -0
- package/skills/project-init/references/front-demo/.claude/settings.local.json +9 -0
- package/skills/wiki-update/SKILL.md +232 -232
- package/src/commands/doctor.js +197 -197
- package/src/commands/init.js +83 -83
- package/src/commands/plugin.js +165 -165
- package/src/commands/update.js +87 -87
- package/src/lib/area-scanner.js +129 -129
- package/src/lib/copier.js +104 -104
- package/src/lib/dir-utils.js +133 -133
- package/src/lib/json-merge.js +114 -114
- package/src/lib/paths-defaults.js +37 -37
- package/src/lib/prompts.js +347 -345
- package/src/lib/readme-gen.js +143 -143
- package/src/lib/report.js +327 -327
- package/src/lib/scaffolder.js +518 -518
- package/src/lib/short-name.js +36 -36
- package/src/utils.js +80 -80
- package/references/antd-front-demo/.env +0 -15
- package/references/antd-front-demo/src/pages/aps/coil/components/CoilDrawer.tsx +0 -141
- package/references/antd-front-demo/src/pages/aps/coil/index.tsx +0 -209
- package/references/antd-front-demo/src/pages/aps/optimization/index.tsx +0 -192
- package/references/antd-front-demo/src/pages/aps/order/components/OrderDrawer.tsx +0 -251
- package/references/antd-front-demo/src/pages/aps/order/index.tsx +0 -273
- package/references/antd-front-demo/src/pages/aps/param/index.tsx +0 -153
- package/references/antd-front-demo/src/pages/aps/plan/components/CuttingPlanVisualization.tsx +0 -821
- package/references/antd-front-demo/src/pages/aps/plan/index.tsx +0 -254
- package/references/antd-front-demo/src/pages/aps/task/index.tsx +0 -190
package/src/lib/scaffolder.js
CHANGED
|
@@ -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 };
|