erii 2.0.6 → 3.0.0-beta.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.
@@ -0,0 +1,117 @@
1
+ import validator from "validator";
2
+ //#region src/types.d.ts
3
+ type ArgumentValue = string | number | boolean | ArgumentValue[] | {
4
+ [key: string]: ArgumentValue;
5
+ };
6
+ type CommandOptions = Record<string, ArgumentValue | undefined>;
7
+ interface ParsedArguments extends CommandOptions {
8
+ _: Array<string | number>;
9
+ }
10
+ type CommandHandler = (ctx: CommandCtx, options: CommandOptions) => unknown;
11
+ type LifecycleHandler = () => unknown;
12
+ type ArgumentValidator = (value: ArgumentValue | undefined, logger: (message: string) => void) => boolean;
13
+ type ValidatorName = { [K in keyof typeof validator]: typeof validator[K] extends ((value: string) => boolean) ? K : never; }[keyof typeof validator];
14
+ interface MetaInfo {
15
+ version?: string;
16
+ name?: string;
17
+ }
18
+ interface Command {
19
+ name: string | string[];
20
+ description?: string;
21
+ argument?: Argument;
22
+ alias?: string[];
23
+ redirect?: string;
24
+ options?: Option[];
25
+ handler?: CommandHandler;
26
+ }
27
+ interface Option {
28
+ name: string | string[];
29
+ description?: string;
30
+ command?: string;
31
+ argument?: Argument;
32
+ }
33
+ interface CommandMap {
34
+ [key: string]: Command | undefined;
35
+ }
36
+ interface CommandCtx {
37
+ showVersion: () => void;
38
+ showHelp: () => void;
39
+ getArgument: (commandName?: string) => ArgumentValue | undefined;
40
+ }
41
+ interface Argument {
42
+ name: string;
43
+ description: string;
44
+ validate?: ValidatorName | ArgumentValidator;
45
+ }
46
+ //#endregion
47
+ //#region src/erii.d.ts
48
+ export declare class Erii {
49
+ rawArguments: string[];
50
+ parsedArguments: ParsedArguments;
51
+ private version;
52
+ private name;
53
+ commands: CommandMap;
54
+ commonOptions: Option[];
55
+ validator: typeof validator;
56
+ alwaysHandler?: LifecycleHandler;
57
+ defaultHandler?: LifecycleHandler;
58
+ constructor();
59
+ /**
60
+ * 绑定命令处理函数
61
+ * @param config
62
+ * @param handler
63
+ */
64
+ bind(config: Command, handler: CommandHandler): void;
65
+ /**
66
+ * 总是执行
67
+ * @param handler
68
+ */
69
+ always(handler: LifecycleHandler): void;
70
+ default(handler: LifecycleHandler): void;
71
+ /**
72
+ * 增加设置项
73
+ * @param config
74
+ */
75
+ addOption(config: Option): void;
76
+ private commandCtx;
77
+ /**
78
+ * 设定基础信息
79
+ * @param metaInfo
80
+ */
81
+ setMetaInfo({ version, name }?: MetaInfo): void;
82
+ /**
83
+ * 显示帮助信息
84
+ */
85
+ showHelp(command?: string): void;
86
+ /**
87
+ * 显示版本号
88
+ */
89
+ showVersion(): void;
90
+ /**
91
+ * 启动
92
+ */
93
+ start(): void;
94
+ /**
95
+ * 执行命令担当函数
96
+ * @param command
97
+ */
98
+ private exec;
99
+ validateArgument(argumentValue: ArgumentValue | undefined, argument?: Argument): boolean;
100
+ /**
101
+ * 获得命令的参数
102
+ * @param commandName
103
+ * @param followRedirect 是否遵循重定向
104
+ */
105
+ getArgument(commandName: string, followRedirect?: boolean): ArgumentValue | undefined;
106
+ private findArgument;
107
+ /**
108
+ * 启动
109
+ * エリイ 起きてます❤
110
+ */
111
+ okite(): void;
112
+ }
113
+ //#endregion
114
+ //#region src/index.d.ts
115
+ declare const _default: Erii;
116
+ //#endregion
117
+ export { type Argument, type ArgumentValidator, type ArgumentValue, type Command, type CommandCtx, type CommandHandler, type CommandMap, type CommandOptions, type LifecycleHandler, type MetaInfo, type Option, type ParsedArguments, type ValidatorName, _default as default };
package/dist/index.mjs ADDED
@@ -0,0 +1,239 @@
1
+ import yargs from "yargs-parser";
2
+ import chalk from "chalk";
3
+ import validator from "validator";
4
+ import CLI from "clui";
5
+ import clc from "cli-color";
6
+ //#region src/help.ts
7
+ /** Render the help body; the caller prints the version first. */
8
+ function renderHelp(commands, commonOptions) {
9
+ console.log("\nHelp:");
10
+ const Line = CLI.Line;
11
+ new Line().padding(5).column("Commands", 30, [clc.cyan]).column("Description", 30, [clc.cyan]).column("Alias", 20, [clc.cyan]).output();
12
+ new Line().output();
13
+ for (const [key, command] of Object.entries(commands)) {
14
+ if (!command || command.redirect) continue;
15
+ const commandText = "--" + key + (command.argument ? ` <${command.argument.name}>` : "");
16
+ const aliasText = (command.alias ?? []).map((alias) => alias.length === 1 ? `-${alias}` : `--${alias}`).join(" / ");
17
+ new Line().padding(5).column(commandText, 30).column(command.description ?? "", 30).column(aliasText, 20).output();
18
+ if (command.argument) new Line().padding(9).column(`<${command.argument.name}>`, 26).column(command.argument.description, 30).output();
19
+ for (const option of command.options ?? []) renderOption(option, 9, 26);
20
+ }
21
+ new Line().output();
22
+ if (commonOptions.length > 0) {
23
+ console.log("Options:\n");
24
+ new Line().padding(5).column("Options", 30, [clc.cyan]).column("Description", 30, [clc.cyan]).output();
25
+ for (const option of commonOptions) renderOption(option, 5, 30);
26
+ }
27
+ }
28
+ /** Scoped and common options share formatting, with different indentation. */
29
+ function renderOption(option, padding, width) {
30
+ const text = `--${(Array.isArray(option.name) ? option.name : [option.name]).join(", ")}` + (option.argument ? ` <${option.argument.name}>` : "");
31
+ new CLI.Line().padding(padding).column(text, width).column(option.description || "", 30).output();
32
+ if (option.argument) new CLI.Line().padding(padding + 4).column(`<${option.argument.name}>`, width - 4).column(option.argument.description || "", 30).output();
33
+ }
34
+ //#endregion
35
+ //#region src/utils/convert.ts
36
+ function toKebabCase(str) {
37
+ return (str[0]?.toLowerCase() ?? "") + str.slice(1).replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
38
+ }
39
+ function createCamelProxifiedObject(before) {
40
+ return new Proxy(before, {
41
+ get(target, key, receiver) {
42
+ const value = Reflect.get(target, key, receiver);
43
+ return typeof key === "string" ? value ?? Reflect.get(target, toKebabCase(key), receiver) : value;
44
+ },
45
+ set(target, key, value, receiver) {
46
+ if (typeof key === "string" && !key.includes("-") && Reflect.get(target, key, receiver) == null && Reflect.get(target, toKebabCase(key), receiver) != null) return Reflect.set(target, toKebabCase(key), value, receiver);
47
+ return Reflect.set(target, key, value, receiver);
48
+ }
49
+ });
50
+ }
51
+ //#endregion
52
+ //#region src/erii.ts
53
+ var Erii = class {
54
+ rawArguments;
55
+ parsedArguments;
56
+ version = "1.0.0";
57
+ name = "Erii";
58
+ commands = {};
59
+ commonOptions = [];
60
+ validator;
61
+ alwaysHandler;
62
+ defaultHandler;
63
+ constructor() {
64
+ this.rawArguments = process.argv.slice(2);
65
+ this.parsedArguments = yargs(process.argv.slice(2), { configuration: { "boolean-negation": false } });
66
+ this.validator = validator;
67
+ for (const key of Object.keys(this.parsedArguments)) if (key !== "_" && !this.rawArguments.includes("--" + key) && !this.rawArguments.includes("-" + key)) delete this.parsedArguments[key];
68
+ }
69
+ /**
70
+ * 绑定命令处理函数
71
+ * @param config
72
+ * @param handler
73
+ */
74
+ bind(config, handler) {
75
+ if (config.name === void 0) return console.error(chalk.red("Invalid command binding, ignored."));
76
+ const { name, description, argument } = config;
77
+ const [mainCommand, ...aliases] = Array.isArray(name) ? name : [name];
78
+ if (!mainCommand) {
79
+ console.error(chalk.red("Invalid command binding, ignored."));
80
+ return;
81
+ }
82
+ this.commands[mainCommand] = {
83
+ name: mainCommand,
84
+ description,
85
+ argument,
86
+ alias: aliases,
87
+ options: [],
88
+ handler
89
+ };
90
+ if (Array.isArray(name)) for (const alias of aliases) this.commands[alias] = {
91
+ name: alias,
92
+ redirect: mainCommand
93
+ };
94
+ }
95
+ /**
96
+ * 总是执行
97
+ * @param handler
98
+ */
99
+ always(handler) {
100
+ this.alwaysHandler = handler;
101
+ }
102
+ default(handler) {
103
+ this.defaultHandler = handler;
104
+ }
105
+ /**
106
+ * 增加设置项
107
+ * @param config
108
+ */
109
+ addOption(config) {
110
+ config.name = Array.isArray(config.name) ? config.name : [config.name];
111
+ if (!config.command) this.commonOptions.push(config);
112
+ else {
113
+ if (!(config.command in this.commands)) return console.error(chalk.red(`Command for option [${config.name.join(", ")}] not found, ignored.`));
114
+ const command = this.commands[config.command];
115
+ const target = command?.redirect ? this.commands[command.redirect] : command;
116
+ if (target) (target.options ??= []).push(config);
117
+ }
118
+ }
119
+ commandCtx(command) {
120
+ return {
121
+ showVersion: () => {
122
+ this.showVersion();
123
+ },
124
+ showHelp: () => {
125
+ this.showHelp();
126
+ },
127
+ getArgument: (commandName = command) => {
128
+ return this.getArgument(commandName);
129
+ }
130
+ };
131
+ }
132
+ /**
133
+ * 设定基础信息
134
+ * @param metaInfo
135
+ */
136
+ setMetaInfo({ version = "", name = "" } = {}) {
137
+ this.version = version;
138
+ this.name = name;
139
+ }
140
+ /**
141
+ * 显示帮助信息
142
+ */
143
+ showHelp(command) {
144
+ this.showVersion();
145
+ renderHelp(this.commands, this.commonOptions);
146
+ }
147
+ /**
148
+ * 显示版本号
149
+ */
150
+ showVersion() {
151
+ console.log(`${this.name} / ${this.version}`);
152
+ }
153
+ /**
154
+ * 启动
155
+ */
156
+ start() {
157
+ if (this.alwaysHandler) this.alwaysHandler();
158
+ if (this.defaultHandler) {
159
+ if (this.parsedArguments["_"].length === 0 && Object.keys(this.parsedArguments).length === 1) this.defaultHandler();
160
+ }
161
+ for (const key of Object.keys(this.parsedArguments)) if (key in this.commands) this.exec(key);
162
+ for (const key of this.parsedArguments["_"]) if (key in this.commands) this.exec(String(key));
163
+ }
164
+ /**
165
+ * 执行命令担当函数
166
+ * @param command
167
+ */
168
+ exec(command) {
169
+ const boundCommand = this.commands[command];
170
+ if (!boundCommand) return;
171
+ if (boundCommand.redirect) {
172
+ this.exec(boundCommand.redirect);
173
+ return;
174
+ }
175
+ const options = {};
176
+ for (const option of [...boundCommand.options ?? [], ...this.commonOptions]) {
177
+ const names = Array.isArray(option.name) ? option.name : [option.name];
178
+ const primaryName = names[0];
179
+ if (!primaryName) continue;
180
+ for (const name of names) {
181
+ if (!(name in this.parsedArguments)) continue;
182
+ if (this.validateArgument(this.parsedArguments[name], option.argument)) options[primaryName] = this.parsedArguments[name];
183
+ else {
184
+ console.error(chalk.red(`Argument validation failed for option '${name}'.`));
185
+ if (typeof option.argument?.validate === "string") console.error(chalk.red(`<${option.argument.name}> should be a/an ${option.argument.validate.slice(2)}.`));
186
+ }
187
+ }
188
+ }
189
+ const argumentValue = this.findArgument(command);
190
+ if (this.validateArgument(argumentValue, boundCommand.argument)) boundCommand.handler?.(this.commandCtx(command), createCamelProxifiedObject(options));
191
+ else {
192
+ console.error(chalk.red(`Argument validation failed for command ${command}`));
193
+ if (typeof boundCommand.argument?.validate === "string") console.error(chalk.red(`<${boundCommand.argument.name}> should be a/an ${boundCommand.argument.validate.slice(2)}.`));
194
+ }
195
+ }
196
+ validateArgument(argumentValue, argument) {
197
+ if (!argument || !argument.validate) return true;
198
+ if (typeof argument.validate === "string") {
199
+ if (argument.validate in this.validator) {
200
+ if (argumentValue === void 0) return false;
201
+ return this.validator[argument.validate](String(argumentValue));
202
+ } else {
203
+ console.error(chalk.red(`Unknown validate method for ${argument.name}.`));
204
+ return true;
205
+ }
206
+ } else return argument.validate(argumentValue, (message) => {
207
+ console.log(chalk.red(message));
208
+ });
209
+ }
210
+ /**
211
+ * 获得命令的参数
212
+ * @param commandName
213
+ * @param followRedirect 是否遵循重定向
214
+ */
215
+ getArgument(commandName, followRedirect = true) {
216
+ const value = this.findArgument(commandName, followRedirect);
217
+ if (value === void 0) console.error(chalk.red(`Command ${commandName} not found.`));
218
+ return value;
219
+ }
220
+ findArgument(commandName, followRedirect = true) {
221
+ const command = this.commands[commandName];
222
+ if (!command) return void 0;
223
+ if (command.redirect && followRedirect) return this.findArgument(command.redirect);
224
+ if (commandName !== "_" && Object.hasOwn(this.parsedArguments, commandName)) return this.parsedArguments[commandName];
225
+ for (const alias of command.alias ?? []) if (Object.hasOwn(this.parsedArguments, alias)) return this.parsedArguments[alias];
226
+ }
227
+ /**
228
+ * 启动
229
+ * エリイ 起きてます❤
230
+ */
231
+ okite() {
232
+ return this.start();
233
+ }
234
+ };
235
+ //#endregion
236
+ //#region src/index.ts
237
+ var src_default = new Erii();
238
+ //#endregion
239
+ export { Erii, src_default as default };
package/package.json CHANGED
@@ -1,30 +1,55 @@
1
- {
2
- "name": "erii",
3
- "version": "2.0.6",
4
- "description": "",
5
- "main": "./dist/index.js",
6
- "types": "./dist/index.d.ts",
7
- "scripts": {
8
- "build": "tsc"
9
- },
10
- "repository": {
11
- "type": "git",
12
- "url": "git+https://github.com/Last-Order/erii.git"
13
- },
14
- "author": "",
15
- "license": "MIT",
16
- "bugs": {
17
- "url": "https://github.com/Last-Order/erii/issues"
18
- },
19
- "homepage": "https://github.com/Last-Order/erii#readme",
20
- "dependencies": {
21
- "chalk": "^2.4.1",
22
- "clui": "^0.3.6",
23
- "validator": "^13.7.0",
24
- "yargs-parser": "^21.0.0"
25
- },
26
- "devDependencies": {
27
- "@types/node": "^10.17.48",
28
- "typescript": "^4.1.2"
29
- }
30
- }
1
+ {
2
+ "name": "erii",
3
+ "version": "3.0.0-beta.1",
4
+ "description": "",
5
+ "main": "./dist/index.mjs",
6
+ "types": "./dist/index.d.mts",
7
+ "scripts": {
8
+ "build": "tsdown",
9
+ "typecheck": "tsc --noEmit",
10
+ "test": "npm run build && npm run typecheck && node --test test/*.test.mjs",
11
+ "test:coverage": "npm run build && npm run typecheck && node --test --experimental-test-coverage --test-coverage-include=dist/index.mjs --test-coverage-include=src/utils/convert.ts --test-coverage-lines=100 --test-coverage-functions=100 --test-coverage-branches=95 test/*.test.mjs",
12
+ "prepack": "npm run build"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/Last-Order/erii.git"
17
+ },
18
+ "author": "",
19
+ "license": "MIT",
20
+ "bugs": {
21
+ "url": "https://github.com/Last-Order/erii/issues"
22
+ },
23
+ "homepage": "https://github.com/Last-Order/erii#readme",
24
+ "dependencies": {
25
+ "@types/validator": "^13.15.10",
26
+ "chalk": "^6.0.0",
27
+ "cli-color": "^2.0.0",
28
+ "clui": "^0.3.6",
29
+ "validator": "^13.15.35",
30
+ "yargs-parser": "^21.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/cli-color": "^2.0.6",
34
+ "@types/clui": "^0.3.5",
35
+ "@types/node": "^24.13.5",
36
+ "@types/yargs-parser": "^21.0.3",
37
+ "tsdown": "^0.23.0",
38
+ "typescript": "^7.0.2"
39
+ },
40
+ "type": "module",
41
+ "exports": {
42
+ ".": {
43
+ "types": "./dist/index.d.mts",
44
+ "import": "./dist/index.mjs"
45
+ }
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "readme.md",
50
+ "logo.png"
51
+ ],
52
+ "engines": {
53
+ "node": ">=22.18.0"
54
+ }
55
+ }