@zhin.js/command 1.0.6 → 1.0.9

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/src/definition.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  createCapabilityContext,
4
4
  type CapabilityContext,
5
5
  } from '@zhin.js/feature-kit';
6
+ import { assertPermitSyntax } from '@zhin.js/permission';
6
7
 
7
8
  const commandBrand = 'zhin.command/1' as const;
8
9
 
@@ -25,9 +26,38 @@ export type CommandParameterValue =
25
26
  | string
26
27
  | number
27
28
  | boolean
29
+ | ReadonlyArray<string | number | boolean>
28
30
  | Readonly<Record<string, unknown>>
29
31
  | null;
30
32
 
33
+ export const commandParameterTypes: ReadonlySet<CommandParameterType> = new Set([
34
+ 'string',
35
+ 'number',
36
+ 'integer',
37
+ 'float',
38
+ 'boolean',
39
+ 'word',
40
+ 'text',
41
+ 'mention',
42
+ 'image',
43
+ 'face',
44
+ 'reply',
45
+ 'forward',
46
+ 'dice',
47
+ 'rps',
48
+ ]);
49
+
50
+ /**
51
+ * Next.js 风格参数声明(`defineCommand({ params: ... })`)。
52
+ * 文件名只声明参数形态(`[name]` / `[[name]]` / `[...name]` / `[[...name]]`),
53
+ * 类型与默认值统一在这里声明。
54
+ */
55
+ export interface CommandParamSchema {
56
+ readonly type: CommandParameterType;
57
+ readonly default?: CommandParameterValue;
58
+ readonly description?: string;
59
+ }
60
+
31
61
  /** Minimal structural contract shared with canonical IM segments. */
32
62
  export interface CommandSegment {
33
63
  readonly type: string | { readonly name: string };
@@ -38,6 +68,11 @@ export interface CommandParameterDefinition {
38
68
  readonly name: string;
39
69
  readonly type: CommandParameterType;
40
70
  readonly defaultValue?: CommandParameterValue;
71
+ /** `[[name]]` / `[[...name]]` 可选段;缺省按 `defaultValue === undefined` 推断。 */
72
+ readonly optional?: boolean;
73
+ /** `[...name]` / `[[...name]]` 捕获所有段,运行时值为 `string[]`。 */
74
+ readonly rest?: boolean;
75
+ readonly description?: string;
41
76
  }
42
77
 
43
78
  /** 场景:群 / 私聊 / 频道等。 */
@@ -57,6 +92,25 @@ export interface CommandSender {
57
92
  readonly role: readonly string[];
58
93
  }
59
94
 
95
+ /**
96
+ * 命令侧入站会话契约(结构对齐 `@zhin.js/im-contract` 的 ConversationRef;
97
+ * command 为 Feature 层,不能 import core / IM 契约包,故独立声明)。
98
+ */
99
+ export interface CommandConversation {
100
+ readonly endpoint: Readonly<{
101
+ readonly id: string;
102
+ /** 适配器插件 owner(PluginId),与 `snapshot.config.get(adapter)` 对齐。 */
103
+ readonly adapter: string;
104
+ }>;
105
+ readonly kind: 'private' | 'group' | 'channel';
106
+ readonly id: string;
107
+ readonly parent?: Readonly<{
108
+ readonly kind: 'private' | 'group' | 'channel';
109
+ readonly id: string;
110
+ }>;
111
+ readonly threadId?: string;
112
+ }
113
+
60
114
  /**
61
115
  * 命令侧入站消息契约。
62
116
  *
@@ -64,17 +118,37 @@ export interface CommandSender {
64
118
  * 因架构分层(command 为 Feature 层,不能 import core),此处独立声明。
65
119
  */
66
120
  export interface CommandMessage {
67
- readonly adapter: string;
68
- readonly target: string;
121
+ readonly conversation: CommandConversation;
69
122
  readonly content: string;
70
- /** 发送者 id(扁平字段;结构化视图见 CommandContext.sender)。 */
71
- readonly sender?: string;
123
+ /** 发送者(结构化视图见 CommandContext.sender)。 */
124
+ readonly sender?: { readonly id: string; readonly name?: string; readonly roles?: readonly string[] };
72
125
  readonly id?: string;
73
126
  readonly metadata?: Readonly<Record<string, unknown>>;
74
127
  /** 若上游已结构化,优先采用。 */
75
128
  readonly scene?: CommandScene;
76
- readonly $reply?: (content: unknown) => Promise<unknown>;
77
- readonly $replyFrom?: (requester: string, content: unknown) => Promise<unknown>;
129
+ // 方法式声明(而非属性式函数类型):方法参数双变,runtime `Message`
130
+ // `$reply(content: SendContent)` 等才能鸭式兼容本契约(属性式是抗变,会报错)。
131
+ $reply?(content: unknown): Promise<unknown>;
132
+ $replyFrom?(requester: string, content: unknown): Promise<unknown>;
133
+ /** 向同 Endpoint 的另一个通道发送消息(结构兼容 `Message.$sendTo`)。 */
134
+ $sendTo?(
135
+ conversation: {
136
+ readonly kind: 'private' | 'group' | 'channel';
137
+ readonly id: string;
138
+ readonly parent?: Readonly<{ readonly kind: 'private' | 'group' | 'channel'; readonly id: string }>;
139
+ readonly threadId?: string;
140
+ },
141
+ content: unknown,
142
+ ): Promise<unknown>;
143
+ /** 私信当前消息的发送者(结构兼容 `Message.$replyToPrivate`)。 */
144
+ $replyToPrivate?(
145
+ content: unknown,
146
+ from?: boolean | { readonly kind: 'group' | 'channel'; readonly id: string },
147
+ ): Promise<unknown>;
148
+ /** 向指定群发送消息(结构兼容 `Message.$replyToGroup`)。 */
149
+ $replyToGroup?(groupId: string, content: unknown): Promise<unknown>;
150
+ /** 向指定频道发送消息(结构兼容 `Message.$replyToChannel`)。 */
151
+ $replyToChannel?(channelId: string, guildId: string, content: unknown, threadId?: string): Promise<unknown>;
78
152
  }
79
153
 
80
154
  /**
@@ -119,6 +193,26 @@ export interface CommandDefinition<
119
193
  readonly $feature: typeof commandBrand;
120
194
  readonly $parameter?: CommandParameterDefinition;
121
195
  readonly description?: string;
196
+ /**
197
+ * Next.js 风格参数声明:动态段文件名(`[name]` 等)的形态配合这里的
198
+ * 类型 / 默认值 / 描述使用。静态命令可忽略本字段。
199
+ */
200
+ readonly params?: Readonly<Record<string, CommandParamSchema>>;
201
+ /**
202
+ * 本地静态段别名(可多词,如 `'gh issue'`)。替换全部本地静态段后仍挂
203
+ * owner 前缀;不打破子插件命名空间。
204
+ */
205
+ readonly alias?: readonly string[];
206
+ /**
207
+ * 内置 permit DSL(AND)。单项内逗号为 OR。
208
+ * 例:`adapter(icqq)`、`role(master)`、`group(123,456)`。
209
+ */
210
+ readonly permit?: readonly string[];
211
+ /**
212
+ * 全局整句快捷方式:触发串(trim 后全文相等)→ 预填 params。
213
+ * 可打破 owner 命名空间。
214
+ */
215
+ readonly shortcut?: Readonly<Record<string, Readonly<Record<string, CommandParameterValue>>>>;
122
216
  execute(context: CommandContext<TConfig, TInput>): TResult | Promise<TResult>;
123
217
  }
124
218
 
@@ -145,9 +239,69 @@ export function defineCommand<
145
239
  if (typeof definition.execute !== 'function') {
146
240
  throw new TypeError('Command execute must be a function');
147
241
  }
242
+ if (definition.params !== undefined) {
243
+ if (!definition.params || typeof definition.params !== 'object') {
244
+ throw new TypeError('Command params must be a Record<string, CommandParamSchema>');
245
+ }
246
+ for (const [name, schema] of Object.entries(definition.params)) {
247
+ if (!schema || typeof schema !== 'object'
248
+ || !commandParameterTypes.has((schema as CommandParamSchema).type)) {
249
+ throw new TypeError(`Command params.${name} requires a valid type`);
250
+ }
251
+ }
252
+ }
253
+ validateCommandAlias(definition.alias);
254
+ validateCommandPermit(definition.permit);
255
+ validateCommandShortcutShape(definition.shortcut);
148
256
  return Object.freeze({ $feature: commandBrand, ...definition });
149
257
  }
150
258
 
259
+ function validateCommandAlias(alias: readonly string[] | undefined): void {
260
+ if (alias === undefined) return;
261
+ if (!Array.isArray(alias)) {
262
+ throw new TypeError('Command alias must be a readonly string[]');
263
+ }
264
+ for (const [index, entry] of alias.entries()) {
265
+ if (typeof entry !== 'string') {
266
+ throw new TypeError(`Command alias[${index}] must be a string`);
267
+ }
268
+ const tokens = entry.trim().split(/\s+/u).filter(Boolean);
269
+ if (tokens.length === 0) {
270
+ throw new TypeError(`Command alias[${index}] must contain at least one token`);
271
+ }
272
+ }
273
+ }
274
+
275
+ function validateCommandPermit(permit: readonly string[] | undefined): void {
276
+ if (permit === undefined) return;
277
+ if (!Array.isArray(permit)) {
278
+ throw new TypeError('Command permit must be a readonly string[]');
279
+ }
280
+ for (const [index, entry] of permit.entries()) {
281
+ if (typeof entry !== 'string') {
282
+ throw new TypeError(`Command permit[${index}] must be a string`);
283
+ }
284
+ }
285
+ assertPermitSyntax(permit);
286
+ }
287
+
288
+ function validateCommandShortcutShape(
289
+ shortcut: Readonly<Record<string, Readonly<Record<string, CommandParameterValue>>>> | undefined,
290
+ ): void {
291
+ if (shortcut === undefined) return;
292
+ if (!shortcut || typeof shortcut !== 'object' || Array.isArray(shortcut)) {
293
+ throw new TypeError('Command shortcut must be a Record<string, Record<string, value>>');
294
+ }
295
+ for (const [trigger, params] of Object.entries(shortcut)) {
296
+ if (!trigger.trim()) {
297
+ throw new TypeError('Command shortcut keys must be non-empty after trim');
298
+ }
299
+ if (!params || typeof params !== 'object' || Array.isArray(params)) {
300
+ throw new TypeError(`Command shortcut[${JSON.stringify(trigger)}] must be a params Record`);
301
+ }
302
+ }
303
+ }
304
+
151
305
  export function bindCommandParameter<
152
306
  TConfig,
153
307
  TResult,
@@ -202,10 +356,9 @@ export function resolveCommandSession(input: unknown): CommandSession {
202
356
  ? input.metadata as Readonly<Record<string, unknown>>
203
357
  : undefined;
204
358
 
205
- const adapter = input.adapter.split('\0')[0] || undefined;
206
- const endpoint = typeof metadata?.endpoint === 'string' && metadata.endpoint
207
- ? metadata.endpoint
208
- : undefined;
359
+ const adapter = input.conversation.endpoint.adapter || undefined;
360
+ const endpoint = (input as { endpointId?: string }).endpointId
361
+ || (typeof metadata?.endpoint === 'string' && metadata.endpoint ? metadata.endpoint : undefined);
209
362
 
210
363
  const scene = resolveScene(input, metadata);
211
364
  const sender = resolveSender(input, metadata);
@@ -221,8 +374,8 @@ export function resolveCommandSession(input: unknown): CommandSession {
221
374
  function isCommandMessageLike(input: unknown): input is CommandMessage {
222
375
  if (!input || typeof input !== 'object') return false;
223
376
  const value = input as Partial<CommandMessage>;
224
- return typeof value.adapter === 'string'
225
- && typeof value.target === 'string'
377
+ return !!value.conversation
378
+ && typeof value.conversation === 'object'
226
379
  && typeof value.content === 'string';
227
380
  }
228
381
 
@@ -238,12 +391,9 @@ function resolveScene(
238
391
  });
239
392
  }
240
393
 
241
- const parsed = parseTarget(input.target);
242
- const type = (typeof metadata?.channelType === 'string' && metadata.channelType)
243
- || (typeof metadata?.type === 'string' && metadata.type)
244
- || parsed?.type;
245
- const id = (typeof metadata?.channelId === 'string' && metadata.channelId)
246
- || parsed?.id;
394
+ const conversation = input.conversation;
395
+ const type = conversation.kind;
396
+ const id = conversation.id;
247
397
  if (!type || !id) return undefined;
248
398
 
249
399
  const name = firstString(
@@ -268,18 +418,12 @@ function resolveSender(
268
418
  if (isCommandSender(structured)) {
269
419
  return freezeSender(structured);
270
420
  }
271
- // 允许上游把 sender 直接做成对象(未来 Runtime Message 演进)
272
- if (isCommandSender(input.sender)) {
273
- return freezeSender(input.sender);
274
- }
275
421
 
276
- const id = typeof input.sender === 'string' && input.sender
277
- ? input.sender
278
- : firstString(metadata?.user_id, metadata?.userId);
422
+ const id = input.sender?.id || firstString(metadata?.user_id, metadata?.userId);
279
423
  if (!id) return undefined;
280
424
 
281
- const name = firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
282
- const role = resolveRoles(metadata);
425
+ const name = input.sender?.name || firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
426
+ const role = resolveRoles(input, metadata);
283
427
 
284
428
  return Object.freeze({
285
429
  id,
@@ -289,8 +433,12 @@ function resolveSender(
289
433
  }
290
434
 
291
435
  function resolveRoles(
436
+ input: CommandMessage,
292
437
  metadata: Readonly<Record<string, unknown>> | undefined,
293
438
  ): readonly string[] {
439
+ if (input.sender?.roles?.length) {
440
+ return Object.freeze([...input.sender.roles]);
441
+ }
294
442
  const roles: string[] = [];
295
443
  const push = (value: unknown) => {
296
444
  if (typeof value !== 'string') return;
@@ -309,22 +457,6 @@ function resolveRoles(
309
457
  return Object.freeze(roles);
310
458
  }
311
459
 
312
- function parseTarget(target: string): { readonly type: string; readonly id: string } | undefined {
313
- const parts = target.split(':').filter(Boolean);
314
- if (parts.length < 2) return undefined;
315
- const [kind, ...rest] = parts;
316
- if (!kind) return undefined;
317
- const lastPart = parts.at(-1);
318
- if (!lastPart) return undefined;
319
- if (kind === 'channel' && parts.length >= 3) {
320
- return { type: 'channel', id: lastPart };
321
- }
322
- if (kind === 'temp' && parts.length >= 3) {
323
- return { type: 'private', id: lastPart };
324
- }
325
- return { type: kind, id: rest.join(':') };
326
- }
327
-
328
460
  function isCommandScene(value: unknown): value is CommandScene {
329
461
  if (!value || typeof value !== 'object') return false;
330
462
  const scene = value as Partial<CommandScene>;
package/src/index.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  export * from './command-index.js';
2
2
  export * from './definition.js';
3
+ export {
4
+ assertBuiltinPermits,
5
+ checkBuiltinPermit,
6
+ checkBuiltinPermitList,
7
+ isBuiltinPermit,
8
+ isPlatformPermit,
9
+ parsePermitName,
10
+ type ParsedPermit,
11
+ type PermitKind,
12
+ } from './permit.js';
3
13
  export {
4
14
  CommandPathSyntaxError,
5
15
  commandFeatureId,
package/src/permit.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Re-export permit 解析/校验 from @zhin.js/permission (SSOT)。
3
+ * 保留 CommandSession 兼容签名供 command-index 过渡使用。
4
+ */
5
+ export {
6
+ type PermitKind,
7
+ type ParsedPermit,
8
+ parsePermitName,
9
+ isBuiltinPermit,
10
+ isPlatformPermit,
11
+ assertPermitSyntax as assertBuiltinPermits,
12
+ checkBuiltinPermit,
13
+ checkBuiltinPermitList,
14
+ } from '@zhin.js/permission';
package/src/provider.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { basename, join, parse, sep } from 'node:path';
2
- import { featureId } from '@zhin.js/plugin-runtime';
2
+ import { featureId, isCapabilityLocalSegment } from '@zhin.js/plugin-runtime';
3
3
  import {
4
4
  defineFeatureProvider,
5
5
  type DiscoveryContext,
@@ -10,9 +10,8 @@ import { CommandIndex } from './command-index.js';
10
10
  import {
11
11
  bindCommandParameter,
12
12
  parseCommandDefinition,
13
+ type CommandDefinition,
13
14
  type CommandParameterDefinition,
14
- type CommandParameterType,
15
- type CommandParameterValue,
16
15
  } from './definition.js';
17
16
 
18
17
  export const commandFeatureId = featureId('zhin.command');
@@ -27,7 +26,7 @@ const commandFiles: SourceConvention = {
27
26
  const module = await context.host.loadModule<{ default?: unknown }>(source.source);
28
27
  const definition = parseCommandDefinition(module.default);
29
28
  const file = parseCommandFile(basename(source.source));
30
- return bindCommandParameter(definition, file?.parameter);
29
+ return bindCommandParameter(definition, resolveParameter(definition, file, source.source));
31
30
  },
32
31
  };
33
32
 
@@ -55,7 +54,7 @@ async function* discoverCommandDirectory(
55
54
  }
56
55
  }
57
56
  for (const entry of entries) {
58
- if (entry.kind === 'directory' && isCommandSegment(entry.name)) {
57
+ if (entry.kind === 'directory' && isCapabilityLocalSegment(entry.name)) {
59
58
  yield* discoverCommandDirectory(
60
59
  context,
61
60
  join(directory, entry.name),
@@ -75,51 +74,48 @@ async function* discoverCommandDirectory(
75
74
  }
76
75
  }
77
76
 
78
- function isCommandSegment(value: string): boolean {
79
- return /^[a-z0-9][a-z0-9-]*$/.test(value);
80
- }
81
-
82
77
  interface ParsedCommandFile {
83
78
  readonly localSegment: string;
84
- readonly parameter?: CommandParameterDefinition;
79
+ readonly parameter?: CommandParameterHint;
80
+ }
81
+
82
+ /** 文件名声明的参数形态;类型与默认值来自 `defineCommand({ params })`。 */
83
+ interface CommandParameterHint {
84
+ readonly name: string;
85
+ readonly optional: boolean;
86
+ readonly rest: boolean;
85
87
  }
86
88
 
87
- const dynamicCommandFilePattern =
88
- /^\[([a-z][a-zA-Z0-9]*):([a-z][a-z0-9-]*)(?:=([^\]]*))?\]\.(?:tsx?|[cm]?js)$/;
89
+ const dynamicCommandFilePatterns: ReadonlyArray<{
90
+ readonly pattern: RegExp;
91
+ readonly optional: boolean;
92
+ readonly rest: boolean;
93
+ }> = [
94
+ { pattern: /^\[\[\.\.\.([a-zA-Z][a-zA-Z0-9]*)\]\]\.(?:tsx?|[cm]?js)$/, optional: true, rest: true },
95
+ { pattern: /^\[\.\.\.([a-zA-Z][a-zA-Z0-9]*)\]\.(?:tsx?|[cm]?js)$/, optional: false, rest: true },
96
+ { pattern: /^\[\[([a-zA-Z][a-zA-Z0-9]*)\]\]\.(?:tsx?|[cm]?js)$/, optional: true, rest: false },
97
+ { pattern: /^\[([a-zA-Z][a-zA-Z0-9]*)\]\.(?:tsx?|[cm]?js)$/, optional: false, rest: false },
98
+ ];
89
99
 
90
- const commandParameterTypes = new Set<CommandParameterType>([
91
- 'string',
92
- 'number',
93
- 'integer',
94
- 'float',
95
- 'boolean',
96
- 'word',
97
- 'text',
98
- 'mention',
99
- 'image',
100
- 'face',
101
- 'reply',
102
- 'forward',
103
- 'dice',
104
- 'rps',
105
- ]);
100
+ const commandModuleExtension = /\.(?:tsx?|[cm]?js)$/u;
106
101
 
107
102
  function parseCommandFile(value: string): ParsedCommandFile | undefined {
108
- if (/^[a-z0-9][a-z0-9-]*\.(?:tsx?|[cm]?js)$/.test(value)) {
109
- return { localSegment: parse(value).name };
110
- }
111
- const match = dynamicCommandFilePattern.exec(value);
112
- if (match) {
113
- const [, name, rawType, rawDefault] = match;
114
- if (!name || !rawType || !commandParameterTypes.has(rawType as CommandParameterType)) {
115
- throw new CommandPathSyntaxError(value, `unsupported parameter type: ${rawType ?? ''}`);
103
+ // 静态段:ASCII kebab(hello.ts)或 Unicode 名(赞我.ts);与 isCapabilityLocalSegment 对齐。
104
+ if (commandModuleExtension.test(value)) {
105
+ const localSegment = parse(value).name;
106
+ if (isCapabilityLocalSegment(localSegment)) {
107
+ return { localSegment };
116
108
  }
117
- const type = rawType as CommandParameterType;
109
+ }
110
+ for (const { pattern, optional, rest } of dynamicCommandFilePatterns) {
111
+ const match = pattern.exec(value);
112
+ if (!match || !match[1]) continue;
113
+ const name = match[1];
118
114
  // Metadata can change during HMR while $name keeps the Capability identity stable.
119
- const parameter = rawDefault === undefined
120
- ? { name, type }
121
- : { name, type, defaultValue: parseParameterValue(name, type, rawDefault, value) };
122
- return { localSegment: `$${name}`, parameter };
115
+ return {
116
+ localSegment: `$${name}`,
117
+ parameter: { name, optional, rest },
118
+ };
123
119
  }
124
120
  if (value.startsWith('[') || value.includes(']')) {
125
121
  throw new CommandPathSyntaxError(value);
@@ -127,66 +123,51 @@ function parseCommandFile(value: string): ParsedCommandFile | undefined {
127
123
  return undefined;
128
124
  }
129
125
 
130
- function commandFilePriority(value: string, preferJavaScript: boolean): number {
131
- const extension = value.slice(value.lastIndexOf('.') + 1);
132
- const order = preferJavaScript
133
- ? ['js', 'mjs', 'cjs', 'ts', 'tsx']
134
- : ['ts', 'tsx', 'js', 'mjs', 'cjs'];
135
- const priority = order.indexOf(extension);
136
- return priority < 0 ? Number.MAX_SAFE_INTEGER : priority;
137
- }
138
-
139
- function parseParameterValue(
140
- name: string,
141
- type: CommandParameterType,
142
- value: string,
126
+ /** 把文件名形态与 `definition.params` 合并成完整参数定义。 */
127
+ function resolveParameter(
128
+ definition: CommandDefinition,
129
+ file: ParsedCommandFile | undefined,
143
130
  source: string,
144
- ): CommandParameterValue {
145
- if (type === 'string' || type === 'word' || type === 'text') return value;
146
- if (type === 'number' || type === 'integer' || type === 'float') {
147
- const number = Number(value);
148
- if (
149
- value.trim().length > 0
150
- && Number.isFinite(number)
151
- && (type !== 'integer' || Number.isInteger(number))
152
- && (type !== 'float' || value.includes('.'))
153
- ) return number;
131
+ ): CommandParameterDefinition | undefined {
132
+ const hint = file?.parameter;
133
+ if (!hint) return undefined;
134
+ const schema = definition.params?.[hint.name];
135
+ if (!schema) {
154
136
  throw new CommandPathSyntaxError(
155
137
  source,
156
- `default for ${name}:${type} is invalid`,
138
+ `missing params.${hint.name} declaration in defineCommand({ params })`,
157
139
  );
158
140
  }
159
- if (type === 'boolean') {
160
- if (value === 'true' || value === 'false') return value === 'true';
141
+ if (!hint.optional && schema.default !== undefined) {
161
142
  throw new CommandPathSyntaxError(
162
143
  source,
163
- `default for ${name}:${type} is invalid`,
144
+ `params.${hint.name} has a default but the file is required: rename to [[${hint.name}]]`,
164
145
  );
165
146
  }
166
- if (isStructuredParameter(type)) {
167
- throw new CommandPathSyntaxError(
168
- source,
169
- `default for structured parameter ${name}:${type} is not supported`,
170
- );
171
- }
172
- throw new CommandPathSyntaxError(
173
- source,
174
- `default for ${name}:${type} is invalid`,
175
- );
147
+ return {
148
+ name: hint.name,
149
+ type: schema.type,
150
+ ...(schema.default !== undefined ? { defaultValue: schema.default } : {}),
151
+ optional: hint.optional,
152
+ rest: hint.rest,
153
+ ...(schema.description !== undefined ? { description: schema.description } : {}),
154
+ };
176
155
  }
177
156
 
178
- function isStructuredParameter(type: CommandParameterType): boolean {
179
- return type === 'mention'
180
- || type === 'image'
181
- || type === 'face'
182
- || type === 'reply'
183
- || type === 'forward'
184
- || type === 'dice'
185
- || type === 'rps';
157
+ function commandFilePriority(value: string, preferJavaScript: boolean): number {
158
+ const extension = value.slice(value.lastIndexOf('.') + 1);
159
+ const order = preferJavaScript
160
+ ? ['js', 'mjs', 'cjs', 'ts', 'tsx']
161
+ : ['ts', 'tsx', 'js', 'mjs', 'cjs'];
162
+ const priority = order.indexOf(extension);
163
+ return priority < 0 ? Number.MAX_SAFE_INTEGER : priority;
186
164
  }
187
165
 
188
166
  export class CommandPathSyntaxError extends TypeError {
189
- constructor(file: string, detail = 'expected [name:type=default].ts(x)') {
167
+ constructor(
168
+ file: string,
169
+ detail = 'expected [name].ts(x), [[name]].ts(x), [...name].ts(x) or [[...name]].ts(x)',
170
+ ) {
190
171
  super(`Invalid Command path ${file}: ${detail}`);
191
172
  this.name = 'CommandPathSyntaxError';
192
173
  }