@niping9542/create-nova-admin 0.0.0-stage → 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.
package/README.md CHANGED
@@ -1,3 +1,89 @@
1
- # Temporary Holding Version
1
+ # @niping9542/create-nova-admin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 创建 Vue 3 + TypeScript + Element Plus + Pinia 后台脚手架。模板来自 [npora/admin-template](https://github.com/npora/admin-template),默认 `main`,核心源码只维护一份。
4
+
5
+ ## 选择模板
6
+
7
+ ```sh
8
+ npx @niping9542/create-nova-admin@latest
9
+ ```
10
+
11
+ 按提示输入项目名并选择:
12
+
13
+ | 预设 | 内容 |
14
+ | ----------------- | ----------------------------------------------------------------------------------------- |
15
+ | `starter`(默认) | 纯净后台,保留登录、布局、用户/部门/角色/菜单管理、权限、公共组件、多语言、主题和正式 SQL |
16
+ | `worker` | 纯净后台 + 独立 Worker 后端源码与 SQLite 迁移,后端菜单只引用核心页面 |
17
+ | `demo` | 完整演示首页与示例页面、测试后端和展示站实现 |
18
+
19
+ 默认纯净版不生成 `examples/`、`workers/`、`deploy/`,也不保留相关依赖、构建插件和命令。Worker 版只额外生成后端,无需手动删除示例目录。完整演示需要网络访问测试 API,不使用本地 Mock。
20
+
21
+ 明确指定模板:
22
+
23
+ ```sh
24
+ npx @niping9542/create-nova-admin@latest my-admin --preset starter
25
+ npx @niping9542/create-nova-admin@latest my-worker --preset worker
26
+ npx @niping9542/create-nova-admin@latest my-demo --preset demo
27
+ ```
28
+
29
+ 原 `--demo` 兼容为 `--preset demo`,不能与其他预设同时指定。`--yes` 或非交互终端不询问模板,默认 `starter`。
30
+
31
+ ## 启动
32
+
33
+ 需要 Node.js 22.12+(后端检查需 22.13+),使用生成项目 `packageManager` 指定版本的 pnpm。
34
+
35
+ ```sh
36
+ cd my-admin
37
+ pnpm install
38
+ pnpm dev
39
+ ```
40
+
41
+ 纯净版与 Worker 版默认代理到 `http://localhost:8080`,先启动自己的后端,或在生成时使用 `--api-url https://api.example.com`,之后也可修改 `.env.development.local`。Worker 版先完成迁移、密钥和账号初始化,见生成的后端说明;生成器不自动创建资源、执行 SQL 或部署。
42
+
43
+ 完整演示无自定义 API 时使用生成器打印的 `pnpm dev:demo`;指定 `--api-url` 后使用 `pnpm dev`。API 前缀默认 `/api`,开发代理移除该前缀再转发。生产需要站点 API 转发,响应格式可在 `src/api/protocol/` 适配。
44
+
45
+ [完整展示网站](https://admin-demo.npora.workers.dev):账号 `admin`,密码 `123456`。此账号只用于展示站。
46
+
47
+ ## 目录与 SQL
48
+
49
+ | 目录 | 用途 |
50
+ | ---------------------- | -------------------------------------- |
51
+ | `src/` | 核心后台与权限管理 |
52
+ | `database/mysql/` | 所有预设始终保留的正式表结构与基础授权 |
53
+ | `public/`、`build/` | 静态资源与公共构建配置 |
54
+ | `scripts/`、`tests/` | 验证命令与回归测试 |
55
+ | `docs/` | 与所选预设匹配的使用和接口说明 |
56
+ | `workers/api/` | Worker 与 demo 预设的独立后端 |
57
+ | `examples/`、`deploy/` | 仅完整演示预设包含 |
58
+
59
+ 在空 MySQL 数据库依次导入 `database/mysql/01-schema.sql`、`02-seed.sql`。正式 SQL 不包含测试账号,管理员及密码哈希由自己的后端初始化,与 Worker SQLite 迁移分开使用。
60
+
61
+ ## 选项
62
+
63
+ | 选项 | 用途 |
64
+ | ---------------------------- | ----------------------------------------------- |
65
+ | `[project-name]` | 小写单级项目名,不接受路径 |
66
+ | `-y, --yes` | 无交互,缺省名称 `nova-admin-app`、缺省纯净版 |
67
+ | `--preset <preset>` | `starter`、`worker`、`demo` |
68
+ | `--demo` | `--preset demo` 的快捷方式 |
69
+ | `--api-url <url>` | HTTP/HTTPS 后端,不包含账号密码、查询参数或片段 |
70
+ | `--ref <ref>` | 模板分支/标签,须支持当前预设目录结构 |
71
+ | `--template-dir <directory>` | 使用本地模板,不下载仓库 |
72
+ | `--version`、`--help` | 版本和帮助 |
73
+
74
+ 目标已存在则失败,不覆盖已有内容。模板先在临时目录准备、校验再生成;失败清理本次创建的目录。本机配置、密钥、依赖、符号链接、构建缓存和 Git 历史不会复制。生成器不自动安装项目依赖或启动服务。
75
+
76
+ ## 维护
77
+
78
+ ```sh
79
+ npm ci
80
+ npm run check
81
+ npm pack --dry-run
82
+ node bin/index.js local-admin --yes --preset starter --template-dir ../admin-template
83
+ ```
84
+
85
+ npm 包只包含生成器和说明;模板创建时获取。根模板的 `scaffold/` 保存少量差异文件,不进入生成结果。依赖变更后由模板维护者执行 `pnpm run update:presets`,防止纯净锁文件过期。
86
+
87
+ 模板仓库与 CLI 需配套提交和发布;本地修改不会更新已经发布的 npm 包。新预设选项需要在生成器新版发布后才可通过 npm 使用。
88
+
89
+ 许可证声明:MIT。
package/bin/index.js ADDED
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { createInterface } from 'node:readline/promises';
5
+ import { readFile } from 'node:fs/promises';
6
+ import ora from 'ora';
7
+ import chalk from 'chalk';
8
+ import { createProject } from '../src/create-project.js';
9
+ import { PRESETS, resolvePreset } from '../src/presets.js';
10
+
11
+ const pkg = JSON.parse(
12
+ await readFile(new URL('../package.json', import.meta.url), 'utf8')
13
+ );
14
+ const program = new Command();
15
+
16
+ program
17
+ .name('create-nova-admin')
18
+ .version(pkg.version)
19
+ .description('创建 Vue 3 + TypeScript + Element Plus + Pinia 后台项目')
20
+ .argument('[project-name]', '项目名称,例如 my-admin')
21
+ .option('-y, --yes', '无交互创建;未指定名称时使用 nova-admin-app')
22
+ .option('--preset <preset>', '模板:starter(默认)、worker、demo')
23
+ .option('--demo', '完整演示模板,等同 --preset demo')
24
+ .option('--api-url <url>', '配置自己的 HTTP 后端地址')
25
+ .option('--ref <ref>', '模板分支或标签', 'main')
26
+ .option('--template-dir <directory>', '从本地模板创建,供离线开发和验证')
27
+ .action(async (projectName, options) => {
28
+ let preset = options.preset;
29
+ if (!projectName && !options.yes && !process.stdin.isTTY)
30
+ throw new Error('请提供项目名称,或使用 --yes 采用默认名称。');
31
+ if (
32
+ process.stdin.isTTY &&
33
+ !options.yes &&
34
+ (!projectName || (!preset && !options.demo))
35
+ ) {
36
+ const prompt = createInterface({
37
+ input: process.stdin,
38
+ output: process.stdout
39
+ });
40
+ try {
41
+ if (!projectName)
42
+ projectName =
43
+ (await prompt.question('项目名称 (nova-admin-app): ')).trim() ||
44
+ 'nova-admin-app';
45
+ if (!preset && !options.demo) {
46
+ console.log(
47
+ '选择模板:\n 1. 纯净后台(默认)\n 2. 后台 + Worker 后端\n 3. 完整演示'
48
+ );
49
+ while (!preset) {
50
+ const answer =
51
+ (await prompt.question('请选择 (1): ')).trim() || '1';
52
+ preset = {
53
+ 1: 'starter',
54
+ 2: 'worker',
55
+ 3: 'demo',
56
+ starter: 'starter',
57
+ worker: 'worker',
58
+ demo: 'demo'
59
+ }[answer];
60
+ if (!preset) console.log('请输入 1、2、3 或模板名称。');
61
+ }
62
+ }
63
+ } finally {
64
+ prompt.close();
65
+ }
66
+ }
67
+ projectName ||= 'nova-admin-app';
68
+ preset = resolvePreset(preset, options.demo);
69
+ const spinner = ora(
70
+ options.templateDir ? '正在准备本地模板…' : '正在获取 Npora Admin 模板…'
71
+ ).start();
72
+ try {
73
+ const project = await createProject({
74
+ projectName,
75
+ ...options,
76
+ preset,
77
+ apiURL: options.apiUrl
78
+ });
79
+ spinner.succeed('项目已创建');
80
+ console.log(
81
+ chalk.green(
82
+ `\n${project.projectName} 初始化完成(${project.presetLabel})。`
83
+ )
84
+ );
85
+ console.log(
86
+ `\n cd ${project.projectName}\n pnpm install\n ${project.startCommand}\n`
87
+ );
88
+ console.log(`包管理器版本:${project.packageManager}`);
89
+ if (project.demo)
90
+ console.log('展示需要网络。演示账号:admin,密码:123456。');
91
+ else
92
+ console.log(
93
+ '请先启动自己的后端,或修改 .env.development.local 中的 VITE_API_URL。'
94
+ );
95
+ console.log('正式权限 SQL:database/mysql/01-schema.sql → 02-seed.sql。');
96
+ if (project.preset === 'worker')
97
+ console.log(
98
+ '后端源码:workers/api/;先迁移数据库并初始化账号,再配置自己的 API 地址。'
99
+ );
100
+ console.log(
101
+ `已生成${PRESETS[project.preset]}所需文件,无需手动删除演示目录。`
102
+ );
103
+ } catch (error) {
104
+ spinner.fail('项目创建失败');
105
+ throw error;
106
+ }
107
+ });
108
+
109
+ try {
110
+ await program.parseAsync(process.argv);
111
+ } catch (error) {
112
+ console.error(
113
+ chalk.red(error.message || '创建失败,请检查模板和网络后重试。')
114
+ );
115
+ process.exitCode = 1;
116
+ }
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@niping9542/create-nova-admin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.4",
4
+ "description": "Npora Admin 后台脚手架生成器:核心模式、独立展示与正式权限 SQL",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-nova-admin": "bin/index.js"
8
+ },
9
+ "keywords": [
10
+ "vue",
11
+ "admin",
12
+ "cli",
13
+ "scaffolding",
14
+ "nova"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "author": "niping9542",
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "chalk": "^5.3.0",
23
+ "commander": "^12.1.0",
24
+ "giget": "^3.3.0",
25
+ "ora": "^8.0.1"
26
+ },
27
+ "engines": {
28
+ "node": ">=22.12.0"
29
+ },
30
+ "scripts": {
31
+ "test": "node --test tests/cli.test.mjs tests/create-project.test.mjs",
32
+ "check": "node --check bin/index.js && node --check src/create-project.js && npm test"
33
+ },
34
+ "files": [
35
+ "bin/",
36
+ "src/",
37
+ "README.md"
38
+ ]
39
+ }
@@ -0,0 +1,228 @@
1
+ import {
2
+ cp,
3
+ lstat,
4
+ mkdir,
5
+ mkdtemp,
6
+ readFile,
7
+ rm,
8
+ writeFile
9
+ } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { tmpdir } from 'node:os';
12
+ import { downloadTemplate } from 'giget';
13
+ import { composePreset, PRESETS, resolvePreset } from './presets.js';
14
+
15
+ export const TEMPLATE_REPOSITORY = 'github:npora/admin-template';
16
+
17
+ export function validateProjectName(name) {
18
+ if (
19
+ typeof name !== 'string' ||
20
+ name.length > 214 ||
21
+ !/^[a-z0-9][a-z0-9._-]*$/.test(name) ||
22
+ ['node_modules', 'favicon.ico'].includes(name) ||
23
+ /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(name)
24
+ ) {
25
+ throw new Error(
26
+ '项目名请使用小写字母、数字、连字符、下划线或点,不能是路径或保留名称。'
27
+ );
28
+ }
29
+ return name;
30
+ }
31
+
32
+ export function normalizeApiURL(value) {
33
+ let url;
34
+ try {
35
+ url = new URL(value);
36
+ } catch {
37
+ throw new Error('后端地址必须是完整的 http:// 或 https:// URL。');
38
+ }
39
+ if (
40
+ !['http:', 'https:'].includes(url.protocol) ||
41
+ url.username ||
42
+ url.password ||
43
+ url.search ||
44
+ url.hash
45
+ ) {
46
+ throw new Error(
47
+ '后端地址只支持 HTTP/HTTPS,不能包含账号、密码、查询参数或片段。'
48
+ );
49
+ }
50
+ return url.toString().replace(/\/$/, '');
51
+ }
52
+
53
+ const excluded = new Set([
54
+ '.git',
55
+ '.agents',
56
+ '.codex',
57
+ '.aws',
58
+ '.idea',
59
+ 'node_modules',
60
+ 'dist',
61
+ 'dist-ssr',
62
+ '.cloudflare',
63
+ '.wrangler',
64
+ 'coverage',
65
+ '.pnpm-store',
66
+ '.DS_Store',
67
+ 'report.html'
68
+ ]);
69
+
70
+ async function copyTemplate(source, destination) {
71
+ await cp(source, destination, {
72
+ recursive: true,
73
+ force: false,
74
+ errorOnExist: true,
75
+ filter: async (entry) => {
76
+ const name = path.basename(entry);
77
+ return (
78
+ !excluded.has(name) &&
79
+ !name.endsWith('.local') &&
80
+ !name.endsWith('.log') &&
81
+ !(await lstat(entry)).isSymbolicLink()
82
+ );
83
+ }
84
+ });
85
+ }
86
+
87
+ async function validateTemplate(directory) {
88
+ const required = [
89
+ 'src/main.ts',
90
+ 'database/mysql/01-schema.sql',
91
+ 'database/mysql/02-seed.sql',
92
+ 'build/optional-examples.ts',
93
+ 'scripts/demo.mjs',
94
+ 'scaffold/metadata.json',
95
+ 'scaffold/demo/README.md',
96
+ 'scaffold/core/package.json',
97
+ 'scaffold/core/pnpm-lock.yaml',
98
+ 'scaffold/core/vite.config.ts',
99
+ 'scaffold/core/src/router/extensions.ts',
100
+ 'scaffold/worker/workers/api/src/config/pages.mjs',
101
+ 'scaffold/worker/workers/api/migrations/0002_seed.sql'
102
+ ];
103
+ for (const file of required) {
104
+ try {
105
+ if (!(await lstat(path.join(directory, file))).isFile())
106
+ throw new Error();
107
+ } catch {
108
+ throw new Error(
109
+ `模板缺少 ${file},请使用已完成核心/展示分离的新版本模板。`
110
+ );
111
+ }
112
+ }
113
+ const pkg = JSON.parse(
114
+ await readFile(path.join(directory, 'package.json'), 'utf8')
115
+ );
116
+ if (
117
+ !pkg.scripts?.dev ||
118
+ !pkg.scripts?.['dev:demo'] ||
119
+ !pkg.packageManager?.startsWith('pnpm@')
120
+ ) {
121
+ throw new Error('模板缺少当前 pnpm 开发或展示命令。');
122
+ }
123
+ return pkg;
124
+ }
125
+
126
+ export async function createProject({
127
+ projectName,
128
+ cwd = process.cwd(),
129
+ demo = false,
130
+ preset,
131
+ apiURL,
132
+ ref = 'main',
133
+ templateDir,
134
+ download = downloadTemplate
135
+ }) {
136
+ validateProjectName(projectName);
137
+ const selected = resolvePreset(preset, demo);
138
+ demo = selected === 'demo';
139
+ if (
140
+ !/^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/.test(ref) ||
141
+ ref.includes('..') ||
142
+ ref.includes('//') ||
143
+ ref.endsWith('/')
144
+ ) {
145
+ throw new Error('模板版本必须是有效的分支或标签名称。');
146
+ }
147
+ const backend = apiURL === undefined ? undefined : normalizeApiURL(apiURL);
148
+ const target = path.resolve(cwd, projectName);
149
+ try {
150
+ await lstat(target);
151
+ throw new Error(
152
+ `目录 ${projectName} 已存在,请换一个项目名;不会覆盖已有内容。`
153
+ );
154
+ } catch (error) {
155
+ if (error.code !== 'ENOENT') throw error;
156
+ }
157
+
158
+ const temporary = await mkdtemp(path.join(tmpdir(), 'create-nova-admin-'));
159
+ const prepared = path.join(temporary, 'prepared');
160
+ let created = false;
161
+ try {
162
+ if (templateDir) {
163
+ const source = path.resolve(cwd, templateDir);
164
+ if (!(await lstat(source)).isDirectory())
165
+ throw new Error('本地模板必须是目录。');
166
+ const relative = path.relative(source, temporary);
167
+ if (
168
+ !relative ||
169
+ (!relative.startsWith(`..${path.sep}`) &&
170
+ relative !== '..' &&
171
+ !path.isAbsolute(relative))
172
+ ) {
173
+ throw new Error('本地模板不能包含生成器的临时工作目录。');
174
+ }
175
+ await copyTemplate(source, prepared);
176
+ } else {
177
+ const downloaded = path.join(temporary, 'downloaded');
178
+ await download(`${TEMPLATE_REPOSITORY}#${ref}`, {
179
+ dir: downloaded,
180
+ force: false,
181
+ registry: false
182
+ });
183
+ await copyTemplate(downloaded, prepared);
184
+ }
185
+ const original = await validateTemplate(prepared);
186
+ const pkg = await composePreset(prepared, selected, original);
187
+ pkg.name = projectName;
188
+ pkg.private = true;
189
+ await writeFile(
190
+ path.join(prepared, 'package.json'),
191
+ `${JSON.stringify(pkg, null, 2)}\n`
192
+ );
193
+ const config = [
194
+ '# 此文件只用于本机开发,不提交版本控制。',
195
+ ...(demo ? ['VITE_DEMO_ENABLED=true'] : []),
196
+ ...(!demo || backend
197
+ ? [`VITE_API_URL=${backend || 'http://localhost:8080'}`]
198
+ : []),
199
+ ''
200
+ ].join('\n');
201
+ await writeFile(path.join(prepared, '.env.development.local'), config, {
202
+ mode: 0o600
203
+ });
204
+
205
+ // 独占创建目标:下载期间出现同名目录时也不覆盖。
206
+ await mkdir(target);
207
+ created = true;
208
+ await copyTemplate(prepared, target);
209
+ // 本机环境只由生成器写入,不从模板源继承。
210
+ await writeFile(path.join(target, '.env.development.local'), config, {
211
+ mode: 0o600
212
+ });
213
+ return {
214
+ directory: target,
215
+ projectName,
216
+ demo,
217
+ preset: selected,
218
+ presetLabel: PRESETS[selected],
219
+ startCommand: demo && !backend ? 'pnpm dev:demo' : 'pnpm dev',
220
+ packageManager: pkg.packageManager
221
+ };
222
+ } catch (error) {
223
+ if (created) await rm(target, { recursive: true, force: true });
224
+ throw error;
225
+ } finally {
226
+ await rm(temporary, { recursive: true, force: true });
227
+ }
228
+ }
package/src/presets.js ADDED
@@ -0,0 +1,133 @@
1
+ import { cp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+
5
+ export const PRESETS = {
6
+ starter: '纯净后台',
7
+ worker: '后台 + Worker 后端',
8
+ demo: '完整演示'
9
+ };
10
+
11
+ export function resolvePreset(preset, demo = false) {
12
+ const selected = preset ?? (demo ? 'demo' : 'starter');
13
+ if (!Object.hasOwn(PRESETS, selected))
14
+ throw new Error('模板仅支持 starter、worker、demo。');
15
+ if (demo && selected !== 'demo')
16
+ throw new Error('--demo 不能与其他 --preset 同时使用。');
17
+ return selected;
18
+ }
19
+
20
+ const removeCore = [
21
+ 'examples',
22
+ 'deploy',
23
+ 'cloudflare.config.ts',
24
+ 'tsconfig.demo.json',
25
+ 'build/optional-examples.ts',
26
+ 'src/types/examples.d.ts',
27
+ 'scripts/demo.mjs',
28
+ 'scripts/check-backend.mjs',
29
+ 'scripts/check-standalone.mjs'
30
+ ];
31
+
32
+ // Compose data files only; never execute scripts from a downloaded template.
33
+ export async function composePreset(directory, preset, original) {
34
+ const overlays = path.join(directory, 'scaffold');
35
+ if (preset !== 'demo') {
36
+ const metadata = JSON.parse(
37
+ await readFile(path.join(overlays, 'metadata.json'), 'utf8')
38
+ );
39
+ const sourceLockHash = createHash('sha256')
40
+ .update(await readFile(path.join(directory, 'pnpm-lock.yaml')))
41
+ .digest('hex');
42
+ if (metadata.version !== 1 || metadata.sourceLockHash !== sourceLockHash)
43
+ throw new Error(
44
+ '模板的纯净版锁文件快照已过期,请维护者运行 pnpm run update:presets。'
45
+ );
46
+ const core = JSON.parse(
47
+ await readFile(path.join(overlays, 'core/package.json'), 'utf8')
48
+ );
49
+ const expected = { ...original.devDependencies };
50
+ delete expected['@cloudflare/vite-plugin'];
51
+ delete expected.cf;
52
+ const equal = (a, b) =>
53
+ JSON.stringify(Object.entries(a ?? {}).sort()) ===
54
+ JSON.stringify(Object.entries(b ?? {}).sort());
55
+ if (
56
+ !equal(core.dependencies, original.dependencies) ||
57
+ !equal(core.devDependencies, expected) ||
58
+ core.packageManager !== original.packageManager ||
59
+ Object.entries(core.scripts).some(
60
+ ([key, value]) => value !== original.scripts[key]
61
+ )
62
+ )
63
+ throw new Error(
64
+ '模板的纯净版依赖快照已过期,请维护者运行 pnpm run update:presets。'
65
+ );
66
+ for (const entry of removeCore)
67
+ await rm(path.join(directory, entry), { recursive: true, force: true });
68
+ if (preset === 'starter')
69
+ await rm(path.join(directory, 'workers'), {
70
+ recursive: true,
71
+ force: true
72
+ });
73
+ // Keep common contracts, replace repository-specific usage documentation.
74
+ for (const entry of await readdir(path.join(directory, 'docs'))) {
75
+ if (
76
+ !['api.md', 'rbac.md', 'i18n.md', 'deployment.md', 'database'].includes(
77
+ entry
78
+ )
79
+ )
80
+ await rm(path.join(directory, 'docs', entry), {
81
+ recursive: true,
82
+ force: true
83
+ });
84
+ }
85
+ await cp(path.join(overlays, 'core'), directory, {
86
+ recursive: true,
87
+ force: true
88
+ });
89
+ if (preset === 'worker') {
90
+ await cp(path.join(overlays, 'worker'), directory, {
91
+ recursive: true,
92
+ force: true
93
+ });
94
+ await rm(path.join(directory, 'workers/api/integration'), {
95
+ recursive: true,
96
+ force: true
97
+ });
98
+ core.scripts['check:backend'] =
99
+ 'pnpm --dir workers/api run build && pnpm --dir workers/api test';
100
+ await writeFile(
101
+ path.join(directory, 'package.json'),
102
+ JSON.stringify(core, null, 2) + '\n'
103
+ );
104
+ await writeFile(
105
+ path.join(directory, 'README.md'),
106
+ '\n## 已选择 Worker 后端\n\n项目包含 `workers/api/`,使用前先完成数据库迁移和账号初始化,再配置自己的 API 地址。`pnpm run check:backend` 检查后端。见 [后端说明](workers/api/README.md)。\n',
107
+ { flag: 'a' }
108
+ );
109
+ }
110
+ }
111
+ if (preset === 'demo')
112
+ await cp(path.join(overlays, 'demo'), directory, {
113
+ recursive: true,
114
+ force: true
115
+ });
116
+ for (const entry of [
117
+ 'scripts/update-presets.mjs',
118
+ 'scripts/check-presets.mjs'
119
+ ]) {
120
+ await rm(path.join(directory, entry), { force: true });
121
+ }
122
+ await rm(overlays, { recursive: true, force: true });
123
+ const pkg = JSON.parse(
124
+ await readFile(path.join(directory, 'package.json'), 'utf8')
125
+ );
126
+ delete pkg.scripts['update:presets'];
127
+ delete pkg.scripts['check:presets'];
128
+ await writeFile(
129
+ path.join(directory, 'package.json'),
130
+ JSON.stringify(pkg, null, 2) + '\n'
131
+ );
132
+ return pkg;
133
+ }