create-koishi-ce 1.3.1 → 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.mjs +1 -1
- package/lib/{src-Dh_67mkm.mjs → src-B1-ko5wA.mjs} +10 -3
- package/package.json +1 -1
- package/src/__tests__/manifest.test.ts +16 -5
- package/src/__tests__/registry.test.ts +34 -10
- package/src/__tests__/run-default.test.ts +89 -30
- package/src/__tests__/run-help.test.ts +18 -4
- package/src/__tests__/run-remote.test.ts +75 -26
- package/src/__tests__/tar-pack.ts +33 -10
- package/src/__tests__/template.test.ts +62 -19
- package/src/index.ts +73 -19
- package/src/registry.ts +13 -4
- package/src/remote.ts +33 -8
- package/src/template/README.md +3 -2
- package/src/template/koishi.yml +4 -0
- package/src/template.ts +36 -16
- package/src/utils.ts +10 -4
|
@@ -34,20 +34,26 @@ function paxRecord(key: string, value: string): string {
|
|
|
34
34
|
let length = body.length + 2;
|
|
35
35
|
for (;;) {
|
|
36
36
|
const prefix = `${length} `;
|
|
37
|
-
if (prefix.length + body.length === length)
|
|
37
|
+
if (prefix.length + body.length === length)
|
|
38
|
+
return `${prefix}${body}`;
|
|
38
39
|
length = prefix.length + body.length;
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
/** 收集条目父目录(含显式目录自身),保证目录条目先于引用内容出现 */
|
|
43
|
-
function collectDirs(
|
|
44
|
+
function collectDirs(
|
|
45
|
+
entries: readonly TarEntry[],
|
|
46
|
+
): string[] {
|
|
44
47
|
const dirs = new Set<string>();
|
|
45
48
|
for (const entry of entries) {
|
|
46
49
|
if (entry.type === "dir" || entry.path.endsWith("/")) {
|
|
47
50
|
dirs.add(entry.path.replace(/\/+$/, ""));
|
|
48
51
|
continue;
|
|
49
52
|
}
|
|
50
|
-
let parent = entry.path.slice(
|
|
53
|
+
let parent = entry.path.slice(
|
|
54
|
+
0,
|
|
55
|
+
entry.path.lastIndexOf("/"),
|
|
56
|
+
);
|
|
51
57
|
while (parent) {
|
|
52
58
|
dirs.add(parent);
|
|
53
59
|
const index = parent.lastIndexOf("/");
|
|
@@ -78,7 +84,10 @@ function createHeader(
|
|
|
78
84
|
header.set(encoder.encode("00"), 263);
|
|
79
85
|
// 校验和:先以空格占位,再求和回填(八进制 6 位 + \0 + 空格)
|
|
80
86
|
header.fill(0x20, 148, 156);
|
|
81
|
-
const sum = header.reduce(
|
|
87
|
+
const sum = header.reduce(
|
|
88
|
+
(total, byte) => total + byte,
|
|
89
|
+
0,
|
|
90
|
+
);
|
|
82
91
|
const checksum = `${sum.toString(8).padStart(6, "0")}\0 `;
|
|
83
92
|
header.set(encoder.encode(checksum), 148);
|
|
84
93
|
return header;
|
|
@@ -111,7 +120,10 @@ function writeOctal(
|
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
/** 追加内容块并按 512 补齐 */
|
|
114
|
-
function pushPadded(
|
|
123
|
+
function pushPadded(
|
|
124
|
+
blocks: Uint8Array[],
|
|
125
|
+
content: Uint8Array,
|
|
126
|
+
): void {
|
|
115
127
|
blocks.push(content);
|
|
116
128
|
const rest = content.length % BLOCK;
|
|
117
129
|
if (rest) blocks.push(new Uint8Array(BLOCK - rest));
|
|
@@ -138,10 +150,14 @@ function pushEntry(
|
|
|
138
150
|
// pax 扩展头自身也须有非空 name(node-tar 校验「path is
|
|
139
151
|
// required」,空名会把整条 x 头判为无效并导致后续流错位);
|
|
140
152
|
// PaxHeader/ 前缀与 node-tar pack 的命名约定一致
|
|
141
|
-
blocks.push(
|
|
153
|
+
blocks.push(
|
|
154
|
+
createHeader(`PaxHeader/${name}`, "x", body.length),
|
|
155
|
+
);
|
|
142
156
|
pushPadded(blocks, body);
|
|
143
157
|
}
|
|
144
|
-
blocks.push(
|
|
158
|
+
blocks.push(
|
|
159
|
+
createHeader(name.slice(0, 100), type, size, link),
|
|
160
|
+
);
|
|
145
161
|
if (size) pushPadded(blocks, content);
|
|
146
162
|
}
|
|
147
163
|
|
|
@@ -157,8 +173,12 @@ export async function tarPack(
|
|
|
157
173
|
blocks.push(createHeader(`${dir}/`, "5", 0));
|
|
158
174
|
}
|
|
159
175
|
for (const entry of entries) {
|
|
160
|
-
if (entry.type === "dir" || entry.path.endsWith("/"))
|
|
161
|
-
|
|
176
|
+
if (entry.type === "dir" || entry.path.endsWith("/"))
|
|
177
|
+
continue;
|
|
178
|
+
if (
|
|
179
|
+
entry.type === "symlink" ||
|
|
180
|
+
entry.type === "hardlink"
|
|
181
|
+
) {
|
|
162
182
|
const type = entry.type === "symlink" ? "2" : "1";
|
|
163
183
|
pushEntry(
|
|
164
184
|
blocks,
|
|
@@ -175,7 +195,10 @@ export async function tarPack(
|
|
|
175
195
|
}
|
|
176
196
|
blocks.push(new Uint8Array(BLOCK * 2)); // 归档结束标志(全零块)
|
|
177
197
|
|
|
178
|
-
const total = blocks.reduce(
|
|
198
|
+
const total = blocks.reduce(
|
|
199
|
+
(sum, block) => sum + block.length,
|
|
200
|
+
0,
|
|
201
|
+
);
|
|
179
202
|
const raw = new Uint8Array(total);
|
|
180
203
|
let cursor = 0;
|
|
181
204
|
for (const block of blocks) {
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
import { expect, test } from "bun:test";
|
|
5
5
|
import { renderManifest } from "../index.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
baseManifest,
|
|
8
|
+
templateFiles,
|
|
9
|
+
} from "../template.ts";
|
|
7
10
|
|
|
8
11
|
test("内置模板依赖纯度:不含任何 @koishijs / koishi-plugin 官方生态包", () => {
|
|
9
12
|
const { dependencies, devDependencies } = baseManifest();
|
|
@@ -16,10 +19,14 @@ test("内置模板依赖纯度:不含任何 @koishijs / koishi-plugin 官方
|
|
|
16
19
|
"@koishijs/loader",
|
|
17
20
|
]);
|
|
18
21
|
for (const key of Object.keys(dependencies ?? {})) {
|
|
19
|
-
expect(
|
|
22
|
+
expect(
|
|
23
|
+
guarded.has(key) || key.startsWith("@koishi-ce/"),
|
|
24
|
+
).toBe(true);
|
|
20
25
|
}
|
|
21
26
|
for (const key of Object.keys(devDependencies ?? {})) {
|
|
22
|
-
expect(
|
|
27
|
+
expect(
|
|
28
|
+
key === "bun-types" || key.startsWith("@koishi-ce/"),
|
|
29
|
+
).toBe(true);
|
|
23
30
|
}
|
|
24
31
|
});
|
|
25
32
|
|
|
@@ -29,10 +36,16 @@ test("内置模板 packageManager 钉 Bun,脚本用 Bun Shell 环境变量前
|
|
|
29
36
|
scripts: Record<string, string>;
|
|
30
37
|
};
|
|
31
38
|
// 本项目只支持 Bun 运行时:packageManager 形如 bun@1.x.y
|
|
32
|
-
expect(manifest["packageManager"] ?? "").toMatch(
|
|
39
|
+
expect(manifest["packageManager"] ?? "").toMatch(
|
|
40
|
+
/^bun@\d+\.\d+\.\d+/,
|
|
41
|
+
);
|
|
33
42
|
// cross-env 已移除:bun run 走 Bun Shell,环境变量前缀天然跨平台
|
|
34
|
-
expect(scripts["dev"]).toBe(
|
|
35
|
-
|
|
43
|
+
expect(scripts["dev"]).toBe(
|
|
44
|
+
"NODE_ENV=development koishi start",
|
|
45
|
+
);
|
|
46
|
+
expect(JSON.stringify(manifest)).not.toContain(
|
|
47
|
+
"cross-env",
|
|
48
|
+
);
|
|
36
49
|
});
|
|
37
50
|
|
|
38
51
|
test("内置模板以 npm alias 钉住 koishi 裸名,版本保持 4.18.x 冻结线", () => {
|
|
@@ -40,7 +53,9 @@ test("内置模板以 npm alias 钉住 koishi 裸名,版本保持 4.18.x 冻
|
|
|
40
53
|
expect(typeof request).toBe("string");
|
|
41
54
|
// alias 形态 npm:@koishi-ce/koishi-shim@^4.18.x——版本若漂出 ^4 区间,
|
|
42
55
|
// 上游插件的 peer `koishi ^4` 判定为不满足,market UI 会试图改写它
|
|
43
|
-
expect(request ?? "").toMatch(
|
|
56
|
+
expect(request ?? "").toMatch(
|
|
57
|
+
/^npm:@koishi-ce\/koishi-shim@\^4\./,
|
|
58
|
+
);
|
|
44
59
|
});
|
|
45
60
|
|
|
46
61
|
test("内置模板钉住 console / core / loader 上游 peer 名,防 Bun 自动装官方全家桶", () => {
|
|
@@ -68,49 +83,73 @@ test("内置模板静态文件齐备,koishi.yml 预配 market 镜像源", () =
|
|
|
68
83
|
"tsconfig.json",
|
|
69
84
|
"README.md",
|
|
70
85
|
]) {
|
|
71
|
-
expect(
|
|
86
|
+
expect(
|
|
87
|
+
templateFiles[file]?.length ?? 0,
|
|
88
|
+
).toBeGreaterThan(0);
|
|
72
89
|
}
|
|
73
90
|
expect(templateFiles["koishi.yml"]).toContain(
|
|
74
91
|
"endpoint: https://registry.koishi.chat/index.json",
|
|
75
92
|
);
|
|
76
93
|
});
|
|
77
94
|
|
|
78
|
-
test("koishi.yml 预写策略对齐官方实例:CE
|
|
95
|
+
test("koishi.yml 预写策略对齐官方实例:CE 插件全量预装(依赖数据库者禁用),官方 adapter / 未分发 database 只写不装", () => {
|
|
79
96
|
const yml = templateFiles["koishi.yml"] ?? "";
|
|
80
97
|
// 依赖数据库 / 暂无需启用的 CE 插件:预装但 ~ 禁用
|
|
81
98
|
for (const name of [
|
|
82
99
|
"~admin",
|
|
83
100
|
"~bind",
|
|
101
|
+
"~broadcast",
|
|
102
|
+
"~callme",
|
|
84
103
|
"~analytics",
|
|
85
104
|
"~auth",
|
|
86
105
|
"~dataview",
|
|
87
106
|
"~rate-limit",
|
|
88
107
|
"~inspect",
|
|
89
108
|
"~server-temp",
|
|
109
|
+
"~mock",
|
|
90
110
|
]) {
|
|
91
111
|
expect(yml).toContain(name);
|
|
92
112
|
}
|
|
93
113
|
// 无需数据库即可工作的 CE 插件:直接启用
|
|
94
|
-
for (const name of [
|
|
114
|
+
for (const name of [
|
|
115
|
+
"assets-local",
|
|
116
|
+
"echo",
|
|
117
|
+
"theme-vanilla",
|
|
118
|
+
"status",
|
|
119
|
+
"sandbox",
|
|
120
|
+
]) {
|
|
95
121
|
expect(yml).toContain(name);
|
|
96
122
|
}
|
|
97
|
-
//
|
|
123
|
+
// database-sqlite 已随模板预装:依赖在册 + yml 保持 ~ 禁用(启用即可用)
|
|
124
|
+
expect(
|
|
125
|
+
baseManifest().dependencies?.[
|
|
126
|
+
"@koishi-ce/plugin-database-sqlite"
|
|
127
|
+
],
|
|
128
|
+
).toBe("^1.0.0");
|
|
129
|
+
expect(yml).toContain("~database-sqlite");
|
|
130
|
+
// 官方 adapter / 未再分发的 database 只以 ~ 禁用条目预写(未预装,市场装后启用)
|
|
98
131
|
for (const name of [
|
|
99
132
|
"~adapter-discord",
|
|
100
133
|
"~adapter-telegram",
|
|
101
134
|
"~adapter-qq",
|
|
102
|
-
"~database-
|
|
135
|
+
"~database-mongo",
|
|
103
136
|
"~database-postgres",
|
|
104
137
|
]) {
|
|
105
138
|
expect(yml).toContain(name);
|
|
106
139
|
}
|
|
107
140
|
// 模板依赖里不得出现官方 adapter / database 包名(只预写不预装)
|
|
108
|
-
expect(JSON.stringify(baseManifest())).not.toContain(
|
|
109
|
-
|
|
141
|
+
expect(JSON.stringify(baseManifest())).not.toContain(
|
|
142
|
+
"adapter-",
|
|
143
|
+
);
|
|
144
|
+
expect(JSON.stringify(baseManifest())).not.toContain(
|
|
145
|
+
"@koishijs/plugin-database-",
|
|
146
|
+
);
|
|
110
147
|
});
|
|
111
148
|
|
|
112
149
|
test("renderManifest 渲染内置模板:常规改写生效,prod 模式保留 koishi alias", () => {
|
|
113
|
-
const output = JSON.parse(
|
|
150
|
+
const output = JSON.parse(
|
|
151
|
+
renderManifest(baseManifest(), "my-app", false),
|
|
152
|
+
);
|
|
114
153
|
expect(output.name).toBe("my-app");
|
|
115
154
|
expect(output.private).toBe(true);
|
|
116
155
|
expect(output.version).toBe("0.0.0");
|
|
@@ -120,14 +159,18 @@ test("renderManifest 渲染内置模板:常规改写生效,prod 模式保留
|
|
|
120
159
|
"npm:@koishi-ce/koishi-shim@^4.18.11",
|
|
121
160
|
);
|
|
122
161
|
|
|
123
|
-
const prod = JSON.parse(
|
|
162
|
+
const prod = JSON.parse(
|
|
163
|
+
renderManifest(baseManifest(), "my-app", true),
|
|
164
|
+
);
|
|
124
165
|
expect("workspaces" in prod).toBe(false);
|
|
125
166
|
expect("devDependencies" in prod).toBe(false);
|
|
126
167
|
// 四个上游名 alias 是运行时依赖,prod 模式下必须原样保留
|
|
127
|
-
expect(prod.dependencies.koishi).toBe(
|
|
128
|
-
|
|
129
|
-
"npm:@koishi-ce/console-shim@^5.30.11",
|
|
168
|
+
expect(prod.dependencies.koishi).toBe(
|
|
169
|
+
"npm:@koishi-ce/koishi-shim@^4.18.11",
|
|
130
170
|
);
|
|
171
|
+
expect(
|
|
172
|
+
prod.dependencies["@koishijs/plugin-console"],
|
|
173
|
+
).toBe("npm:@koishi-ce/console-shim@^5.30.11");
|
|
131
174
|
expect(prod.dependencies["@koishijs/core"]).toBe(
|
|
132
175
|
"npm:@koishi-ce/koishi-shim@4.18.11",
|
|
133
176
|
);
|
package/src/index.ts
CHANGED
|
@@ -23,24 +23,45 @@
|
|
|
23
23
|
* - template.ts 内置 @koishi-ce 模板(静态文件与 baseManifest)
|
|
24
24
|
*/
|
|
25
25
|
import { spawnSync } from "node:child_process";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
existsSync,
|
|
28
|
+
mkdirSync,
|
|
29
|
+
readdirSync,
|
|
30
|
+
writeFileSync,
|
|
31
|
+
} from "node:fs";
|
|
27
32
|
import { basename, join, relative } from "node:path";
|
|
28
33
|
import { parseArgs } from "node:util";
|
|
29
34
|
import * as p from "@clack/prompts";
|
|
30
35
|
import pc from "picocolors";
|
|
31
36
|
import pkg from "../package.json" with { type: "json" };
|
|
32
|
-
import {
|
|
33
|
-
|
|
37
|
+
import {
|
|
38
|
+
type Manifest,
|
|
39
|
+
renderManifest,
|
|
40
|
+
} from "./manifest.ts";
|
|
41
|
+
import {
|
|
42
|
+
getLocalRegistry,
|
|
43
|
+
readNpmrcRegistry,
|
|
44
|
+
} from "./registry.ts";
|
|
34
45
|
import { scaffoldRemote } from "./remote.ts";
|
|
35
46
|
import { baseManifest, templateFiles } from "./template.ts";
|
|
36
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
detectAgent,
|
|
49
|
+
emptyDir,
|
|
50
|
+
gitConfig,
|
|
51
|
+
supports,
|
|
52
|
+
} from "./utils.ts";
|
|
37
53
|
|
|
38
54
|
const { version } = pkg;
|
|
39
55
|
|
|
40
56
|
export type { Manifest };
|
|
41
57
|
// 公共导出面:与拆分前保持一致(bin.ts 与测试文件按 ../index.ts 的既有
|
|
42
58
|
// 导入路径原样可用);子模块的其余导出属于包内部实现,不在此暴露。
|
|
43
|
-
export {
|
|
59
|
+
export {
|
|
60
|
+
detectAgent,
|
|
61
|
+
getLocalRegistry,
|
|
62
|
+
readNpmrcRegistry,
|
|
63
|
+
renderManifest,
|
|
64
|
+
};
|
|
44
65
|
|
|
45
66
|
/** CLI 参数(node:util parseArgs 解析,别名映射见 bin 帮助文本) */
|
|
46
67
|
interface Args {
|
|
@@ -92,7 +113,8 @@ async function getName(): Promise<string> {
|
|
|
92
113
|
const answer = await p.text({
|
|
93
114
|
message: "项目名:",
|
|
94
115
|
initialValue: "koishi-app",
|
|
95
|
-
validate: (value) =>
|
|
116
|
+
validate: (value) =>
|
|
117
|
+
value?.trim() ? undefined : "项目名不能为空",
|
|
96
118
|
});
|
|
97
119
|
if (p.isCancel(answer)) process.exit(0);
|
|
98
120
|
const trimmed = answer.trim();
|
|
@@ -102,7 +124,10 @@ async function getName(): Promise<string> {
|
|
|
102
124
|
|
|
103
125
|
/** 交互式确认框:返回用户是否选择了「是」(Ctrl+C 取消直接退出) */
|
|
104
126
|
async function confirm(message: string) {
|
|
105
|
-
const answer = await p.confirm({
|
|
127
|
+
const answer = await p.confirm({
|
|
128
|
+
message,
|
|
129
|
+
initialValue: true,
|
|
130
|
+
});
|
|
106
131
|
if (p.isCancel(answer)) process.exit(0);
|
|
107
132
|
return answer === true;
|
|
108
133
|
}
|
|
@@ -121,7 +146,9 @@ async function prepare() {
|
|
|
121
146
|
if (!files.length) return;
|
|
122
147
|
|
|
123
148
|
if (!argv.forced && !argv.yes) {
|
|
124
|
-
console.log(
|
|
149
|
+
console.log(
|
|
150
|
+
pc.yellow(` 目标目录 "${project}" 非空。`),
|
|
151
|
+
);
|
|
125
152
|
const yes = await confirm("清空现有文件并继续?");
|
|
126
153
|
if (!yes) process.exit(0);
|
|
127
154
|
}
|
|
@@ -134,12 +161,18 @@ async function prepare() {
|
|
|
134
161
|
* 加上由 baseManifest() 渲染出的 package.json。纯本地写入,无网络请求。
|
|
135
162
|
*/
|
|
136
163
|
function scaffoldBuiltin() {
|
|
137
|
-
for (const [file, content] of Object.entries(
|
|
164
|
+
for (const [file, content] of Object.entries(
|
|
165
|
+
templateFiles,
|
|
166
|
+
)) {
|
|
138
167
|
writeFileSync(join(rootDir, file), content);
|
|
139
168
|
}
|
|
140
169
|
writeFileSync(
|
|
141
170
|
join(rootDir, "package.json"),
|
|
142
|
-
renderManifest(
|
|
171
|
+
renderManifest(
|
|
172
|
+
baseManifest(),
|
|
173
|
+
project,
|
|
174
|
+
argv.prod === true,
|
|
175
|
+
),
|
|
143
176
|
);
|
|
144
177
|
}
|
|
145
178
|
|
|
@@ -149,7 +182,9 @@ function scaffoldBuiltin() {
|
|
|
149
182
|
* scaffoldRemote),registry 取值 --registry 参数 > 本机 npm 配置 > 官方源。
|
|
150
183
|
*/
|
|
151
184
|
async function scaffold() {
|
|
152
|
-
console.log(
|
|
185
|
+
console.log(
|
|
186
|
+
pc.dim(" 正在 ") + project + pc.dim(" 中生成项目 ..."),
|
|
187
|
+
);
|
|
153
188
|
|
|
154
189
|
if (argv.template) {
|
|
155
190
|
const registry = (
|
|
@@ -181,8 +216,13 @@ async function scaffold() {
|
|
|
181
216
|
async function initGit() {
|
|
182
217
|
if (!argv.git || !supports(["git", "--version"])) return;
|
|
183
218
|
const branch = gitConfig("init.defaultBranch") || "main";
|
|
184
|
-
spawnSync("git", ["init", "-b", branch], {
|
|
185
|
-
|
|
219
|
+
spawnSync("git", ["init", "-b", branch], {
|
|
220
|
+
stdio: "ignore",
|
|
221
|
+
cwd: rootDir,
|
|
222
|
+
});
|
|
223
|
+
console.log(
|
|
224
|
+
pc.green(` 已初始化 git 仓库(分支 ${branch})。\n`),
|
|
225
|
+
);
|
|
186
226
|
}
|
|
187
227
|
|
|
188
228
|
/**
|
|
@@ -194,7 +234,8 @@ async function install() {
|
|
|
194
234
|
if (argv.yes) return;
|
|
195
235
|
|
|
196
236
|
const agent = detectAgent();
|
|
197
|
-
const startArgs =
|
|
237
|
+
const startArgs =
|
|
238
|
+
agent === "yarn" ? ["start"] : ["run", "start"];
|
|
198
239
|
const yes = await confirm("现在安装依赖并启动吗?");
|
|
199
240
|
if (yes) {
|
|
200
241
|
const installed = spawnSync(agent, ["install"], {
|
|
@@ -202,19 +243,30 @@ async function install() {
|
|
|
202
243
|
cwd: rootDir,
|
|
203
244
|
});
|
|
204
245
|
if (installed.status !== 0) {
|
|
205
|
-
console.log(
|
|
246
|
+
console.log(
|
|
247
|
+
pc.red(" 依赖安装失败,请检查上方日志。"),
|
|
248
|
+
);
|
|
206
249
|
return;
|
|
207
250
|
}
|
|
208
|
-
spawnSync(agent, startArgs, {
|
|
251
|
+
spawnSync(agent, startArgs, {
|
|
252
|
+
stdio: "inherit",
|
|
253
|
+
cwd: rootDir,
|
|
254
|
+
});
|
|
209
255
|
} else {
|
|
210
256
|
console.log(pc.dim(" 稍后可以这样启动:\n"));
|
|
211
257
|
if (rootDir !== cwd) {
|
|
212
258
|
const related = relative(cwd, rootDir);
|
|
213
259
|
console.log(pc.blue(` cd ${pc.bold(related)}`));
|
|
214
260
|
}
|
|
215
|
-
console.log(pc.blue(` ${agent === "yarn" ? "yarn" : `${agent} install`}`));
|
|
216
261
|
console.log(
|
|
217
|
-
pc.blue(
|
|
262
|
+
pc.blue(
|
|
263
|
+
` ${agent === "yarn" ? "yarn" : `${agent} install`}`,
|
|
264
|
+
),
|
|
265
|
+
);
|
|
266
|
+
console.log(
|
|
267
|
+
pc.blue(
|
|
268
|
+
` ${agent === "yarn" ? "yarn" : `${agent} run`} start`,
|
|
269
|
+
),
|
|
218
270
|
);
|
|
219
271
|
console.log();
|
|
220
272
|
}
|
|
@@ -243,7 +295,9 @@ export async function start() {
|
|
|
243
295
|
}
|
|
244
296
|
|
|
245
297
|
console.log();
|
|
246
|
-
console.log(
|
|
298
|
+
console.log(
|
|
299
|
+
` ${pc.bold("Create Koishi")} ${pc.blue(`v${version}`)}`,
|
|
300
|
+
);
|
|
247
301
|
console.log();
|
|
248
302
|
|
|
249
303
|
const name = await getName();
|
package/src/registry.ts
CHANGED
|
@@ -16,10 +16,16 @@ import { join } from "node:path";
|
|
|
16
16
|
* 从单个 .npmrc 文件提取 registry 配置项(非注释、非 scoped 的 registry= 行)。
|
|
17
17
|
* 文件不存在或未配置时返回 undefined,读文件异常一律静默吞掉。
|
|
18
18
|
*/
|
|
19
|
-
export function readNpmrcRegistry(
|
|
19
|
+
export function readNpmrcRegistry(
|
|
20
|
+
file: string,
|
|
21
|
+
): string | undefined {
|
|
20
22
|
try {
|
|
21
|
-
for (const line of readFileSync(file, "utf8").split(
|
|
22
|
-
|
|
23
|
+
for (const line of readFileSync(file, "utf8").split(
|
|
24
|
+
/\r?\n/,
|
|
25
|
+
)) {
|
|
26
|
+
const matched = /^\s*registry\s*=\s*(\S+)\s*$/.exec(
|
|
27
|
+
line,
|
|
28
|
+
);
|
|
23
29
|
const value = matched?.[1];
|
|
24
30
|
if (value) return value;
|
|
25
31
|
}
|
|
@@ -44,7 +50,10 @@ export function getLocalRegistry(
|
|
|
44
50
|
readNpmrcRegistry(join(userHome, ".npmrc")),
|
|
45
51
|
];
|
|
46
52
|
for (const candidate of candidates) {
|
|
47
|
-
if (
|
|
53
|
+
if (
|
|
54
|
+
candidate?.startsWith("https://") ||
|
|
55
|
+
candidate?.startsWith("http://")
|
|
56
|
+
) {
|
|
48
57
|
return candidate;
|
|
49
58
|
}
|
|
50
59
|
}
|
package/src/remote.ts
CHANGED
|
@@ -16,7 +16,10 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { downloadTemplate } from "giget";
|
|
18
18
|
import pc from "picocolors";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
type Manifest,
|
|
21
|
+
renderManifest,
|
|
22
|
+
} from "./manifest.ts";
|
|
20
23
|
|
|
21
24
|
/** registry 包元数据中本流程消费的字段 */
|
|
22
25
|
interface RegistryMeta {
|
|
@@ -69,15 +72,25 @@ export async function scaffoldRemote({
|
|
|
69
72
|
}: RemoteOptions) {
|
|
70
73
|
try {
|
|
71
74
|
const metaRes = await fetch(`${registry}/${template}`);
|
|
72
|
-
if (!metaRes.ok)
|
|
75
|
+
if (!metaRes.ok)
|
|
76
|
+
throw new HttpError(
|
|
77
|
+
metaRes.status,
|
|
78
|
+
metaRes.statusText,
|
|
79
|
+
);
|
|
73
80
|
const remote = (await metaRes.json()) as RegistryMeta;
|
|
74
81
|
const version = remote["dist-tags"][ref];
|
|
75
82
|
if (version === undefined) {
|
|
76
|
-
throw new HttpError(
|
|
83
|
+
throw new HttpError(
|
|
84
|
+
404,
|
|
85
|
+
`模板 ${template}@${ref} 不存在`,
|
|
86
|
+
);
|
|
77
87
|
}
|
|
78
88
|
const url = remote.versions[version]?.dist?.tarball;
|
|
79
89
|
if (url === undefined) {
|
|
80
|
-
throw new HttpError(
|
|
90
|
+
throw new HttpError(
|
|
91
|
+
404,
|
|
92
|
+
`模板 ${template}@${ref} 不存在`,
|
|
93
|
+
);
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
// 解包交给 giget:provider 的 tar 字段是函数,giget 需要时才发起
|
|
@@ -93,7 +106,10 @@ export async function scaffoldRemote({
|
|
|
93
106
|
tar: async () => {
|
|
94
107
|
const tarballRes = await fetch(url);
|
|
95
108
|
if (!tarballRes.ok || !tarballRes.body) {
|
|
96
|
-
throw new HttpError(
|
|
109
|
+
throw new HttpError(
|
|
110
|
+
tarballRes.status,
|
|
111
|
+
tarballRes.statusText,
|
|
112
|
+
);
|
|
97
113
|
}
|
|
98
114
|
return tarballRes.body;
|
|
99
115
|
},
|
|
@@ -113,8 +129,17 @@ export async function scaffoldRemote({
|
|
|
113
129
|
}
|
|
114
130
|
|
|
115
131
|
/** 把改写结果写回下载解包出的 package.json */
|
|
116
|
-
function writePackageJson(
|
|
132
|
+
function writePackageJson(
|
|
133
|
+
rootDir: string,
|
|
134
|
+
project: string,
|
|
135
|
+
prod: boolean,
|
|
136
|
+
) {
|
|
117
137
|
const filename = join(rootDir, "package.json");
|
|
118
|
-
const meta = JSON.parse(
|
|
119
|
-
|
|
138
|
+
const meta = JSON.parse(
|
|
139
|
+
readFileSync(filename, "utf8"),
|
|
140
|
+
) as Manifest;
|
|
141
|
+
writeFileSync(
|
|
142
|
+
filename,
|
|
143
|
+
renderManifest(meta, project, prod),
|
|
144
|
+
);
|
|
120
145
|
}
|
package/src/template/README.md
CHANGED
|
@@ -18,8 +18,9 @@ bun run dev # 启动(开发模式,启用 HMR 热更新)
|
|
|
18
18
|
|
|
19
19
|
## 预装与预写
|
|
20
20
|
|
|
21
|
-
- **已预装**:基础与控制台插件(@koishi-ce 全家桶)。其中依赖数据库的(admin / bind / analytics / auth / dataview / rate-limit 等)与暂无需启用的(inspect / server-temp)在 `koishi.yml` 中以 `~`
|
|
22
|
-
-
|
|
21
|
+
- **已预装**:基础与控制台插件(@koishi-ce 全家桶)。其中依赖数据库的(admin / bind / broadcast / callme / analytics / auth / dataview / rate-limit 等)与暂无需启用的(inspect / server-temp / mock)在 `koishi.yml` 中以 `~` 前缀保持禁用——启用任一数据库插件后,去掉对应 `~` 即可。
|
|
22
|
+
- **数据库开箱即用**:`@koishi-ce/plugin-database-sqlite` 已预装(`~database-sqlite` 保持禁用),无需市场安装,在配置页启用即可,数据落在 `data/koishi.db`。
|
|
23
|
+
- **只预写、未预装**:adapter(discord / telegram / qq …)与数据库(mongo / mysql / postgres …)官方插件以 `~` 禁用条目预写在 `koishi.yml` 的 `group:adapter` / `group:storage`——loader 会跳过禁用条目,不安装也能正常启动;需要时在控制台「插件市场」搜索安装,回到配置页点击启用即可。
|
|
23
24
|
|
|
24
25
|
## 安装插件
|
|
25
26
|
|
package/src/template/koishi.yml
CHANGED