goke 6.5.0 → 6.5.2
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/README.md +176 -9
- package/dist/__test__/readme-examples.test.d.ts +5 -0
- package/dist/__test__/readme-examples.test.d.ts.map +1 -0
- package/dist/__test__/readme-examples.test.js +169 -0
- package/dist/__test__/types.test-d.js +238 -1
- package/dist/goke.d.ts +105 -16
- package/dist/goke.d.ts.map +1 -1
- package/dist/goke.js +22 -2
- package/dist/picocolors.d.ts +55 -0
- package/dist/picocolors.d.ts.map +1 -0
- package/dist/picocolors.js +78 -0
- package/dist/runtime-node.js +2 -2
- package/package.json +3 -5
- package/src/__test__/readme-examples.test.ts +225 -0
- package/src/__test__/types.test-d.ts +262 -1
- package/src/goke.ts +149 -16
- package/src/picocolors.ts +140 -0
- package/src/runtime-node.ts +2 -2
package/src/goke.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - Utility functions: string helpers, bracket parsing, dot-prop access
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import pc from 'picocolors'
|
|
13
|
+
import pc from './picocolors.js'
|
|
14
14
|
import mri from "./mri.js"
|
|
15
15
|
import { GokeError, coerceBySchema, extractJsonSchema, extractSchemaMetadata, isStandardSchema } from "./coerce.js"
|
|
16
16
|
import type { StandardJSONSchemaV1 } from "./coerce.js"
|
|
@@ -348,6 +348,92 @@ type OptionEntry<RawName extends string, Schema> =
|
|
|
348
348
|
? { [K in ExtractOptionName<RawName>]?: InferSchemaOutput<Schema> }
|
|
349
349
|
: { [K in ExtractOptionName<RawName>]: InferSchemaOutput<Schema> }
|
|
350
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Infer the raw runtime value shape for an option declared without a schema.
|
|
353
|
+
*
|
|
354
|
+
* Required value options (`--port <port>`) always reach actions as strings.
|
|
355
|
+
* Optional value options (`--host [host]`) can be strings, the sentinel
|
|
356
|
+
* boolean `true` when passed without a value, or `undefined` when omitted.
|
|
357
|
+
* Plain flags (`--verbose`) are booleans.
|
|
358
|
+
*/
|
|
359
|
+
type UntypedOptionValue<RawName extends string> =
|
|
360
|
+
RawName extends `${string}<${string}>` ? string :
|
|
361
|
+
RawName extends `${string}[${string}]` ? string | boolean | undefined :
|
|
362
|
+
boolean | undefined
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Build the option type entry for a `.option()` call that uses a plain
|
|
366
|
+
* description (no schema).
|
|
367
|
+
*/
|
|
368
|
+
type UntypedOptionEntry<RawName extends string> =
|
|
369
|
+
RawName extends `${string}<${string}>`
|
|
370
|
+
? { [K in ExtractOptionName<RawName>]: UntypedOptionValue<RawName> }
|
|
371
|
+
: { [K in ExtractOptionName<RawName>]?: UntypedOptionValue<RawName> }
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Tokenize a command raw name by splitting on whitespace.
|
|
375
|
+
* "mcp getNodeXml <id>" → ["mcp", "getNodeXml", "<id>"]
|
|
376
|
+
* "" → []
|
|
377
|
+
*/
|
|
378
|
+
type TokenizeName<S extends string, Acc extends readonly string[] = []> =
|
|
379
|
+
S extends `${infer Head} ${infer Rest}`
|
|
380
|
+
? TokenizeName<Rest, [...Acc, Head]>
|
|
381
|
+
: S extends ''
|
|
382
|
+
? Acc
|
|
383
|
+
: [...Acc, S]
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Given a single token, return the corresponding positional arg type or
|
|
387
|
+
* `never` if the token is not a bracketed arg.
|
|
388
|
+
*
|
|
389
|
+
* `<id>` → string (required)
|
|
390
|
+
* `[id]` → string | undefined (optional)
|
|
391
|
+
* `<...files>` → string[] (variadic required)
|
|
392
|
+
* `[...files]` → string[] (variadic optional)
|
|
393
|
+
* Anything else → never (filtered out by ExtractCommandArgs)
|
|
394
|
+
*/
|
|
395
|
+
type TokenToArgType<T extends string> =
|
|
396
|
+
T extends `<...${string}>` ? string[] :
|
|
397
|
+
T extends `[...${string}]` ? string[] :
|
|
398
|
+
T extends `<${string}>` ? string :
|
|
399
|
+
T extends `[${string}]` ? string | undefined :
|
|
400
|
+
never
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Filter a tokenized command raw name down to the positional arg tokens
|
|
404
|
+
* and map each to its inferred type.
|
|
405
|
+
*/
|
|
406
|
+
type ExtractCommandArgs<T extends readonly string[]> =
|
|
407
|
+
T extends readonly [infer Head extends string, ...infer Tail extends string[]]
|
|
408
|
+
? [TokenToArgType<Head>] extends [never]
|
|
409
|
+
? ExtractCommandArgs<Tail>
|
|
410
|
+
: [TokenToArgType<Head>, ...ExtractCommandArgs<Tail>]
|
|
411
|
+
: []
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Extract the tuple of positional arg types from a command raw name.
|
|
415
|
+
*
|
|
416
|
+
* "mcp getNodeXml <id>" → [string]
|
|
417
|
+
* "convert <input> <output>" → [string, string]
|
|
418
|
+
* "run [script]" → [string | undefined]
|
|
419
|
+
* "exec [...args]" → [string[]]
|
|
420
|
+
* "deploy" → []
|
|
421
|
+
*/
|
|
422
|
+
type ExtractPositionalArgs<RawName extends string> =
|
|
423
|
+
ExtractCommandArgs<TokenizeName<RawName>>
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Build the full argument tuple passed to a command's action callback.
|
|
427
|
+
*
|
|
428
|
+
* Format: [...positionalArgs, options, executionContext]
|
|
429
|
+
*
|
|
430
|
+
* This matches the runtime behavior in Goke.runMatchedCommand(): the action
|
|
431
|
+
* is called with positional args from the parsed command, then the parsed
|
|
432
|
+
* options object, then the injected GokeExecutionContext.
|
|
433
|
+
*/
|
|
434
|
+
type ActionArgs<RawName extends string, Opts> =
|
|
435
|
+
[...ExtractPositionalArgs<RawName>, Opts, GokeExecutionContext]
|
|
436
|
+
|
|
351
437
|
interface CommandArg {
|
|
352
438
|
required: boolean
|
|
353
439
|
value: string
|
|
@@ -368,7 +454,7 @@ type HelpCallback = (sections: HelpSection[]) => void | HelpSection[]
|
|
|
368
454
|
|
|
369
455
|
type CommandExample = ((bin: string) => string) | string
|
|
370
456
|
|
|
371
|
-
class Command {
|
|
457
|
+
class Command<RawName extends string = string, Opts = {}> {
|
|
372
458
|
options: Option[]
|
|
373
459
|
aliasNames: string[]
|
|
374
460
|
/* Parsed command name */
|
|
@@ -383,7 +469,7 @@ class Command {
|
|
|
383
469
|
_hidden?: boolean
|
|
384
470
|
|
|
385
471
|
constructor(
|
|
386
|
-
public rawName:
|
|
472
|
+
public rawName: RawName,
|
|
387
473
|
public description: string,
|
|
388
474
|
public config: CommandConfig = {},
|
|
389
475
|
public cli: Goke<any>
|
|
@@ -426,7 +512,9 @@ class Command {
|
|
|
426
512
|
*
|
|
427
513
|
* The second argument is either a description string or a StandardJSONSchemaV1
|
|
428
514
|
* schema. When a schema is provided, description and default are extracted from
|
|
429
|
-
* the JSON Schema automatically
|
|
515
|
+
* the JSON Schema automatically, and the option's type is tracked on the
|
|
516
|
+
* Command's `Opts` type parameter so that subsequent `.action()` callbacks
|
|
517
|
+
* receive a fully-typed options object.
|
|
430
518
|
*
|
|
431
519
|
* @example
|
|
432
520
|
* ```ts
|
|
@@ -438,10 +526,16 @@ class Command {
|
|
|
438
526
|
* ```
|
|
439
527
|
*/
|
|
440
528
|
option<
|
|
441
|
-
|
|
529
|
+
OptionRawName extends string,
|
|
442
530
|
S extends StandardJSONSchemaV1
|
|
443
|
-
>(
|
|
444
|
-
|
|
531
|
+
>(
|
|
532
|
+
rawName: OptionRawName,
|
|
533
|
+
schema: S,
|
|
534
|
+
): Command<RawName, Opts & OptionEntry<OptionRawName, S>>
|
|
535
|
+
option<OptionRawName extends string>(
|
|
536
|
+
rawName: OptionRawName,
|
|
537
|
+
description?: string,
|
|
538
|
+
): Command<RawName, Opts & UntypedOptionEntry<OptionRawName>>
|
|
445
539
|
option(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1): any {
|
|
446
540
|
const option = new Option(rawName, descriptionOrSchema)
|
|
447
541
|
this.options.push(option)
|
|
@@ -458,7 +552,24 @@ class Command {
|
|
|
458
552
|
return this
|
|
459
553
|
}
|
|
460
554
|
|
|
461
|
-
|
|
555
|
+
/**
|
|
556
|
+
* Register the action callback that runs when this command is matched.
|
|
557
|
+
*
|
|
558
|
+
* The callback receives positional args extracted from the command's raw name,
|
|
559
|
+
* followed by the parsed options object and the injected GokeExecutionContext.
|
|
560
|
+
*
|
|
561
|
+
* Positional arg types are inferred from the raw name at the type level:
|
|
562
|
+
* `command('convert <input> <output>')` → `(input: string, output: string, options, ctx)`
|
|
563
|
+
* `command('run [script]')` → `(script: string | undefined, options, ctx)`
|
|
564
|
+
* `command('exec [...args]')` → `(args: string[], options, ctx)`
|
|
565
|
+
*
|
|
566
|
+
* The options object is typed according to every `.option()` call chained
|
|
567
|
+
* on this command, plus any global options declared on the parent Goke
|
|
568
|
+
* instance before `.command()` was called.
|
|
569
|
+
*/
|
|
570
|
+
action(
|
|
571
|
+
callback: (...args: ActionArgs<RawName, Opts>) => unknown | Promise<unknown>,
|
|
572
|
+
): this {
|
|
462
573
|
this.commandAction = callback
|
|
463
574
|
return this
|
|
464
575
|
}
|
|
@@ -919,14 +1030,14 @@ interface ParsedArgv {
|
|
|
919
1030
|
}
|
|
920
1031
|
}
|
|
921
1032
|
|
|
922
|
-
class Goke<Opts
|
|
1033
|
+
class Goke<Opts = {}> extends EventEmitter {
|
|
923
1034
|
/** The program name to display in help and version message */
|
|
924
1035
|
name: string
|
|
925
|
-
commands: Command[]
|
|
1036
|
+
commands: Command<any, any>[]
|
|
926
1037
|
/** Middleware functions that run before the matched command action, in registration order */
|
|
927
1038
|
middlewares: Array<{ action: (options: any, context: GokeExecutionContext) => void | Promise<void> }>
|
|
928
1039
|
globalCommand: GlobalCommand
|
|
929
|
-
matchedCommand?: Command
|
|
1040
|
+
matchedCommand?: Command<any, any>
|
|
930
1041
|
matchedCommandName?: string
|
|
931
1042
|
/**
|
|
932
1043
|
* Raw CLI arguments
|
|
@@ -1056,10 +1167,24 @@ class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
|
|
|
1056
1167
|
}
|
|
1057
1168
|
|
|
1058
1169
|
/**
|
|
1059
|
-
* Add a sub-command
|
|
1170
|
+
* Add a sub-command.
|
|
1171
|
+
*
|
|
1172
|
+
* The returned Command is parameterized by the literal `rawName` (so positional
|
|
1173
|
+
* args can be inferred at the type level) and by this Goke's accumulated global
|
|
1174
|
+
* `Opts` (so global options declared before `.command()` are visible inside
|
|
1175
|
+
* `.action()` callbacks alongside the command's own options).
|
|
1060
1176
|
*/
|
|
1061
|
-
command
|
|
1062
|
-
|
|
1177
|
+
command<CommandRawName extends string>(
|
|
1178
|
+
rawName: CommandRawName,
|
|
1179
|
+
description?: string,
|
|
1180
|
+
config?: CommandConfig,
|
|
1181
|
+
): Command<CommandRawName, Opts> {
|
|
1182
|
+
const command = new Command<CommandRawName, Opts>(
|
|
1183
|
+
rawName,
|
|
1184
|
+
description || '',
|
|
1185
|
+
config,
|
|
1186
|
+
this,
|
|
1187
|
+
)
|
|
1063
1188
|
command.globalCommand = this.globalCommand
|
|
1064
1189
|
this.commands.push(command)
|
|
1065
1190
|
return command
|
|
@@ -1071,13 +1196,21 @@ class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
|
|
|
1071
1196
|
* Which is also applied to sub-commands.
|
|
1072
1197
|
*
|
|
1073
1198
|
* When a StandardJSONSchemaV1 schema is provided, the return type is narrowed
|
|
1074
|
-
* to include the inferred option type — enabling type-safe `.use()` callbacks
|
|
1199
|
+
* to include the inferred option type — enabling type-safe `.use()` callbacks
|
|
1200
|
+
* and typed `options` params inside command `.action()` handlers.
|
|
1201
|
+
*
|
|
1202
|
+
* When a plain description string is provided, the option is still tracked on
|
|
1203
|
+
* the Goke's `Opts` type, but with a loose `string | boolean | undefined` value
|
|
1204
|
+
* type (since no coercion schema is available).
|
|
1075
1205
|
*/
|
|
1076
1206
|
option<
|
|
1077
1207
|
RawName extends string,
|
|
1078
1208
|
S extends StandardJSONSchemaV1
|
|
1079
1209
|
>(rawName: RawName, schema: S): Goke<Opts & OptionEntry<RawName, S>>
|
|
1080
|
-
option
|
|
1210
|
+
option<RawName extends string>(
|
|
1211
|
+
rawName: RawName,
|
|
1212
|
+
description?: string,
|
|
1213
|
+
): Goke<Opts & UntypedOptionEntry<RawName>>
|
|
1081
1214
|
option(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1): any {
|
|
1082
1215
|
const option = new Option(rawName, descriptionOrSchema)
|
|
1083
1216
|
this.globalCommand.options.push(option)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored from picocolors by Alexey Raspopov (MIT license).
|
|
3
|
+
* Source: https://github.com/alexeyraspopov/picocolors/blob/main/picocolors.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { process } from '#runtime'
|
|
7
|
+
|
|
8
|
+
type Formatter = (input: unknown) => string
|
|
9
|
+
|
|
10
|
+
interface PicoColors {
|
|
11
|
+
isColorSupported: boolean
|
|
12
|
+
reset: Formatter
|
|
13
|
+
bold: Formatter
|
|
14
|
+
dim: Formatter
|
|
15
|
+
italic: Formatter
|
|
16
|
+
underline: Formatter
|
|
17
|
+
inverse: Formatter
|
|
18
|
+
hidden: Formatter
|
|
19
|
+
strikethrough: Formatter
|
|
20
|
+
black: Formatter
|
|
21
|
+
red: Formatter
|
|
22
|
+
green: Formatter
|
|
23
|
+
yellow: Formatter
|
|
24
|
+
blue: Formatter
|
|
25
|
+
magenta: Formatter
|
|
26
|
+
cyan: Formatter
|
|
27
|
+
white: Formatter
|
|
28
|
+
gray: Formatter
|
|
29
|
+
bgBlack: Formatter
|
|
30
|
+
bgRed: Formatter
|
|
31
|
+
bgGreen: Formatter
|
|
32
|
+
bgYellow: Formatter
|
|
33
|
+
bgBlue: Formatter
|
|
34
|
+
bgMagenta: Formatter
|
|
35
|
+
bgCyan: Formatter
|
|
36
|
+
bgWhite: Formatter
|
|
37
|
+
blackBright: Formatter
|
|
38
|
+
redBright: Formatter
|
|
39
|
+
greenBright: Formatter
|
|
40
|
+
yellowBright: Formatter
|
|
41
|
+
blueBright: Formatter
|
|
42
|
+
magentaBright: Formatter
|
|
43
|
+
cyanBright: Formatter
|
|
44
|
+
whiteBright: Formatter
|
|
45
|
+
bgBlackBright: Formatter
|
|
46
|
+
bgRedBright: Formatter
|
|
47
|
+
bgGreenBright: Formatter
|
|
48
|
+
bgYellowBright: Formatter
|
|
49
|
+
bgBlueBright: Formatter
|
|
50
|
+
bgMagentaBright: Formatter
|
|
51
|
+
bgCyanBright: Formatter
|
|
52
|
+
bgWhiteBright: Formatter
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const argv = process.argv || []
|
|
56
|
+
const env = process.env || {}
|
|
57
|
+
const isColorSupported =
|
|
58
|
+
!(!!env.NO_COLOR || argv.includes('--no-color'))
|
|
59
|
+
&& (
|
|
60
|
+
!!env.FORCE_COLOR
|
|
61
|
+
|| argv.includes('--color')
|
|
62
|
+
|| process.platform === 'win32'
|
|
63
|
+
|| (process.stdout.isTTY && env.TERM !== 'dumb')
|
|
64
|
+
|| !!env.CI
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const replaceClose = (string: string, close: string, replace: string, index: number) => {
|
|
68
|
+
let result = ''
|
|
69
|
+
let cursor = 0
|
|
70
|
+
|
|
71
|
+
do {
|
|
72
|
+
result += string.substring(cursor, index) + replace
|
|
73
|
+
cursor = index + close.length
|
|
74
|
+
index = string.indexOf(close, cursor)
|
|
75
|
+
} while (~index)
|
|
76
|
+
|
|
77
|
+
return result + string.substring(cursor)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const formatter = (open: string, close: string, replace = open): Formatter =>
|
|
81
|
+
(input) => {
|
|
82
|
+
const string = String(input)
|
|
83
|
+
const index = string.indexOf(close, open.length)
|
|
84
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const createColors = (enabled = isColorSupported): PicoColors => {
|
|
88
|
+
const f = enabled ? formatter : () => String
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
isColorSupported: enabled,
|
|
92
|
+
reset: f('\x1b[0m', '\x1b[0m'),
|
|
93
|
+
bold: f('\x1b[1m', '\x1b[22m', '\x1b[22m\x1b[1m'),
|
|
94
|
+
dim: f('\x1b[2m', '\x1b[22m', '\x1b[22m\x1b[2m'),
|
|
95
|
+
italic: f('\x1b[3m', '\x1b[23m'),
|
|
96
|
+
underline: f('\x1b[4m', '\x1b[24m'),
|
|
97
|
+
inverse: f('\x1b[7m', '\x1b[27m'),
|
|
98
|
+
hidden: f('\x1b[8m', '\x1b[28m'),
|
|
99
|
+
strikethrough: f('\x1b[9m', '\x1b[29m'),
|
|
100
|
+
black: f('\x1b[30m', '\x1b[39m'),
|
|
101
|
+
red: f('\x1b[31m', '\x1b[39m'),
|
|
102
|
+
green: f('\x1b[32m', '\x1b[39m'),
|
|
103
|
+
yellow: f('\x1b[33m', '\x1b[39m'),
|
|
104
|
+
blue: f('\x1b[34m', '\x1b[39m'),
|
|
105
|
+
magenta: f('\x1b[35m', '\x1b[39m'),
|
|
106
|
+
cyan: f('\x1b[36m', '\x1b[39m'),
|
|
107
|
+
white: f('\x1b[37m', '\x1b[39m'),
|
|
108
|
+
gray: f('\x1b[90m', '\x1b[39m'),
|
|
109
|
+
bgBlack: f('\x1b[40m', '\x1b[49m'),
|
|
110
|
+
bgRed: f('\x1b[41m', '\x1b[49m'),
|
|
111
|
+
bgGreen: f('\x1b[42m', '\x1b[49m'),
|
|
112
|
+
bgYellow: f('\x1b[43m', '\x1b[49m'),
|
|
113
|
+
bgBlue: f('\x1b[44m', '\x1b[49m'),
|
|
114
|
+
bgMagenta: f('\x1b[45m', '\x1b[49m'),
|
|
115
|
+
bgCyan: f('\x1b[46m', '\x1b[49m'),
|
|
116
|
+
bgWhite: f('\x1b[47m', '\x1b[49m'),
|
|
117
|
+
blackBright: f('\x1b[90m', '\x1b[39m'),
|
|
118
|
+
redBright: f('\x1b[91m', '\x1b[39m'),
|
|
119
|
+
greenBright: f('\x1b[92m', '\x1b[39m'),
|
|
120
|
+
yellowBright: f('\x1b[93m', '\x1b[39m'),
|
|
121
|
+
blueBright: f('\x1b[94m', '\x1b[39m'),
|
|
122
|
+
magentaBright: f('\x1b[95m', '\x1b[39m'),
|
|
123
|
+
cyanBright: f('\x1b[96m', '\x1b[39m'),
|
|
124
|
+
whiteBright: f('\x1b[97m', '\x1b[39m'),
|
|
125
|
+
bgBlackBright: f('\x1b[100m', '\x1b[49m'),
|
|
126
|
+
bgRedBright: f('\x1b[101m', '\x1b[49m'),
|
|
127
|
+
bgGreenBright: f('\x1b[102m', '\x1b[49m'),
|
|
128
|
+
bgYellowBright: f('\x1b[103m', '\x1b[49m'),
|
|
129
|
+
bgBlueBright: f('\x1b[104m', '\x1b[49m'),
|
|
130
|
+
bgMagentaBright: f('\x1b[105m', '\x1b[49m'),
|
|
131
|
+
bgCyanBright: f('\x1b[106m', '\x1b[49m'),
|
|
132
|
+
bgWhiteBright: f('\x1b[107m', '\x1b[49m'),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const pc = createColors()
|
|
137
|
+
|
|
138
|
+
export { createColors }
|
|
139
|
+
export type { PicoColors }
|
|
140
|
+
export default pc
|
package/src/runtime-node.ts
CHANGED
|
@@ -12,7 +12,7 @@ const fs: GokeFs = nodeFs
|
|
|
12
12
|
|
|
13
13
|
function openInBrowser(url: string): void {
|
|
14
14
|
if (!process.stdout.isTTY) {
|
|
15
|
-
process.
|
|
15
|
+
process.stdout.write(url + '\n')
|
|
16
16
|
return
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -25,7 +25,7 @@ function openInBrowser(url: string): void {
|
|
|
25
25
|
execSync(`xdg-open ${JSON.stringify(url)}`, { stdio: 'ignore' })
|
|
26
26
|
}
|
|
27
27
|
} catch {
|
|
28
|
-
process.
|
|
28
|
+
process.stdout.write(url + '\n')
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|