bain-riper-cli 0.1.1
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/bin/cli.js +14 -0
- package/package.json +34 -0
- package/src/commands/add.js +28 -0
- package/src/commands/init.js +58 -0
- package/src/commands/list.js +44 -0
- package/src/index.js +86 -0
- package/src/modules.js +136 -0
- package/src/utils/checklist.js +49 -0
- package/src/utils/copier.js +100 -0
- package/src/utils/logger.js +18 -0
- package/templates/docs/reference-code/controller/EmployeeTrainingController.java +107 -0
- package/templates/docs/reference-code/domain/EmployeeTraining.java +89 -0
- package/templates/docs/reference-code/domain/bo/EmployeeTrainingBo.java +96 -0
- package/templates/docs/reference-code/domain/vo/EmployeeTrainingVo.java +100 -0
- package/templates/docs/reference-code/mapper/EmployeeTrainingMapper.java +17 -0
- package/templates/docs/reference-code/resources/mapper/EmployeeTrainingMapper.xml +7 -0
- package/templates/docs/reference-code/service/IEmployeeTrainingService.java +69 -0
- package/templates/docs/reference-code/service/impl/EmployeeTrainingServiceImpl.java +144 -0
- package/templates/openspec/config.yaml +126 -0
- package/templates/openspec/project-profile.md +107 -0
- package/templates/qoder/mcp.json +9 -0
- package/templates/qoder/rules/openspec_codegraph.md +162 -0
- package/templates/qoder/rules/openspec_output_specs.md +112 -0
- package/templates/qoder/rules/openspec_preflight.md +230 -0
- package/templates/qoder/rules/openspec_reverse_sync.md +97 -0
- package/templates/qoder/rules/openspec_specifications.md +84 -0
- package/templates/qoder/rules/openspec_task_tiers.md +109 -0
- package/templates/qoder/rules/project_constitution.md +90 -0
- package/templates/qoder/skills/requirement-breakdown/SKILL.md +126 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { main } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
// Handle Ctrl+C gracefully
|
|
6
|
+
process.on('uncaughtException', (error) => {
|
|
7
|
+
if (error?.name === 'ExitPromptError') {
|
|
8
|
+
console.log('\n 已取消。\n');
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}
|
|
11
|
+
throw error;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
main(process.argv.slice(2));
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bain-riper-cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "CLI tool to scaffold bain-riper AI engineering assets into your project",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"bain-riper-cli": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"templates/"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"prepublishOnly": "node scripts/sync-templates.js",
|
|
19
|
+
"sync": "node scripts/sync-templates.js",
|
|
20
|
+
"test": "node --test test/"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@inquirer/prompts": "^7.0.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"cli",
|
|
27
|
+
"ai",
|
|
28
|
+
"engineering",
|
|
29
|
+
"scaffold",
|
|
30
|
+
"openspec",
|
|
31
|
+
"bain-riper"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT"
|
|
34
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getModuleById, modules } from '../modules.js';
|
|
2
|
+
import { copyModule, printSummary } from '../utils/copier.js';
|
|
3
|
+
import { printChecklist } from '../utils/checklist.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Add a specific module by ID.
|
|
8
|
+
*/
|
|
9
|
+
export async function addCommand(moduleId, flags = {}) {
|
|
10
|
+
const mod = getModuleById(moduleId);
|
|
11
|
+
|
|
12
|
+
if (!mod) {
|
|
13
|
+
logger.error(`未知模块:${moduleId}`);
|
|
14
|
+
console.log();
|
|
15
|
+
logger.plain('可用模块:');
|
|
16
|
+
for (const m of modules) {
|
|
17
|
+
logger.plain(` ${m.id.padEnd(18)} ${m.description}`);
|
|
18
|
+
}
|
|
19
|
+
console.log();
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
logger.header(`正在安装:${mod.name}`);
|
|
24
|
+
|
|
25
|
+
const results = await copyModule(mod, { force: flags.force });
|
|
26
|
+
printSummary(results);
|
|
27
|
+
printChecklist([mod.id]);
|
|
28
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { checkbox } from '@inquirer/prompts';
|
|
2
|
+
import { modules } from '../modules.js';
|
|
3
|
+
import { copyModule, printSummary } from '../utils/copier.js';
|
|
4
|
+
import { printChecklist } from '../utils/checklist.js';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Init command: interactive module selection or --all for full install.
|
|
9
|
+
*/
|
|
10
|
+
export async function initCommand(flags = {}) {
|
|
11
|
+
let selectedIds;
|
|
12
|
+
|
|
13
|
+
if (flags.all) {
|
|
14
|
+
// Non-interactive: select all modules
|
|
15
|
+
selectedIds = modules.map(m => m.id);
|
|
16
|
+
} else {
|
|
17
|
+
// Check if stdin is a TTY (interactive terminal)
|
|
18
|
+
if (!process.stdin.isTTY) {
|
|
19
|
+
logger.error('未检测到交互式终端,请使用 --all 参数全量安装。');
|
|
20
|
+
logger.plain('示例:npx bain-riper-cli init --all');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Interactive: show checkbox selection
|
|
25
|
+
logger.header('bain-riper CLI - 项目初始化');
|
|
26
|
+
|
|
27
|
+
selectedIds = await checkbox({
|
|
28
|
+
message: '选择要安装的模块(空格切换,回车确认)',
|
|
29
|
+
choices: modules.map(mod => ({
|
|
30
|
+
name: `${mod.name.padEnd(18)} - ${mod.description}`,
|
|
31
|
+
value: mod.id,
|
|
32
|
+
checked: false,
|
|
33
|
+
})),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (selectedIds.length === 0) {
|
|
37
|
+
logger.info('未选择任何模块,已退出。');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Install selected modules
|
|
43
|
+
const allResults = { created: [], skipped: [], overwritten: [], errors: [] };
|
|
44
|
+
|
|
45
|
+
for (const id of selectedIds) {
|
|
46
|
+
const mod = modules.find(m => m.id === id);
|
|
47
|
+
logger.header(`正在安装:${mod.name}`);
|
|
48
|
+
const results = await copyModule(mod, { force: flags.force });
|
|
49
|
+
|
|
50
|
+
allResults.created.push(...results.created);
|
|
51
|
+
allResults.skipped.push(...results.skipped);
|
|
52
|
+
allResults.overwritten.push(...results.overwritten);
|
|
53
|
+
allResults.errors.push(...results.errors);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
printSummary(allResults);
|
|
57
|
+
printChecklist(selectedIds);
|
|
58
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { modules, getTargetPath } from '../modules.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* List command: show installation status of all modules.
|
|
8
|
+
*/
|
|
9
|
+
export async function listCommand() {
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
|
|
12
|
+
logger.header('bain-riper 模块状态');
|
|
13
|
+
|
|
14
|
+
for (const mod of modules) {
|
|
15
|
+
let present = 0;
|
|
16
|
+
const missing = [];
|
|
17
|
+
|
|
18
|
+
for (const file of mod.files) {
|
|
19
|
+
const filePath = getTargetPath(mod, file, cwd);
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(filePath);
|
|
22
|
+
present++;
|
|
23
|
+
} catch {
|
|
24
|
+
missing.push(file);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const total = mod.files.length;
|
|
29
|
+
const ratio = present / total;
|
|
30
|
+
|
|
31
|
+
if (ratio === 1) {
|
|
32
|
+
logger.success(`${mod.id.padEnd(18)} [已安装] ${present}/${total} 个文件就位`);
|
|
33
|
+
} else if (ratio > 0) {
|
|
34
|
+
logger.plain(` ◐ ${mod.id.padEnd(16)} [部分安装] ${present}/${total} 个文件就位`);
|
|
35
|
+
for (const f of missing) {
|
|
36
|
+
logger.plain(` 缺失:${path.join(mod.targetBase, f)}`);
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
logger.skip(`${mod.id.padEnd(18)} [未安装]`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log();
|
|
44
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { initCommand } from './commands/init.js';
|
|
6
|
+
import { addCommand } from './commands/add.js';
|
|
7
|
+
import { listCommand } from './commands/list.js';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
const HELP_TEXT = `
|
|
12
|
+
bain-riper-cli - 将 AI 工程化资产脚手架化到你的项目
|
|
13
|
+
|
|
14
|
+
用法:
|
|
15
|
+
bain-riper-cli init [--all] [--force] 交互式选择模块(--all 全量安装)
|
|
16
|
+
bain-riper-cli add <module> [--force] 安装指定模块
|
|
17
|
+
bain-riper-cli list 查看模块安装状态
|
|
18
|
+
|
|
19
|
+
模块:
|
|
20
|
+
rules 7 个 AI 规则文件(宪法 / Preflight / Task Tiers 等)
|
|
21
|
+
skills 需求拆分 Skill(requirement-breakdown)
|
|
22
|
+
mcp CodeGraph MCP 配置(mcp.json)
|
|
23
|
+
openspec OpenSpec 工作流配置 + 项目 Profile
|
|
24
|
+
reference-code Java CRUD 参考模板(EmployeeTraining 八层示例)
|
|
25
|
+
|
|
26
|
+
参数:
|
|
27
|
+
--all 非交互模式,安装全部模块
|
|
28
|
+
--force 覆盖已存在的文件
|
|
29
|
+
|
|
30
|
+
示例:
|
|
31
|
+
npx bain-riper-cli init # 交互式选择
|
|
32
|
+
npx bain-riper-cli init --all # 全量安装
|
|
33
|
+
npx bain-riper-cli add rules # 只安装规则
|
|
34
|
+
npx bain-riper-cli list # 查看安装状态
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
export async function main(args) {
|
|
38
|
+
const command = args[0];
|
|
39
|
+
const flags = {
|
|
40
|
+
all: args.includes('--all'),
|
|
41
|
+
force: args.includes('--force'),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
switch (command) {
|
|
45
|
+
case 'init':
|
|
46
|
+
await initCommand(flags);
|
|
47
|
+
break;
|
|
48
|
+
|
|
49
|
+
case 'add': {
|
|
50
|
+
const moduleId = args[1];
|
|
51
|
+
if (!moduleId || moduleId.startsWith('--')) {
|
|
52
|
+
console.error('\n 错误:缺少模块名称。');
|
|
53
|
+
console.error(' 用法:bain-riper-cli add <module>');
|
|
54
|
+
console.error(' 可用模块:rules, skills, mcp, openspec, reference-code\n');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
await addCommand(moduleId, flags);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case 'list':
|
|
62
|
+
await listCommand();
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case '--help':
|
|
66
|
+
case '-h':
|
|
67
|
+
case 'help':
|
|
68
|
+
console.log(HELP_TEXT);
|
|
69
|
+
break;
|
|
70
|
+
|
|
71
|
+
case '--version':
|
|
72
|
+
case '-v': {
|
|
73
|
+
const pkgPath = path.resolve(__dirname, '..', 'package.json');
|
|
74
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
|
|
75
|
+
console.log(`bain-riper-cli v${pkg.version}`);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
default:
|
|
80
|
+
if (command) {
|
|
81
|
+
console.error(`\n 未知命令:${command}\n`);
|
|
82
|
+
}
|
|
83
|
+
console.log(HELP_TEXT);
|
|
84
|
+
process.exit(command ? 1 : 0);
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/modules.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
export const TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates');
|
|
6
|
+
|
|
7
|
+
export const modules = [
|
|
8
|
+
{
|
|
9
|
+
id: 'rules',
|
|
10
|
+
name: 'Rules',
|
|
11
|
+
description: '7 个 AI 规则文件(宪法 / Preflight / Task Tiers 等)',
|
|
12
|
+
templateBase: 'qoder/rules',
|
|
13
|
+
targetBase: '.qoder/rules',
|
|
14
|
+
files: [
|
|
15
|
+
'project_constitution.md',
|
|
16
|
+
'openspec_preflight.md',
|
|
17
|
+
'openspec_task_tiers.md',
|
|
18
|
+
'openspec_specifications.md',
|
|
19
|
+
'openspec_codegraph.md',
|
|
20
|
+
'openspec_output_specs.md',
|
|
21
|
+
'openspec_reverse_sync.md',
|
|
22
|
+
],
|
|
23
|
+
checklistItems: [],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'skills',
|
|
27
|
+
name: 'Skills',
|
|
28
|
+
description: '需求拆分 Skill(requirement-breakdown)',
|
|
29
|
+
templateBase: 'qoder/skills',
|
|
30
|
+
targetBase: '.qoder/skills',
|
|
31
|
+
files: [
|
|
32
|
+
'requirement-breakdown/SKILL.md',
|
|
33
|
+
],
|
|
34
|
+
checklistItems: [],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'mcp',
|
|
38
|
+
name: 'MCP',
|
|
39
|
+
description: 'CodeGraph MCP 配置(mcp.json)',
|
|
40
|
+
templateBase: 'qoder',
|
|
41
|
+
targetBase: '.qoder',
|
|
42
|
+
files: [
|
|
43
|
+
'mcp.json',
|
|
44
|
+
],
|
|
45
|
+
checklistItems: [
|
|
46
|
+
{
|
|
47
|
+
file: '.qoder/mcp.json',
|
|
48
|
+
field: 'args -p 参数',
|
|
49
|
+
action: '将 "/path/to/your-java-project" 替换为你的项目绝对路径',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'openspec',
|
|
55
|
+
name: 'OpenSpec',
|
|
56
|
+
description: 'OpenSpec 工作流配置 + 项目 Profile',
|
|
57
|
+
templateBase: 'openspec',
|
|
58
|
+
targetBase: 'openspec',
|
|
59
|
+
files: [
|
|
60
|
+
'config.yaml',
|
|
61
|
+
'project-profile.md',
|
|
62
|
+
],
|
|
63
|
+
checklistItems: [
|
|
64
|
+
{
|
|
65
|
+
file: 'openspec/project-profile.md',
|
|
66
|
+
field: '§1 技术栈',
|
|
67
|
+
action: '替换为你的项目技术栈(语言版本、框架、ORM、构建工具等)',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
file: 'openspec/project-profile.md',
|
|
71
|
+
field: '§2 项目骨架',
|
|
72
|
+
action: '替换 Maven groupId、基础包路径、Mapper XML 路径、构建命令等',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
file: 'openspec/project-profile.md',
|
|
76
|
+
field: '§3 标准分层',
|
|
77
|
+
action: '根据你的项目结构调整分层目录',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
file: 'openspec/project-profile.md',
|
|
81
|
+
field: '§4 领域约定',
|
|
82
|
+
action: '替换基类名称、注解名称、多租户字段名等',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
file: 'openspec/project-profile.md',
|
|
86
|
+
field: '§5 参考模板',
|
|
87
|
+
action: '替换为你自己项目中的 CRUD 参考文件清单',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
file: 'openspec/project-profile.md',
|
|
91
|
+
field: '§6 repowiki',
|
|
92
|
+
action: '根据实际 repowiki 目录树重写章节映射表',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
extraDirs: [
|
|
96
|
+
'openspec/changes',
|
|
97
|
+
'openspec/specs',
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'reference-code',
|
|
102
|
+
name: 'Reference Code',
|
|
103
|
+
description: 'Java CRUD 参考模板(EmployeeTraining 八层示例)',
|
|
104
|
+
templateBase: 'docs/reference-code',
|
|
105
|
+
targetBase: 'docs/reference-code',
|
|
106
|
+
files: [
|
|
107
|
+
'controller/EmployeeTrainingController.java',
|
|
108
|
+
'service/IEmployeeTrainingService.java',
|
|
109
|
+
'service/impl/EmployeeTrainingServiceImpl.java',
|
|
110
|
+
'mapper/EmployeeTrainingMapper.java',
|
|
111
|
+
'resources/mapper/EmployeeTrainingMapper.xml',
|
|
112
|
+
'domain/EmployeeTraining.java',
|
|
113
|
+
'domain/bo/EmployeeTrainingBo.java',
|
|
114
|
+
'domain/vo/EmployeeTrainingVo.java',
|
|
115
|
+
],
|
|
116
|
+
checklistItems: [
|
|
117
|
+
{
|
|
118
|
+
file: 'docs/reference-code/',
|
|
119
|
+
field: '整体',
|
|
120
|
+
action: '替换为你自己项目中的 CRUD 示例代码,保持八层结构',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
export function getModuleById(id) {
|
|
127
|
+
return modules.find(m => m.id === id);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getTemplatePath(mod, file) {
|
|
131
|
+
return path.join(TEMPLATES_DIR, mod.templateBase, file);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getTargetPath(mod, file, cwd = process.cwd()) {
|
|
135
|
+
return path.join(cwd, mod.targetBase, file);
|
|
136
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { modules } from '../modules.js';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Print post-install checklist for the given installed module IDs.
|
|
6
|
+
*/
|
|
7
|
+
export function printChecklist(installedIds) {
|
|
8
|
+
const items = [];
|
|
9
|
+
|
|
10
|
+
for (const id of installedIds) {
|
|
11
|
+
const mod = modules.find(m => m.id === id);
|
|
12
|
+
if (mod && mod.checklistItems.length > 0) {
|
|
13
|
+
items.push(...mod.checklistItems);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (items.length === 0) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
logger.header('安装后须知');
|
|
22
|
+
logger.plain('以下文件需要手动编辑后才能使用:\n');
|
|
23
|
+
|
|
24
|
+
// Group by file
|
|
25
|
+
const grouped = new Map();
|
|
26
|
+
for (const item of items) {
|
|
27
|
+
if (!grouped.has(item.file)) {
|
|
28
|
+
grouped.set(item.file, []);
|
|
29
|
+
}
|
|
30
|
+
grouped.get(item.file).push(item);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const [file, fileItems] of grouped) {
|
|
34
|
+
logger.plain(`[ ] ${file}`);
|
|
35
|
+
for (const item of fileItems) {
|
|
36
|
+
logger.plain(` ${item.field} → ${item.action}`);
|
|
37
|
+
}
|
|
38
|
+
console.log();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// General suggestions
|
|
42
|
+
logger.plain('─'.repeat(60));
|
|
43
|
+
logger.plain('其他建议:');
|
|
44
|
+
logger.plain('[ ] 创建 .qoder/repowiki/ 项目知识库');
|
|
45
|
+
logger.plain('[ ] 安装 OpenSpec CLI:npm install -g @openspec/cli');
|
|
46
|
+
logger.plain('[ ] 安装 CodeGraph: npm install -g codegraph');
|
|
47
|
+
logger.plain('[ ] 执行 openspec init 初始化工作流');
|
|
48
|
+
console.log();
|
|
49
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getTemplatePath, getTargetPath } from '../modules.js';
|
|
4
|
+
import { logger } from './logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if a file exists.
|
|
8
|
+
*/
|
|
9
|
+
async function fileExists(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
await fs.access(filePath);
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Copy all files for a given module from templates/ to the target project.
|
|
20
|
+
* @param {Object} mod - Module definition from modules.js
|
|
21
|
+
* @param {Object} options - { force: boolean, cwd: string }
|
|
22
|
+
* @returns {Promise<{ created: string[], skipped: string[], overwritten: string[], errors: {file: string, error: string}[] }>}
|
|
23
|
+
*/
|
|
24
|
+
export async function copyModule(mod, options = {}) {
|
|
25
|
+
const { force = false, cwd = process.cwd() } = options;
|
|
26
|
+
const results = { created: [], skipped: [], overwritten: [], errors: [] };
|
|
27
|
+
|
|
28
|
+
for (const file of mod.files) {
|
|
29
|
+
const srcPath = getTemplatePath(mod, file);
|
|
30
|
+
const destPath = getTargetPath(mod, file, cwd);
|
|
31
|
+
const relDest = path.join(mod.targetBase, file);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const exists = await fileExists(destPath);
|
|
35
|
+
|
|
36
|
+
if (exists && !force) {
|
|
37
|
+
results.skipped.push(relDest);
|
|
38
|
+
logger.skip(`${relDest} (已存在)`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Create target directory
|
|
43
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
44
|
+
|
|
45
|
+
// Copy file
|
|
46
|
+
await fs.copyFile(srcPath, destPath);
|
|
47
|
+
|
|
48
|
+
if (exists && force) {
|
|
49
|
+
results.overwritten.push(relDest);
|
|
50
|
+
logger.overwrite(relDest);
|
|
51
|
+
} else {
|
|
52
|
+
results.created.push(relDest);
|
|
53
|
+
logger.success(relDest);
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
results.errors.push({ file: relDest, error: err.message });
|
|
57
|
+
logger.error(`${relDest}: ${err.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create extra directories if defined (e.g., openspec/changes, openspec/specs)
|
|
62
|
+
if (mod.extraDirs) {
|
|
63
|
+
for (const dir of mod.extraDirs) {
|
|
64
|
+
const dirPath = path.join(cwd, dir);
|
|
65
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
66
|
+
const gitkeepPath = path.join(dirPath, '.gitkeep');
|
|
67
|
+
if (!(await fileExists(gitkeepPath))) {
|
|
68
|
+
await fs.writeFile(gitkeepPath, '');
|
|
69
|
+
}
|
|
70
|
+
logger.info(`已创建目录:${dir}/`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Print a summary of copy results.
|
|
79
|
+
*/
|
|
80
|
+
export function printSummary(results) {
|
|
81
|
+
const total = results.created.length + results.skipped.length
|
|
82
|
+
+ results.overwritten.length + results.errors.length;
|
|
83
|
+
|
|
84
|
+
logger.header('汇总');
|
|
85
|
+
if (results.created.length) {
|
|
86
|
+
logger.success(`已创建 ${results.created.length} 个文件`);
|
|
87
|
+
}
|
|
88
|
+
if (results.overwritten.length) {
|
|
89
|
+
logger.info(`已覆盖 ${results.overwritten.length} 个文件`);
|
|
90
|
+
}
|
|
91
|
+
if (results.skipped.length) {
|
|
92
|
+
logger.skip(`已跳过 ${results.skipped.length} 个文件(已存在,使用 --force 可覆盖)`);
|
|
93
|
+
}
|
|
94
|
+
if (results.errors.length) {
|
|
95
|
+
logger.error(`失败 ${results.errors.length} 个文件`);
|
|
96
|
+
}
|
|
97
|
+
if (total === 0) {
|
|
98
|
+
logger.info('未处理任何文件。');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const c = {
|
|
2
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
3
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
4
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
5
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
6
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
7
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const logger = {
|
|
11
|
+
success(msg) { console.log(` ${c.green('✓')} ${msg}`); },
|
|
12
|
+
skip(msg) { console.log(` ${c.yellow('⊘')} ${c.dim(msg)}`); },
|
|
13
|
+
error(msg) { console.error(` ${c.red('✗')} ${msg}`); },
|
|
14
|
+
info(msg) { console.log(` ${c.cyan('ℹ')} ${msg}`); },
|
|
15
|
+
header(msg) { console.log(`\n${c.bold(msg)}\n`); },
|
|
16
|
+
overwrite(m) { console.log(` ${c.yellow('↻')} ${m} ${c.dim('(已覆盖)')}`); },
|
|
17
|
+
plain(msg) { console.log(` ${msg}`); },
|
|
18
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
package com.reference.controller;
|
|
2
|
+
|
|
3
|
+
import cn.dev33.satoken.annotation.SaCheckPermission;
|
|
4
|
+
import com.reference.domain.bo.EmployeeTrainingBo;
|
|
5
|
+
import com.reference.domain.vo.EmployeeTrainingVo;
|
|
6
|
+
import com.reference.service.IEmployeeTrainingService;
|
|
7
|
+
import jakarta.servlet.http.HttpServletResponse;
|
|
8
|
+
import jakarta.validation.constraints.NotEmpty;
|
|
9
|
+
import jakarta.validation.constraints.NotNull;
|
|
10
|
+
import lombok.RequiredArgsConstructor;
|
|
11
|
+
import org.dromara.common.core.domain.R;
|
|
12
|
+
import org.dromara.common.core.validate.AddGroup;
|
|
13
|
+
import org.dromara.common.core.validate.EditGroup;
|
|
14
|
+
import org.dromara.common.excel.utils.ExcelUtil;
|
|
15
|
+
import org.dromara.common.idempotent.annotation.RepeatSubmit;
|
|
16
|
+
import org.dromara.common.log.annotation.Log;
|
|
17
|
+
import org.dromara.common.log.enums.BusinessType;
|
|
18
|
+
import org.dromara.common.mybatis.core.page.PageQuery;
|
|
19
|
+
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
|
20
|
+
import org.dromara.common.web.core.BaseController;
|
|
21
|
+
import org.springframework.validation.annotation.Validated;
|
|
22
|
+
import org.springframework.web.bind.annotation.*;
|
|
23
|
+
|
|
24
|
+
import java.util.List;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 员工培训档案
|
|
28
|
+
*
|
|
29
|
+
* @author LiuBin
|
|
30
|
+
* @date 2025-12-07
|
|
31
|
+
*/
|
|
32
|
+
@Validated
|
|
33
|
+
@RequiredArgsConstructor
|
|
34
|
+
@RestController
|
|
35
|
+
@RequestMapping("/employee/training")
|
|
36
|
+
public class EmployeeTrainingController extends BaseController {
|
|
37
|
+
|
|
38
|
+
private final IEmployeeTrainingService employeeTrainingService;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 查询员工培训档案列表
|
|
42
|
+
*/
|
|
43
|
+
@SaCheckPermission("employee:training:list")
|
|
44
|
+
@GetMapping("/list")
|
|
45
|
+
public TableDataInfo<EmployeeTrainingVo> list(EmployeeTrainingBo bo, PageQuery pageQuery) {
|
|
46
|
+
return employeeTrainingService.queryPageList(bo, pageQuery);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 导出员工培训档案列表
|
|
51
|
+
*/
|
|
52
|
+
@SaCheckPermission("employee:training:export")
|
|
53
|
+
@Log(title = "员工培训档案", businessType = BusinessType.EXPORT)
|
|
54
|
+
@PostMapping("/export")
|
|
55
|
+
public void export(EmployeeTrainingBo bo, HttpServletResponse response) {
|
|
56
|
+
List<EmployeeTrainingVo> list = employeeTrainingService.queryList(bo);
|
|
57
|
+
ExcelUtil.exportExcel(list, "员工培训档案", EmployeeTrainingVo.class, response);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 获取员工培训档案详细信息
|
|
62
|
+
*
|
|
63
|
+
* @param id 主键
|
|
64
|
+
*/
|
|
65
|
+
@SaCheckPermission("employee:training:query")
|
|
66
|
+
@GetMapping("/{id}")
|
|
67
|
+
public R<EmployeeTrainingVo> getInfo(@NotNull(message = "主键不能为空")
|
|
68
|
+
@PathVariable Long id) {
|
|
69
|
+
return R.ok(employeeTrainingService.queryById(id));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 新增员工培训档案
|
|
74
|
+
*/
|
|
75
|
+
@SaCheckPermission("employee:training:add")
|
|
76
|
+
@Log(title = "员工培训档案", businessType = BusinessType.INSERT)
|
|
77
|
+
@RepeatSubmit()
|
|
78
|
+
@PostMapping()
|
|
79
|
+
public R<Void> add(@Validated(AddGroup.class) @RequestBody EmployeeTrainingBo bo) {
|
|
80
|
+
return toAjax(employeeTrainingService.insertByBo(bo));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 修改员工培训档案
|
|
85
|
+
*/
|
|
86
|
+
@SaCheckPermission("employee:training:edit")
|
|
87
|
+
@Log(title = "员工培训档案", businessType = BusinessType.UPDATE)
|
|
88
|
+
@RepeatSubmit()
|
|
89
|
+
@PutMapping()
|
|
90
|
+
public R<Void> edit(@Validated(EditGroup.class) @RequestBody EmployeeTrainingBo bo) {
|
|
91
|
+
return toAjax(employeeTrainingService.updateByBo(bo));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 删除员工培训档案
|
|
96
|
+
*
|
|
97
|
+
* @param ids 主键串
|
|
98
|
+
*/
|
|
99
|
+
@SaCheckPermission("employee:training:remove")
|
|
100
|
+
@Log(title = "员工培训档案", businessType = BusinessType.DELETE)
|
|
101
|
+
@DeleteMapping("/{ids}")
|
|
102
|
+
public R<Void> remove(@NotEmpty(message = "主键不能为空")
|
|
103
|
+
@PathVariable Long[] ids) {
|
|
104
|
+
return toAjax(employeeTrainingService.deleteWithValidByIds(List.of(ids), true));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
}
|