create-koa-boot 1.0.2 → 1.0.4

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 (4) hide show
  1. package/README.md +110 -0
  2. package/index.js +103 -36
  3. package/interactive.js +112 -0
  4. package/package.json +2 -1
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # create-koa-boot
2
+
3
+ 基于 Koa 3 + Prisma + tsoa 的 **AI 驱动全栈框架**脚手架 CLI。
4
+
5
+ 一键生成全栈工程,内置 **AI 辅助命令**(`ai:sync` / `ai:model` / `ai:api` / `ai:vue-page`):AI 通过扫描项目生成 `.ai/` 元数据理解代码库,再一键生成后端模型、CRUD 接口与前端页面,形成"AI 读懂项目 → 按规范生成 → 人工审核微调"的落地工作流。
6
+
7
+ ## 快速开始
8
+
9
+ ### 交互式(推荐,create-vue 风格)
10
+
11
+ ```bash
12
+ npx create-koa-boot
13
+ ```
14
+
15
+ 按提示依次选择即可:
16
+
17
+ ```bash
18
+ ? Project name (my-app): my-app
19
+ ? Include Vue 3 + Element Plus frontend? (Y/n): Yes
20
+ ? Include uniapp mobile app? (y/N): No
21
+ ? Auto install dependencies? (y/N): No
22
+ ```
23
+
24
+ - 后端(Koa 3)**始终生成**,前端 / uniapp 按需勾选;
25
+ - 选 `Auto install dependencies` 会自动安装依赖(自动检测 pnpm / npm)。
26
+
27
+ ### 非交互(脚本 / CI)
28
+
29
+ ```bash
30
+ npx create-koa-boot my-app # 纯后端骨架
31
+ npx create-koa-boot my-app --example # 三端完整示例(backend + frontend + uniapp)
32
+ ```
33
+
34
+ 生成后:
35
+
36
+ ```bash
37
+ cd my-app
38
+ npm install
39
+ # 编辑 .env 配置 DATABASE_URL(MySQL),然后:
40
+ npx prisma db push # 建表(首次启动自动创建 admin/admin123)
41
+ npm run dev # http://localhost:3003
42
+ ```
43
+
44
+ 生成项目的端点:
45
+
46
+ - 系统文档:`http://localhost:3003/api-docs/system`
47
+ - 业务文档:`http://localhost:3003/api-docs/business`
48
+ - 登录:`POST /api/system/user/login`(`admin` / `admin123`)
49
+
50
+ ## AI 辅助命令
51
+
52
+ 在**已有项目根目录**执行(`npx create-koa-boot ai:*`):
53
+
54
+ | 命令 | 作用 |
55
+ | --- | --- |
56
+ | `ai:sync` | 扫描项目生成 `.ai/` 元数据(`entities.json` 模型清单 / `api-list.json` 接口清单 / `permissions.json` 权限清单),供 AI 理解代码库 |
57
+ | `ai:model --name <m> --model <M> [--fields "f:t,f2:t?"]` | 生成业务模型 + DTO(Create 必填中文校验 / Update 全可选,`?` 后缀生成可选字段) |
58
+ | `ai:api --name <m> --model <M>` | 生成 tsoa controller + service(CRUD + 搜索 + 分页六件套,遵循框架规范) |
59
+ | `ai:vue-page --name <m> --model <M>` | 生成前端 API 封装 + CRUD 页面(Element Plus,定位到 `frontend/src/`) |
60
+
61
+ 示例:
62
+
63
+ ```bash
64
+ create-koa-boot ai:sync
65
+ create-koa-boot ai:model --name product --model Product --fields "name:string,price:int,stock:int?"
66
+ create-koa-boot ai:api --name product --model Product
67
+ create-koa-boot ai:vue-page --name product --model Product
68
+ ```
69
+
70
+ **推荐工作流**:
71
+
72
+ 1. 先 `ai:sync` 刷新 `.ai/` 元数据;
73
+ 2. AI 依据 `.ai/entities.json` 理解模型,用 `ai:model` / `ai:api` / `ai:vue-page` 生成代码;
74
+ 3. 复杂业务手写时,严格参照 `src/business/demo/` 蓝本(模型校验、分页、响应包装、异常处理)。
75
+
76
+ 生成项目自带 `AGENTS.md`(AI 开发规则),约束 AI 的目录约定、分层规范、权限模型与代码风格;修改 Prisma schema 后运行 `pnpm gen:types` → `pnpm db:push`,新增 controller 后运行 `pnpm doc` 重新生成路由。
77
+
78
+ ## 生成的项目结构
79
+
80
+ ```
81
+ my-app/
82
+ ├── backend/ # Koa 3 + tsoa 后端(--example 时)
83
+ │ ├── prisma/schema.prisma # 合并后的 Schema(系统 15 表 + 业务表)
84
+ │ └── src/
85
+ │ ├── business/ # 业务模块(demo 为蓝本)
86
+ │ ├── init/ # 初始化种子(菜单/权限)
87
+ │ └── build/ # 生成的 swagger.json / routes
88
+ ├── frontend/ # Vue 3 + Element Plus + Vite 前端(--example 时)
89
+ ├── uniapp/ # 移动端(--example 时)
90
+ └── .ai/ # ai:sync 生成的元数据(entities/api-list/permissions)
91
+ ```
92
+
93
+ ## 技术栈
94
+
95
+ - **运行时**:Koa 3 + @koa/router + tsoa 6.6(OpenAPI 代码生成)
96
+ - **ORM**:Prisma 5.19(MySQL)
97
+ - **认证**:JWT + RBAC 角色权限(DB 表 `sys_permissions` 路径匹配)
98
+ - **缓存/队列**:Redis(可降级)
99
+ - **前端**:Vue 3 + Element Plus + Vite + Pinia + axios
100
+ - **语言**:TypeScript 5.9
101
+
102
+ ## 环境要求
103
+
104
+ - Node.js >= 18.19
105
+ - MySQL 8.x(运行时数据)
106
+ - Redis(可选,缺失时自动降级)
107
+
108
+ ## 相关
109
+
110
+ - 运行时核心:`koa-boot-runtime`
package/index.js CHANGED
@@ -10,16 +10,19 @@
10
10
  * 3. 生成 package.json / tsconfig / tsoa.json / .env.example 等
11
11
  *
12
12
  * 用法:
13
- * npx create-koa-boot <project-name> # 纯后端骨架
14
- * npx create-koa-boot <project-name> --example # 三端完整示例(backend + frontend + uniapp)
13
+ * create-koa-boot # 交互式(create-vue 风格提问)
14
+ * create-koa-boot <project-name> # 纯后端骨架
15
+ * create-koa-boot <project-name> --example # 三端完整示例(backend + frontend + uniapp)
15
16
  */
16
17
  const fs = require('fs');
17
18
  const path = require('path');
19
+ const { spawnSync } = require('child_process');
20
+ const { runInteractive, c } = require('./interactive');
18
21
 
19
22
  const TEMPLATE_DIR = path.join(__dirname, 'templates');
20
23
 
21
- // 复制时排除的目录/文件(生成物由用户项目自行产生)
22
- const SKIP_BASE = new Set(['node_modules', 'dist', 'build', '.git']);
24
+ // 复制时排除的目录/文件(生成物由用户项目自行产生;example 仅 --example/交互选择时单独复制)
25
+ const SKIP_BASE = new Set(['node_modules', 'dist', 'build', '.git', 'example']);
23
26
  // 三端示例额外排除:本机 workaround 产物、构建产物、敏感配置
24
27
  const SKIP_EXAMPLE = new Set([
25
28
  'node_modules', 'dist', 'build', '.git',
@@ -111,8 +114,72 @@ function parseArgs(argv) {
111
114
  };
112
115
  }
113
116
 
114
- function main() {
115
- const { projectName, skipInstall, example } = parseArgs(process.argv.slice(2));
117
+ // 自动安装依赖:优先 pnpm,其次 npm(检测本机可用性)
118
+ function runInstall(targetDir) {
119
+ const detect = (cmd) => {
120
+ const r = spawnSync(cmd, ['--version'], { shell: true, stdio: 'ignore' });
121
+ return !r.error && r.status === 0;
122
+ };
123
+ const pm = detect('pnpm') ? 'pnpm' : 'npm';
124
+ console.log(`\n ⚡ 正在使用 ${pm} 安装依赖(可能需要几分钟)...\n`);
125
+ const r = spawnSync(pm, ['install'], { cwd: targetDir, shell: true, stdio: 'inherit' });
126
+ if (r.error || r.status !== 0) {
127
+ console.log(`\n ${c.red('✖')} 自动安装失败,请手动执行:`);
128
+ console.log(` cd ${path.basename(targetDir)}`);
129
+ console.log(` ${pm} install\n`);
130
+ } else {
131
+ console.log(`\n ${c.green('✓')} 依赖安装完成\n`);
132
+ }
133
+ return pm;
134
+ }
135
+
136
+ // 按选择的端打印后续步骤(useSubdir=true 表示三端布局,后端在 backend/ 子目录)
137
+ function printNextSteps(opts) {
138
+ const { projectName, frontend, uniapp, pm, useSubdir } = opts;
139
+ console.log('\n ✅ 项目生成完成!下一步:\n');
140
+ console.log(` cd ${projectName}`);
141
+ console.log(` ${pm} install # 或 npm install,安装全部依赖`);
142
+ if (useSubdir) {
143
+ console.log(' # 后端:');
144
+ console.log(' cd backend && cp .env.example .env # Windows: copy .env.example .env');
145
+ console.log(' 编辑 .env 配置 DATABASE_URL(MySQL)后:');
146
+ console.log(' pnpm gen:types && pnpm db:push && pnpm dev');
147
+ console.log(' API 文档: http://localhost:3003/api-docs/business\n');
148
+ } else {
149
+ console.log(' 编辑 .env 配置 DATABASE_URL(MySQL)后:');
150
+ console.log(' npx prisma db push # 建表(自动创建 admin/admin123)');
151
+ console.log(' npm run dev # 启动: http://localhost:3003');
152
+ console.log(' API 文档: http://localhost:3003/api-docs/business');
153
+ console.log(' 系统文档: http://localhost:3003/api-docs/system\n');
154
+ }
155
+ if (frontend) {
156
+ console.log(' # 前端(另开终端):');
157
+ console.log(' cd frontend && pnpm dev # http://localhost:5175\n');
158
+ }
159
+ if (uniapp) {
160
+ console.log(' # uniapp:用 HBuilderX 打开 uniapp 目录启动预览\n');
161
+ }
162
+ }
163
+
164
+ async function main() {
165
+ const argv = process.argv.slice(2);
166
+
167
+ // 无参数 → 交互式(create-vue 风格)
168
+ let opts;
169
+ if (argv.length === 0) {
170
+ const ans = await runInteractive();
171
+ opts = {
172
+ projectName: ans.projectName,
173
+ frontend: ans.frontend,
174
+ uniapp: ans.uniapp,
175
+ autoInstall: ans.autoInstall,
176
+ skipInstall: false,
177
+ };
178
+ } else {
179
+ opts = parseArgs(argv);
180
+ }
181
+
182
+ const { projectName, skipInstall, example = false, frontend = false, uniapp = false, autoInstall = false } = opts;
116
183
  const targetDir = path.resolve(process.cwd(), projectName);
117
184
 
118
185
  if (fs.existsSync(targetDir)) {
@@ -120,34 +187,30 @@ function main() {
120
187
  process.exit(1);
121
188
  }
122
189
 
123
- if (example) {
124
- console.log(`\n 正在生成 koa-boot 三端完整示例: ${projectName}\n`);
190
+ // --example 等价于「后端 + 前端 + uniapp」全选
191
+ const withExample = example || frontend || uniapp;
192
+
193
+ if (withExample) {
194
+ console.log(`\n ⚡ 正在生成 koa-boot 项目: ${projectName}\n`);
195
+
196
+ copyDir(path.join(TEMPLATE_DIR, 'example', 'backend'), path.join(targetDir, 'backend'), SKIP_EXAMPLE);
197
+ console.log(' ✓ backend 模板已复制(koa-boot-runtime ^1.0.0)');
125
198
 
126
- for (const sub of ['backend', 'frontend', 'uniapp']) {
127
- copyDir(path.join(TEMPLATE_DIR, 'example', sub), path.join(targetDir, sub), SKIP_EXAMPLE);
199
+ if (frontend || example) {
200
+ copyDir(path.join(TEMPLATE_DIR, 'example', 'frontend'), path.join(targetDir, 'frontend'), SKIP_EXAMPLE);
201
+ console.log(' ✓ frontend 模板已复制(Vue 3 + Element Plus)');
202
+ }
203
+ if (uniapp || example) {
204
+ copyDir(path.join(TEMPLATE_DIR, 'example', 'uniapp'), path.join(targetDir, 'uniapp'), SKIP_EXAMPLE);
205
+ console.log(' ✓ uniapp 模板已复制(含 uni_modules 组件库)');
128
206
  }
129
207
  if (fs.existsSync(path.join(TEMPLATE_DIR, 'example', 'AGENTS.md'))) {
130
208
  fs.copyFileSync(path.join(TEMPLATE_DIR, 'example', 'AGENTS.md'), path.join(targetDir, 'AGENTS.md'));
131
209
  console.log(' ✓ AGENTS.md(AI 开发规则)已生成');
132
210
  }
133
- console.log(' ✓ backend 模板已复制(koa-boot-runtime ^1.0.0)');
134
- console.log(' ✓ frontend 模板已复制');
135
- console.log(' ✓ uniapp 模板已复制(含 uni_modules 组件库)');
136
211
 
137
212
  patchExampleBackend(targetDir, projectName);
138
213
  console.log(' ✓ backend/package.json 已配置');
139
-
140
- console.log('\n ✅ 三端示例生成完成!下一步:\n');
141
- console.log(` cd ${projectName}`);
142
- console.log(' pnpm install # 或 npm install,安装全部依赖');
143
- console.log(' # 后端:');
144
- console.log(' cd backend && cp .env.example .env # Windows: copy .env.example .env');
145
- console.log(' 编辑 .env 配置 DATABASE_URL(MySQL)后:');
146
- console.log(' pnpm gen:types && pnpm db:push && pnpm dev');
147
- console.log(' API 文档: http://localhost:3003/api-docs/business\n');
148
- console.log(' # 前端(另开终端):');
149
- console.log(' cd frontend && pnpm dev\n');
150
- console.log(' # uniapp:用 HBuilderX 打开 uniapp 目录启动预览\n');
151
214
  } else {
152
215
  console.log(`\n ⚡ 正在生成 koa-boot 项目: ${projectName}\n`);
153
216
 
@@ -165,21 +228,25 @@ function main() {
165
228
  pkg.name = projectName;
166
229
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
167
230
  console.log(' ✓ package.json 已生成');
168
-
169
- // 4. 提示后续步骤
170
- console.log('\n ✅ 项目生成完成!下一步:\n');
171
- console.log(` cd ${projectName}`);
172
- console.log(' npm install # 或 pnpm install');
173
- console.log(' 编辑 .env 配置 DATABASE_URL(MySQL)后:');
174
- console.log(' npx prisma db push # 建表(自动创建 admin/admin123)');
175
- console.log(' npm run dev # 启动: http://localhost:3003');
176
- console.log(' API 文档: http://localhost:3003/api-docs/business');
177
- console.log(' 系统文档: http://localhost:3003/api-docs/system\n');
178
231
  }
179
232
 
233
+ let pm = 'pnpm';
180
234
  if (skipInstall) {
181
235
  console.log(' (已跳过自动安装,请手动 npm install)\n');
236
+ } else if (autoInstall) {
237
+ pm = runInstall(targetDir);
182
238
  }
239
+
240
+ printNextSteps({
241
+ projectName,
242
+ frontend: frontend || example,
243
+ uniapp: uniapp || example,
244
+ pm,
245
+ useSubdir: withExample,
246
+ });
183
247
  }
184
248
 
185
- main();
249
+ main().catch((err) => {
250
+ console.error(err);
251
+ process.exit(1);
252
+ });
package/interactive.js ADDED
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+ /**
3
+ * create-koa-boot 交互式提示(create-vue 风格)
4
+ *
5
+ * 零依赖实现:基于 Node 内置 readline。
6
+ * 仅在「无参数」运行时启用;带参数运行走原有非交互逻辑。
7
+ *
8
+ * 实现说明:采用「行队列」模型——所有到达的输入行先入队,
9
+ * 每个问题从队列取一行消费,避免一次性 stdin(管道/脚本)下
10
+ * readline 同步派发行而 await 恢复滞后导致丢行的问题。
11
+ */
12
+
13
+ const readline = require('readline');
14
+
15
+ // 轻量 ANSI 颜色(Windows Terminal / PowerShell 7+ 均支持)
16
+ const c = {
17
+ cyan: (s) => `\x1b[36m${s}\x1b[39m`,
18
+ green: (s) => `\x1b[32m${s}\x1b[39m`,
19
+ yellow: (s) => `\x1b[33m${s}\x1b[39m`,
20
+ red: (s) => `\x1b[31m${s}\x1b[39m`,
21
+ };
22
+
23
+ function createInterface() {
24
+ const rl = readline.createInterface({
25
+ input: process.stdin,
26
+ output: process.stdout,
27
+ });
28
+ const lines = []; // 已到达、待消费的输入行
29
+ const pending = []; // 等待输入的问题回调(参数为行内容,null 表示 EOF)
30
+ let closed = false;
31
+
32
+ rl.on('line', (l) => {
33
+ const cb = pending.shift();
34
+ if (cb) cb(l);
35
+ else lines.push(l);
36
+ });
37
+ rl.on('close', () => {
38
+ closed = true;
39
+ while (pending.length) pending.shift()(null); // EOF:未答问题取默认值
40
+ });
41
+
42
+ return { rl, lines, pending, closed: () => closed };
43
+ }
44
+
45
+ /** 取一行输入:优先消费缓冲行,否则注册等待;EOF/关闭时返回 defaultValue */
46
+ function nextLine(io, promptText, defaultValue) {
47
+ return new Promise((resolve) => {
48
+ if (io.closed()) return resolve(defaultValue);
49
+ if (io.lines.length) {
50
+ process.stdout.write(promptText);
51
+ return resolve(io.lines.shift());
52
+ }
53
+ process.stdout.write(promptText);
54
+ io.pending.push((l) => resolve(l === null ? defaultValue : l));
55
+ });
56
+ }
57
+
58
+ /** 文本输入:返回用户输入;空输入取默认值;validate 返回错误文案时重新询问 */
59
+ async function askText(io, question, defaultValue, validate) {
60
+ const suffix = defaultValue ? ` ${c.cyan(`(${defaultValue})`)}` : '';
61
+ const prompt = ` ${c.cyan('?')} ${question}${suffix}: `;
62
+ for (;;) {
63
+ const line = await nextLine(io, prompt, defaultValue);
64
+ const val = (line || '').trim() || defaultValue;
65
+ if (validate) {
66
+ const err = validate(val);
67
+ if (err) {
68
+ console.log(` ${c.red('✖')} ${err}`);
69
+ continue;
70
+ }
71
+ }
72
+ return val;
73
+ }
74
+ }
75
+
76
+ /** 确认输入:y/n/回车(默认值);返回 boolean */
77
+ async function askConfirm(io, question, defaultYes) {
78
+ const hint = defaultYes ? '(Y/n)' : '(y/N)';
79
+ const prompt = ` ${c.cyan('?')} ${question} ${c.yellow(hint)}: `;
80
+ for (;;) {
81
+ const line = await nextLine(io, prompt, defaultYes ? 'y' : 'n');
82
+ const a = (line || '').trim().toLowerCase();
83
+ if (a === 'y' || a === 'yes') return true;
84
+ if (a === 'n' || a === 'no') return false;
85
+ if (!a) return defaultYes;
86
+ // 无效输入:重新询问
87
+ }
88
+ }
89
+
90
+ /**
91
+ * 交互式提问主流程,返回:
92
+ * { projectName, frontend, uniapp, autoInstall }
93
+ */
94
+ async function runInteractive() {
95
+ const io = createInterface();
96
+ try {
97
+ const projectName = await askText(io, 'Project name', 'my-app', (v) => {
98
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9-_.]*$/.test(v)) {
99
+ return '仅允许字母、数字、- _ .,且不能以符号开头';
100
+ }
101
+ return null;
102
+ });
103
+ const frontend = await askConfirm(io, 'Include Vue 3 + Element Plus frontend?', true);
104
+ const uniapp = await askConfirm(io, 'Include uniapp mobile app?', false);
105
+ const autoInstall = await askConfirm(io, 'Auto install dependencies?', false);
106
+ return { projectName, frontend, uniapp, autoInstall };
107
+ } finally {
108
+ io.rl.close();
109
+ }
110
+ }
111
+
112
+ module.exports = { runInteractive, askText, askConfirm, c };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-koa-boot",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Scaffolding CLI: generate a koa-boot-runtime project with system routes, demo business modules and merged Prisma schema",
5
5
  "keywords": [
6
6
  "koa",
@@ -26,6 +26,7 @@
26
26
  "files": [
27
27
  "bin",
28
28
  "index.js",
29
+ "interactive.js",
29
30
  "ai.js",
30
31
  "ai",
31
32
  "templates"