create-koishi-ce 1.0.1 → 1.2.0

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/lib/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { a as start } from "./src-B_s70NbL.mjs";
2
+ import { a as start } from "./src-BwIHbUAf.mjs";
3
3
  //#region src/bin.ts
4
4
  /**
5
5
  * create-koishi-ce 的 CLI 可执行入口:shebang 由 rolldown 原样保留到
package/lib/index.d.ts CHANGED
@@ -8,7 +8,8 @@ interface Manifest {
8
8
  private?: boolean;
9
9
  version?: string;
10
10
  workspaces?: unknown;
11
- devDependencies?: unknown;
11
+ dependencies?: Record<string, string>;
12
+ devDependencies?: Record<string, string>;
12
13
  [key: string]: unknown;
13
14
  }
14
15
  /**
package/lib/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as start, i as renderManifest, n as getLocalRegistry, r as readNpmrcRegistry, t as detectAgent } from "./src-B_s70NbL.mjs";
1
+ import { a as start, i as renderManifest, n as getLocalRegistry, r as readNpmrcRegistry, t as detectAgent } from "./src-BwIHbUAf.mjs";
2
2
  export { detectAgent, getLocalRegistry, readNpmrcRegistry, renderManifest, start };
@@ -7,23 +7,11 @@ import kleur from "kleur";
7
7
  import prompts from "prompts";
8
8
  import { extract } from "tar";
9
9
  import parse from "yargs-parser";
10
- //#endregion
11
- //#region src/index.ts
12
- /**
13
- * create-koishi-ce 脚手架(npm 包名 create-koishi-ce,目录名为
14
- * apps/koishi-create,二者不一致是历史遗留,以目录名为准)。
15
- *
16
- * 通过 `bunx create-koishi-ce [name]`(npx 亦可)交互式创建 Koishi 机器人
17
- * 应用项目:确定项目名 → 准备目标目录 → 从 npm registry 下载模板包(默认
18
- * @koishijs/boilerplate,刻意沿用上游官方模板以保持与上游插件生态一致)
19
- * 并解包 → 改写 package.json → 按需初始化 git → 询问是否立即安装依赖并
20
- * 启动。CLI 可执行入口在 src/bin.ts(构建产物 lib/bin.mjs,bin 字段指向
21
- * 它);本文件只承载主流程与可单测的纯函数(范式对齐 @koishi-ce/scripts)。
22
- */
23
- const { version } = {
10
+ //#region package.json
11
+ var package_default = {
24
12
  name: "create-koishi-ce",
25
13
  description: "Setup a Koishi application",
26
- version: "1.0.1",
14
+ version: "1.2.0",
27
15
  type: "module",
28
16
  main: "./lib/index.mjs",
29
17
  module: "./lib/index.mjs",
@@ -72,6 +60,166 @@ const { version } = {
72
60
  "./package.json": "./package.json"
73
61
  }
74
62
  };
63
+ //#endregion
64
+ //#region src/template.ts
65
+ /** 模板静态文件(相对项目根的路径 → 文件内容),package.json 另行渲染 */
66
+ const templateFiles = {
67
+ ".env": [
68
+ "GITHUB_MIRROR = https://ghproxy.com/https://github.com",
69
+ "GITHUB_CONTENT_MIRROR = https://ghproxy.com/https://raw.githubusercontent.com",
70
+ "GRAVATAR_MIRROR = https://cravatar.cn"
71
+ ].join("\n"),
72
+ ".gitignore": ["node_modules/", "data/"].join("\n"),
73
+ "koishi.yml": [
74
+ "plugins:",
75
+ " group:server:",
76
+ " server:",
77
+ " port: 5140",
78
+ " maxPort: 5149",
79
+ " group:basic:",
80
+ " ~admin: {}",
81
+ " ~bind: {}",
82
+ " commands: {}",
83
+ " help: {}",
84
+ " http: {}",
85
+ " ~inspect: {}",
86
+ " locales: {}",
87
+ " proxy-agent: {}",
88
+ " group:console:",
89
+ " actions: {}",
90
+ " ~analytics: {}",
91
+ " ~auth: {}",
92
+ " config: {}",
93
+ " console:",
94
+ " open: true",
95
+ " explorer: {}",
96
+ " insight: {}",
97
+ " logger: {}",
98
+ " market:",
99
+ " search:",
100
+ " endpoint: https://registry.koishi.chat/index.json",
101
+ " notifier: {}",
102
+ " oobe: {}",
103
+ " sandbox: {}",
104
+ " status: {}",
105
+ " group:develop:",
106
+ " $if: env.NODE_ENV === 'development'",
107
+ " hmr:",
108
+ " root: ."
109
+ ].join("\n"),
110
+ "tsconfig.json": [
111
+ "{",
112
+ " \"compilerOptions\": {",
113
+ " \"target\": \"ESNext\",",
114
+ " \"module\": \"ESNext\",",
115
+ " \"moduleResolution\": \"bundler\",",
116
+ " \"lib\": [\"ESNext\"],",
117
+ " \"types\": [\"bun-types\"],",
118
+ " \"strict\": true,",
119
+ " \"skipLibCheck\": true,",
120
+ " \"noEmit\": true,",
121
+ " \"allowImportingTsExtensions\": true,",
122
+ " \"verbatimModuleSyntax\": true,",
123
+ " \"erasableSyntaxOnly\": true",
124
+ " },",
125
+ " \"include\": [\"plugins/*/src\", \"external/*/src\"]",
126
+ "}"
127
+ ].join("\n"),
128
+ "README.md": `# Koishi 机器人项目
129
+
130
+ 由 \`bun create koishi-ce\` 生成,运行于 [@koishi-ce 社区再分发版](https://github.com/Koishi-CE/koishi) 生态。
131
+
132
+ ## 环境要求
133
+
134
+ - [Bun](https://bun.sh) ≥ 1.2(运行时:koishi CLI 与插件加载链以 Bun 为准)
135
+
136
+ ## 快速开始
137
+
138
+ \`\`\`bash
139
+ bun install
140
+ bun run start # 启动(生产模式)
141
+ bun run dev # 启动(开发模式,启用 HMR 热更新)
142
+ \`\`\`
143
+
144
+ 启动后访问控制台:<http://127.0.0.1:5140>
145
+
146
+ ## 安装插件
147
+
148
+ 推荐在控制台「插件市场」页安装(已预配 registry.koishi.chat 镜像源);也可以手动 \`bun add <包名>\` 后在 \`koishi.yml\` 中启用。
149
+
150
+ 上游官方 adapter(如 adapter-discord / adapter-telegram)、数据库插件(如 database-sqlite)与社区 koishi-plugin-* 插件均可直接安装:根依赖中的四行 npm alias——\`"koishi": "npm:@koishi-ce/koishi-shim@^4.18.11"\`、\`"@koishijs/plugin-console": "npm:@koishi-ce/console-shim@^5.30.11"\`、\`"@koishijs/core": "npm:@koishi-ce/core-shim@4.18.11"\`、\`"@koishijs/loader": "npm:@koishi-ce/loader-shim@^4.6.11"\`——已把上游生态的 peer 依赖全部钉回 @koishi-ce 框架(**请勿删除或改写这四行**),不会形成第二份框架 / console / loader 副本。analytics 等依赖数据库的插件请先安装数据库插件,再去掉配置中对应的 \`~\` 前缀启用。
151
+
152
+ ## 自定义插件
153
+
154
+ 在 \`plugins/\` 目录下创建插件包(可用 \`bun run new <名称>\` 生成骨架),在 \`koishi.yml\` 中以相对路径引用(如 \`./plugins/my-plugin\`)即可启用;Bun 直接加载 TypeScript 源码,无需预编译。
155
+ `
156
+ };
157
+ /**
158
+ * 内置模板的 package.json 基础内容(name/version 会被 renderManifest 覆写,
159
+ * prod 模式下 workspaces 与 devDependencies 会被移除)。
160
+ */
161
+ function baseManifest() {
162
+ return {
163
+ type: "module",
164
+ workspaces: ["plugins/*", "external/*"],
165
+ scripts: {
166
+ start: "koishi start",
167
+ dev: "cross-env NODE_ENV=development koishi start",
168
+ new: "koishi-scripts setup"
169
+ },
170
+ dependencies: {
171
+ "@koishi-ce/koishi": "^1.0.0",
172
+ "@koishi-ce/plugin-actions": "^1.0.0",
173
+ "@koishi-ce/plugin-admin": "^1.0.0",
174
+ "@koishi-ce/plugin-analytics": "^1.0.0",
175
+ "@koishi-ce/plugin-auth": "^1.0.0",
176
+ "@koishi-ce/plugin-bind": "^1.0.0",
177
+ "@koishi-ce/plugin-commands": "^1.0.0",
178
+ "@koishi-ce/plugin-config": "^1.0.0",
179
+ "@koishi-ce/plugin-console": "^1.0.0",
180
+ "@koishi-ce/plugin-explorer": "^1.0.0",
181
+ "@koishi-ce/plugin-help": "^1.0.0",
182
+ "@koishi-ce/plugin-http": "^1.0.0",
183
+ "@koishi-ce/plugin-insight": "^1.0.0",
184
+ "@koishi-ce/plugin-inspect": "^1.0.0",
185
+ "@koishi-ce/plugin-locales": "^1.0.0",
186
+ "@koishi-ce/plugin-logger": "^1.0.0",
187
+ "@koishi-ce/plugin-market": "^1.0.0",
188
+ "@koishi-ce/plugin-notifier": "^1.0.0",
189
+ "@koishi-ce/plugin-oobe": "^1.0.0",
190
+ "@koishi-ce/plugin-proxy-agent": "^1.0.0",
191
+ "@koishi-ce/plugin-sandbox": "^1.0.0",
192
+ "@koishi-ce/plugin-server": "^1.0.0",
193
+ "@koishi-ce/plugin-status": "^1.0.0",
194
+ koishi: "npm:@koishi-ce/koishi-shim@^4.18.11",
195
+ "@koishijs/plugin-console": "npm:@koishi-ce/console-shim@^5.30.11",
196
+ "@koishijs/core": "npm:@koishi-ce/core-shim@4.18.11",
197
+ "@koishijs/loader": "npm:@koishi-ce/loader-shim@^4.6.11"
198
+ },
199
+ devDependencies: {
200
+ "@koishi-ce/client": "^1.0.0",
201
+ "@koishi-ce/plugin-hmr": "^1.0.0",
202
+ "@koishi-ce/scripts": "^1.0.0",
203
+ "bun-types": "^1.4.0",
204
+ "cross-env": "^7.0.3"
205
+ }
206
+ };
207
+ }
208
+ //#endregion
209
+ //#region src/index.ts
210
+ /**
211
+ * create-koishi-ce 脚手架(npm 包名 create-koishi-ce,目录名为
212
+ * apps/koishi-create,二者不一致是历史遗留,以目录名为准)。
213
+ *
214
+ * 通过 `bunx create-koishi-ce [name]`(npx 亦可)交互式创建 Koishi 机器人
215
+ * 应用项目:确定项目名 → 准备目标目录 → 写入内置 @koishi-ce 模板(默认,
216
+ * 见 src/template.ts;--template <包名> 可改用 npm registry 远程模板,如
217
+ * 上游官方 @koishijs/boilerplate)→ 按需初始化 git → 询问是否立即安装
218
+ * 依赖并启动。CLI 可执行入口在 src/bin.ts(构建产物 lib/bin.mjs,bin 字段
219
+ * 指向它);本文件只承载主流程与可单测的纯函数(范式对齐
220
+ * @koishi-ce/scripts)。
221
+ */
222
+ const { version } = package_default;
75
223
  /** fetch 请求非 2xx 时抛出的错误,携带 HTTP 状态码与状态文本 */
76
224
  var HttpError = class extends Error {
77
225
  status;
@@ -217,18 +365,22 @@ function writePackageJson() {
217
365
  writeFileSync(filename, renderManifest(meta, project, argv.prod === true));
218
366
  }
219
367
  /**
220
- * 模板下载与解包的主流程:
221
- * 1. 确定 npm registry(--registry 参数 > 本机 npm 配置 > 官方源);
222
- * 2. 拉取模板包元数据,按 dist-tags 解析目标版本(--ref,默认 latest);
223
- * 3. 流式下载 tarball 并解包到目标目录(strip: 1 去掉包根目录层级),
368
+ * 写入内置模板(默认路径):静态文件(src/template.ts 的 templateFiles)
369
+ * 加上由 baseManifest() 渲染出的 package.json。纯本地写入,无网络请求。
370
+ */
371
+ function scaffoldBuiltin() {
372
+ for (const [file, content] of Object.entries(templateFiles)) writeFileSync(join(rootDir, file), content);
373
+ writeFileSync(join(rootDir, "package.json"), renderManifest(baseManifest(), project, argv.prod === true));
374
+ }
375
+ /**
376
+ * 远程模板下载与解包(--template 指定时):
377
+ * 1. 拉取模板包元数据,按 dist-tags 解析目标版本(--ref,默认 latest);
378
+ * 2. 流式下载 tarball 并解包到目标目录(strip: 1 去掉包根目录层级),
224
379
  * 网络错误统一以 HttpError 提示后退出;
225
- * 4. 最后改写 package.json。
380
+ * 3. 最后改写 package.json。
226
381
  */
227
- async function scaffold() {
228
- console.log(kleur.dim(" 正在 ") + project + kleur.dim(" 中生成项目 ..."));
229
- const registry = (argv.registry || getLocalRegistry() || "https://registry.npmjs.org").replace(/\/$/, "");
230
- console.log(kleur.dim(` 使用 registry:${registry}\n`));
231
- const template = argv.template || "@koishijs/boilerplate";
382
+ async function scaffoldRemote(registry) {
383
+ const template = argv.template;
232
384
  const ref = argv.ref || "latest";
233
385
  try {
234
386
  const metaRes = await fetch(`${registry}/${template}`);
@@ -253,6 +405,19 @@ async function scaffold() {
253
405
  process.exit(1);
254
406
  }
255
407
  writePackageJson();
408
+ }
409
+ /**
410
+ * 生成项目的主入口:默认写内置 @koishi-ce 模板(scaffoldBuiltin);
411
+ * --template <包名> 时改为从 npm registry 下载远程模板(scaffoldRemote),
412
+ * registry 取值 --registry 参数 > 本机 npm 配置 > 官方源。
413
+ */
414
+ async function scaffold() {
415
+ console.log(kleur.dim(" 正在 ") + project + kleur.dim(" 中生成项目 ..."));
416
+ if (argv.template) {
417
+ const registry = (argv.registry || getLocalRegistry() || "https://registry.npmjs.org").replace(/\/$/, "");
418
+ console.log(kleur.dim(` 使用 registry:${registry}\n`));
419
+ await scaffoldRemote(registry);
420
+ } else scaffoldBuiltin();
256
421
  console.log(kleur.green(" 完成。\n"));
257
422
  }
258
423
  /**
@@ -313,8 +478,8 @@ async function start() {
313
478
  用法:create-koishi-ce [名称] [选项]
314
479
 
315
480
  选项:
316
- -t, --template <名称> 模板包名(默认 @koishijs/boilerplate)
317
- -r, --ref <引用> 模板版本引用(默认 latest)
481
+ -t, --template <名称> npm registry 下载指定模板包(默认使用内置 @koishi-ce 模板)
482
+ -r, --ref <引用> 远程模板版本引用(默认 latest)
318
483
  -f, --forced 强制清空目标目录
319
484
  -g, --git 初始化 git 仓库
320
485
  --registry <地址> 指定 npm registry(如 https://registry.npmmirror.com)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-koishi-ce",
3
3
  "description": "Setup a Koishi application",
4
- "version": "1.0.1",
4
+ "version": "1.2.0",
5
5
  "type": "module",
6
6
  "main": "./lib/index.mjs",
7
7
  "module": "./lib/index.mjs",
@@ -0,0 +1,275 @@
1
+ import { afterAll, beforeAll, describe, expect, it, mock } from "bun:test";
2
+ import { spawnSync } from "node:child_process";
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ readdirSync,
8
+ readFileSync,
9
+ rmSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { basename, join } from "node:path";
14
+
15
+ /**
16
+ * start() 默认(内置模板)主流程的端到端测试。
17
+ *
18
+ * 手段:把 process.argv 指为无位置参数 + --git,chdir 到临时目录后动态
19
+ * 导入被测模块(其内部 cwd / argv 在模块顶层固化);prompts 与
20
+ * node:child_process 均 mock,分别提供可编程的问询答案与子进程记录,
21
+ * process.exit 替换为可捕获的异常,从而覆盖全部交互分支。
22
+ */
23
+
24
+ const workspaceRoot = mkdtempSync(join(tmpdir(), "ckc-run-default-"));
25
+ const previousCwd = process.cwd();
26
+ const previousArgv = process.argv.slice();
27
+ const previousExit = process.exit;
28
+ // 保持无 user-agent 的探测环境(detectAgent 走 bun 分支)
29
+ const userAgentKey = "npm_config_user_agent";
30
+ const hadUserAgent = userAgentKey in process.env;
31
+ const previousUserAgent = process.env[userAgentKey];
32
+ delete process.env[userAgentKey];
33
+
34
+ /** prompts 的可编程答案队列(按 type 分发) */
35
+ const nameAnswers: string[] = [];
36
+ const confirmAnswers: boolean[] = [];
37
+
38
+ mock.module("prompts", () => ({
39
+ default: async (question: { type?: string }) => {
40
+ if (question.type === "confirm") {
41
+ return { yes: confirmAnswers.shift() ?? false };
42
+ }
43
+ return { name: nameAnswers.shift() ?? "" };
44
+ },
45
+ }));
46
+
47
+ /** spawnSync 的调用记录与可编程行为 */
48
+ const spawnLog: Array<{ cmd: string; args: string[]; cwd?: string }> = [];
49
+ let installExit = 0;
50
+
51
+ // 本文件求值时 node:child_process 恒为真实实现(本仓测试中唯一的
52
+ // child_process mock 注册点),持有真实引用供未知命令透传;mock 的
53
+ // 命名空间改写会波及其它测试文件经活绑定发起的调用(如 setup 的
54
+ // git init、release 的子进程测试),透传保证它们拿到真实行为
55
+ const realSpawnSync = spawnSync;
56
+
57
+ mock.module("node:child_process", () => ({
58
+ spawnSync: (
59
+ cmd: string,
60
+ args: readonly string[],
61
+ options: { cwd?: string },
62
+ ) => {
63
+ if (cmd === "git" && args[0] === "--version") return { status: 0 };
64
+ if (cmd === "git" && args[0] === "config" && args[1] === "--get") {
65
+ const key = args[2];
66
+ const table: Record<string, string> = {
67
+ "user.name": "Tester",
68
+ "user.email": "t@e.st",
69
+ "init.defaultBranch": "trunk",
70
+ };
71
+ const value = key !== undefined ? table[key] : undefined;
72
+ return value === undefined
73
+ ? { status: 1, stdout: "" }
74
+ : { status: 0, stdout: `${value}\n` };
75
+ }
76
+ // exactOptionalPropertyTypes:cwd 未传时不落键,避免显式写入 undefined
77
+ spawnLog.push({
78
+ cmd,
79
+ args: [...args],
80
+ ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
81
+ });
82
+ // 依赖安装可编程;agent 启动不真实拉起
83
+ if (args[0] === "install") return { status: installExit };
84
+ if (cmd === "bun" && args[0] === "run") return { status: 0 };
85
+ // 其余命令透传真实执行(副作用均在各自测试的临时目录内)
86
+ return realSpawnSync(cmd, args, options as Parameters<typeof spawnSync>[2]);
87
+ },
88
+ }));
89
+
90
+ /** process.exit 替换为异常,供断言「直接退出」的分支 */
91
+ // erasableSyntaxOnly 禁用参数属性,改为显式字段声明
92
+ class ExitError extends Error {
93
+ code: number;
94
+ constructor(code: number) {
95
+ super(`process.exit(${code})`);
96
+ this.code = code;
97
+ }
98
+ }
99
+
100
+ process.exit = ((code?: number) => {
101
+ throw new ExitError(code ?? 0);
102
+ }) as typeof process.exit;
103
+
104
+ process.argv = [process.argv[0] ?? "bun", "create-koishi-ce", "--git"];
105
+ process.chdir(workspaceRoot);
106
+ // query 强制独立实例(说明与写法见 run-help.test.ts)
107
+ const specifier = "../index.ts?default-run";
108
+ const { start, getLocalRegistry, readNpmrcRegistry } = (await import(
109
+ specifier
110
+ )) as typeof import("../index.ts");
111
+ // cwd 已在被测模块内固化,立即切回,缩小对进程全局的影响窗口
112
+ process.chdir(previousCwd);
113
+
114
+ const logs: string[] = [];
115
+ const originalLog = console.log;
116
+
117
+ beforeAll(() => {
118
+ console.log = (...args: unknown[]) => {
119
+ logs.push(args.map((arg) => `${arg}`).join(" "));
120
+ };
121
+ });
122
+
123
+ afterAll(() => {
124
+ console.log = originalLog;
125
+ process.exit = previousExit;
126
+ process.argv = previousArgv;
127
+ if (hadUserAgent) {
128
+ process.env[userAgentKey] = previousUserAgent;
129
+ } else {
130
+ delete process.env[userAgentKey];
131
+ }
132
+ rmSync(workspaceRoot, { recursive: true, force: true });
133
+ });
134
+
135
+ /** 断言内置模板的静态文件齐备 */
136
+ function expectTemplateFiles(dir: string): void {
137
+ for (const file of [
138
+ "package.json",
139
+ "koishi.yml",
140
+ "tsconfig.json",
141
+ ".env",
142
+ ".gitignore",
143
+ "README.md",
144
+ ]) {
145
+ expect(existsSync(join(dir, file)), file).toBe(true);
146
+ }
147
+ const manifest = JSON.parse(
148
+ readFileSync(join(dir, "package.json"), "utf8"),
149
+ ) as Record<string, unknown>;
150
+ expect(manifest["private"]).toBe(true);
151
+ expect(manifest["version"]).toBe("0.0.0");
152
+ expect(manifest["workspaces"]).toEqual(["plugins/*", "external/*"]);
153
+ }
154
+
155
+ describe("create-koishi-ce 默认模板主流程", () => {
156
+ // 补齐本实例下纯函数分支的行覆盖(registry 既有测试走的是共享实例)
157
+ it("registry 探测纯函数在本实例同样可用", () => {
158
+ const npmrc = join(workspaceRoot, ".npmrc");
159
+ writeFileSync(npmrc, "registry=https://registry.example.com/\n");
160
+ expect(readNpmrcRegistry(npmrc)).toBe("https://registry.example.com/");
161
+ expect(readNpmrcRegistry(join(workspaceRoot, "absent"))).toBeUndefined();
162
+ expect(getLocalRegistry(workspaceRoot, workspaceRoot)).toBe(
163
+ "https://registry.example.com/",
164
+ );
165
+ expect(getLocalRegistry(workspaceRoot, workspaceRoot)).toBeDefined();
166
+ rmSync(npmrc, { force: true });
167
+ });
168
+
169
+ it("项目名指向当前目录(rootDir === cwd):直接在原地生成,不打印 cd 提示", async () => {
170
+ // workspaceRoot 此刻为空目录:prepare 直接通过,无需清空确认
171
+ nameAnswers.push(".");
172
+ confirmAnswers.push(false);
173
+ await start();
174
+ expectTemplateFiles(workspaceRoot);
175
+ expect(
176
+ (
177
+ JSON.parse(
178
+ readFileSync(join(workspaceRoot, "package.json"), "utf8"),
179
+ ) as Record<string, unknown>
180
+ )["name"],
181
+ ).toBe(basename(workspaceRoot));
182
+ // rootDir 与 cwd 相同 → 稍后启动提示里没有 cd 行
183
+ expect(logs.join("\n")).not.toContain("cd ");
184
+ logs.length = 0;
185
+ });
186
+
187
+ it("常规新目录:内置模板 + git init(分支名取 init.defaultBranch)+ 安装并启动", async () => {
188
+ nameAnswers.push("app-a");
189
+ confirmAnswers.push(true);
190
+ installExit = 0;
191
+ await start();
192
+ expectTemplateFiles(join(workspaceRoot, "app-a"));
193
+ expect(
194
+ (
195
+ JSON.parse(
196
+ readFileSync(join(workspaceRoot, "app-a", "package.json"), "utf8"),
197
+ ) as Record<string, unknown>
198
+ )["name"],
199
+ ).toBe("app-a");
200
+ // git init 使用 git config 探测到的分支名
201
+ expect(spawnLog).toContainEqual({
202
+ cmd: "git",
203
+ args: ["init", "-b", "trunk"],
204
+ cwd: join(workspaceRoot, "app-a"),
205
+ });
206
+ // install 确认后:agent install → agent run start(bun-first 探测)
207
+ expect(spawnLog).toContainEqual({
208
+ cmd: "bun",
209
+ args: ["install"],
210
+ cwd: join(workspaceRoot, "app-a"),
211
+ });
212
+ expect(spawnLog).toContainEqual({
213
+ cmd: "bun",
214
+ args: ["run", "start"],
215
+ cwd: join(workspaceRoot, "app-a"),
216
+ });
217
+ logs.length = 0;
218
+ });
219
+
220
+ it("目标目录非空且用户拒绝清空:直接退出且不改动现有文件", async () => {
221
+ const dir = join(workspaceRoot, "app-b");
222
+ mkdirSync(dir, { recursive: true });
223
+ writeFileSync(join(dir, "occupier.txt"), "保留我");
224
+ nameAnswers.push("app-b");
225
+ confirmAnswers.push(false);
226
+ await expect(start()).rejects.toThrow("process.exit(0)");
227
+ expect(readFileSync(join(dir, "occupier.txt"), "utf8")).toBe("保留我");
228
+ });
229
+
230
+ it("用户确认清空:emptyDir 后重建模板", async () => {
231
+ const dir = join(workspaceRoot, "app-b");
232
+ nameAnswers.push("app-b");
233
+ confirmAnswers.push(true);
234
+ await start();
235
+ expect(existsSync(join(dir, "occupier.txt"))).toBe(false);
236
+ expectTemplateFiles(dir);
237
+ logs.length = 0;
238
+ });
239
+
240
+ it("项目名问询被取消(空白输入):直接退出", async () => {
241
+ nameAnswers.push(" ");
242
+ await expect(start()).rejects.toThrow("process.exit(0)");
243
+ });
244
+
245
+ it("依赖安装失败:提示后不再尝试启动", async () => {
246
+ nameAnswers.push("app-f");
247
+ confirmAnswers.push(true);
248
+ installExit = 1;
249
+ spawnLog.length = 0;
250
+ await start();
251
+ const commands = spawnLog.map(
252
+ (call) => `${call.cmd} ${call.args.join(" ")}`,
253
+ );
254
+ expect(commands).toContain("bun install");
255
+ expect(commands).not.toContain("bun run start");
256
+ expect(logs.join("\n")).toContain("依赖安装失败");
257
+ logs.length = 0;
258
+ });
259
+
260
+ it("拒绝立即安装:打印后续手动命令(含相对路径 cd 提示)", async () => {
261
+ nameAnswers.push("sub/app-c");
262
+ confirmAnswers.push(false);
263
+ await start();
264
+ const output = logs.join("\n");
265
+ expect(output).toContain("cd");
266
+ expect(output).toContain("bun install");
267
+ expect(output).toContain("bun run start");
268
+ expect(existsSync(join(workspaceRoot, "sub", "app-c", "koishi.yml"))).toBe(
269
+ true,
270
+ );
271
+ expect(
272
+ readdirSync(join(workspaceRoot, "sub", "app-c")).length,
273
+ ).toBeGreaterThan(0);
274
+ });
275
+ });
@@ -0,0 +1,41 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "bun:test";
2
+
3
+ /**
4
+ * start() 的 --help 分支:打印用法后直接返回(不产生任何写盘)。
5
+ * argv 在模块顶层解析,故在动态导入前改写 process.argv。
6
+ */
7
+
8
+ const logs: string[] = [];
9
+ const originalLog = console.log;
10
+
11
+ process.argv = [process.argv[0] ?? "bun", "create-koishi-ce", "--help"];
12
+
13
+ // 以 query 强制独立实例:index.ts 顶层固化 argv / cwd,而 bun test 的
14
+ // 模块注册表跨测试文件共享(不带 query 会拿到既有测试文件已加载的
15
+ // 实例,其 argv / cwd 均为测试进程默认值)。query 须以相对说明符的
16
+ // 形式交给运行时(file URL 形式的 query 不参与缓存键;TS 亦不解析
17
+ // 带 query 的说明符),故经变量动态构造,类型以 typeof import() 收紧
18
+ const specifier = "../index.ts?help";
19
+ const { start } = (await import(specifier)) as typeof import("../index.ts");
20
+
21
+ beforeAll(() => {
22
+ console.log = (...args: unknown[]) => {
23
+ logs.push(args.map((arg) => `${arg}`).join(" "));
24
+ };
25
+ });
26
+
27
+ afterAll(() => {
28
+ console.log = originalLog;
29
+ });
30
+
31
+ describe("create-koishi-ce --help", () => {
32
+ it("打印用法说明后即返回", async () => {
33
+ await start();
34
+ const output = logs.join("\n");
35
+ expect(output).toContain("用法:create-koishi-ce [名称] [选项]");
36
+ expect(output).toContain("--template");
37
+ expect(output).toContain("--prod");
38
+ // 帮助分支不应询问项目名(无 "项目名:" 提示输出)
39
+ expect(output).not.toContain("项目名:");
40
+ });
41
+ });
@@ -0,0 +1,242 @@
1
+ import { afterAll, beforeAll, describe, expect, it, mock } from "bun:test";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readdirSync,
7
+ readFileSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { c as tarCreate } from "tar";
14
+
15
+ /**
16
+ * start() 远程模板分支(--template)的端到端测试。
17
+ *
18
+ * 以 Bun.serve 起本地伪 registry(127.0.0.1 随机端口),按用例切换
19
+ * 场景(正常 / 元数据 404 / dist-tag 缺失 / tarball 404 / tarball 损坏);
20
+ * tarball 由 tar 包真实打包,scaffoldRemote 的下载-解包-改写全链路真实执行。
21
+ *
22
+ * 覆盖率口径说明:bun 的覆盖率对同一路径的多个 query 实例只统计最后
23
+ * 求值的那份,本文件是 koishi-create 全部测试中最后加载的实例,因此
24
+ * 在此补齐 registry 探测纯函数、emptyDir 与 initGit(--git)等只在其它
25
+ * 实例出现过的分支,保证 src/index.ts 的行覆盖完整。
26
+ */
27
+
28
+ const workspaceRoot = mkdtempSync(join(tmpdir(), "ckc-run-remote-"));
29
+ const previousCwd = process.cwd();
30
+ const previousArgv = process.argv.slice();
31
+ const previousExit = process.exit;
32
+
33
+ /** 伪 registry 的场景开关 */
34
+ let scenario: "ok" | "meta-404" | "missing-ref" | "tarball-404" | "corrupt" =
35
+ "ok";
36
+
37
+ /** 预先打包好的模板 tarball(package/ 前缀目录,strip:1 解包) */
38
+ let tarball: Buffer;
39
+
40
+ // fetch 回调内引用 registry.port,显式标注返回类型以切断 initializer 的循环推断
41
+ const registry = Bun.serve({
42
+ port: 0,
43
+ hostname: "127.0.0.1",
44
+ // serve 的 fetch 允许同步或异步返回,显式标注以切断 registry 的循环推断
45
+ fetch: (request): Response | Promise<Response> => {
46
+ const url = new URL(request.url);
47
+ if (url.pathname === "/fake-tpl") {
48
+ if (scenario === "meta-404") {
49
+ return new Response(null, { status: 404, statusText: "Not Found" });
50
+ }
51
+ if (scenario === "missing-ref") {
52
+ return Response.json({
53
+ "dist-tags": { latest: "1.0.0" },
54
+ versions: { "1.0.0": {} },
55
+ });
56
+ }
57
+ return Response.json({
58
+ "dist-tags": { latest: "1.0.0", beta: "2.0.0" },
59
+ versions: {
60
+ "2.0.0": {
61
+ dist: {
62
+ tarball: `http://127.0.0.1:${registry.port}/fake-tpl/-/fake-tpl-2.0.0.tgz`,
63
+ },
64
+ },
65
+ },
66
+ });
67
+ }
68
+ if (url.pathname === "/fake-tpl/-/fake-tpl-2.0.0.tgz") {
69
+ if (scenario === "tarball-404") {
70
+ return new Response(null, { status: 404, statusText: "Not Found" });
71
+ }
72
+ if (scenario === "corrupt") {
73
+ return new Response("definitely not a tarball", { status: 200 });
74
+ }
75
+ return new Response(tarball);
76
+ }
77
+ return new Response(null, { status: 404 });
78
+ },
79
+ });
80
+
81
+ /** prompts mock:install 的 confirm 默认拒绝,可按用例入队覆盖 */
82
+ const confirmAnswers: boolean[] = [];
83
+
84
+ mock.module("prompts", () => ({
85
+ default: async (question: { type?: string }) => {
86
+ if (question.type === "confirm") {
87
+ return { yes: confirmAnswers.shift() ?? false };
88
+ }
89
+ return { name: "" };
90
+ },
91
+ }));
92
+
93
+ // erasableSyntaxOnly 禁用参数属性,改为显式字段声明
94
+ class ExitError extends Error {
95
+ code: number;
96
+ constructor(code: number) {
97
+ super(`process.exit(${code})`);
98
+ this.code = code;
99
+ }
100
+ }
101
+
102
+ process.exit = ((code?: number) => {
103
+ throw new ExitError(code ?? 0);
104
+ }) as typeof process.exit;
105
+
106
+ process.argv = [
107
+ process.argv[0] ?? "bun",
108
+ "create-koishi-ce",
109
+ "myapp",
110
+ "--template",
111
+ "fake-tpl",
112
+ "--registry",
113
+ `http://127.0.0.1:${registry.port}`,
114
+ "--ref",
115
+ "beta",
116
+ // 附带 --git 使 initGit 的 supports / gitConfig 分支也得到执行
117
+ // (spawnSync 由 run-default 注册的 mock 拦截,无真实子进程)
118
+ "--git",
119
+ ];
120
+ process.chdir(workspaceRoot);
121
+ // query 强制独立实例(说明见 run-help.test.ts)
122
+ const specifier = "../index.ts?remote-run";
123
+ const { start, getLocalRegistry, readNpmrcRegistry } = (await import(
124
+ specifier
125
+ )) as typeof import("../index.ts");
126
+ process.chdir(previousCwd);
127
+
128
+ const logs: string[] = [];
129
+ const originalLog = console.log;
130
+
131
+ beforeAll(async () => {
132
+ console.log = (...args: unknown[]) => {
133
+ logs.push(args.map((arg) => `${arg}`).join(" "));
134
+ };
135
+ // 造模板载荷并打包:package/package.json + package/index.js
136
+ const payload = join(workspaceRoot, "payload", "package");
137
+ mkdirSync(payload, { recursive: true });
138
+ writeFileSync(
139
+ join(payload, "package.json"),
140
+ JSON.stringify({
141
+ name: "fake-tpl",
142
+ version: "2.0.0",
143
+ private: false,
144
+ workspaces: ["src/*"],
145
+ scripts: { start: "node index.js" },
146
+ }),
147
+ );
148
+ writeFileSync(join(payload, "index.js"), "console.log('tpl');\n");
149
+ const tarballPath = join(workspaceRoot, "fake-tpl.tgz");
150
+ await tarCreate(
151
+ { file: tarballPath, cwd: join(workspaceRoot, "payload"), portable: true },
152
+ ["package"],
153
+ );
154
+ tarball = readFileSync(tarballPath);
155
+ });
156
+
157
+ afterAll(() => {
158
+ console.log = originalLog;
159
+ process.exit = previousExit;
160
+ process.argv = previousArgv;
161
+ void registry.stop(true);
162
+ rmSync(workspaceRoot, { recursive: true, force: true });
163
+ });
164
+
165
+ /** 每个场景前移除上一场景的产物,prepare 走「新建目录」分支 */
166
+ function reset(sc: typeof scenario): void {
167
+ scenario = sc;
168
+ logs.length = 0;
169
+ rmSync(join(workspaceRoot, "myapp"), { recursive: true, force: true });
170
+ }
171
+
172
+ describe("create-koishi-ce 远程模板", () => {
173
+ it("正常链路:元数据 → tarball → 解包 → package.json 改写", async () => {
174
+ reset("ok");
175
+ await start();
176
+ const dir = join(workspaceRoot, "myapp");
177
+ expect(existsSync(join(dir, "index.js"))).toBe(true);
178
+ const manifest = JSON.parse(
179
+ readFileSync(join(dir, "package.json"), "utf8"),
180
+ ) as Record<string, unknown>;
181
+ // writePackageJson:替换项目名、标记 private、版本归零;模板其余字段保留
182
+ expect(manifest["name"]).toBe("myapp");
183
+ expect(manifest["private"]).toBe(true);
184
+ expect(manifest["version"]).toBe("0.0.0");
185
+ expect(manifest["workspaces"]).toEqual(["src/*"]);
186
+ expect(logs.join("\n")).toContain("使用 registry");
187
+ });
188
+
189
+ it("模板包元数据 404:HttpError 提示后以码 1 退出", async () => {
190
+ reset("meta-404");
191
+ await expect(start()).rejects.toThrow("process.exit(1)");
192
+ expect(logs.join("\n")).toContain("请求失败:HTTP 404");
193
+ });
194
+
195
+ it("dist-tag 引用不存在:以码 1 退出", async () => {
196
+ reset("missing-ref");
197
+ await expect(start()).rejects.toThrow("process.exit(1)");
198
+ expect(logs.join("\n")).toContain("模板 fake-tpl@beta 不存在");
199
+ });
200
+
201
+ it("tarball 下载 404:以码 1 退出", async () => {
202
+ reset("tarball-404");
203
+ await expect(start()).rejects.toThrow("process.exit(1)");
204
+ expect(logs.join("\n")).toContain("请求失败:HTTP 404");
205
+ });
206
+
207
+ it("tarball 内容损坏:解包错误原样抛出(非 HttpError 不吞)", async () => {
208
+ reset("corrupt");
209
+ await expect(start()).rejects.toThrow();
210
+ // 非 HttpError 路径不打印友好提示、不调用 process.exit
211
+ expect(logs.join("\n")).not.toContain("请求失败");
212
+ });
213
+
214
+ it("非空目录确认清空:emptyDir 后重新解包(覆盖 prepare 清空分支)", async () => {
215
+ scenario = "ok";
216
+ logs.length = 0;
217
+ const dir = join(workspaceRoot, "myapp");
218
+ rmSync(dir, { recursive: true, force: true });
219
+ mkdirSync(dir, { recursive: true });
220
+ writeFileSync(join(dir, "occupier.txt"), "占位");
221
+ confirmAnswers.push(true);
222
+ await start();
223
+ expect(existsSync(join(dir, "occupier.txt"))).toBe(false);
224
+ expect(existsSync(join(dir, "index.js"))).toBe(true);
225
+ });
226
+
227
+ it("registry 探测纯函数(补本实例的行覆盖)", () => {
228
+ const npmrc = join(workspaceRoot, ".npmrc");
229
+ writeFileSync(npmrc, "registry=https://registry.example.com/\n");
230
+ expect(readNpmrcRegistry(npmrc)).toBe("https://registry.example.com/");
231
+ expect(
232
+ readNpmrcRegistry(join(workspaceRoot, "absent.npmrc")),
233
+ ).toBeUndefined();
234
+ expect(getLocalRegistry(workspaceRoot, workspaceRoot)).toBe(
235
+ "https://registry.example.com/",
236
+ );
237
+ rmSync(npmrc, { force: true });
238
+ expect(getLocalRegistry(workspaceRoot, workspaceRoot)).toBeUndefined();
239
+ // 临时目录内容核对后由 afterAll 统一清理
240
+ expect(readdirSync(workspaceRoot).length).toBeGreaterThan(0);
241
+ });
242
+ });
@@ -0,0 +1,89 @@
1
+ import { expect, test } from "bun:test";
2
+ import { renderManifest } from "../index.ts";
3
+ import { baseManifest, templateFiles } from "../template.ts";
4
+
5
+ test("内置模板依赖纯度:不含任何 @koishijs / koishi-plugin 官方生态包", () => {
6
+ const { dependencies, devDependencies } = baseManifest();
7
+ // 四个上游名是刻意的 npm alias 占位(koishi 裸名 / @koishijs/plugin-console /
8
+ // @koishijs/core / @koishijs/loader),其余必须是 @koishi-ce 作用域
9
+ const guarded = new Set([
10
+ "koishi",
11
+ "@koishijs/plugin-console",
12
+ "@koishijs/core",
13
+ "@koishijs/loader",
14
+ ]);
15
+ for (const key of Object.keys(dependencies ?? {})) {
16
+ expect(guarded.has(key) || key.startsWith("@koishi-ce/")).toBe(true);
17
+ }
18
+ const toolchain = new Set(["bun-types", "cross-env"]);
19
+ for (const key of Object.keys(devDependencies ?? {})) {
20
+ expect(key.startsWith("@koishi-ce/") || toolchain.has(key)).toBe(true);
21
+ }
22
+ });
23
+
24
+ test("内置模板以 npm alias 钉住 koishi 裸名,版本保持 4.18.x 冻结线", () => {
25
+ const request = baseManifest().dependencies?.["koishi"];
26
+ expect(typeof request).toBe("string");
27
+ // alias 形态 npm:@koishi-ce/koishi-shim@^4.18.x——版本若漂出 ^4 区间,
28
+ // 上游插件的 peer `koishi ^4` 判定为不满足,market UI 会试图改写它
29
+ expect(request ?? "").toMatch(/^npm:@koishi-ce\/koishi-shim@\^4\./);
30
+ });
31
+
32
+ test("内置模板钉住 console / core / loader 上游 peer 名,防 Bun 自动装官方全家桶", () => {
33
+ const { dependencies } = baseManifest();
34
+ // CE webui 插件 peer ^5.30.11:alias 版本冻结 5.30.x 线
35
+ expect(dependencies?.["@koishijs/plugin-console"]).toBe(
36
+ "npm:@koishi-ce/console-shim@^5.30.11",
37
+ );
38
+ // @koishi-ce/loader 的 peer 精确锁 4.18.11:alias 必须逐字相等(不带 ^)
39
+ expect(dependencies?.["@koishijs/core"]).toBe(
40
+ "npm:@koishi-ce/core-shim@4.18.11",
41
+ );
42
+ // config / hmr 插件的 loader peer:alias 版本冻结 4.6.x 线
43
+ expect(dependencies?.["@koishijs/loader"]).toBe(
44
+ "npm:@koishi-ce/loader-shim@^4.6.11",
45
+ );
46
+ });
47
+
48
+ test("内置模板静态文件齐备,koishi.yml 预配 market 镜像源", () => {
49
+ for (const file of [
50
+ ".env",
51
+ ".gitignore",
52
+ "koishi.yml",
53
+ "tsconfig.json",
54
+ "README.md",
55
+ ]) {
56
+ expect(templateFiles[file]?.length ?? 0).toBeGreaterThan(0);
57
+ }
58
+ expect(templateFiles["koishi.yml"]).toContain(
59
+ "endpoint: https://registry.koishi.chat/index.json",
60
+ );
61
+ // 模板不预装数据库,依赖数据库的插件保持 ~ 禁用
62
+ expect(templateFiles["koishi.yml"]).toContain("~analytics");
63
+ });
64
+
65
+ test("renderManifest 渲染内置模板:常规改写生效,prod 模式保留 koishi alias", () => {
66
+ const output = JSON.parse(renderManifest(baseManifest(), "my-app", false));
67
+ expect(output.name).toBe("my-app");
68
+ expect(output.private).toBe(true);
69
+ expect(output.version).toBe("0.0.0");
70
+ expect(output.scripts.start).toBe("koishi start");
71
+ expect(output.dependencies.koishi).toBe(
72
+ "npm:@koishi-ce/koishi-shim@^4.18.11",
73
+ );
74
+
75
+ const prod = JSON.parse(renderManifest(baseManifest(), "my-app", true));
76
+ expect("workspaces" in prod).toBe(false);
77
+ expect("devDependencies" in prod).toBe(false);
78
+ // 四个上游名 alias 是运行时依赖,prod 模式下必须原样保留
79
+ expect(prod.dependencies.koishi).toBe("npm:@koishi-ce/koishi-shim@^4.18.11");
80
+ expect(prod.dependencies["@koishijs/plugin-console"]).toBe(
81
+ "npm:@koishi-ce/console-shim@^5.30.11",
82
+ );
83
+ expect(prod.dependencies["@koishijs/core"]).toBe(
84
+ "npm:@koishi-ce/core-shim@4.18.11",
85
+ );
86
+ expect(prod.dependencies["@koishijs/loader"]).toBe(
87
+ "npm:@koishi-ce/loader-shim@^4.6.11",
88
+ );
89
+ });
package/src/index.ts CHANGED
@@ -3,11 +3,12 @@
3
3
  * apps/koishi-create,二者不一致是历史遗留,以目录名为准)。
4
4
  *
5
5
  * 通过 `bunx create-koishi-ce [name]`(npx 亦可)交互式创建 Koishi 机器人
6
- * 应用项目:确定项目名 → 准备目标目录 → npm registry 下载模板包(默认
7
- * @koishijs/boilerplate,刻意沿用上游官方模板以保持与上游插件生态一致)
8
- * 并解包 改写 package.json → 按需初始化 git → 询问是否立即安装依赖并
9
- * 启动。CLI 可执行入口在 src/bin.ts(构建产物 lib/bin.mjs,bin 字段指向
10
- * 它);本文件只承载主流程与可单测的纯函数(范式对齐 @koishi-ce/scripts)。
6
+ * 应用项目:确定项目名 → 准备目标目录 → 写入内置 @koishi-ce 模板(默认,
7
+ * 见 src/template.ts;--template <包名> 可改用 npm registry 远程模板,如
8
+ * 上游官方 @koishijs/boilerplate)→ 按需初始化 git → 询问是否立即安装
9
+ * 依赖并启动。CLI 可执行入口在 src/bin.ts(构建产物 lib/bin.mjs,bin 字段
10
+ * 指向它);本文件只承载主流程与可单测的纯函数(范式对齐
11
+ * @koishi-ce/scripts)。
11
12
  */
12
13
  import { spawnSync } from "node:child_process";
13
14
  import {
@@ -27,6 +28,7 @@ import prompts from "prompts";
27
28
  import { extract } from "tar";
28
29
  import parse from "yargs-parser";
29
30
  import pkg from "../package.json" with { type: "json" };
31
+ import { baseManifest, templateFiles } from "./template.ts";
30
32
 
31
33
  const { version } = pkg;
32
34
 
@@ -52,7 +54,8 @@ export interface Manifest {
52
54
  private?: boolean;
53
55
  version?: string;
54
56
  workspaces?: unknown;
55
- devDependencies?: unknown;
57
+ dependencies?: Record<string, string>;
58
+ devDependencies?: Record<string, string>;
56
59
  [key: string]: unknown;
57
60
  }
58
61
 
@@ -250,23 +253,28 @@ function writePackageJson() {
250
253
  }
251
254
 
252
255
  /**
253
- * 模板下载与解包的主流程:
254
- * 1. 确定 npm registry(--registry 参数 > 本机 npm 配置 > 官方源);
255
- * 2. 拉取模板包元数据,按 dist-tags 解析目标版本(--ref,默认 latest);
256
- * 3. 流式下载 tarball 并解包到目标目录(strip: 1 去掉包根目录层级),
257
- * 网络错误统一以 HttpError 提示后退出;
258
- * 4. 最后改写 package.json。
256
+ * 写入内置模板(默认路径):静态文件(src/template.ts 的 templateFiles)
257
+ * 加上由 baseManifest() 渲染出的 package.json。纯本地写入,无网络请求。
259
258
  */
260
- async function scaffold() {
261
- console.log(kleur.dim(" 正在 ") + project + kleur.dim(" 中生成项目 ..."));
259
+ function scaffoldBuiltin() {
260
+ for (const [file, content] of Object.entries(templateFiles)) {
261
+ writeFileSync(join(rootDir, file), content);
262
+ }
263
+ writeFileSync(
264
+ join(rootDir, "package.json"),
265
+ renderManifest(baseManifest(), project, argv.prod === true),
266
+ );
267
+ }
262
268
 
263
- const registry = (
264
- argv.registry ||
265
- getLocalRegistry() ||
266
- "https://registry.npmjs.org"
267
- ).replace(/\/$/, "");
268
- console.log(kleur.dim(` 使用 registry:${registry}\n`));
269
- const template = argv.template || "@koishijs/boilerplate";
269
+ /**
270
+ * 远程模板下载与解包(--template 指定时):
271
+ * 1. 拉取模板包元数据,按 dist-tags 解析目标版本(--ref,默认 latest);
272
+ * 2. 流式下载 tarball 并解包到目标目录(strip: 1 去掉包根目录层级),
273
+ * 网络错误统一以 HttpError 提示后退出;
274
+ * 3. 最后改写 package.json。
275
+ */
276
+ async function scaffoldRemote(registry: string) {
277
+ const template = argv.template as string;
270
278
  const ref = argv.ref || "latest";
271
279
 
272
280
  try {
@@ -302,6 +310,27 @@ async function scaffold() {
302
310
  }
303
311
 
304
312
  writePackageJson();
313
+ }
314
+
315
+ /**
316
+ * 生成项目的主入口:默认写内置 @koishi-ce 模板(scaffoldBuiltin);
317
+ * --template <包名> 时改为从 npm registry 下载远程模板(scaffoldRemote),
318
+ * registry 取值 --registry 参数 > 本机 npm 配置 > 官方源。
319
+ */
320
+ async function scaffold() {
321
+ console.log(kleur.dim(" 正在 ") + project + kleur.dim(" 中生成项目 ..."));
322
+
323
+ if (argv.template) {
324
+ const registry = (
325
+ argv.registry ||
326
+ getLocalRegistry() ||
327
+ "https://registry.npmjs.org"
328
+ ).replace(/\/$/, "");
329
+ console.log(kleur.dim(` 使用 registry:${registry}\n`));
330
+ await scaffoldRemote(registry);
331
+ } else {
332
+ scaffoldBuiltin();
333
+ }
305
334
 
306
335
  console.log(kleur.green(" 完成。\n"));
307
336
  }
@@ -364,8 +393,8 @@ export async function start() {
364
393
  用法:create-koishi-ce [名称] [选项]
365
394
 
366
395
  选项:
367
- -t, --template <名称> 模板包名(默认 @koishijs/boilerplate)
368
- -r, --ref <引用> 模板版本引用(默认 latest)
396
+ -t, --template <名称> npm registry 下载指定模板包(默认使用内置 @koishi-ce 模板)
397
+ -r, --ref <引用> 远程模板版本引用(默认 latest)
369
398
  -f, --forced 强制清空目标目录
370
399
  -g, --git 初始化 git 仓库
371
400
  --registry <地址> 指定 npm registry(如 https://registry.npmmirror.com)
@@ -0,0 +1,175 @@
1
+ /**
2
+ * 内置项目模板(create-koishi-ce 的默认模板)。
3
+ *
4
+ * 历史包袱修正:本脚手架最初直接下载上游官方 @koishijs/boilerplate 解包
5
+ * 生成项目,产物依赖全是 npm 官方包(koishi / @koishijs/*),完全绕开了
6
+ * 本仓的 @koishi-ce 再分发生态(官方 market 还带 Bun 下必炸的 get-registry)。
7
+ * 默认模板改为内置的纯 @koishi-ce 依赖集;确需上游官方模板时用
8
+ * --template <包名> 走 npm 远程下载(见 index.ts 的 scaffoldRemote)。
9
+ *
10
+ * 模板要点:
11
+ * - 运行时 Bun:koishi CLI(@koishi-ce/koishi 的 bin)与 loader 插件加载链
12
+ * 均以 Bun 为准(ESM-only 产物,Bun 原生加载 TS / yml);
13
+ * - "koishi" 裸名用 npm alias 钉到 @koishi-ce/koishi-shim(4.18.x 冻结线,
14
+ * 见 packages/node/koishi-shim):上游官方 adapter / database 插件与社区
15
+ * koishi-plugin-* 的 peer `koishi ^4.x` 由此满足,不会拉入 npm 官方
16
+ * koishi 形成第二份框架副本;市场安装亦不改写该声明(installer 的
17
+ * isGuardedRequest 护栏将 npm:@koishi-ce alias 与 workspace: 同等保护);
18
+ * - 本仓没有 adapter / database 插件的再分发,模板不预装这两类;依赖数据库
19
+ * 的插件(如 analytics)在 koishi.yml 中保持 ~ 禁用,待用户从市场安装
20
+ * 数据库插件后再启用;
21
+ * - 依赖版本统一 ^1.0.0 区间(安装时取最新 1.x),shim 版本例外(冻结线)。
22
+ */
23
+ import type { Manifest } from "./index.ts";
24
+
25
+ /** 模板静态文件(相对项目根的路径 → 文件内容),package.json 另行渲染 */
26
+ export const templateFiles: Record<string, string> = {
27
+ ".env": [
28
+ "GITHUB_MIRROR = https://ghproxy.com/https://github.com",
29
+ "GITHUB_CONTENT_MIRROR = https://ghproxy.com/https://raw.githubusercontent.com",
30
+ "GRAVATAR_MIRROR = https://cravatar.cn",
31
+ ].join("\n"),
32
+ ".gitignore": ["node_modules/", "data/"].join("\n"),
33
+ "koishi.yml": [
34
+ "plugins:",
35
+ " group:server:",
36
+ " server:",
37
+ " port: 5140",
38
+ " maxPort: 5149",
39
+ " group:basic:",
40
+ " ~admin: {}",
41
+ " ~bind: {}",
42
+ " commands: {}",
43
+ " help: {}",
44
+ " http: {}",
45
+ " ~inspect: {}",
46
+ " locales: {}",
47
+ " proxy-agent: {}",
48
+ " group:console:",
49
+ " actions: {}",
50
+ " ~analytics: {}",
51
+ " ~auth: {}",
52
+ " config: {}",
53
+ " console:",
54
+ " open: true",
55
+ " explorer: {}",
56
+ " insight: {}",
57
+ " logger: {}",
58
+ " market:",
59
+ " search:",
60
+ " endpoint: https://registry.koishi.chat/index.json",
61
+ " notifier: {}",
62
+ " oobe: {}",
63
+ " sandbox: {}",
64
+ " status: {}",
65
+ " group:develop:",
66
+ " $if: env.NODE_ENV === 'development'",
67
+ " hmr:",
68
+ " root: .",
69
+ ].join("\n"),
70
+ "tsconfig.json": [
71
+ "{",
72
+ ' "compilerOptions": {',
73
+ ' "target": "ESNext",',
74
+ ' "module": "ESNext",',
75
+ ' "moduleResolution": "bundler",',
76
+ ' "lib": ["ESNext"],',
77
+ ' "types": ["bun-types"],',
78
+ ' "strict": true,',
79
+ ' "skipLibCheck": true,',
80
+ ' "noEmit": true,',
81
+ ' "allowImportingTsExtensions": true,',
82
+ ' "verbatimModuleSyntax": true,',
83
+ ' "erasableSyntaxOnly": true',
84
+ " },",
85
+ ' "include": ["plugins/*/src", "external/*/src"]',
86
+ "}",
87
+ ].join("\n"),
88
+ "README.md": `# Koishi 机器人项目
89
+
90
+ 由 \`bun create koishi-ce\` 生成,运行于 [@koishi-ce 社区再分发版](https://github.com/Koishi-CE/koishi) 生态。
91
+
92
+ ## 环境要求
93
+
94
+ - [Bun](https://bun.sh) ≥ 1.2(运行时:koishi CLI 与插件加载链以 Bun 为准)
95
+
96
+ ## 快速开始
97
+
98
+ \`\`\`bash
99
+ bun install
100
+ bun run start # 启动(生产模式)
101
+ bun run dev # 启动(开发模式,启用 HMR 热更新)
102
+ \`\`\`
103
+
104
+ 启动后访问控制台:<http://127.0.0.1:5140>
105
+
106
+ ## 安装插件
107
+
108
+ 推荐在控制台「插件市场」页安装(已预配 registry.koishi.chat 镜像源);也可以手动 \`bun add <包名>\` 后在 \`koishi.yml\` 中启用。
109
+
110
+ 上游官方 adapter(如 adapter-discord / adapter-telegram)、数据库插件(如 database-sqlite)与社区 koishi-plugin-* 插件均可直接安装:根依赖中的四行 npm alias——\`"koishi": "npm:@koishi-ce/koishi-shim@^4.18.11"\`、\`"@koishijs/plugin-console": "npm:@koishi-ce/console-shim@^5.30.11"\`、\`"@koishijs/core": "npm:@koishi-ce/core-shim@4.18.11"\`、\`"@koishijs/loader": "npm:@koishi-ce/loader-shim@^4.6.11"\`——已把上游生态的 peer 依赖全部钉回 @koishi-ce 框架(**请勿删除或改写这四行**),不会形成第二份框架 / console / loader 副本。analytics 等依赖数据库的插件请先安装数据库插件,再去掉配置中对应的 \`~\` 前缀启用。
111
+
112
+ ## 自定义插件
113
+
114
+ 在 \`plugins/\` 目录下创建插件包(可用 \`bun run new <名称>\` 生成骨架),在 \`koishi.yml\` 中以相对路径引用(如 \`./plugins/my-plugin\`)即可启用;Bun 直接加载 TypeScript 源码,无需预编译。
115
+ `,
116
+ };
117
+
118
+ /**
119
+ * 内置模板的 package.json 基础内容(name/version 会被 renderManifest 覆写,
120
+ * prod 模式下 workspaces 与 devDependencies 会被移除)。
121
+ */
122
+ export function baseManifest(): Manifest {
123
+ return {
124
+ type: "module",
125
+ workspaces: ["plugins/*", "external/*"],
126
+ scripts: {
127
+ start: "koishi start",
128
+ dev: "cross-env NODE_ENV=development koishi start",
129
+ new: "koishi-scripts setup",
130
+ },
131
+ dependencies: {
132
+ "@koishi-ce/koishi": "^1.0.0",
133
+ "@koishi-ce/plugin-actions": "^1.0.0",
134
+ "@koishi-ce/plugin-admin": "^1.0.0",
135
+ "@koishi-ce/plugin-analytics": "^1.0.0",
136
+ "@koishi-ce/plugin-auth": "^1.0.0",
137
+ "@koishi-ce/plugin-bind": "^1.0.0",
138
+ "@koishi-ce/plugin-commands": "^1.0.0",
139
+ "@koishi-ce/plugin-config": "^1.0.0",
140
+ "@koishi-ce/plugin-console": "^1.0.0",
141
+ "@koishi-ce/plugin-explorer": "^1.0.0",
142
+ "@koishi-ce/plugin-help": "^1.0.0",
143
+ "@koishi-ce/plugin-http": "^1.0.0",
144
+ "@koishi-ce/plugin-insight": "^1.0.0",
145
+ "@koishi-ce/plugin-inspect": "^1.0.0",
146
+ "@koishi-ce/plugin-locales": "^1.0.0",
147
+ "@koishi-ce/plugin-logger": "^1.0.0",
148
+ "@koishi-ce/plugin-market": "^1.0.0",
149
+ "@koishi-ce/plugin-notifier": "^1.0.0",
150
+ "@koishi-ce/plugin-oobe": "^1.0.0",
151
+ "@koishi-ce/plugin-proxy-agent": "^1.0.0",
152
+ "@koishi-ce/plugin-sandbox": "^1.0.0",
153
+ "@koishi-ce/plugin-server": "^1.0.0",
154
+ "@koishi-ce/plugin-status": "^1.0.0",
155
+ // 上游裸名占位:npm alias 钉到 @koishi-ce 的 koishi shim(勿删,
156
+ // 语义见文件头注释);版本必须保持 4.18.x 冻结线以满足 ^4 peer
157
+ koishi: "npm:@koishi-ce/koishi-shim@^4.18.11",
158
+ // 上游 console 名占位:CE 插件的 peer 声明上游名,无归属时 Bun
159
+ // 会自动装下 npm 官方 console 全家桶形成双实例;版本冻结 5.30.x
160
+ "@koishijs/plugin-console": "npm:@koishi-ce/console-shim@^5.30.11",
161
+ // 上游 core 名占位:@koishi-ce/loader 的 peer 精确锁 4.18.11,
162
+ // alias 须逐字相等(不带 ^),版本冻结勿 bump
163
+ "@koishijs/core": "npm:@koishi-ce/core-shim@4.18.11",
164
+ // 上游 loader 名占位:config / hmr 插件的 peer;版本冻结 4.6.x
165
+ "@koishijs/loader": "npm:@koishi-ce/loader-shim@^4.6.11",
166
+ },
167
+ devDependencies: {
168
+ "@koishi-ce/client": "^1.0.0",
169
+ "@koishi-ce/plugin-hmr": "^1.0.0",
170
+ "@koishi-ce/scripts": "^1.0.0",
171
+ "bun-types": "^1.4.0",
172
+ "cross-env": "^7.0.3",
173
+ },
174
+ };
175
+ }