@yunzai-ng/cli 0.1.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/LICENSE +661 -0
- package/README.md +34 -0
- package/dist/args.d.ts +44 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +116 -0
- package/dist/args.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +40 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands/doctor.d.ts +16 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +181 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +61 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/plugin.d.ts +14 -0
- package/dist/commands/plugin.d.ts.map +1 -0
- package/dist/commands/plugin.js +115 -0
- package/dist/commands/plugin.js.map +1 -0
- package/dist/commands/start.d.ts +19 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +102 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/help.d.ts +6 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +42 -0
- package/dist/help.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/link.d.ts +16 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +103 -0
- package/dist/link.js.map +1 -0
- package/dist/run.d.ts +7 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +115 -0
- package/dist/run.js.map +1 -0
- package/dist/terminal.d.ts +43 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +94 -0
- package/dist/terminal.js.map +1 -0
- package/package.json +31 -0
- package/src/args.test.ts +52 -0
- package/src/args.ts +132 -0
- package/src/bin.ts +40 -0
- package/src/commands/doctor.ts +205 -0
- package/src/commands/init.ts +65 -0
- package/src/commands/plugin.ts +133 -0
- package/src/commands/start.ts +121 -0
- package/src/help.ts +42 -0
- package/src/index.ts +16 -0
- package/src/link.test.ts +53 -0
- package/src/link.ts +117 -0
- package/src/run.ts +124 -0
- package/src/terminal.ts +102 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** 加粗 */
|
|
2
|
+
export declare const bold: (text: string) => string;
|
|
3
|
+
/** 变暗,用于次要信息 */
|
|
4
|
+
export declare const dim: (text: string) => string;
|
|
5
|
+
/** 红色,用于错误 */
|
|
6
|
+
export declare const red: (text: string) => string;
|
|
7
|
+
/** 绿色,用于成功 */
|
|
8
|
+
export declare const green: (text: string) => string;
|
|
9
|
+
/** 黄色,用于警告 */
|
|
10
|
+
export declare const yellow: (text: string) => string;
|
|
11
|
+
/** 青色,用于路径与命令 */
|
|
12
|
+
export declare const cyan: (text: string) => string;
|
|
13
|
+
/**
|
|
14
|
+
* 向标准输出写入一行
|
|
15
|
+
* @param text 文本,缺省为空行
|
|
16
|
+
*/
|
|
17
|
+
export declare function print(text?: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* 向标准错误写入一行
|
|
20
|
+
*
|
|
21
|
+
* 分流的目的在于使 `yzng doctor > report.txt` 之类的重定向仍能在终端观察到错误信息。
|
|
22
|
+
* @param text 文本
|
|
23
|
+
*/
|
|
24
|
+
export declare function printErr(text: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* 估算字符串在等宽终端中占用的列数
|
|
27
|
+
*
|
|
28
|
+
* 仅区分"宽字符"与"窄字符"两档,不处理组合字符与 emoji 变体选择符 ——
|
|
29
|
+
* CLI 中出现的仅为中文标签与 ASCII 取值,更高的精度并无收益。
|
|
30
|
+
* @param text 文本
|
|
31
|
+
* @returns 列数
|
|
32
|
+
*/
|
|
33
|
+
export declare function displayWidth(text: string): number;
|
|
34
|
+
/**
|
|
35
|
+
* 输出一组"标签 值",标签按显示宽度在右侧补齐
|
|
36
|
+
*
|
|
37
|
+
* 使用 `displayWidth` 而非 `str.length` 度量宽度:标签为中文,中文在等宽字体中
|
|
38
|
+
* 占两列 —— 按 UTF-16 码元数补空格会使整张表格错位。
|
|
39
|
+
* @param rows 标签与值
|
|
40
|
+
* @param indent 每行前缀空格数
|
|
41
|
+
*/
|
|
42
|
+
export declare function printRows(rows: readonly (readonly [string, string])[], indent?: number): void;
|
|
43
|
+
//# sourceMappingURL=terminal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal.d.ts","sourceRoot":"","sources":["../src/terminal.ts"],"names":[],"mappings":"AA8BA,SAAS;AACT,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,KAAG,MAA0B,CAAA;AAC9D,gBAAgB;AAChB,eAAO,MAAM,GAAG,GAAI,MAAM,MAAM,KAAG,MAA0B,CAAA;AAC7D,cAAc;AACd,eAAO,MAAM,GAAG,GAAI,MAAM,MAAM,KAAG,MAA2B,CAAA;AAC9D,cAAc;AACd,eAAO,MAAM,KAAK,GAAI,MAAM,MAAM,KAAG,MAA2B,CAAA;AAChE,cAAc;AACd,eAAO,MAAM,MAAM,GAAI,MAAM,MAAM,KAAG,MAA2B,CAAA;AACjE,iBAAiB;AACjB,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,KAAG,MAA2B,CAAA;AAE/D;;;GAGG;AACH,wBAAgB,KAAK,CAAC,IAAI,SAAK,GAAG,IAAI,CAErC;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE3C;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgBjD;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,EAAE,MAAM,SAAI,GAAG,IAAI,CAMxF"}
|
package/dist/terminal.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:终端输出 —— 换行、缩进、颜色与表格对齐
|
|
3
|
+
* 依赖方向:仅依赖 node:process,不涉及内核
|
|
4
|
+
* 生命周期:无状态,颜色开关于模块加载时确定一次
|
|
5
|
+
* 注意事项:**刻意不使用 `console`,而是直接写入 `process.stdout`。** 理由有二:
|
|
6
|
+
* 1) CLI 的输出是**程序结果**,而非日志。日志具有级别与 scope 并需落盘,
|
|
7
|
+
* 应经由 `loggerHub`;而 `yzng doctor` 输出的表格不应被写入 yunzai.log,
|
|
8
|
+
* 亦不应因日志级别被调整为 warn 而消失。
|
|
9
|
+
* 2) 仓库的 eslint 规则禁用了 `console`(仅保留 `console.error`),其目的在于
|
|
10
|
+
* 使插件必须经由 `ctx.logger` 输出。CLI 自身不应为此开设例外 —— 直接写入
|
|
11
|
+
* stdout 既满足该规则,亦更准确地表达了"此为命令的输出流"。
|
|
12
|
+
*/
|
|
13
|
+
import process from "node:process";
|
|
14
|
+
/** ANSI 转义引导符。写作转义序列而非字面控制字符:后者会被部分编辑器与补丁工具丢弃 */
|
|
15
|
+
const ESC = "\u001b";
|
|
16
|
+
/** 是否启用 ANSI 颜色:非 TTY(输出被管道接收)或已设置 NO_COLOR 时一律关闭 */
|
|
17
|
+
const COLOR = process.stdout.isTTY === true && process.env["NO_COLOR"] === undefined;
|
|
18
|
+
/**
|
|
19
|
+
* 为文本附加 ANSI 颜色
|
|
20
|
+
* @param code SGR 参数
|
|
21
|
+
* @param text 文本
|
|
22
|
+
* @returns 着色后的文本;未启用颜色时原样返回
|
|
23
|
+
*/
|
|
24
|
+
function paint(code, text) {
|
|
25
|
+
return COLOR ? `${ESC}[${code}m${text}${ESC}[0m` : text;
|
|
26
|
+
}
|
|
27
|
+
/** 加粗 */
|
|
28
|
+
export const bold = (text) => paint("1", text);
|
|
29
|
+
/** 变暗,用于次要信息 */
|
|
30
|
+
export const dim = (text) => paint("2", text);
|
|
31
|
+
/** 红色,用于错误 */
|
|
32
|
+
export const red = (text) => paint("31", text);
|
|
33
|
+
/** 绿色,用于成功 */
|
|
34
|
+
export const green = (text) => paint("32", text);
|
|
35
|
+
/** 黄色,用于警告 */
|
|
36
|
+
export const yellow = (text) => paint("33", text);
|
|
37
|
+
/** 青色,用于路径与命令 */
|
|
38
|
+
export const cyan = (text) => paint("36", text);
|
|
39
|
+
/**
|
|
40
|
+
* 向标准输出写入一行
|
|
41
|
+
* @param text 文本,缺省为空行
|
|
42
|
+
*/
|
|
43
|
+
export function print(text = "") {
|
|
44
|
+
process.stdout.write(`${text}\n`);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 向标准错误写入一行
|
|
48
|
+
*
|
|
49
|
+
* 分流的目的在于使 `yzng doctor > report.txt` 之类的重定向仍能在终端观察到错误信息。
|
|
50
|
+
* @param text 文本
|
|
51
|
+
*/
|
|
52
|
+
export function printErr(text) {
|
|
53
|
+
process.stderr.write(`${text}\n`);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 估算字符串在等宽终端中占用的列数
|
|
57
|
+
*
|
|
58
|
+
* 仅区分"宽字符"与"窄字符"两档,不处理组合字符与 emoji 变体选择符 ——
|
|
59
|
+
* CLI 中出现的仅为中文标签与 ASCII 取值,更高的精度并无收益。
|
|
60
|
+
* @param text 文本
|
|
61
|
+
* @returns 列数
|
|
62
|
+
*/
|
|
63
|
+
export function displayWidth(text) {
|
|
64
|
+
let width = 0;
|
|
65
|
+
for (const ch of text) {
|
|
66
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
67
|
+
const wide = (code >= 0x1100 && code <= 0x115f) ||
|
|
68
|
+
(code >= 0x2e80 && code <= 0xa4cf) ||
|
|
69
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
70
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
71
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
72
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
73
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
74
|
+
(code >= 0x20000 && code <= 0x3fffd);
|
|
75
|
+
width += wide ? 2 : 1;
|
|
76
|
+
}
|
|
77
|
+
return width;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 输出一组"标签 值",标签按显示宽度在右侧补齐
|
|
81
|
+
*
|
|
82
|
+
* 使用 `displayWidth` 而非 `str.length` 度量宽度:标签为中文,中文在等宽字体中
|
|
83
|
+
* 占两列 —— 按 UTF-16 码元数补空格会使整张表格错位。
|
|
84
|
+
* @param rows 标签与值
|
|
85
|
+
* @param indent 每行前缀空格数
|
|
86
|
+
*/
|
|
87
|
+
export function printRows(rows, indent = 2) {
|
|
88
|
+
const width = Math.max(0, ...rows.map(([label]) => displayWidth(label)));
|
|
89
|
+
const pad = " ".repeat(indent);
|
|
90
|
+
for (const [label, value] of rows) {
|
|
91
|
+
print(`${pad}${dim(label)}${" ".repeat(width - displayWidth(label) + 1)}${value}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=terminal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal.js","sourceRoot":"","sources":["../src/terminal.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yunzai-ng/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Yunzai NG 命令行:init / start / dev / doctor / plugin",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "AGPL-3.0-or-later",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"yzng": "./dist/bin.js"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@yunzai-ng/core": "0.1.0",
|
|
25
|
+
"@yunzai-ng/jsx": "0.1.0",
|
|
26
|
+
"@yunzai-ng/types": "0.1.0"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -b"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/args.test.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:参数解析的行为测试
|
|
3
|
+
* 依赖方向:测试文件
|
|
4
|
+
* 生命周期:纯函数,无需夹具
|
|
5
|
+
* 注意事项:重点覆盖两处易于出错的边界 —— "选项值与后续子命令"之间的歧义,
|
|
6
|
+
* 以及 `--no-x` 的取反。二者出错的表现均为"命令看似执行成功但行为不符",
|
|
7
|
+
* 其排查难度远高于直接报错。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from "vitest"
|
|
10
|
+
import { flagBoolean, flagString, parseArgs } from "./args.js"
|
|
11
|
+
|
|
12
|
+
describe("参数解析", () => {
|
|
13
|
+
it("第一个非选项 token 为子命令,其余计入位置参数", () => {
|
|
14
|
+
const args = parseArgs(["plugin", "new", "我的插件"])
|
|
15
|
+
expect(args.command).toBe("plugin")
|
|
16
|
+
expect(args.positional).toEqual(["new", "我的插件"])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("--key=value 与 --key value 等价", () => {
|
|
20
|
+
expect(flagString(parseArgs(["start", "--home=D:/yz"]), "home")).toBe("D:/yz")
|
|
21
|
+
expect(flagString(parseArgs(["start", "--home", "D:/yz"]), "home")).toBe("D:/yz")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("下一个 token 形似选项时不将其作为值,而按布尔开关处理", () => {
|
|
25
|
+
const args = parseArgs(["--debug", "start"])
|
|
26
|
+
// start 必须保留给子命令,不可被 --debug 并入
|
|
27
|
+
expect(args.command).toBe("start")
|
|
28
|
+
expect(flagBoolean(args, "debug")).toBe(true)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("--no-x 取反,且优先级高于 fallback", () => {
|
|
32
|
+
expect(flagBoolean(parseArgs(["start", "--no-console"]), "console", true)).toBe(false)
|
|
33
|
+
expect(flagBoolean(parseArgs(["start"]), "console", true)).toBe(true)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("短选项可合并书写,-c 后可接值", () => {
|
|
37
|
+
const args = parseArgs(["-dv"])
|
|
38
|
+
expect(flagBoolean(args, "debug")).toBe(true)
|
|
39
|
+
expect(flagBoolean(args, "version")).toBe(true)
|
|
40
|
+
expect(flagString(parseArgs(["start", "-c", "D:/yz"]), "home")).toBe("D:/yz")
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it("-- 之后原样保留,不作解析", () => {
|
|
44
|
+
const args = parseArgs(["start", "--", "--debug", "任意"])
|
|
45
|
+
expect(args.rest).toEqual(["--debug", "任意"])
|
|
46
|
+
expect(flagBoolean(args, "debug")).toBe(false)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("空串选项值视为未提供,以免 --home= 被解析为空的主目录", () => {
|
|
50
|
+
expect(flagString(parseArgs(["start", "--home="]), "home")).toBeUndefined()
|
|
51
|
+
})
|
|
52
|
+
})
|
package/src/args.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:命令行参数解析
|
|
3
|
+
* 依赖方向:无依赖
|
|
4
|
+
* 生命周期:纯函数
|
|
5
|
+
* 注意事项:手写实现而非引入 commander / yargs。CLI 的参数总计不足十个,
|
|
6
|
+
* 而这两个库各自会引入 10~30 个传递依赖 —— 对一个需在 Termux 上
|
|
7
|
+
* 执行 `pnpm install` 的框架而言,装机体积与安装时长是真实成本。
|
|
8
|
+
*
|
|
9
|
+
* `--` 之后的内容原样收入 `rest`,不作解析:`yzng plugin new x -- --raw`
|
|
10
|
+
* 这类"将其余参数转交给下游"的场景需要该行为。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** 解析结果 */
|
|
14
|
+
export interface ParsedArgs {
|
|
15
|
+
/** 子命令,未提供时为空串 */
|
|
16
|
+
readonly command: string
|
|
17
|
+
/** 子命令之后的位置参数 */
|
|
18
|
+
readonly positional: readonly string[]
|
|
19
|
+
/** 长短选项;出现但未带值的记为 `true` */
|
|
20
|
+
readonly flags: Readonly<Record<string, string | boolean>>
|
|
21
|
+
/** `--` 之后的原样内容 */
|
|
22
|
+
readonly rest: readonly string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 单字母短选项到长名的映射 */
|
|
26
|
+
const SHORT: Readonly<Record<string, string>> = {
|
|
27
|
+
h: "help",
|
|
28
|
+
v: "version",
|
|
29
|
+
d: "debug",
|
|
30
|
+
c: "home"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 会带值的选项
|
|
35
|
+
*
|
|
36
|
+
* 必须显式列出,不可依据"下一个 token 不似选项即视为其值"推断 —— 该规则会使
|
|
37
|
+
* `yzng --debug start` 中的 `start` 被 `--debug` 并入,子命令因此为空,
|
|
38
|
+
* CLI 转而输出帮助,而使用者无从察觉问题所在。清单之外的选项一律为布尔开关;
|
|
39
|
+
* 确需为其传值时应写 `--新选项=值`,等号形式的优先级恒高于本清单。
|
|
40
|
+
*/
|
|
41
|
+
const VALUE_FLAGS: ReadonlySet<string> = new Set(["home", "plugins", "port", "host"])
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 解析 `process.argv.slice(2)`
|
|
45
|
+
* @param argv 参数数组
|
|
46
|
+
* @returns 解析结果
|
|
47
|
+
*/
|
|
48
|
+
export function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
49
|
+
const positional: string[] = []
|
|
50
|
+
const flags: Record<string, string | boolean> = {}
|
|
51
|
+
const rest: string[] = []
|
|
52
|
+
let command = ""
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < argv.length; i++) {
|
|
55
|
+
const token = argv[i] ?? ""
|
|
56
|
+
|
|
57
|
+
if (token === "--") {
|
|
58
|
+
rest.push(...argv.slice(i + 1))
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (token.startsWith("--")) {
|
|
63
|
+
const body = token.slice(2)
|
|
64
|
+
const eq = body.indexOf("=")
|
|
65
|
+
if (eq >= 0) {
|
|
66
|
+
flags[body.slice(0, eq)] = body.slice(eq + 1)
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
if (body.startsWith("no-")) {
|
|
70
|
+
flags[body.slice(3)] = false
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
// 仅清单内的选项会消耗下一个 token 作为其值。代价是以横杠开头的值
|
|
74
|
+
// (`--home -x`)须写作 `--home=-x`;该代价换取的是子命令绝不会被误并入
|
|
75
|
+
const next = argv[i + 1]
|
|
76
|
+
if (VALUE_FLAGS.has(body) && next !== undefined && !next.startsWith("-")) {
|
|
77
|
+
flags[body] = next
|
|
78
|
+
i++
|
|
79
|
+
} else {
|
|
80
|
+
flags[body] = true
|
|
81
|
+
}
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (token.startsWith("-") && token.length > 1) {
|
|
86
|
+
// 允许 `-dv` 形式的合并写法;合并时仅最后一个字母可能带值
|
|
87
|
+
const letters = [...token.slice(1)]
|
|
88
|
+
for (let k = 0; k < letters.length; k++) {
|
|
89
|
+
const name = SHORT[letters[k] ?? ""] ?? letters[k] ?? ""
|
|
90
|
+
const last = k === letters.length - 1
|
|
91
|
+
const next = argv[i + 1]
|
|
92
|
+
if (last && VALUE_FLAGS.has(name) && next !== undefined && !next.startsWith("-")) {
|
|
93
|
+
flags[name] = next
|
|
94
|
+
i++
|
|
95
|
+
} else {
|
|
96
|
+
flags[name] = true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (command === "") command = token
|
|
103
|
+
else positional.push(token)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { command, positional, flags, rest }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 取字符串选项
|
|
111
|
+
* @param args 解析结果
|
|
112
|
+
* @param name 选项名
|
|
113
|
+
* @returns 值;未提供或仅为布尔开关时 undefined
|
|
114
|
+
*/
|
|
115
|
+
export function flagString(args: ParsedArgs, name: string): string | undefined {
|
|
116
|
+
const value = args.flags[name]
|
|
117
|
+
return typeof value === "string" && value !== "" ? value : undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 取布尔选项
|
|
122
|
+
* @param args 解析结果
|
|
123
|
+
* @param name 选项名
|
|
124
|
+
* @param fallback 未提供时的取值
|
|
125
|
+
* @returns 布尔值
|
|
126
|
+
*/
|
|
127
|
+
export function flagBoolean(args: ParsedArgs, name: string, fallback = false): boolean {
|
|
128
|
+
const value = args.flags[name]
|
|
129
|
+
if (value === undefined) return fallback
|
|
130
|
+
if (typeof value === "boolean") return value
|
|
131
|
+
return value !== "false" && value !== "0"
|
|
132
|
+
}
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 模块职责:`yzng` 可执行入口 —— 唯一允许调用 `process.exit` 的位置
|
|
4
|
+
* 依赖方向:仅调用 run()
|
|
5
|
+
* 生命周期:进程入口
|
|
6
|
+
* 注意事项:**未捕获异常在此处理,而非交由 Node 缺省处理。** Node 缺省会输出
|
|
7
|
+
* 完整调用栈,其中绝大部分为内核与 fastify 的栈帧;对使用者不具信息量,
|
|
8
|
+
* 反而将真正有用的第一行挤出屏幕。此处仅输出消息,调用栈保留给 `--debug`。
|
|
9
|
+
*
|
|
10
|
+
* `unhandledRejection` 必须显式接管:自 Node 20 起其缺省行为是终止进程,
|
|
11
|
+
* 而"某个插件的 promise 未附加 catch"不应导致整个机器人下线。
|
|
12
|
+
*/
|
|
13
|
+
import process from "node:process"
|
|
14
|
+
import { run } from "./run.js"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 输出一条致命错误
|
|
18
|
+
* @param label 场景
|
|
19
|
+
* @param err 错误
|
|
20
|
+
*/
|
|
21
|
+
function fatal(label: string, err: unknown): void {
|
|
22
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
23
|
+
process.stderr.write(`\n${label}:${message}\n`)
|
|
24
|
+
if (err instanceof Error && err.stack !== undefined && process.argv.includes("--debug")) {
|
|
25
|
+
process.stderr.write(`${err.stack}\n`)
|
|
26
|
+
} else {
|
|
27
|
+
process.stderr.write("附加 --debug 可查看完整调用栈\n")
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
process.on("unhandledRejection", reason => {
|
|
32
|
+
fatal("未处理的 promise 拒绝", reason)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
process.exitCode = await run(process.argv.slice(2))
|
|
37
|
+
} catch (err) {
|
|
38
|
+
fatal("命令执行失败", err)
|
|
39
|
+
process.exitCode = 1
|
|
40
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:`yzng doctor` —— 环境自检:运行环境、目录、可选原生依赖、端口占用
|
|
3
|
+
* 依赖方向:依赖 @yunzai-ng/core 的公开入口与 node:net
|
|
4
|
+
* 生命周期:一次性,执行完毕即退出
|
|
5
|
+
* 注意事项:**每一项均须给出后续处置方式,否则本命令不具备存在价值。**
|
|
6
|
+
* 一句"Redis 连接失败"不指明该改哪里,等于把排障推回给使用者;故每条不合格项
|
|
7
|
+
* 都附一条可执行的下一步。
|
|
8
|
+
*
|
|
9
|
+
* 退出码:存在 `error` 级问题时为 1,仅有 `warn` 时仍为 0 ——
|
|
10
|
+
* `yzng doctor && yzng start` 是自然的用法,不应被"尚未配置账号"一类提示阻断。
|
|
11
|
+
*/
|
|
12
|
+
import net from "node:net"
|
|
13
|
+
import process from "node:process"
|
|
14
|
+
import { createRequire } from "node:module"
|
|
15
|
+
import { access } from "node:fs/promises"
|
|
16
|
+
import { detectPlatform, resolvePaths, sampleUsage } from "@yunzai-ng/core"
|
|
17
|
+
import { bold, cyan, dim, green, print, printRows, red, yellow } from "../terminal.js"
|
|
18
|
+
|
|
19
|
+
/** Node 最低版本,与根 package.json 的 engines 保持一致 */
|
|
20
|
+
const MIN_NODE_MAJOR = 20
|
|
21
|
+
|
|
22
|
+
/** 一条自检结论 */
|
|
23
|
+
interface Finding {
|
|
24
|
+
/** 严重程度 */
|
|
25
|
+
readonly level: "ok" | "warn" | "error"
|
|
26
|
+
/** 检查项名称 */
|
|
27
|
+
readonly title: string
|
|
28
|
+
/** 结论 */
|
|
29
|
+
readonly detail: string
|
|
30
|
+
/** 不合格时的下一步;合格时省略 */
|
|
31
|
+
readonly next?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 检查 Node 版本
|
|
36
|
+
* @returns 结论
|
|
37
|
+
*/
|
|
38
|
+
function checkNode(): Finding {
|
|
39
|
+
const major = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10)
|
|
40
|
+
if (major >= MIN_NODE_MAJOR) return { level: "ok", title: "Node 版本", detail: process.version }
|
|
41
|
+
return {
|
|
42
|
+
level: "error",
|
|
43
|
+
title: "Node 版本",
|
|
44
|
+
detail: `${process.version},低于要求的 ${MIN_NODE_MAJOR}`,
|
|
45
|
+
next: "将 Node 升级至 20.11 或更高版本:内核使用了 import.meta.dirname 与原生 ESM 解析"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 检查一个可选原生依赖能否加载
|
|
51
|
+
*
|
|
52
|
+
* **解析基准必须是内核而非 CLI 自身。** pnpm 的隔离式 node_modules 将
|
|
53
|
+
* `classic-level` 安装于 `packages/core/node_modules` 之下,CLI 的模块图中并不含有
|
|
54
|
+
* 该包 —— 自 CLI 自身解析将一律得到"未安装",而该结论会将使用者引向
|
|
55
|
+
* 一个并不存在的问题(去安装一个实际已安装完毕的包)。
|
|
56
|
+
*
|
|
57
|
+
* 使用 `createRequire` 而非 `await import()`:这两个包均为 CommonJS 原生扩展,
|
|
58
|
+
* 而此处需要区分的是"未安装"与"已安装但 ABI 不匹配"—— 仅 `require` 的报错信息中
|
|
59
|
+
* 携带 `NODE_MODULE_VERSION` 一段,动态 import 会将其重新包装并丢失该细节。
|
|
60
|
+
* @param name 包名
|
|
61
|
+
* @param purpose 该包的用途
|
|
62
|
+
* @param fallback 缺失时的降级说明
|
|
63
|
+
* @returns 结论
|
|
64
|
+
*/
|
|
65
|
+
function checkNative(name: string, purpose: string, fallback: string): Finding {
|
|
66
|
+
const self = createRequire(import.meta.url)
|
|
67
|
+
let require = self
|
|
68
|
+
try {
|
|
69
|
+
require = createRequire(self.resolve("@yunzai-ng/core/package.json"))
|
|
70
|
+
} catch {
|
|
71
|
+
// 未找到内核时退回自身解析:此情形下 CLI 属于单独安装,
|
|
72
|
+
// 报告"未安装"至少不构成误导,而真正的问题(内核缺失)由其他检查项覆盖
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
require.resolve(name)
|
|
76
|
+
} catch {
|
|
77
|
+
return { level: "warn", title: name, detail: `未安装(${purpose})`, next: `${fallback}。需要时执行:pnpm add -w ${name}` }
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
require(name)
|
|
81
|
+
return { level: "ok", title: name, detail: `可用(${purpose})` }
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return {
|
|
84
|
+
level: "warn",
|
|
85
|
+
title: name,
|
|
86
|
+
detail: `已安装但加载失败:${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
87
|
+
next: `原生模块与当前 Node ABI 不匹配,重建即可解决:pnpm rebuild ${name}。${fallback}`
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 检查目录可达
|
|
94
|
+
* @param label 检查项名称
|
|
95
|
+
* @param dir 路径
|
|
96
|
+
* @returns 结论
|
|
97
|
+
*/
|
|
98
|
+
async function checkDir(label: string, dir: string): Promise<Finding> {
|
|
99
|
+
try {
|
|
100
|
+
await access(dir)
|
|
101
|
+
return { level: "ok", title: label, detail: cyan(dir) }
|
|
102
|
+
} catch {
|
|
103
|
+
return {
|
|
104
|
+
level: "warn",
|
|
105
|
+
title: label,
|
|
106
|
+
detail: `${cyan(dir)} ${dim("尚不存在")}`,
|
|
107
|
+
next: "首次启动时将自动创建;亦可先执行 yzng init"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 检查端口是否可监听
|
|
114
|
+
*
|
|
115
|
+
* 实际执行一次 bind 而非查询进程列表:其行为跨平台一致,且"能否监听"本身即为
|
|
116
|
+
* 内核关注的问题 —— 端口被同一用户的另一实例占用,与被防火墙策略拦截,
|
|
117
|
+
* 对启动而言结果相同。
|
|
118
|
+
* @param host 监听地址
|
|
119
|
+
* @param port 端口
|
|
120
|
+
* @returns 结论
|
|
121
|
+
*/
|
|
122
|
+
function checkPort(host: string, port: number): Promise<Finding> {
|
|
123
|
+
return new Promise<Finding>(resolve => {
|
|
124
|
+
const probe = net.createServer()
|
|
125
|
+
probe.once("error", (err: NodeJS.ErrnoException) => {
|
|
126
|
+
resolve({
|
|
127
|
+
level: "error",
|
|
128
|
+
title: "面板端口",
|
|
129
|
+
detail: `${host}:${port} 不可用(${err.code ?? err.message})`,
|
|
130
|
+
next:
|
|
131
|
+
err.code === "EADDRINUSE"
|
|
132
|
+
? "该端口已被其他程序占用(通常为另一个 Yunzai 实例)。请修改 config/yunzai.yaml 中的 server.port,或先停止该程序"
|
|
133
|
+
: "请检查监听地址是否属于本机网卡"
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
probe.once("listening", () => {
|
|
137
|
+
probe.close(() => resolve({ level: "ok", title: "面板端口", detail: `${host}:${port} 可用` }))
|
|
138
|
+
})
|
|
139
|
+
probe.listen(port, host)
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 输出一条结论
|
|
145
|
+
* @param finding 结论
|
|
146
|
+
*/
|
|
147
|
+
function printFinding(finding: Finding): void {
|
|
148
|
+
const mark = finding.level === "ok" ? green("✓") : finding.level === "warn" ? yellow("!") : red("✗")
|
|
149
|
+
print(` ${mark} ${finding.title} ${finding.detail}`)
|
|
150
|
+
if (finding.next !== undefined) print(` ${dim(`→ ${finding.next}`)}`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 自检参数 */
|
|
154
|
+
export interface DoctorOptions {
|
|
155
|
+
/** 应用主目录 */
|
|
156
|
+
readonly home?: string | undefined
|
|
157
|
+
/** 待探测的面板端口,缺省 2536(与配置缺省值一致) */
|
|
158
|
+
readonly port?: number | undefined
|
|
159
|
+
/** 待探测的监听地址 */
|
|
160
|
+
readonly host?: string | undefined
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 执行一次环境自检
|
|
165
|
+
* @param opts 参数
|
|
166
|
+
* @returns 进程退出码;存在 error 级问题时为 1
|
|
167
|
+
*/
|
|
168
|
+
export async function runDoctor(opts: DoctorOptions = {}): Promise<number> {
|
|
169
|
+
const platform = detectPlatform()
|
|
170
|
+
const paths = resolvePaths({ home: opts.home })
|
|
171
|
+
const usage = sampleUsage()
|
|
172
|
+
|
|
173
|
+
print()
|
|
174
|
+
print(` ${bold("环境")}`)
|
|
175
|
+
printRows([
|
|
176
|
+
["系统", `${platform.os}/${platform.arch}${platform.isTermux ? " · Termux" : ""}${platform.isContainer ? " · 容器" : ""}`],
|
|
177
|
+
["Node", platform.nodeVersion],
|
|
178
|
+
["CPU", `${platform.cpus} 核`],
|
|
179
|
+
["常驻内存", `${(usage.rss / 1024 / 1024).toFixed(1)} MB`]
|
|
180
|
+
])
|
|
181
|
+
|
|
182
|
+
const findings: Finding[] = [
|
|
183
|
+
checkNode(),
|
|
184
|
+
await checkDir("主目录", paths.home),
|
|
185
|
+
await checkDir("配置目录", paths.config),
|
|
186
|
+
await checkDir("数据目录", paths.data),
|
|
187
|
+
checkNative("classic-level", "内嵌 KV,用于计数与冷却", "缺失时内核降级为 JSON 文件存储,功能不受影响但写入速度较低"),
|
|
188
|
+
checkNative("better-sqlite3", "关系数据,用于插件建表", "缺失时依赖 SQL 的插件将取得一条错误说明,而非静默失效"),
|
|
189
|
+
await checkPort(opts.host ?? "127.0.0.1", opts.port ?? 2536)
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
print()
|
|
193
|
+
print(` ${bold("检查")}`)
|
|
194
|
+
for (const finding of findings) printFinding(finding)
|
|
195
|
+
|
|
196
|
+
const errors = findings.filter(f => f.level === "error").length
|
|
197
|
+
const warns = findings.filter(f => f.level === "warn").length
|
|
198
|
+
print()
|
|
199
|
+
if (errors > 0) print(` ${red(`${errors} 项需要处理`)}${warns > 0 ? dim(`,${warns} 项提示`) : ""}`)
|
|
200
|
+
else if (warns > 0) print(` ${yellow(`${warns} 项提示`)}${dim(",不影响启动")}`)
|
|
201
|
+
else print(` ${green("全部检查项正常")}`)
|
|
202
|
+
print()
|
|
203
|
+
|
|
204
|
+
return errors > 0 ? 1 : 0
|
|
205
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:`yzng init` —— 在主目录中建立目录布局与缺省配置文件
|
|
3
|
+
* 依赖方向:依赖 @yunzai-ng/core 的公开入口
|
|
4
|
+
* 生命周期:一次性,执行完毕即退出
|
|
5
|
+
* 注意事项:**刻意不经由 `createApp()`。** `createApp` 会一并打开 KV 数据库、
|
|
6
|
+
* 建立 HTTP 客户端连接池、装配插件宿主 —— 在"仅需查看配置文件结构"
|
|
7
|
+
* 的场景中,上述工作均属额外开销,且 KV 无法打开时会直接抛出异常,
|
|
8
|
+
* 使一次本应成功的初始化转为失败。此处仅执行两项必要工作:建立目录、
|
|
9
|
+
* 声明配置(从而将缺省值落盘)。
|
|
10
|
+
*
|
|
11
|
+
* 幂等:已存在的目录与配置文件均不会被覆盖(`ConfigStore` 仅在文件
|
|
12
|
+
* 缺失时写入缺省值),因此对一个已长期运行的实例再次执行 init 是安全的。
|
|
13
|
+
*/
|
|
14
|
+
import { ConfigStore, createLoggerHub, defineCoreConfig, ensurePaths, resolvePaths } from "@yunzai-ng/core"
|
|
15
|
+
import { linkFramework } from "../link.js"
|
|
16
|
+
import { bold, cyan, dim, green, print, printErr, printRows, red, yellow } from "../terminal.js"
|
|
17
|
+
|
|
18
|
+
/** 初始化参数 */
|
|
19
|
+
export interface InitOptions {
|
|
20
|
+
/** 应用主目录;缺省时按内核的探测顺序确定 */
|
|
21
|
+
readonly home?: string | undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 建立主目录与缺省配置
|
|
26
|
+
* @param opts 参数
|
|
27
|
+
* @returns 进程退出码
|
|
28
|
+
*/
|
|
29
|
+
export async function runInit(opts: InitOptions = {}): Promise<number> {
|
|
30
|
+
// 仅输出至控制台、级别限制为 warn:ConfigStore 自身会输出"已写出缺省配置"一类的
|
|
31
|
+
// info,而本命令需要给出的是一份经过整理的清单,两者叠加将降低可读性
|
|
32
|
+
const hub = createLoggerHub({ level: "warn", console: true })
|
|
33
|
+
try {
|
|
34
|
+
const paths = await ensurePaths(resolvePaths({ home: opts.home }))
|
|
35
|
+
const store = new ConfigStore({ dir: paths.config, logger: hub.root, watch: false })
|
|
36
|
+
const config = await defineCoreConfig(store)
|
|
37
|
+
const link = await linkFramework(paths.home)
|
|
38
|
+
|
|
39
|
+
print()
|
|
40
|
+
print(` ${bold(green("已就绪"))} ${dim("目录与缺省配置均已建立")}`)
|
|
41
|
+
print()
|
|
42
|
+
printRows([
|
|
43
|
+
["主目录", cyan(paths.home)],
|
|
44
|
+
["配置", cyan(paths.config)],
|
|
45
|
+
["数据", cyan(paths.data)],
|
|
46
|
+
["日志", cyan(paths.logs)],
|
|
47
|
+
["插件", cyan(paths.plugins)],
|
|
48
|
+
["临时", cyan(paths.temp)]
|
|
49
|
+
])
|
|
50
|
+
print()
|
|
51
|
+
if (link.failed.length > 0) {
|
|
52
|
+
print(` ${yellow("!")} 框架包未能链接至主目录,插件将无法 import @yunzai-ng/core`)
|
|
53
|
+
for (const [name, reason] of link.failed) print(` ${dim(`${name}:${reason}`)}`)
|
|
54
|
+
print()
|
|
55
|
+
}
|
|
56
|
+
print(` ${dim("下一步")} ${cyan("yzng start")} ${dim(`随后访问 http://${config.get().server.host}:${config.get().server.port}/`)}`)
|
|
57
|
+
print()
|
|
58
|
+
return 0
|
|
59
|
+
} catch (err) {
|
|
60
|
+
printErr(red(`初始化失败:${err instanceof Error ? err.message : String(err)}`))
|
|
61
|
+
return 1
|
|
62
|
+
} finally {
|
|
63
|
+
hub.close()
|
|
64
|
+
}
|
|
65
|
+
}
|