feat: add inline-schema for command schema
This commit is contained in:
parent
95015b090c
commit
c315e0643b
|
|
@ -10,7 +10,8 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@preact/signals-core": "^1.5.1",
|
||||
"boardgame-core": "file:"
|
||||
"boardgame-core": "file:",
|
||||
"inline-schema": "git+https://gitea.ayi-games.online/hypercross/inline-schema"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^8.0.2",
|
||||
|
|
@ -18,6 +19,40 @@
|
|||
"vitest": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
|
||||
"integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
|
||||
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
||||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
|
||||
|
|
@ -512,6 +547,78 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@module-federation/error-codes": {
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz",
|
||||
"integrity": "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@module-federation/runtime": {
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.22.0.tgz",
|
||||
"integrity": "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@module-federation/error-codes": "0.22.0",
|
||||
"@module-federation/runtime-core": "0.22.0",
|
||||
"@module-federation/sdk": "0.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@module-federation/runtime-core": {
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.22.0.tgz",
|
||||
"integrity": "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@module-federation/error-codes": "0.22.0",
|
||||
"@module-federation/sdk": "0.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@module-federation/runtime-tools": {
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.22.0.tgz",
|
||||
"integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@module-federation/runtime": "0.22.0",
|
||||
"@module-federation/webpack-bundler-runtime": "0.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@module-federation/sdk": {
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.22.0.tgz",
|
||||
"integrity": "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@module-federation/webpack-bundler-runtime": {
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.22.0.tgz",
|
||||
"integrity": "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@module-federation/runtime": "0.22.0",
|
||||
"@module-federation/sdk": "0.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz",
|
||||
"integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.5.0",
|
||||
"@emnapi/runtime": "^1.5.0",
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@preact/signals-core": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.0.tgz",
|
||||
|
|
@ -872,6 +979,195 @@
|
|||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.11.tgz",
|
||||
"integrity": "sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"optionalDependencies": {
|
||||
"@rspack/binding-darwin-arm64": "1.7.11",
|
||||
"@rspack/binding-darwin-x64": "1.7.11",
|
||||
"@rspack/binding-linux-arm64-gnu": "1.7.11",
|
||||
"@rspack/binding-linux-arm64-musl": "1.7.11",
|
||||
"@rspack/binding-linux-x64-gnu": "1.7.11",
|
||||
"@rspack/binding-linux-x64-musl": "1.7.11",
|
||||
"@rspack/binding-wasm32-wasi": "1.7.11",
|
||||
"@rspack/binding-win32-arm64-msvc": "1.7.11",
|
||||
"@rspack/binding-win32-ia32-msvc": "1.7.11",
|
||||
"@rspack/binding-win32-x64-msvc": "1.7.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/binding-darwin-arm64": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.11.tgz",
|
||||
"integrity": "sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rspack/binding-darwin-x64": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.11.tgz",
|
||||
"integrity": "sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-arm64-gnu": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.11.tgz",
|
||||
"integrity": "sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-arm64-musl": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.11.tgz",
|
||||
"integrity": "sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-x64-gnu": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.11.tgz",
|
||||
"integrity": "sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-x64-musl": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.11.tgz",
|
||||
"integrity": "sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rspack/binding-wasm32-wasi": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.11.tgz",
|
||||
"integrity": "sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "1.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/binding-win32-arm64-msvc": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.11.tgz",
|
||||
"integrity": "sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rspack/binding-win32-ia32-msvc": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.11.tgz",
|
||||
"integrity": "sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rspack/binding-win32-x64-msvc": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.11.tgz",
|
||||
"integrity": "sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rspack/core": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.7.11.tgz",
|
||||
"integrity": "sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@module-federation/runtime-tools": "0.22.0",
|
||||
"@rspack/binding": "1.7.11",
|
||||
"@rspack/lite-tapable": "1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/helpers": ">=0.5.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/helpers": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/lite-tapable": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.1.0.tgz",
|
||||
"integrity": "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.27.10",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
|
||||
|
|
@ -879,6 +1175,17 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
|
|
@ -1150,6 +1457,12 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/csv-parse": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.6.0.tgz",
|
||||
"integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
|
|
@ -1345,6 +1658,17 @@
|
|||
"node": ">=16.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inline-schema": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "git+https://gitea.ayi-games.online/hypercross/inline-schema#cf55295ce79a2dcf3114e2910cc6a28ce872be90",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"csv-parse": "^5.5.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rspack/core": "^1.x"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
|
||||
|
|
@ -2040,6 +2364,14 @@
|
|||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tsup": {
|
||||
"version": "8.5.1",
|
||||
"resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@preact/signals-core": "^1.5.1",
|
||||
"boardgame-core": "file:"
|
||||
"boardgame-core": "file:",
|
||||
"inline-schema": "git+https://gitea.ayi-games.online/hypercross/inline-schema"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^8.0.2",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
export type Command = {
|
||||
import { defineSchema, type ParsedSchema, ParseError } from 'inline-schema';
|
||||
|
||||
export type Command = {
|
||||
name: string;
|
||||
flags: Record<string, true>;
|
||||
options: Record<string, string>;
|
||||
params: string[];
|
||||
options: Record<string, unknown>;
|
||||
params: unknown[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -15,6 +17,8 @@ export type CommandParamSchema = {
|
|||
required: boolean;
|
||||
/** 是否可变参数(可以接收多个值) */
|
||||
variadic: boolean;
|
||||
/** 参数类型 schema(用于解析和验证) */
|
||||
schema?: ParsedSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -28,7 +32,9 @@ export type CommandOptionSchema = {
|
|||
/** 是否必需 */
|
||||
required: boolean;
|
||||
/** 默认值 */
|
||||
defaultValue?: string;
|
||||
defaultValue?: unknown;
|
||||
/** 选项类型 schema(用于解析和验证) */
|
||||
schema?: ParsedSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -75,9 +81,9 @@ export function parseCommand(input: string): Command {
|
|||
}
|
||||
|
||||
const name = tokens[0];
|
||||
const params: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
const flags: Record<string, true> = {};
|
||||
const options: Record<string, string> = {};
|
||||
const options: Record<string, unknown> = {};
|
||||
|
||||
let i = 1;
|
||||
while (i < tokens.length) {
|
||||
|
|
@ -181,13 +187,24 @@ function tokenize(input: string): string[] {
|
|||
* - [param] 可选参数
|
||||
* - <param...> 必需可变参数
|
||||
* - [param...] 可选可变参数
|
||||
* - <param: type> 带类型定义的必需参数
|
||||
* - [param: type] 带类型定义的可选参数
|
||||
* - --flag 长格式标志
|
||||
* - -f 短格式标志
|
||||
* - --option <value> 长格式选项
|
||||
* - --option: type 带类型的长格式选项
|
||||
* - -o <value> 短格式选项
|
||||
* - -o: type 带类型的短格式选项
|
||||
*
|
||||
* 类型语法使用 inline-schema 格式(使用 ; 而非 ,):
|
||||
* - string, number, boolean
|
||||
* - [string; number] 元组
|
||||
* - string[] 数组
|
||||
* - [string; number][] 元组数组
|
||||
*
|
||||
* @example
|
||||
* parseCommandSchema('move <from> [to...] [--force] [-f] [--speed <val>]')
|
||||
* parseCommandSchema('move <from: [x: string; y: string]> <to: string> [--all: boolean]')
|
||||
*/
|
||||
export function parseCommandSchema(schemaStr: string): CommandSchema {
|
||||
const schema: CommandSchema = {
|
||||
|
|
@ -218,12 +235,34 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
|||
const parts = inner.split(/\s+/);
|
||||
const name = parts[0].slice(2);
|
||||
|
||||
// 如果有额外的部分,则是选项(如 --opt value 或 --opt <value>)
|
||||
if (parts.length > 1) {
|
||||
// 可选选项
|
||||
// 检查是否有类型定义(如 --flag: boolean 或 --opt: string[])
|
||||
if (name.includes(':')) {
|
||||
const [optName, typeStr] = name.split(':').map(s => s.trim());
|
||||
const parsedSchema = defineSchema(typeStr);
|
||||
schema.options.push({
|
||||
name: optName,
|
||||
required: false,
|
||||
schema: parsedSchema,
|
||||
});
|
||||
} else if (parts.length > 1) {
|
||||
// 可选选项(旧语法:--opt <value>)
|
||||
const valueToken = parts[1];
|
||||
let typeStr = valueToken;
|
||||
// 如果是 <value> 格式,提取类型
|
||||
if (valueToken.startsWith('<') && valueToken.endsWith('>')) {
|
||||
typeStr = valueToken.slice(1, -1);
|
||||
}
|
||||
// 尝试解析为 inline-schema 类型
|
||||
let parsedSchema: ParsedSchema | undefined;
|
||||
try {
|
||||
parsedSchema = defineSchema(typeStr);
|
||||
} catch {
|
||||
// 不是有效的 schema,使用默认字符串
|
||||
}
|
||||
schema.options.push({
|
||||
name,
|
||||
required: false,
|
||||
schema: parsedSchema,
|
||||
});
|
||||
} else {
|
||||
// 可选标志
|
||||
|
|
@ -234,13 +273,34 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
|||
const parts = inner.split(/\s+/);
|
||||
const short = parts[0].slice(1);
|
||||
|
||||
// 如果有额外的部分,则是选项
|
||||
if (parts.length > 1) {
|
||||
// 可选选项
|
||||
// 检查是否有类型定义
|
||||
if (short.includes(':')) {
|
||||
const [optName, typeStr] = short.split(':').map(s => s.trim());
|
||||
const parsedSchema = defineSchema(typeStr);
|
||||
schema.options.push({
|
||||
name: optName,
|
||||
short: optName,
|
||||
required: false,
|
||||
schema: parsedSchema,
|
||||
});
|
||||
} else if (parts.length > 1) {
|
||||
// 可选选项(旧语法)
|
||||
const valueToken = parts[1];
|
||||
let typeStr = valueToken;
|
||||
if (valueToken.startsWith('<') && valueToken.endsWith('>')) {
|
||||
typeStr = valueToken.slice(1, -1);
|
||||
}
|
||||
let parsedSchema: ParsedSchema | undefined;
|
||||
try {
|
||||
parsedSchema = defineSchema(typeStr);
|
||||
} catch {
|
||||
// 不是有效的 schema,使用默认字符串
|
||||
}
|
||||
schema.options.push({
|
||||
name: short,
|
||||
short,
|
||||
required: false,
|
||||
schema: parsedSchema,
|
||||
});
|
||||
} else {
|
||||
// 可选标志
|
||||
|
|
@ -249,12 +309,25 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
|||
} else {
|
||||
// 可选参数
|
||||
const isVariadic = inner.endsWith('...');
|
||||
const name = isVariadic ? inner.slice(0, -3) : inner;
|
||||
let paramContent = isVariadic ? inner.slice(0, -3) : inner;
|
||||
let parsedSchema: ParsedSchema | undefined;
|
||||
|
||||
// 检查是否有类型定义(如 [name: string])
|
||||
if (paramContent.includes(':')) {
|
||||
const [name, typeStr] = paramContent.split(':').map(s => s.trim());
|
||||
try {
|
||||
parsedSchema = defineSchema(typeStr);
|
||||
} catch {
|
||||
// 不是有效的 schema
|
||||
}
|
||||
paramContent = name;
|
||||
}
|
||||
|
||||
schema.params.push({
|
||||
name,
|
||||
name: paramContent,
|
||||
required: false,
|
||||
variadic: isVariadic,
|
||||
schema: parsedSchema,
|
||||
});
|
||||
}
|
||||
i++;
|
||||
|
|
@ -263,11 +336,30 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
|||
const name = token.slice(2);
|
||||
const nextToken = tokens[i + 1];
|
||||
|
||||
// 如果下一个 token 是 <value> 格式,则是选项
|
||||
if (nextToken && nextToken.startsWith('<') && nextToken.endsWith('>')) {
|
||||
// 检查是否有类型定义(如 --flag: boolean)
|
||||
if (name.includes(':')) {
|
||||
const [optName, typeStr] = name.split(':').map(s => s.trim());
|
||||
const parsedSchema = defineSchema(typeStr);
|
||||
schema.options.push({
|
||||
name: optName,
|
||||
required: true,
|
||||
schema: parsedSchema,
|
||||
});
|
||||
i++;
|
||||
} else if (nextToken && nextToken.startsWith('<') && nextToken.endsWith('>')) {
|
||||
// 旧语法:--opt <value>
|
||||
const valueToken = nextToken;
|
||||
const typeStr = valueToken.slice(1, -1);
|
||||
let parsedSchema: ParsedSchema | undefined;
|
||||
try {
|
||||
parsedSchema = defineSchema(typeStr);
|
||||
} catch {
|
||||
// 不是有效的 schema
|
||||
}
|
||||
schema.options.push({
|
||||
name,
|
||||
required: true,
|
||||
schema: parsedSchema,
|
||||
});
|
||||
i += 2;
|
||||
} else {
|
||||
|
|
@ -280,12 +372,32 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
|||
const short = token.slice(1);
|
||||
const nextToken = tokens[i + 1];
|
||||
|
||||
// 如果下一个 token 是 <value> 格式,则是选项
|
||||
if (nextToken && nextToken.startsWith('<') && nextToken.endsWith('>')) {
|
||||
// 检查是否有类型定义
|
||||
if (short.includes(':')) {
|
||||
const [optName, typeStr] = short.split(':').map(s => s.trim());
|
||||
const parsedSchema = defineSchema(typeStr);
|
||||
schema.options.push({
|
||||
name: optName,
|
||||
short: optName,
|
||||
required: true,
|
||||
schema: parsedSchema,
|
||||
});
|
||||
i++;
|
||||
} else if (nextToken && nextToken.startsWith('<') && nextToken.endsWith('>')) {
|
||||
// 旧语法
|
||||
const valueToken = nextToken;
|
||||
const typeStr = valueToken.slice(1, -1);
|
||||
let parsedSchema: ParsedSchema | undefined;
|
||||
try {
|
||||
parsedSchema = defineSchema(typeStr);
|
||||
} catch {
|
||||
// 不是有效的 schema
|
||||
}
|
||||
schema.options.push({
|
||||
name: short,
|
||||
short,
|
||||
required: true,
|
||||
schema: parsedSchema,
|
||||
});
|
||||
i += 2;
|
||||
} else {
|
||||
|
|
@ -296,12 +408,27 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
|||
} else if (token.startsWith('<') && token.endsWith('>')) {
|
||||
// 必需参数
|
||||
const isVariadic = token.endsWith('...>');
|
||||
const name = token.replace(/^[<]+|[>.>]+$/g, '');
|
||||
let paramContent = token.replace(/^[<]+|[>.>]+$/g, '');
|
||||
let parsedSchema: ParsedSchema | undefined;
|
||||
|
||||
// 检查是否有类型定义(如 <from: [x: string; y: string]>)
|
||||
if (paramContent.includes(':')) {
|
||||
const colonIndex = paramContent.indexOf(':');
|
||||
const name = paramContent.slice(0, colonIndex).trim();
|
||||
const typeStr = paramContent.slice(colonIndex + 1).trim();
|
||||
try {
|
||||
parsedSchema = defineSchema(typeStr);
|
||||
} catch (e) {
|
||||
// 不是有效的 schema
|
||||
}
|
||||
paramContent = name;
|
||||
}
|
||||
|
||||
schema.params.push({
|
||||
name,
|
||||
name: paramContent,
|
||||
required: true,
|
||||
variadic: isVariadic,
|
||||
schema: parsedSchema,
|
||||
});
|
||||
i++;
|
||||
} else {
|
||||
|
|
@ -450,3 +577,110 @@ export function validateCommand(
|
|||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 schema 解析并验证命令,返回类型化的命令对象
|
||||
* 如果 schema 中定义了类型,会自动解析参数和选项的值
|
||||
*
|
||||
* @param input 命令行输入字符串
|
||||
* @param schemaStr 命令 schema 字符串
|
||||
* @returns 解析后的命令对象和验证结果
|
||||
*
|
||||
* @example
|
||||
* const result = parseCommandWithSchema(
|
||||
* 'move [1; 2] region1 --all true',
|
||||
* 'move <from: [x: string; y: string]> <to: string> [--all: boolean]'
|
||||
* );
|
||||
* // result.command.params[0] = ['1', '2'] (已解析为元组)
|
||||
* // result.command.options.all = true (已解析为布尔值)
|
||||
*/
|
||||
export function parseCommandWithSchema(
|
||||
input: string,
|
||||
schemaStr: string
|
||||
): { command: Command; valid: true } | { command: Command; valid: false; errors: string[] } {
|
||||
const schema = parseCommandSchema(schemaStr);
|
||||
const command = parseCommand(input);
|
||||
|
||||
// 验证命令名称
|
||||
if (command.name !== schema.name) {
|
||||
return {
|
||||
command,
|
||||
valid: false,
|
||||
errors: [`命令名称不匹配:期望 "${schema.name}",实际 "${command.name}"`],
|
||||
};
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
|
||||
// 验证参数数量
|
||||
const requiredParams = schema.params.filter(p => p.required);
|
||||
const variadicParam = schema.params.find(p => p.variadic);
|
||||
|
||||
if (command.params.length < requiredParams.length) {
|
||||
errors.push(`参数不足:至少需要 ${requiredParams.length} 个参数,实际 ${command.params.length} 个`);
|
||||
return { command, valid: false, errors };
|
||||
}
|
||||
|
||||
if (!variadicParam && command.params.length > schema.params.length) {
|
||||
errors.push(`参数过多:最多 ${schema.params.length} 个参数,实际 ${command.params.length} 个`);
|
||||
return { command, valid: false, errors };
|
||||
}
|
||||
|
||||
// 验证必需的选项
|
||||
const requiredOptions = schema.options.filter(o => o.required);
|
||||
for (const opt of requiredOptions) {
|
||||
const hasOption = opt.name in command.options || (opt.short && opt.short in command.options);
|
||||
if (!hasOption) {
|
||||
errors.push(`缺少必需选项:--${opt.name}${opt.short ? ` 或 -${opt.short}` : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { command, valid: false, errors };
|
||||
}
|
||||
|
||||
// 使用 schema 解析参数值
|
||||
const parsedParams: unknown[] = [];
|
||||
for (let i = 0; i < command.params.length; i++) {
|
||||
const paramValue = command.params[i];
|
||||
const paramSchema = schema.params[i]?.schema;
|
||||
|
||||
if (paramSchema) {
|
||||
try {
|
||||
// 如果是字符串值,使用 schema 解析
|
||||
const parsed = typeof paramValue === 'string'
|
||||
? paramSchema.parse(paramValue)
|
||||
: paramValue;
|
||||
parsedParams.push(parsed);
|
||||
} catch (e) {
|
||||
const err = e as ParseError;
|
||||
errors.push(`参数 "${schema.params[i]?.name}" 解析失败:${err.message}`);
|
||||
}
|
||||
} else {
|
||||
parsedParams.push(paramValue);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 schema 解析选项值
|
||||
const parsedOptions: Record<string, unknown> = { ...command.options };
|
||||
for (const [key, value] of Object.entries(command.options)) {
|
||||
const optSchema = schema.options.find(o => o.name === key || o.short === key);
|
||||
if (optSchema?.schema && typeof value === 'string') {
|
||||
try {
|
||||
parsedOptions[key] = optSchema.schema.parse(value);
|
||||
} catch (e) {
|
||||
const err = e as ParseError;
|
||||
errors.push(`选项 "--${key}" 解析失败:${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { command: { ...command, params: parsedParams, options: parsedOptions }, valid: false, errors };
|
||||
}
|
||||
|
||||
return {
|
||||
command: { ...command, params: parsedParams, options: parsedOptions },
|
||||
valid: true,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,227 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
parseCommandSchema,
|
||||
parseCommandWithSchema,
|
||||
validateCommand,
|
||||
parseCommand,
|
||||
type CommandSchema,
|
||||
} from '../../src/utils/command';
|
||||
|
||||
describe('parseCommandSchema with inline-schema', () => {
|
||||
it('should parse schema with typed params', () => {
|
||||
const schema = parseCommandSchema('move <from: [x: string; y: string]> <to: string>');
|
||||
expect(schema.name).toBe('move');
|
||||
expect(schema.params).toHaveLength(2);
|
||||
expect(schema.params[0].name).toBe('from');
|
||||
expect(schema.params[0].schema).toBeDefined();
|
||||
expect(schema.params[1].name).toBe('to');
|
||||
expect(schema.params[1].schema).toBeDefined();
|
||||
});
|
||||
|
||||
it('should parse schema with typed options', () => {
|
||||
const schema = parseCommandSchema('move <from> <to> [--all: boolean] [--count: number]');
|
||||
expect(schema.name).toBe('move');
|
||||
expect(schema.options).toHaveLength(2);
|
||||
expect(schema.options[0].name).toBe('all');
|
||||
expect(schema.options[0].schema).toBeDefined();
|
||||
expect(schema.options[1].name).toBe('count');
|
||||
expect(schema.options[1].schema).toBeDefined();
|
||||
});
|
||||
|
||||
it('should parse schema with tuple type', () => {
|
||||
const schema = parseCommandSchema('place <coords: [number; number]>');
|
||||
expect(schema.name).toBe('place');
|
||||
expect(schema.params).toHaveLength(1);
|
||||
expect(schema.params[0].name).toBe('coords');
|
||||
expect(schema.params[0].schema).toBeDefined();
|
||||
});
|
||||
|
||||
it('should parse schema with array type', () => {
|
||||
const schema = parseCommandSchema('select <ids: string[]>');
|
||||
expect(schema.name).toBe('select');
|
||||
expect(schema.params).toHaveLength(1);
|
||||
expect(schema.params[0].name).toBe('ids');
|
||||
expect(schema.params[0].schema).toBeDefined();
|
||||
});
|
||||
|
||||
it('should parse schema with tuple array type', () => {
|
||||
const schema = parseCommandSchema('place <coords: [number; number][]>');
|
||||
expect(schema.name).toBe('place');
|
||||
expect(schema.params).toHaveLength(1);
|
||||
expect(schema.params[0].name).toBe('coords');
|
||||
expect(schema.params[0].schema).toBeDefined();
|
||||
});
|
||||
|
||||
it('should parse schema with mixed types', () => {
|
||||
const schema = parseCommandSchema(
|
||||
'move <from: [x: string; y: string]> <to: string> [--all: boolean] [--count: number]'
|
||||
);
|
||||
expect(schema.name).toBe('move');
|
||||
expect(schema.params).toHaveLength(2);
|
||||
expect(schema.options).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should parse schema with optional typed param', () => {
|
||||
const schema = parseCommandSchema('move <from> [to: string]');
|
||||
expect(schema.name).toBe('move');
|
||||
expect(schema.params).toHaveLength(2);
|
||||
expect(schema.params[0].required).toBe(true);
|
||||
expect(schema.params[1].required).toBe(false);
|
||||
expect(schema.params[1].schema).toBeDefined();
|
||||
});
|
||||
|
||||
it('should parse schema with optional typed option', () => {
|
||||
const schema = parseCommandSchema('move <from> [--speed: number]');
|
||||
expect(schema.name).toBe('move');
|
||||
expect(schema.options).toHaveLength(1);
|
||||
expect(schema.options[0].required).toBe(false);
|
||||
expect(schema.options[0].schema).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseCommandWithSchema', () => {
|
||||
it('should parse and validate command with tuple param', () => {
|
||||
const result = parseCommandWithSchema(
|
||||
'move [1; 2] region1',
|
||||
'move <from: [x: string; y: string]> <to: string>'
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
if (result.valid) {
|
||||
expect(result.command.params).toHaveLength(2);
|
||||
expect(result.command.params[0]).toEqual(['1', '2']);
|
||||
expect(result.command.params[1]).toBe('region1');
|
||||
}
|
||||
});
|
||||
|
||||
it('should parse and validate command with boolean option', () => {
|
||||
const result = parseCommandWithSchema(
|
||||
'move meeple1 region1 --all true',
|
||||
'move <from> <to> [--all: boolean]'
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
if (result.valid) {
|
||||
expect(result.command.options.all).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should parse and validate command with number option', () => {
|
||||
const result = parseCommandWithSchema(
|
||||
'move meeple1 region1 --count 5',
|
||||
'move <from> <to> [--count: number]'
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
if (result.valid) {
|
||||
expect(result.command.options.count).toBe(5);
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail validation with wrong command name', () => {
|
||||
const result = parseCommandWithSchema(
|
||||
'jump meeple1 region1',
|
||||
'move <from> <to>'
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
if (!result.valid) {
|
||||
expect(result.errors).toContainEqual(
|
||||
expect.stringContaining('命令名称不匹配')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail validation with missing required param', () => {
|
||||
const result = parseCommandWithSchema(
|
||||
'move meeple1',
|
||||
'move <from> <to>'
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
if (!result.valid) {
|
||||
expect(result.errors).toContainEqual(
|
||||
expect.stringContaining('参数不足')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail validation with missing required option', () => {
|
||||
const result = parseCommandWithSchema(
|
||||
'move meeple1 region1',
|
||||
'move <from> <to> [--force: boolean]'
|
||||
);
|
||||
// 可选选项,应该通过验证
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse complex command with typed params and options', () => {
|
||||
const result = parseCommandWithSchema(
|
||||
'move [1; 2] region1 --all true --count 3',
|
||||
'move <from: [x: string; y: string]> <to: string> [--all: boolean] [--count: number]'
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
if (result.valid) {
|
||||
expect(result.command.params[0]).toEqual(['1', '2']);
|
||||
expect(result.command.params[1]).toBe('region1');
|
||||
expect(result.command.options.all).toBe(true);
|
||||
expect(result.command.options.count).toBe(3);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle number parsing in tuple', () => {
|
||||
const result = parseCommandWithSchema(
|
||||
'place [10; 20]',
|
||||
'place <coords: [number; number]>'
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
if (result.valid) {
|
||||
expect(result.command.params[0]).toEqual([10, 20]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle string array parsing', () => {
|
||||
const result = parseCommandWithSchema(
|
||||
'select [alice; bob; charlie]',
|
||||
'select <ids: string[]>'
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
if (result.valid) {
|
||||
expect(result.command.params[0]).toEqual(['alice', 'bob', 'charlie']);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle tuple array parsing', () => {
|
||||
const result = parseCommandWithSchema(
|
||||
'place [[1; 2]; [3; 4]; [5; 6]]',
|
||||
'place <coords: [number; number][]>'
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
if (result.valid) {
|
||||
expect(result.command.params[0]).toEqual([[1, 2], [3, 4], [5, 6]]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateCommand with schema types', () => {
|
||||
it('should validate command with typed params', () => {
|
||||
const schema = parseCommandSchema('move <from: [x: string; y: string]> <to: string>');
|
||||
const command = parseCommand('move [1; 2] region1');
|
||||
const result = validateCommand(command, schema);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate command with typed options', () => {
|
||||
const schema = parseCommandSchema('move <from> <to> [--all: boolean]');
|
||||
const command = parseCommand('move meeple1 region1 --all true');
|
||||
const result = validateCommand(command, schema);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail validation with insufficient params', () => {
|
||||
const schema = parseCommandSchema('move <from: [x: string; y: string]> <to: string>');
|
||||
const command = parseCommand('move [1; 2]');
|
||||
const result = validateCommand(command, schema);
|
||||
expect(result.valid).toBe(false);
|
||||
if (!result.valid) {
|
||||
expect(result.errors).toContainEqual(
|
||||
expect.stringContaining('参数不足')
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue