create-koishi-ce 1.2.1 → 1.3.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/README.md +77 -0
- package/lib/bin.mjs +1 -1
- package/lib/index.d.ts +24 -16
- package/lib/index.mjs +1 -1
- package/lib/{src-CwJZjC5d.mjs → src-Dh_67mkm.mjs} +302 -263
- package/package.json +58 -63
- package/src/__tests__/manifest.test.ts +3 -0
- package/src/__tests__/registry.test.ts +3 -0
- package/src/__tests__/run-default.test.ts +10 -9
- package/src/__tests__/run-help.test.ts +3 -0
- package/src/__tests__/run-remote.test.ts +51 -32
- package/src/__tests__/tar-pack.ts +188 -0
- package/src/__tests__/template.test.ts +51 -4
- package/src/bin.ts +3 -0
- package/src/index.ts +80 -243
- package/src/manifest.ts +45 -0
- package/src/registry.ts +52 -0
- package/src/remote.ts +120 -0
- package/src/template/README.md +32 -0
- package/src/template/env +3 -0
- package/src/template/gitignore +3 -0
- package/src/template/koishi.yml +65 -0
- package/src/template/tsconfig.json +16 -0
- package/src/template.ts +64 -103
- package/src/utils.ts +46 -0
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import pc from "picocolors";
|
|
3
7
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import kleur from "kleur";
|
|
7
|
-
import prompts from "prompts";
|
|
8
|
-
import { extract } from "tar";
|
|
9
|
-
import parse from "yargs-parser";
|
|
8
|
+
import { downloadTemplate } from "giget";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
10
|
//#region package.json
|
|
11
11
|
var package_default = {
|
|
12
12
|
name: "create-koishi-ce",
|
|
13
13
|
description: "Setup a Koishi application",
|
|
14
|
-
version: "1.
|
|
14
|
+
version: "1.3.1",
|
|
15
15
|
type: "module",
|
|
16
16
|
main: "./lib/index.mjs",
|
|
17
17
|
module: "./lib/index.mjs",
|
|
@@ -40,15 +40,10 @@ var package_default = {
|
|
|
40
40
|
"generator",
|
|
41
41
|
"boilerplate"
|
|
42
42
|
],
|
|
43
|
-
devDependencies: {
|
|
44
|
-
"@types/prompts": "^2.4.9",
|
|
45
|
-
"@types/yargs-parser": "^21.0.3"
|
|
46
|
-
},
|
|
47
43
|
dependencies: {
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"yargs-parser": "^22.0.0"
|
|
44
|
+
"@clack/prompts": "^1.7.0",
|
|
45
|
+
"picocolors": "^1.1.1",
|
|
46
|
+
"giget": "^3.3.1"
|
|
52
47
|
},
|
|
53
48
|
exports: {
|
|
54
49
|
".": {
|
|
@@ -61,99 +56,175 @@ var package_default = {
|
|
|
61
56
|
}
|
|
62
57
|
};
|
|
63
58
|
//#endregion
|
|
59
|
+
//#region src/manifest.ts
|
|
60
|
+
/**
|
|
61
|
+
* 改写模板的 package.json(纯函数,导出供单测):替换项目名、标记
|
|
62
|
+
* private、版本归零。
|
|
63
|
+
*/
|
|
64
|
+
function renderManifest(source, project, prod) {
|
|
65
|
+
const meta = { ...source };
|
|
66
|
+
meta["name"] = project;
|
|
67
|
+
meta["private"] = true;
|
|
68
|
+
meta["version"] = "0.0.0";
|
|
69
|
+
if (prod) {
|
|
70
|
+
delete meta["workspaces"];
|
|
71
|
+
delete meta["devDependencies"];
|
|
72
|
+
}
|
|
73
|
+
return `${JSON.stringify(meta, null, 2)}\n`;
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/registry.ts
|
|
77
|
+
/**
|
|
78
|
+
* 本机 npm registry 配置探测(纯函数域,零子进程、零网络):优先级对齐
|
|
79
|
+
* npm 自身(环境变量 > 项目 .npmrc > 用户 ~/.npmrc)。刻意不 spawn 子进程
|
|
80
|
+
* 探测——既有实现按 user-agent 选 `bun config get registry`,而 Bun 没有
|
|
81
|
+
* config 子命令,bunx 场景下子进程退出码 1 直接炸掉脚手架;读 npmrc 是
|
|
82
|
+
* 零依赖、零子进程的等价路径。远程模板下载流程见 remote.ts,不在本文件。
|
|
83
|
+
*/
|
|
84
|
+
/**
|
|
85
|
+
* 从单个 .npmrc 文件提取 registry 配置项(非注释、非 scoped 的 registry= 行)。
|
|
86
|
+
* 文件不存在或未配置时返回 undefined,读文件异常一律静默吞掉。
|
|
87
|
+
*/
|
|
88
|
+
function readNpmrcRegistry(file) {
|
|
89
|
+
try {
|
|
90
|
+
for (const line of readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
91
|
+
const value = /^\s*registry\s*=\s*(\S+)\s*$/.exec(line)?.[1];
|
|
92
|
+
if (value) return value;
|
|
93
|
+
}
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 读取本机 npm registry 配置(优先级对齐 npm 自身:环境变量 > 项目
|
|
98
|
+
* .npmrc > 用户 ~/.npmrc)。任何一步都拿不到时返回 undefined,由调用方
|
|
99
|
+
* 回落官方源。
|
|
100
|
+
*/
|
|
101
|
+
function getLocalRegistry(cwd = process.cwd(), userHome = homedir()) {
|
|
102
|
+
const candidates = [
|
|
103
|
+
process.env["npm_config_registry"],
|
|
104
|
+
readNpmrcRegistry(join(cwd, ".npmrc")),
|
|
105
|
+
readNpmrcRegistry(join(userHome, ".npmrc"))
|
|
106
|
+
];
|
|
107
|
+
for (const candidate of candidates) if (candidate?.startsWith("https://") || candidate?.startsWith("http://")) return candidate;
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/remote.ts
|
|
111
|
+
/**
|
|
112
|
+
* 远程模板分支(--template <包名>)的下载-解包-改写全流程:拉取 npm
|
|
113
|
+
* registry 包元数据解析目标版本,经 giget 下载 tarball 并解包到目标
|
|
114
|
+
* 目录,最后改写 package.json。HttpError 与 RegistryMeta 只在本流程
|
|
115
|
+
* 消费,就近定义不另立文件;本机 registry 配置探测见 registry.ts。
|
|
116
|
+
*
|
|
117
|
+
* 运行期状态(argv / project / rootDir)不跨文件共享——它们留在
|
|
118
|
+
* index.ts 顶层(e2e 测试以模块 query 隔离实例);本模块所需字段一律
|
|
119
|
+
* 经 RemoteOptions 显式传入,由 index.ts 的 scaffold 组装,避免子模块
|
|
120
|
+
* 与顶层状态的隐式耦合。
|
|
121
|
+
*/
|
|
122
|
+
/** fetch 请求非 2xx 时抛出的错误,携带 HTTP 状态码与状态文本 */
|
|
123
|
+
var HttpError = class extends Error {
|
|
124
|
+
status;
|
|
125
|
+
statusText;
|
|
126
|
+
constructor(status, statusText) {
|
|
127
|
+
super(`HTTP ${status} ${statusText}`);
|
|
128
|
+
this.status = status;
|
|
129
|
+
this.statusText = statusText;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* 远程模板下载与解包:
|
|
134
|
+
* 1. 拉取模板包元数据,按 dist-tags 解析目标版本(ref,默认 latest);
|
|
135
|
+
* 2. 下载 tarball 并解包到目标目录(解包走 giget,其按 npm tarball 惯例
|
|
136
|
+
* 剥离顶层 package/ 目录,等价于 strip: 1),网络错误统一以 HttpError
|
|
137
|
+
* 提示后退出;
|
|
138
|
+
* 3. 最后改写 package.json。
|
|
139
|
+
*/
|
|
140
|
+
async function scaffoldRemote({ registry, template, ref, prod, project, rootDir }) {
|
|
141
|
+
try {
|
|
142
|
+
const metaRes = await fetch(`${registry}/${template}`);
|
|
143
|
+
if (!metaRes.ok) throw new HttpError(metaRes.status, metaRes.statusText);
|
|
144
|
+
const remote = await metaRes.json();
|
|
145
|
+
const version = remote["dist-tags"][ref];
|
|
146
|
+
if (version === void 0) throw new HttpError(404, `模板 ${template}@${ref} 不存在`);
|
|
147
|
+
const url = remote.versions[version]?.dist?.tarball;
|
|
148
|
+
if (url === void 0) throw new HttpError(404, `模板 ${template}@${ref} 不存在`);
|
|
149
|
+
await downloadTemplate(template, {
|
|
150
|
+
provider: "npm",
|
|
151
|
+
providers: { npm: async () => ({
|
|
152
|
+
name: template,
|
|
153
|
+
version,
|
|
154
|
+
tar: async () => {
|
|
155
|
+
const tarballRes = await fetch(url);
|
|
156
|
+
if (!tarballRes.ok || !tarballRes.body) throw new HttpError(tarballRes.status, tarballRes.statusText);
|
|
157
|
+
return tarballRes.body;
|
|
158
|
+
}
|
|
159
|
+
}) },
|
|
160
|
+
dir: rootDir
|
|
161
|
+
});
|
|
162
|
+
} catch (err) {
|
|
163
|
+
if (!(err instanceof HttpError)) throw err;
|
|
164
|
+
console.log(`${pc.red("error")} 请求失败:HTTP ${err.status} ${err.statusText}`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
writePackageJson(rootDir, project, prod);
|
|
168
|
+
}
|
|
169
|
+
/** 把改写结果写回下载解包出的 package.json */
|
|
170
|
+
function writePackageJson(rootDir, project, prod) {
|
|
171
|
+
const filename = join(rootDir, "package.json");
|
|
172
|
+
const meta = JSON.parse(readFileSync(filename, "utf8"));
|
|
173
|
+
writeFileSync(filename, renderManifest(meta, project, prod));
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
64
176
|
//#region src/template.ts
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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/koishi-shim@4.18.11"\`、\`"@koishijs/loader": "npm:@koishi-ce/koishi-shim@^4.18.11"\`——已把上游生态的 peer 依赖全部钉回 @koishi-ce 框架(前两行与后两行分别只涉及 koishi-shim / console-shim 两个包;**请勿删除或改写这四行**),不会形成第二份框架 / console / loader 副本。analytics 等依赖数据库的插件请先安装数据库插件,再去掉配置中对应的 \`~\` 前缀启用。
|
|
151
|
-
|
|
152
|
-
## 自定义插件
|
|
153
|
-
|
|
154
|
-
在 \`plugins/\` 目录下创建插件包(可用 \`bun run new <名称>\` 生成骨架),在 \`koishi.yml\` 中以相对路径引用(如 \`./plugins/my-plugin\`)即可启用;Bun 直接加载 TypeScript 源码,无需预编译。
|
|
155
|
-
`
|
|
177
|
+
/**
|
|
178
|
+
* 内置项目模板(create-koishi-ce 的默认模板)。
|
|
179
|
+
*
|
|
180
|
+
* 历史包袱修正:本脚手架最初直接下载上游官方 @koishijs/boilerplate 解包
|
|
181
|
+
* 生成项目,产物依赖全是 npm 官方包(koishi / @koishijs/*),完全绕开了
|
|
182
|
+
* 本仓的 @koishi-ce 再分发生态(官方 market 还带 Bun 下必炸的 get-registry)。
|
|
183
|
+
* 默认模板改为内置的纯 @koishi-ce 依赖集;确需上游官方模板时用
|
|
184
|
+
* --template <包名> 走 npm 远程下载(见 remote.ts 的 scaffoldRemote)。
|
|
185
|
+
*
|
|
186
|
+
* 模板的静态文本一律放在 src/template/ 下的真实文件里(本模块只负责
|
|
187
|
+
* 定位与读取,不再内嵌字符串常量)。npm 包的 files 含 src,因此 lib
|
|
188
|
+
* 产物与 src 直跑两种形态都能按相对路径定位到模板目录。
|
|
189
|
+
*
|
|
190
|
+
* 模板要点:
|
|
191
|
+
* - 运行时 Bun:koishi CLI(@koishi-ce/koishi 的 bin)与 loader 插件加载链
|
|
192
|
+
* 均以 Bun 为准(ESM-only 产物,Bun 原生加载 TS / yml);脚本里的环境
|
|
193
|
+
* 变量注入直接写 `NODE_ENV=... ` 前缀——bun run 走 Bun Shell,跨平台
|
|
194
|
+
* 原生支持,无需 cross-env 一类的依赖;
|
|
195
|
+
* - "koishi" 裸名与 @koishijs/core / @koishijs/loader / @koishijs/plugin-console
|
|
196
|
+
* 上游名用 npm alias 钉到 @koishi-ce shim(版本冻结线,见 packages/shim):
|
|
197
|
+
* 上游官方 adapter / database 插件与社区 koishi-plugin-* 的 peer 由此满足,
|
|
198
|
+
* 不会拉入 npm 官方全家桶形成第二份框架副本;市场安装亦不改写该声明
|
|
199
|
+
* (installer 的 isGuardedRequest 护栏将 npm:@koishi-ce alias 与
|
|
200
|
+
* workspace: 同等保护);
|
|
201
|
+
* - koishi.yml 对齐官方实例的预写策略:控制台与基础插件全量预装(依赖
|
|
202
|
+
* 数据库的保持 ~ 禁用);本仓不再分发的 adapter / database 官方插件只以
|
|
203
|
+
* ~ 禁用条目预写、不预装——loader 跳过禁用条目,装好后在控制台启用;
|
|
204
|
+
* - 依赖版本统一 ^1.0.0 区间(安装时取最新 1.x),shim 版本例外(冻结线)。
|
|
205
|
+
*/
|
|
206
|
+
/**
|
|
207
|
+
* 定位内置模板目录:src 直跑(开发 / 测试)与 lib 产物(npm 消费)两种
|
|
208
|
+
* 形态各按相对路径探测(发布包 files 含 src,lib 同级的 ../src/template
|
|
209
|
+
* 恒存在)。
|
|
210
|
+
*/
|
|
211
|
+
function locateTemplateDir() {
|
|
212
|
+
const base = dirname(fileURLToPath(import.meta.url));
|
|
213
|
+
for (const dir of [join(base, "template"), join(base, "../src/template")]) if (existsSync(dir)) return dir;
|
|
214
|
+
throw new Error("create-koishi-ce 内置模板目录缺失(src/template)");
|
|
215
|
+
}
|
|
216
|
+
const templateDir = locateTemplateDir();
|
|
217
|
+
/**
|
|
218
|
+
* 无点前缀的模板源文件名 → 生成项目中的目标路径。点开头文件会被 npm
|
|
219
|
+
* 发布规则与各路工具的特殊处理波及(.env 恒不入包、嵌套 .gitignore 会被
|
|
220
|
+
* git 当真),模板目录里一律存无点文件名。
|
|
221
|
+
*/
|
|
222
|
+
const dotFiles = {
|
|
223
|
+
env: ".env",
|
|
224
|
+
gitignore: ".gitignore"
|
|
156
225
|
};
|
|
226
|
+
/** 模板静态文件(相对项目根的路径 → 文件内容),package.json 另行渲染 */
|
|
227
|
+
const templateFiles = Object.fromEntries(readdirSync(templateDir).filter((file) => !file.startsWith(".")).map((file) => [dotFiles[file] ?? file, readFileSync(join(templateDir, file), "utf8")]));
|
|
157
228
|
/**
|
|
158
229
|
* 内置模板的 package.json 基础内容(name/version 会被 renderManifest 覆写,
|
|
159
230
|
* prod 模式下 workspaces 与 devDependencies 会被移除)。
|
|
@@ -161,10 +232,11 @@ bun run dev # 启动(开发模式,启用 HMR 热更新)
|
|
|
161
232
|
function baseManifest() {
|
|
162
233
|
return {
|
|
163
234
|
type: "module",
|
|
235
|
+
packageManager: `bun@${Bun.version}`,
|
|
164
236
|
workspaces: ["plugins/*", "external/*"],
|
|
165
237
|
scripts: {
|
|
166
238
|
start: "koishi start",
|
|
167
|
-
dev: "
|
|
239
|
+
dev: "NODE_ENV=development koishi start",
|
|
168
240
|
new: "koishi-scripts setup"
|
|
169
241
|
},
|
|
170
242
|
dependencies: {
|
|
@@ -172,11 +244,13 @@ function baseManifest() {
|
|
|
172
244
|
"@koishi-ce/plugin-actions": "^1.0.0",
|
|
173
245
|
"@koishi-ce/plugin-admin": "^1.0.0",
|
|
174
246
|
"@koishi-ce/plugin-analytics": "^1.0.0",
|
|
247
|
+
"@koishi-ce/plugin-assets-local": "^1.0.0",
|
|
175
248
|
"@koishi-ce/plugin-auth": "^1.0.0",
|
|
176
249
|
"@koishi-ce/plugin-bind": "^1.0.0",
|
|
177
250
|
"@koishi-ce/plugin-commands": "^1.0.0",
|
|
178
251
|
"@koishi-ce/plugin-config": "^1.0.0",
|
|
179
252
|
"@koishi-ce/plugin-console": "^1.0.0",
|
|
253
|
+
"@koishi-ce/plugin-dataview": "^1.0.0",
|
|
180
254
|
"@koishi-ce/plugin-explorer": "^1.0.0",
|
|
181
255
|
"@koishi-ce/plugin-help": "^1.0.0",
|
|
182
256
|
"@koishi-ce/plugin-http": "^1.0.0",
|
|
@@ -188,9 +262,12 @@ function baseManifest() {
|
|
|
188
262
|
"@koishi-ce/plugin-notifier": "^1.0.0",
|
|
189
263
|
"@koishi-ce/plugin-oobe": "^1.0.0",
|
|
190
264
|
"@koishi-ce/plugin-proxy-agent": "^1.0.0",
|
|
265
|
+
"@koishi-ce/plugin-rate-limit": "^1.0.0",
|
|
191
266
|
"@koishi-ce/plugin-sandbox": "^1.0.0",
|
|
192
267
|
"@koishi-ce/plugin-server": "^1.0.0",
|
|
268
|
+
"@koishi-ce/plugin-server-temp": "^1.0.0",
|
|
193
269
|
"@koishi-ce/plugin-status": "^1.0.0",
|
|
270
|
+
"@koishi-ce/plugin-theme-vanilla": "^1.0.0",
|
|
194
271
|
koishi: "npm:@koishi-ce/koishi-shim@^4.18.11",
|
|
195
272
|
"@koishijs/plugin-console": "npm:@koishi-ce/console-shim@^5.30.11",
|
|
196
273
|
"@koishijs/core": "npm:@koishi-ce/koishi-shim@4.18.11",
|
|
@@ -200,50 +277,19 @@ function baseManifest() {
|
|
|
200
277
|
"@koishi-ce/client": "^1.0.0",
|
|
201
278
|
"@koishi-ce/plugin-hmr": "^1.0.0",
|
|
202
279
|
"@koishi-ce/scripts": "^1.0.0",
|
|
203
|
-
"bun-types": "^1.4.0"
|
|
204
|
-
"cross-env": "^7.0.3"
|
|
280
|
+
"bun-types": "^1.4.0"
|
|
205
281
|
}
|
|
206
282
|
};
|
|
207
283
|
}
|
|
208
284
|
//#endregion
|
|
209
|
-
//#region src/
|
|
285
|
+
//#region src/utils.ts
|
|
210
286
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
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)。
|
|
287
|
+
* 平台小工具(无业务状态、不触碰 CLI 全局状态的零散函数):包管理器
|
|
288
|
+
* 探测(detectAgent)、git 可用性与配置读取(supports / gitConfig)、
|
|
289
|
+
* 目录清空(emptyDir)。分别被 index.ts 主流程的 install / initGit /
|
|
290
|
+
* prepare 步骤调用,也是除 manifest.ts / registry.ts 之外可脱离交互
|
|
291
|
+
* 直接单测的函数集合。
|
|
221
292
|
*/
|
|
222
|
-
const { version } = package_default;
|
|
223
|
-
/** fetch 请求非 2xx 时抛出的错误,携带 HTTP 状态码与状态文本 */
|
|
224
|
-
var HttpError = class extends Error {
|
|
225
|
-
status;
|
|
226
|
-
statusText;
|
|
227
|
-
constructor(status, statusText) {
|
|
228
|
-
super(`HTTP ${status} ${statusText}`);
|
|
229
|
-
this.status = status;
|
|
230
|
-
this.statusText = statusText;
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
const argv = parse(process.argv.slice(2), { alias: {
|
|
234
|
-
ref: ["r"],
|
|
235
|
-
forced: ["f"],
|
|
236
|
-
git: ["g"],
|
|
237
|
-
prod: ["p"],
|
|
238
|
-
template: ["t"],
|
|
239
|
-
yes: ["y"],
|
|
240
|
-
help: ["h"]
|
|
241
|
-
} });
|
|
242
|
-
/** 项目目录名(rootDir 的最后一段,写入生成项目的 package.json 的 name) */
|
|
243
|
-
let project;
|
|
244
|
-
/** 目标目录的绝对路径(由用户输入的项目名拼接 cwd 得到) */
|
|
245
|
-
let rootDir;
|
|
246
|
-
const cwd = process.cwd();
|
|
247
293
|
/**
|
|
248
294
|
* 探测后续安装/启动使用的包管理器(Bun-first):yarn / pnpm 用户跟随其
|
|
249
295
|
* 生态习惯;其余场景(npm、bun 及探测不到 user-agent)一律走 bun——
|
|
@@ -255,34 +301,6 @@ function detectAgent() {
|
|
|
255
301
|
if (ua.startsWith("pnpm")) return "pnpm";
|
|
256
302
|
return "bun";
|
|
257
303
|
}
|
|
258
|
-
/**
|
|
259
|
-
* 从单个 .npmrc 文件提取 registry 配置项(非注释、非 scoped 的 registry= 行)。
|
|
260
|
-
* 文件不存在或未配置时返回 undefined,读文件异常一律静默吞掉。
|
|
261
|
-
*/
|
|
262
|
-
function readNpmrcRegistry(file) {
|
|
263
|
-
try {
|
|
264
|
-
for (const line of readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
265
|
-
const value = /^\s*registry\s*=\s*(\S+)\s*$/.exec(line)?.[1];
|
|
266
|
-
if (value) return value;
|
|
267
|
-
}
|
|
268
|
-
} catch {}
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* 读取本机 npm registry 配置(优先级对齐 npm 自身:环境变量 > 项目
|
|
272
|
-
* .npmrc > 用户 ~/.npmrc)。刻意不 spawn 子进程探测——既有实现按
|
|
273
|
-
* user-agent 选 `bun config get registry`,而 Bun 没有 config 子命令,
|
|
274
|
-
* bunx 场景下子进程退出码 1 直接炸掉脚手架;读 npmrc 是零依赖、零
|
|
275
|
-
* 子进程的等价路径。任何一步都拿不到时返回 undefined,由调用方回落
|
|
276
|
-
* 官方源。
|
|
277
|
-
*/
|
|
278
|
-
function getLocalRegistry(cwd = process.cwd(), userHome = homedir()) {
|
|
279
|
-
const candidates = [
|
|
280
|
-
process.env["npm_config_registry"],
|
|
281
|
-
readNpmrcRegistry(join(cwd, ".npmrc")),
|
|
282
|
-
readNpmrcRegistry(join(userHome, ".npmrc"))
|
|
283
|
-
];
|
|
284
|
-
for (const candidate of candidates) if (candidate?.startsWith("https://") || candidate?.startsWith("http://")) return candidate;
|
|
285
|
-
}
|
|
286
304
|
/** 静默执行命令探测其是否可用(如 git --version),失败即视为不可用 */
|
|
287
305
|
function supports(command) {
|
|
288
306
|
return spawnSync(command[0] ?? "", command.slice(1), { stdio: "ignore" }).status === 0;
|
|
@@ -296,36 +314,105 @@ function gitConfig(key) {
|
|
|
296
314
|
], { encoding: "utf8" });
|
|
297
315
|
return res.status === 0 ? res.stdout?.trim() ?? "" : "";
|
|
298
316
|
}
|
|
317
|
+
/** 递归清空目录内容(目录本身保留)。 */
|
|
318
|
+
function emptyDir(root) {
|
|
319
|
+
for (const file of readdirSync(root)) rmSync(join(root, file), {
|
|
320
|
+
recursive: true,
|
|
321
|
+
force: true
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/index.ts
|
|
326
|
+
/**
|
|
327
|
+
* create-koishi-ce 脚手架(npm 包名 create-koishi-ce,目录名为
|
|
328
|
+
* apps/koishi-create,二者不一致是历史遗留,以目录名为准)。
|
|
329
|
+
*
|
|
330
|
+
* 通过 `bunx create-koishi-ce [name]`(npx 亦可)交互式创建 Koishi 机器人
|
|
331
|
+
* 应用项目:确定项目名 → 准备目标目录 → 写入内置 @koishi-ce 模板(默认,
|
|
332
|
+
* 见 src/template.ts;--template <包名> 可改用 npm registry 远程模板,如
|
|
333
|
+
* 上游官方 @koishijs/boilerplate)→ 按需初始化 git → 询问是否立即安装
|
|
334
|
+
* 依赖并启动。CLI 可执行入口在 src/bin.ts(构建产物 lib/bin.mjs,bin 字段
|
|
335
|
+
* 指向它)。
|
|
336
|
+
*
|
|
337
|
+
* 本文件是 CLI 的编排层:承载参数解析与运行期状态(argv / cwd /
|
|
338
|
+
* project / rootDir —— 它们被 e2e 测试以模块 query 隔离实例,必须留在
|
|
339
|
+
* 顶层模块内)与交互主流程。纯函数与单一职责的子流程按域拆到同目录
|
|
340
|
+
* 子模块,公共导出面在文件尾原样 re-export(bin 与测试的导入路径不变):
|
|
341
|
+
* - manifest.ts package.json 改写(Manifest / renderManifest)
|
|
342
|
+
* - registry.ts 本机 npm registry 配置探测
|
|
343
|
+
* - utils.ts 平台小工具(包管理器 / git 探测、清空目录)
|
|
344
|
+
* - remote.ts --template 远程模板的下载-解包-改写全流程
|
|
345
|
+
* - template.ts 内置 @koishi-ce 模板(静态文件与 baseManifest)
|
|
346
|
+
*/
|
|
347
|
+
const { version } = package_default;
|
|
348
|
+
const { values, positionals } = parseArgs({
|
|
349
|
+
args: process.argv.slice(2),
|
|
350
|
+
options: {
|
|
351
|
+
registry: { type: "string" },
|
|
352
|
+
ref: {
|
|
353
|
+
type: "string",
|
|
354
|
+
short: "r"
|
|
355
|
+
},
|
|
356
|
+
forced: {
|
|
357
|
+
type: "boolean",
|
|
358
|
+
short: "f"
|
|
359
|
+
},
|
|
360
|
+
git: {
|
|
361
|
+
type: "boolean",
|
|
362
|
+
short: "g"
|
|
363
|
+
},
|
|
364
|
+
prod: {
|
|
365
|
+
type: "boolean",
|
|
366
|
+
short: "p"
|
|
367
|
+
},
|
|
368
|
+
template: {
|
|
369
|
+
type: "string",
|
|
370
|
+
short: "t"
|
|
371
|
+
},
|
|
372
|
+
yes: {
|
|
373
|
+
type: "boolean",
|
|
374
|
+
short: "y"
|
|
375
|
+
},
|
|
376
|
+
help: {
|
|
377
|
+
type: "boolean",
|
|
378
|
+
short: "h"
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
allowPositionals: true
|
|
382
|
+
});
|
|
383
|
+
const argv = {
|
|
384
|
+
...values,
|
|
385
|
+
_: positionals
|
|
386
|
+
};
|
|
387
|
+
/** 项目目录名(rootDir 的最后一段,写入生成项目的 package.json 的 name) */
|
|
388
|
+
let project;
|
|
389
|
+
/** 目标目录的绝对路径(由用户输入的项目名拼接 cwd 得到) */
|
|
390
|
+
let rootDir;
|
|
391
|
+
const cwd = process.cwd();
|
|
299
392
|
/**
|
|
300
393
|
* 获取项目名:优先取第一个位置参数,否则交互式询问(默认 koishi-app)。
|
|
301
|
-
*
|
|
394
|
+
* 用户取消(Ctrl+C)或输入为空时直接退出(不强行兜底默认值)。
|
|
302
395
|
*/
|
|
303
396
|
async function getName() {
|
|
304
397
|
if (argv._[0]) return `${argv._[0]}`;
|
|
305
|
-
const
|
|
306
|
-
type: "text",
|
|
307
|
-
name: "name",
|
|
398
|
+
const answer = await p.text({
|
|
308
399
|
message: "项目名:",
|
|
309
|
-
|
|
310
|
-
|
|
400
|
+
initialValue: "koishi-app",
|
|
401
|
+
validate: (value) => value?.trim() ? void 0 : "项目名不能为空"
|
|
402
|
+
});
|
|
403
|
+
if (p.isCancel(answer)) process.exit(0);
|
|
404
|
+
const trimmed = answer.trim();
|
|
311
405
|
if (!trimmed) process.exit(0);
|
|
312
406
|
return trimmed;
|
|
313
407
|
}
|
|
314
|
-
/**
|
|
315
|
-
function emptyDir(root) {
|
|
316
|
-
for (const file of readdirSync(root)) rmSync(join(root, file), {
|
|
317
|
-
recursive: true,
|
|
318
|
-
force: true
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
/** 交互式确认框:返回用户是否选择了「是」(取消视为否) */
|
|
408
|
+
/** 交互式确认框:返回用户是否选择了「是」(Ctrl+C 取消直接退出) */
|
|
322
409
|
async function confirm(message) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
410
|
+
const answer = await p.confirm({
|
|
411
|
+
message,
|
|
412
|
+
initialValue: true
|
|
413
|
+
});
|
|
414
|
+
if (p.isCancel(answer)) process.exit(0);
|
|
415
|
+
return answer === true;
|
|
329
416
|
}
|
|
330
417
|
/**
|
|
331
418
|
* 准备目标目录:不存在则创建;已存在且非空时,未指定 --forced / --yes
|
|
@@ -338,33 +425,12 @@ async function prepare() {
|
|
|
338
425
|
}
|
|
339
426
|
if (!readdirSync(rootDir).length) return;
|
|
340
427
|
if (!argv.forced && !argv.yes) {
|
|
341
|
-
console.log(
|
|
428
|
+
console.log(pc.yellow(` 目标目录 "${project}" 非空。`));
|
|
342
429
|
if (!await confirm("清空现有文件并继续?")) process.exit(0);
|
|
343
430
|
}
|
|
344
431
|
emptyDir(rootDir);
|
|
345
432
|
}
|
|
346
433
|
/**
|
|
347
|
-
* 改写模板的 package.json(纯函数,导出供单测):替换项目名、标记
|
|
348
|
-
* private、版本归零。
|
|
349
|
-
*/
|
|
350
|
-
function renderManifest(source, project, prod) {
|
|
351
|
-
const meta = { ...source };
|
|
352
|
-
meta["name"] = project;
|
|
353
|
-
meta["private"] = true;
|
|
354
|
-
meta["version"] = "0.0.0";
|
|
355
|
-
if (prod) {
|
|
356
|
-
delete meta["workspaces"];
|
|
357
|
-
delete meta["devDependencies"];
|
|
358
|
-
}
|
|
359
|
-
return `${JSON.stringify(meta, null, 2)}\n`;
|
|
360
|
-
}
|
|
361
|
-
/** 把改写结果写回生成项目的 package.json */
|
|
362
|
-
function writePackageJson() {
|
|
363
|
-
const filename = join(rootDir, "package.json");
|
|
364
|
-
const meta = JSON.parse(readFileSync(filename, "utf8"));
|
|
365
|
-
writeFileSync(filename, renderManifest(meta, project, argv.prod === true));
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
434
|
* 写入内置模板(默认路径):静态文件(src/template.ts 的 templateFiles)
|
|
369
435
|
* 加上由 baseManifest() 渲染出的 package.json。纯本地写入,无网络请求。
|
|
370
436
|
*/
|
|
@@ -373,52 +439,25 @@ function scaffoldBuiltin() {
|
|
|
373
439
|
writeFileSync(join(rootDir, "package.json"), renderManifest(baseManifest(), project, argv.prod === true));
|
|
374
440
|
}
|
|
375
441
|
/**
|
|
376
|
-
* 远程模板下载与解包(--template 指定时):
|
|
377
|
-
* 1. 拉取模板包元数据,按 dist-tags 解析目标版本(--ref,默认 latest);
|
|
378
|
-
* 2. 流式下载 tarball 并解包到目标目录(strip: 1 去掉包根目录层级),
|
|
379
|
-
* 网络错误统一以 HttpError 提示后退出;
|
|
380
|
-
* 3. 最后改写 package.json。
|
|
381
|
-
*/
|
|
382
|
-
async function scaffoldRemote(registry) {
|
|
383
|
-
const template = argv.template;
|
|
384
|
-
const ref = argv.ref || "latest";
|
|
385
|
-
try {
|
|
386
|
-
const metaRes = await fetch(`${registry}/${template}`);
|
|
387
|
-
if (!metaRes.ok) throw new HttpError(metaRes.status, metaRes.statusText);
|
|
388
|
-
const remote = await metaRes.json();
|
|
389
|
-
const version = remote["dist-tags"][ref];
|
|
390
|
-
const url = version === void 0 ? void 0 : remote.versions[version]?.dist?.tarball;
|
|
391
|
-
if (url === void 0) throw new HttpError(404, `模板 ${template}@${ref} 不存在`);
|
|
392
|
-
const tarballRes = await fetch(url);
|
|
393
|
-
const body = tarballRes.body;
|
|
394
|
-
if (!tarballRes.ok || !body) throw new HttpError(tarballRes.status, tarballRes.statusText);
|
|
395
|
-
await new Promise((resolve, reject) => {
|
|
396
|
-
Readable.fromWeb(body).pipe(extract({
|
|
397
|
-
cwd: rootDir,
|
|
398
|
-
newer: true,
|
|
399
|
-
strip: 1
|
|
400
|
-
})).on("finish", resolve).on("error", reject);
|
|
401
|
-
});
|
|
402
|
-
} catch (err) {
|
|
403
|
-
if (!(err instanceof HttpError)) throw err;
|
|
404
|
-
console.log(`${kleur.red("error")} 请求失败:HTTP ${err.status} ${err.statusText}`);
|
|
405
|
-
process.exit(1);
|
|
406
|
-
}
|
|
407
|
-
writePackageJson();
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
442
|
* 生成项目的主入口:默认写内置 @koishi-ce 模板(scaffoldBuiltin);
|
|
411
|
-
* --template <包名> 时改为从 npm registry 下载远程模板(
|
|
412
|
-
* registry 取值 --registry 参数 > 本机 npm 配置 > 官方源。
|
|
443
|
+
* --template <包名> 时改为从 npm registry 下载远程模板(remote.ts 的
|
|
444
|
+
* scaffoldRemote),registry 取值 --registry 参数 > 本机 npm 配置 > 官方源。
|
|
413
445
|
*/
|
|
414
446
|
async function scaffold() {
|
|
415
|
-
console.log(
|
|
447
|
+
console.log(pc.dim(" 正在 ") + project + pc.dim(" 中生成项目 ..."));
|
|
416
448
|
if (argv.template) {
|
|
417
449
|
const registry = (argv.registry || getLocalRegistry() || "https://registry.npmjs.org").replace(/\/$/, "");
|
|
418
|
-
console.log(
|
|
419
|
-
await scaffoldRemote(
|
|
450
|
+
console.log(pc.dim(` 使用 registry:${registry}\n`));
|
|
451
|
+
await scaffoldRemote({
|
|
452
|
+
registry,
|
|
453
|
+
template: argv.template,
|
|
454
|
+
ref: argv.ref || "latest",
|
|
455
|
+
prod: argv.prod === true,
|
|
456
|
+
project,
|
|
457
|
+
rootDir
|
|
458
|
+
});
|
|
420
459
|
} else scaffoldBuiltin();
|
|
421
|
-
console.log(
|
|
460
|
+
console.log(pc.green(" 完成。\n"));
|
|
422
461
|
}
|
|
423
462
|
/**
|
|
424
463
|
* 初始化 git 仓库:仅在显式传入 --git 且本机装有 git 时执行,分支名取
|
|
@@ -435,7 +474,7 @@ async function initGit() {
|
|
|
435
474
|
stdio: "ignore",
|
|
436
475
|
cwd: rootDir
|
|
437
476
|
});
|
|
438
|
-
console.log(
|
|
477
|
+
console.log(pc.green(` 已初始化 git 仓库(分支 ${branch})。\n`));
|
|
439
478
|
}
|
|
440
479
|
/**
|
|
441
480
|
* 收尾交互:询问是否立即安装依赖并启动,包管理器由 detectAgent()
|
|
@@ -450,7 +489,7 @@ async function install() {
|
|
|
450
489
|
stdio: "inherit",
|
|
451
490
|
cwd: rootDir
|
|
452
491
|
}).status !== 0) {
|
|
453
|
-
console.log(
|
|
492
|
+
console.log(pc.red(" 依赖安装失败,请检查上方日志。"));
|
|
454
493
|
return;
|
|
455
494
|
}
|
|
456
495
|
spawnSync(agent, startArgs, {
|
|
@@ -458,13 +497,13 @@ async function install() {
|
|
|
458
497
|
cwd: rootDir
|
|
459
498
|
});
|
|
460
499
|
} else {
|
|
461
|
-
console.log(
|
|
500
|
+
console.log(pc.dim(" 稍后可以这样启动:\n"));
|
|
462
501
|
if (rootDir !== cwd) {
|
|
463
502
|
const related = relative(cwd, rootDir);
|
|
464
|
-
console.log(
|
|
503
|
+
console.log(pc.blue(` cd ${pc.bold(related)}`));
|
|
465
504
|
}
|
|
466
|
-
console.log(
|
|
467
|
-
console.log(
|
|
505
|
+
console.log(pc.blue(` ${agent === "yarn" ? "yarn" : `${agent} install`}`));
|
|
506
|
+
console.log(pc.blue(` ${agent === "yarn" ? "yarn" : `${agent} run`} start`));
|
|
468
507
|
console.log();
|
|
469
508
|
}
|
|
470
509
|
}
|
|
@@ -490,7 +529,7 @@ async function start() {
|
|
|
490
529
|
return;
|
|
491
530
|
}
|
|
492
531
|
console.log();
|
|
493
|
-
console.log(` ${
|
|
532
|
+
console.log(` ${pc.bold("Create Koishi")} ${pc.blue(`v${version}`)}`);
|
|
494
533
|
console.log();
|
|
495
534
|
const name = await getName();
|
|
496
535
|
rootDir = join(cwd, name);
|
|
@@ -501,4 +540,4 @@ async function start() {
|
|
|
501
540
|
await install();
|
|
502
541
|
}
|
|
503
542
|
//#endregion
|
|
504
|
-
export {
|
|
543
|
+
export { renderManifest as a, readNpmrcRegistry as i, detectAgent as n, getLocalRegistry as r, start as t };
|