create-koishi-ce 1.3.0 → 1.4.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 +1 -1
- package/lib/index.d.ts +24 -16
- package/lib/index.mjs +1 -1
- package/lib/{src-DiZgIQu5.mjs → src-B1-ko5wA.mjs} +253 -172
- package/package.json +58 -63
- package/src/__tests__/manifest.test.ts +16 -5
- package/src/__tests__/registry.test.ts +34 -10
- package/src/__tests__/run-default.test.ts +96 -39
- package/src/__tests__/run-help.test.ts +18 -4
- package/src/__tests__/run-remote.test.ts +121 -56
- package/src/__tests__/tar-pack.ts +211 -0
- package/src/__tests__/template.test.ts +62 -19
- package/src/index.ts +127 -239
- package/src/manifest.ts +45 -0
- package/src/registry.ts +61 -0
- package/src/remote.ts +145 -0
- package/src/template/README.md +3 -2
- package/src/template/koishi.yml +4 -0
- package/src/template.ts +38 -18
- package/src/utils.ts +52 -0
package/lib/bin.mjs
CHANGED
package/lib/index.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
//#region src/
|
|
1
|
+
//#region src/manifest.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* 模板项目 package.json 的类型与改写(纯函数域,无副作用):定义本流程
|
|
4
|
+
* 触碰字段的类型并提供渲染函数。内置模板(index.ts 的 scaffoldBuiltin
|
|
5
|
+
* 直接渲染写入)与远程模板(remote.ts 读回解包产物后改写)共用这里。
|
|
6
|
+
*/
|
|
2
7
|
/**
|
|
3
|
-
* 模板项目的 package.json
|
|
4
|
-
*
|
|
8
|
+
* 模板项目的 package.json(改写源 / 目标):只需要类型化本流程触碰的
|
|
9
|
+
* 字段,其余字段经 index signature 原样保留。
|
|
5
10
|
*/
|
|
6
11
|
interface Manifest {
|
|
7
12
|
name?: string;
|
|
@@ -13,11 +18,12 @@ interface Manifest {
|
|
|
13
18
|
[key: string]: unknown;
|
|
14
19
|
}
|
|
15
20
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* 本 CLI 自身以 bun 为运行时(bin shebang),能执行即已具备 bun 环境。
|
|
21
|
+
* 改写模板的 package.json(纯函数,导出供单测):替换项目名、标记
|
|
22
|
+
* private、版本归零。
|
|
19
23
|
*/
|
|
20
|
-
declare function
|
|
24
|
+
declare function renderManifest(source: Manifest, project: string, prod: boolean): string;
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/registry.d.ts
|
|
21
27
|
/**
|
|
22
28
|
* 从单个 .npmrc 文件提取 registry 配置项(非注释、非 scoped 的 registry= 行)。
|
|
23
29
|
* 文件不存在或未配置时返回 undefined,读文件异常一律静默吞掉。
|
|
@@ -25,22 +31,24 @@ declare function detectAgent(): string;
|
|
|
25
31
|
declare function readNpmrcRegistry(file: string): string | undefined;
|
|
26
32
|
/**
|
|
27
33
|
* 读取本机 npm registry 配置(优先级对齐 npm 自身:环境变量 > 项目
|
|
28
|
-
* .npmrc > 用户 ~/.npmrc
|
|
29
|
-
*
|
|
30
|
-
* bunx 场景下子进程退出码 1 直接炸掉脚手架;读 npmrc 是零依赖、零
|
|
31
|
-
* 子进程的等价路径。任何一步都拿不到时返回 undefined,由调用方回落
|
|
32
|
-
* 官方源。
|
|
34
|
+
* .npmrc > 用户 ~/.npmrc)。任何一步都拿不到时返回 undefined,由调用方
|
|
35
|
+
* 回落官方源。
|
|
33
36
|
*/
|
|
34
37
|
declare function getLocalRegistry(cwd?: string, userHome?: string): string | undefined;
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/utils.d.ts
|
|
35
40
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
41
|
+
* 探测后续安装/启动使用的包管理器(Bun-first):yarn / pnpm 用户跟随其
|
|
42
|
+
* 生态习惯;其余场景(npm、bun 及探测不到 user-agent)一律走 bun——
|
|
43
|
+
* 本 CLI 自身以 bun 为运行时(bin shebang),能执行即已具备 bun 环境。
|
|
38
44
|
*/
|
|
39
|
-
declare function
|
|
45
|
+
declare function detectAgent(): string;
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/index.d.ts
|
|
40
48
|
/**
|
|
41
49
|
* CLI 主流程:--help 打印用法后即返回;否则依次执行
|
|
42
50
|
* 项目名询问 → prepare(目录准备)→ scaffold(模板解包)→ initGit → install。
|
|
43
51
|
*/
|
|
44
52
|
declare function start(): Promise<void>;
|
|
45
53
|
//#endregion
|
|
46
|
-
export { Manifest, detectAgent, getLocalRegistry, readNpmrcRegistry, renderManifest, start };
|
|
54
|
+
export { type Manifest, detectAgent, getLocalRegistry, readNpmrcRegistry, renderManifest, start };
|
package/lib/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as renderManifest, i as readNpmrcRegistry, n as detectAgent, r as getLocalRegistry, t as start } from "./src-B1-ko5wA.mjs";
|
|
2
2
|
export { detectAgent, getLocalRegistry, readNpmrcRegistry, renderManifest, start };
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import { basename, dirname, join, relative } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { downloadTemplate } from "giget";
|
|
10
9
|
import { fileURLToPath } from "node:url";
|
|
11
10
|
//#region package.json
|
|
12
11
|
var package_default = {
|
|
13
12
|
name: "create-koishi-ce",
|
|
14
13
|
description: "Setup a Koishi application",
|
|
15
|
-
version: "1.
|
|
14
|
+
version: "1.4.0",
|
|
16
15
|
type: "module",
|
|
17
16
|
main: "./lib/index.mjs",
|
|
18
17
|
module: "./lib/index.mjs",
|
|
@@ -41,15 +40,10 @@ var package_default = {
|
|
|
41
40
|
"generator",
|
|
42
41
|
"boilerplate"
|
|
43
42
|
],
|
|
44
|
-
devDependencies: {
|
|
45
|
-
"@types/prompts": "^2.4.9",
|
|
46
|
-
"@types/yargs-parser": "^21.0.3"
|
|
47
|
-
},
|
|
48
43
|
dependencies: {
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"yargs-parser": "^22.0.0"
|
|
44
|
+
"@clack/prompts": "^1.7.0",
|
|
45
|
+
"picocolors": "^1.1.1",
|
|
46
|
+
"giget": "^3.3.1"
|
|
53
47
|
},
|
|
54
48
|
exports: {
|
|
55
49
|
".": {
|
|
@@ -62,6 +56,123 @@ var package_default = {
|
|
|
62
56
|
}
|
|
63
57
|
};
|
|
64
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
|
|
65
176
|
//#region src/template.ts
|
|
66
177
|
/**
|
|
67
178
|
* 内置项目模板(create-koishi-ce 的默认模板)。
|
|
@@ -70,7 +181,7 @@ var package_default = {
|
|
|
70
181
|
* 生成项目,产物依赖全是 npm 官方包(koishi / @koishijs/*),完全绕开了
|
|
71
182
|
* 本仓的 @koishi-ce 再分发生态(官方 market 还带 Bun 下必炸的 get-registry)。
|
|
72
183
|
* 默认模板改为内置的纯 @koishi-ce 依赖集;确需上游官方模板时用
|
|
73
|
-
* --template <包名> 走 npm 远程下载(见
|
|
184
|
+
* --template <包名> 走 npm 远程下载(见 remote.ts 的 scaffoldRemote)。
|
|
74
185
|
*
|
|
75
186
|
* 模板的静态文本一律放在 src/template/ 下的真实文件里(本模块只负责
|
|
76
187
|
* 定位与读取,不再内嵌字符串常量)。npm 包的 files 含 src,因此 lib
|
|
@@ -88,8 +199,10 @@ var package_default = {
|
|
|
88
199
|
* (installer 的 isGuardedRequest 护栏将 npm:@koishi-ce alias 与
|
|
89
200
|
* workspace: 同等保护);
|
|
90
201
|
* - koishi.yml 对齐官方实例的预写策略:控制台与基础插件全量预装(依赖
|
|
91
|
-
* 数据库的保持 ~
|
|
92
|
-
*
|
|
202
|
+
* 数据库的保持 ~ 禁用);database-sqlite 已随模板预装,去掉 ~ 即用;
|
|
203
|
+
* 本仓不再分发的 adapter 官方插件与未再分发的 database(mongo / mysql
|
|
204
|
+
* / postgres)只以 ~ 禁用条目预写、不预装——loader 跳过禁用条目,装
|
|
205
|
+
* 好后在控制台启用;
|
|
93
206
|
* - 依赖版本统一 ^1.0.0 区间(安装时取最新 1.x),shim 版本例外(冻结线)。
|
|
94
207
|
*/
|
|
95
208
|
/**
|
|
@@ -136,10 +249,14 @@ function baseManifest() {
|
|
|
136
249
|
"@koishi-ce/plugin-assets-local": "^1.0.0",
|
|
137
250
|
"@koishi-ce/plugin-auth": "^1.0.0",
|
|
138
251
|
"@koishi-ce/plugin-bind": "^1.0.0",
|
|
252
|
+
"@koishi-ce/plugin-broadcast": "^1.0.0",
|
|
253
|
+
"@koishi-ce/plugin-callme": "^1.0.0",
|
|
139
254
|
"@koishi-ce/plugin-commands": "^1.0.0",
|
|
140
255
|
"@koishi-ce/plugin-config": "^1.0.0",
|
|
141
256
|
"@koishi-ce/plugin-console": "^1.0.0",
|
|
257
|
+
"@koishi-ce/plugin-database-sqlite": "^1.0.0",
|
|
142
258
|
"@koishi-ce/plugin-dataview": "^1.0.0",
|
|
259
|
+
"@koishi-ce/plugin-echo": "^1.0.0",
|
|
143
260
|
"@koishi-ce/plugin-explorer": "^1.0.0",
|
|
144
261
|
"@koishi-ce/plugin-help": "^1.0.0",
|
|
145
262
|
"@koishi-ce/plugin-http": "^1.0.0",
|
|
@@ -165,50 +282,21 @@ function baseManifest() {
|
|
|
165
282
|
devDependencies: {
|
|
166
283
|
"@koishi-ce/client": "^1.0.0",
|
|
167
284
|
"@koishi-ce/plugin-hmr": "^1.0.0",
|
|
285
|
+
"@koishi-ce/plugin-mock": "^1.0.0",
|
|
168
286
|
"@koishi-ce/scripts": "^1.0.0",
|
|
169
287
|
"bun-types": "^1.4.0"
|
|
170
288
|
}
|
|
171
289
|
};
|
|
172
290
|
}
|
|
173
291
|
//#endregion
|
|
174
|
-
//#region src/
|
|
292
|
+
//#region src/utils.ts
|
|
175
293
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* 见 src/template.ts;--template <包名> 可改用 npm registry 远程模板,如
|
|
182
|
-
* 上游官方 @koishijs/boilerplate)→ 按需初始化 git → 询问是否立即安装
|
|
183
|
-
* 依赖并启动。CLI 可执行入口在 src/bin.ts(构建产物 lib/bin.mjs,bin 字段
|
|
184
|
-
* 指向它);本文件只承载主流程与可单测的纯函数(范式对齐
|
|
185
|
-
* @koishi-ce/scripts)。
|
|
294
|
+
* 平台小工具(无业务状态、不触碰 CLI 全局状态的零散函数):包管理器
|
|
295
|
+
* 探测(detectAgent)、git 可用性与配置读取(supports / gitConfig)、
|
|
296
|
+
* 目录清空(emptyDir)。分别被 index.ts 主流程的 install / initGit /
|
|
297
|
+
* prepare 步骤调用,也是除 manifest.ts / registry.ts 之外可脱离交互
|
|
298
|
+
* 直接单测的函数集合。
|
|
186
299
|
*/
|
|
187
|
-
const { version } = package_default;
|
|
188
|
-
/** fetch 请求非 2xx 时抛出的错误,携带 HTTP 状态码与状态文本 */
|
|
189
|
-
var HttpError = class extends Error {
|
|
190
|
-
status;
|
|
191
|
-
statusText;
|
|
192
|
-
constructor(status, statusText) {
|
|
193
|
-
super(`HTTP ${status} ${statusText}`);
|
|
194
|
-
this.status = status;
|
|
195
|
-
this.statusText = statusText;
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
const argv = parse(process.argv.slice(2), { alias: {
|
|
199
|
-
ref: ["r"],
|
|
200
|
-
forced: ["f"],
|
|
201
|
-
git: ["g"],
|
|
202
|
-
prod: ["p"],
|
|
203
|
-
template: ["t"],
|
|
204
|
-
yes: ["y"],
|
|
205
|
-
help: ["h"]
|
|
206
|
-
} });
|
|
207
|
-
/** 项目目录名(rootDir 的最后一段,写入生成项目的 package.json 的 name) */
|
|
208
|
-
let project;
|
|
209
|
-
/** 目标目录的绝对路径(由用户输入的项目名拼接 cwd 得到) */
|
|
210
|
-
let rootDir;
|
|
211
|
-
const cwd = process.cwd();
|
|
212
300
|
/**
|
|
213
301
|
* 探测后续安装/启动使用的包管理器(Bun-first):yarn / pnpm 用户跟随其
|
|
214
302
|
* 生态习惯;其余场景(npm、bun 及探测不到 user-agent)一律走 bun——
|
|
@@ -220,34 +308,6 @@ function detectAgent() {
|
|
|
220
308
|
if (ua.startsWith("pnpm")) return "pnpm";
|
|
221
309
|
return "bun";
|
|
222
310
|
}
|
|
223
|
-
/**
|
|
224
|
-
* 从单个 .npmrc 文件提取 registry 配置项(非注释、非 scoped 的 registry= 行)。
|
|
225
|
-
* 文件不存在或未配置时返回 undefined,读文件异常一律静默吞掉。
|
|
226
|
-
*/
|
|
227
|
-
function readNpmrcRegistry(file) {
|
|
228
|
-
try {
|
|
229
|
-
for (const line of readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
230
|
-
const value = /^\s*registry\s*=\s*(\S+)\s*$/.exec(line)?.[1];
|
|
231
|
-
if (value) return value;
|
|
232
|
-
}
|
|
233
|
-
} catch {}
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* 读取本机 npm registry 配置(优先级对齐 npm 自身:环境变量 > 项目
|
|
237
|
-
* .npmrc > 用户 ~/.npmrc)。刻意不 spawn 子进程探测——既有实现按
|
|
238
|
-
* user-agent 选 `bun config get registry`,而 Bun 没有 config 子命令,
|
|
239
|
-
* bunx 场景下子进程退出码 1 直接炸掉脚手架;读 npmrc 是零依赖、零
|
|
240
|
-
* 子进程的等价路径。任何一步都拿不到时返回 undefined,由调用方回落
|
|
241
|
-
* 官方源。
|
|
242
|
-
*/
|
|
243
|
-
function getLocalRegistry(cwd = process.cwd(), userHome = homedir()) {
|
|
244
|
-
const candidates = [
|
|
245
|
-
process.env["npm_config_registry"],
|
|
246
|
-
readNpmrcRegistry(join(cwd, ".npmrc")),
|
|
247
|
-
readNpmrcRegistry(join(userHome, ".npmrc"))
|
|
248
|
-
];
|
|
249
|
-
for (const candidate of candidates) if (candidate?.startsWith("https://") || candidate?.startsWith("http://")) return candidate;
|
|
250
|
-
}
|
|
251
311
|
/** 静默执行命令探测其是否可用(如 git --version),失败即视为不可用 */
|
|
252
312
|
function supports(command) {
|
|
253
313
|
return spawnSync(command[0] ?? "", command.slice(1), { stdio: "ignore" }).status === 0;
|
|
@@ -261,36 +321,105 @@ function gitConfig(key) {
|
|
|
261
321
|
], { encoding: "utf8" });
|
|
262
322
|
return res.status === 0 ? res.stdout?.trim() ?? "" : "";
|
|
263
323
|
}
|
|
324
|
+
/** 递归清空目录内容(目录本身保留)。 */
|
|
325
|
+
function emptyDir(root) {
|
|
326
|
+
for (const file of readdirSync(root)) rmSync(join(root, file), {
|
|
327
|
+
recursive: true,
|
|
328
|
+
force: true
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/index.ts
|
|
333
|
+
/**
|
|
334
|
+
* create-koishi-ce 脚手架(npm 包名 create-koishi-ce,目录名为
|
|
335
|
+
* apps/koishi-create,二者不一致是历史遗留,以目录名为准)。
|
|
336
|
+
*
|
|
337
|
+
* 通过 `bunx create-koishi-ce [name]`(npx 亦可)交互式创建 Koishi 机器人
|
|
338
|
+
* 应用项目:确定项目名 → 准备目标目录 → 写入内置 @koishi-ce 模板(默认,
|
|
339
|
+
* 见 src/template.ts;--template <包名> 可改用 npm registry 远程模板,如
|
|
340
|
+
* 上游官方 @koishijs/boilerplate)→ 按需初始化 git → 询问是否立即安装
|
|
341
|
+
* 依赖并启动。CLI 可执行入口在 src/bin.ts(构建产物 lib/bin.mjs,bin 字段
|
|
342
|
+
* 指向它)。
|
|
343
|
+
*
|
|
344
|
+
* 本文件是 CLI 的编排层:承载参数解析与运行期状态(argv / cwd /
|
|
345
|
+
* project / rootDir —— 它们被 e2e 测试以模块 query 隔离实例,必须留在
|
|
346
|
+
* 顶层模块内)与交互主流程。纯函数与单一职责的子流程按域拆到同目录
|
|
347
|
+
* 子模块,公共导出面在文件尾原样 re-export(bin 与测试的导入路径不变):
|
|
348
|
+
* - manifest.ts package.json 改写(Manifest / renderManifest)
|
|
349
|
+
* - registry.ts 本机 npm registry 配置探测
|
|
350
|
+
* - utils.ts 平台小工具(包管理器 / git 探测、清空目录)
|
|
351
|
+
* - remote.ts --template 远程模板的下载-解包-改写全流程
|
|
352
|
+
* - template.ts 内置 @koishi-ce 模板(静态文件与 baseManifest)
|
|
353
|
+
*/
|
|
354
|
+
const { version } = package_default;
|
|
355
|
+
const { values, positionals } = parseArgs({
|
|
356
|
+
args: process.argv.slice(2),
|
|
357
|
+
options: {
|
|
358
|
+
registry: { type: "string" },
|
|
359
|
+
ref: {
|
|
360
|
+
type: "string",
|
|
361
|
+
short: "r"
|
|
362
|
+
},
|
|
363
|
+
forced: {
|
|
364
|
+
type: "boolean",
|
|
365
|
+
short: "f"
|
|
366
|
+
},
|
|
367
|
+
git: {
|
|
368
|
+
type: "boolean",
|
|
369
|
+
short: "g"
|
|
370
|
+
},
|
|
371
|
+
prod: {
|
|
372
|
+
type: "boolean",
|
|
373
|
+
short: "p"
|
|
374
|
+
},
|
|
375
|
+
template: {
|
|
376
|
+
type: "string",
|
|
377
|
+
short: "t"
|
|
378
|
+
},
|
|
379
|
+
yes: {
|
|
380
|
+
type: "boolean",
|
|
381
|
+
short: "y"
|
|
382
|
+
},
|
|
383
|
+
help: {
|
|
384
|
+
type: "boolean",
|
|
385
|
+
short: "h"
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
allowPositionals: true
|
|
389
|
+
});
|
|
390
|
+
const argv = {
|
|
391
|
+
...values,
|
|
392
|
+
_: positionals
|
|
393
|
+
};
|
|
394
|
+
/** 项目目录名(rootDir 的最后一段,写入生成项目的 package.json 的 name) */
|
|
395
|
+
let project;
|
|
396
|
+
/** 目标目录的绝对路径(由用户输入的项目名拼接 cwd 得到) */
|
|
397
|
+
let rootDir;
|
|
398
|
+
const cwd = process.cwd();
|
|
264
399
|
/**
|
|
265
400
|
* 获取项目名:优先取第一个位置参数,否则交互式询问(默认 koishi-app)。
|
|
266
|
-
*
|
|
401
|
+
* 用户取消(Ctrl+C)或输入为空时直接退出(不强行兜底默认值)。
|
|
267
402
|
*/
|
|
268
403
|
async function getName() {
|
|
269
404
|
if (argv._[0]) return `${argv._[0]}`;
|
|
270
|
-
const
|
|
271
|
-
type: "text",
|
|
272
|
-
name: "name",
|
|
405
|
+
const answer = await p.text({
|
|
273
406
|
message: "项目名:",
|
|
274
|
-
|
|
275
|
-
|
|
407
|
+
initialValue: "koishi-app",
|
|
408
|
+
validate: (value) => value?.trim() ? void 0 : "项目名不能为空"
|
|
409
|
+
});
|
|
410
|
+
if (p.isCancel(answer)) process.exit(0);
|
|
411
|
+
const trimmed = answer.trim();
|
|
276
412
|
if (!trimmed) process.exit(0);
|
|
277
413
|
return trimmed;
|
|
278
414
|
}
|
|
279
|
-
/**
|
|
280
|
-
function emptyDir(root) {
|
|
281
|
-
for (const file of readdirSync(root)) rmSync(join(root, file), {
|
|
282
|
-
recursive: true,
|
|
283
|
-
force: true
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
/** 交互式确认框:返回用户是否选择了「是」(取消视为否) */
|
|
415
|
+
/** 交互式确认框:返回用户是否选择了「是」(Ctrl+C 取消直接退出) */
|
|
287
416
|
async function confirm(message) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
417
|
+
const answer = await p.confirm({
|
|
418
|
+
message,
|
|
419
|
+
initialValue: true
|
|
420
|
+
});
|
|
421
|
+
if (p.isCancel(answer)) process.exit(0);
|
|
422
|
+
return answer === true;
|
|
294
423
|
}
|
|
295
424
|
/**
|
|
296
425
|
* 准备目标目录:不存在则创建;已存在且非空时,未指定 --forced / --yes
|
|
@@ -303,33 +432,12 @@ async function prepare() {
|
|
|
303
432
|
}
|
|
304
433
|
if (!readdirSync(rootDir).length) return;
|
|
305
434
|
if (!argv.forced && !argv.yes) {
|
|
306
|
-
console.log(
|
|
435
|
+
console.log(pc.yellow(` 目标目录 "${project}" 非空。`));
|
|
307
436
|
if (!await confirm("清空现有文件并继续?")) process.exit(0);
|
|
308
437
|
}
|
|
309
438
|
emptyDir(rootDir);
|
|
310
439
|
}
|
|
311
440
|
/**
|
|
312
|
-
* 改写模板的 package.json(纯函数,导出供单测):替换项目名、标记
|
|
313
|
-
* private、版本归零。
|
|
314
|
-
*/
|
|
315
|
-
function renderManifest(source, project, prod) {
|
|
316
|
-
const meta = { ...source };
|
|
317
|
-
meta["name"] = project;
|
|
318
|
-
meta["private"] = true;
|
|
319
|
-
meta["version"] = "0.0.0";
|
|
320
|
-
if (prod) {
|
|
321
|
-
delete meta["workspaces"];
|
|
322
|
-
delete meta["devDependencies"];
|
|
323
|
-
}
|
|
324
|
-
return `${JSON.stringify(meta, null, 2)}\n`;
|
|
325
|
-
}
|
|
326
|
-
/** 把改写结果写回生成项目的 package.json */
|
|
327
|
-
function writePackageJson() {
|
|
328
|
-
const filename = join(rootDir, "package.json");
|
|
329
|
-
const meta = JSON.parse(readFileSync(filename, "utf8"));
|
|
330
|
-
writeFileSync(filename, renderManifest(meta, project, argv.prod === true));
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
441
|
* 写入内置模板(默认路径):静态文件(src/template.ts 的 templateFiles)
|
|
334
442
|
* 加上由 baseManifest() 渲染出的 package.json。纯本地写入,无网络请求。
|
|
335
443
|
*/
|
|
@@ -338,52 +446,25 @@ function scaffoldBuiltin() {
|
|
|
338
446
|
writeFileSync(join(rootDir, "package.json"), renderManifest(baseManifest(), project, argv.prod === true));
|
|
339
447
|
}
|
|
340
448
|
/**
|
|
341
|
-
* 远程模板下载与解包(--template 指定时):
|
|
342
|
-
* 1. 拉取模板包元数据,按 dist-tags 解析目标版本(--ref,默认 latest);
|
|
343
|
-
* 2. 流式下载 tarball 并解包到目标目录(strip: 1 去掉包根目录层级),
|
|
344
|
-
* 网络错误统一以 HttpError 提示后退出;
|
|
345
|
-
* 3. 最后改写 package.json。
|
|
346
|
-
*/
|
|
347
|
-
async function scaffoldRemote(registry) {
|
|
348
|
-
const template = argv.template;
|
|
349
|
-
const ref = argv.ref || "latest";
|
|
350
|
-
try {
|
|
351
|
-
const metaRes = await fetch(`${registry}/${template}`);
|
|
352
|
-
if (!metaRes.ok) throw new HttpError(metaRes.status, metaRes.statusText);
|
|
353
|
-
const remote = await metaRes.json();
|
|
354
|
-
const version = remote["dist-tags"][ref];
|
|
355
|
-
const url = version === void 0 ? void 0 : remote.versions[version]?.dist?.tarball;
|
|
356
|
-
if (url === void 0) throw new HttpError(404, `模板 ${template}@${ref} 不存在`);
|
|
357
|
-
const tarballRes = await fetch(url);
|
|
358
|
-
const body = tarballRes.body;
|
|
359
|
-
if (!tarballRes.ok || !body) throw new HttpError(tarballRes.status, tarballRes.statusText);
|
|
360
|
-
await new Promise((resolve, reject) => {
|
|
361
|
-
Readable.fromWeb(body).pipe(extract({
|
|
362
|
-
cwd: rootDir,
|
|
363
|
-
newer: true,
|
|
364
|
-
strip: 1
|
|
365
|
-
})).on("finish", resolve).on("error", reject);
|
|
366
|
-
});
|
|
367
|
-
} catch (err) {
|
|
368
|
-
if (!(err instanceof HttpError)) throw err;
|
|
369
|
-
console.log(`${kleur.red("error")} 请求失败:HTTP ${err.status} ${err.statusText}`);
|
|
370
|
-
process.exit(1);
|
|
371
|
-
}
|
|
372
|
-
writePackageJson();
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
449
|
* 生成项目的主入口:默认写内置 @koishi-ce 模板(scaffoldBuiltin);
|
|
376
|
-
* --template <包名> 时改为从 npm registry 下载远程模板(
|
|
377
|
-
* registry 取值 --registry 参数 > 本机 npm 配置 > 官方源。
|
|
450
|
+
* --template <包名> 时改为从 npm registry 下载远程模板(remote.ts 的
|
|
451
|
+
* scaffoldRemote),registry 取值 --registry 参数 > 本机 npm 配置 > 官方源。
|
|
378
452
|
*/
|
|
379
453
|
async function scaffold() {
|
|
380
|
-
console.log(
|
|
454
|
+
console.log(pc.dim(" 正在 ") + project + pc.dim(" 中生成项目 ..."));
|
|
381
455
|
if (argv.template) {
|
|
382
456
|
const registry = (argv.registry || getLocalRegistry() || "https://registry.npmjs.org").replace(/\/$/, "");
|
|
383
|
-
console.log(
|
|
384
|
-
await scaffoldRemote(
|
|
457
|
+
console.log(pc.dim(` 使用 registry:${registry}\n`));
|
|
458
|
+
await scaffoldRemote({
|
|
459
|
+
registry,
|
|
460
|
+
template: argv.template,
|
|
461
|
+
ref: argv.ref || "latest",
|
|
462
|
+
prod: argv.prod === true,
|
|
463
|
+
project,
|
|
464
|
+
rootDir
|
|
465
|
+
});
|
|
385
466
|
} else scaffoldBuiltin();
|
|
386
|
-
console.log(
|
|
467
|
+
console.log(pc.green(" 完成。\n"));
|
|
387
468
|
}
|
|
388
469
|
/**
|
|
389
470
|
* 初始化 git 仓库:仅在显式传入 --git 且本机装有 git 时执行,分支名取
|
|
@@ -400,7 +481,7 @@ async function initGit() {
|
|
|
400
481
|
stdio: "ignore",
|
|
401
482
|
cwd: rootDir
|
|
402
483
|
});
|
|
403
|
-
console.log(
|
|
484
|
+
console.log(pc.green(` 已初始化 git 仓库(分支 ${branch})。\n`));
|
|
404
485
|
}
|
|
405
486
|
/**
|
|
406
487
|
* 收尾交互:询问是否立即安装依赖并启动,包管理器由 detectAgent()
|
|
@@ -415,7 +496,7 @@ async function install() {
|
|
|
415
496
|
stdio: "inherit",
|
|
416
497
|
cwd: rootDir
|
|
417
498
|
}).status !== 0) {
|
|
418
|
-
console.log(
|
|
499
|
+
console.log(pc.red(" 依赖安装失败,请检查上方日志。"));
|
|
419
500
|
return;
|
|
420
501
|
}
|
|
421
502
|
spawnSync(agent, startArgs, {
|
|
@@ -423,13 +504,13 @@ async function install() {
|
|
|
423
504
|
cwd: rootDir
|
|
424
505
|
});
|
|
425
506
|
} else {
|
|
426
|
-
console.log(
|
|
507
|
+
console.log(pc.dim(" 稍后可以这样启动:\n"));
|
|
427
508
|
if (rootDir !== cwd) {
|
|
428
509
|
const related = relative(cwd, rootDir);
|
|
429
|
-
console.log(
|
|
510
|
+
console.log(pc.blue(` cd ${pc.bold(related)}`));
|
|
430
511
|
}
|
|
431
|
-
console.log(
|
|
432
|
-
console.log(
|
|
512
|
+
console.log(pc.blue(` ${agent === "yarn" ? "yarn" : `${agent} install`}`));
|
|
513
|
+
console.log(pc.blue(` ${agent === "yarn" ? "yarn" : `${agent} run`} start`));
|
|
433
514
|
console.log();
|
|
434
515
|
}
|
|
435
516
|
}
|
|
@@ -455,7 +536,7 @@ async function start() {
|
|
|
455
536
|
return;
|
|
456
537
|
}
|
|
457
538
|
console.log();
|
|
458
|
-
console.log(` ${
|
|
539
|
+
console.log(` ${pc.bold("Create Koishi")} ${pc.blue(`v${version}`)}`);
|
|
459
540
|
console.log();
|
|
460
541
|
const name = await getName();
|
|
461
542
|
rootDir = join(cwd, name);
|
|
@@ -466,4 +547,4 @@ async function start() {
|
|
|
466
547
|
await install();
|
|
467
548
|
}
|
|
468
549
|
//#endregion
|
|
469
|
-
export {
|
|
550
|
+
export { renderManifest as a, readNpmrcRegistry as i, detectAgent as n, getLocalRegistry as r, start as t };
|