effect-start 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/package.json +57 -0
  4. package/src/Bundle.ts +167 -0
  5. package/src/BundleFiles.ts +174 -0
  6. package/src/BundleHttp.test.ts +160 -0
  7. package/src/BundleHttp.ts +259 -0
  8. package/src/Commander.test.ts +1378 -0
  9. package/src/Commander.ts +672 -0
  10. package/src/Datastar.test.ts +267 -0
  11. package/src/Datastar.ts +68 -0
  12. package/src/Effect_HttpRouter.test.ts +570 -0
  13. package/src/EncryptedCookies.test.ts +427 -0
  14. package/src/EncryptedCookies.ts +451 -0
  15. package/src/FileHttpRouter.test.ts +207 -0
  16. package/src/FileHttpRouter.ts +122 -0
  17. package/src/FileRouter.ts +405 -0
  18. package/src/FileRouterCodegen.test.ts +598 -0
  19. package/src/FileRouterCodegen.ts +251 -0
  20. package/src/FileRouter_files.test.ts +64 -0
  21. package/src/FileRouter_path.test.ts +132 -0
  22. package/src/FileRouter_tree.test.ts +126 -0
  23. package/src/FileSystemExtra.ts +102 -0
  24. package/src/HttpAppExtra.ts +127 -0
  25. package/src/Hyper.ts +194 -0
  26. package/src/HyperHtml.test.ts +90 -0
  27. package/src/HyperHtml.ts +139 -0
  28. package/src/HyperNode.ts +37 -0
  29. package/src/JsModule.test.ts +14 -0
  30. package/src/JsModule.ts +116 -0
  31. package/src/PublicDirectory.test.ts +280 -0
  32. package/src/PublicDirectory.ts +108 -0
  33. package/src/Route.test.ts +873 -0
  34. package/src/Route.ts +992 -0
  35. package/src/Router.ts +80 -0
  36. package/src/SseHttpResponse.ts +55 -0
  37. package/src/Start.ts +133 -0
  38. package/src/StartApp.ts +43 -0
  39. package/src/StartHttp.ts +42 -0
  40. package/src/StreamExtra.ts +146 -0
  41. package/src/TestHttpClient.test.ts +54 -0
  42. package/src/TestHttpClient.ts +100 -0
  43. package/src/bun/BunBundle.test.ts +277 -0
  44. package/src/bun/BunBundle.ts +309 -0
  45. package/src/bun/BunBundle_imports.test.ts +50 -0
  46. package/src/bun/BunFullstackServer.ts +45 -0
  47. package/src/bun/BunFullstackServer_httpServer.ts +541 -0
  48. package/src/bun/BunImportTrackerPlugin.test.ts +77 -0
  49. package/src/bun/BunImportTrackerPlugin.ts +97 -0
  50. package/src/bun/BunTailwindPlugin.test.ts +335 -0
  51. package/src/bun/BunTailwindPlugin.ts +322 -0
  52. package/src/bun/BunVirtualFilesPlugin.ts +59 -0
  53. package/src/bun/index.ts +4 -0
  54. package/src/client/Overlay.ts +34 -0
  55. package/src/client/ScrollState.ts +120 -0
  56. package/src/client/index.ts +101 -0
  57. package/src/index.ts +24 -0
  58. package/src/jsx-datastar.d.ts +63 -0
  59. package/src/jsx-runtime.ts +23 -0
  60. package/src/jsx.d.ts +4402 -0
  61. package/src/testing.ts +55 -0
  62. package/src/x/cloudflare/CloudflareTunnel.ts +110 -0
  63. package/src/x/cloudflare/index.ts +1 -0
  64. package/src/x/datastar/Datastar.test.ts +267 -0
  65. package/src/x/datastar/Datastar.ts +68 -0
  66. package/src/x/datastar/index.ts +4 -0
  67. package/src/x/datastar/jsx-datastar.d.ts +63 -0
@@ -0,0 +1,672 @@
1
+ import * as Data from "effect/Data"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Pipeable from "effect/Pipeable"
4
+ import * as Predicate from "effect/Predicate"
5
+ import * as Schema from "effect/Schema"
6
+
7
+ export class CommanderError extends Data.TaggedError("CommanderError")<{
8
+ message: string
9
+ cause?: unknown
10
+ }> {}
11
+
12
+ const TypeId: unique symbol = Symbol.for("effect-start/Commander")
13
+
14
+ type KebabToCamel<S extends string> = S extends `${infer First}-${infer Rest}`
15
+ ? `${First}${KebabToCamel<Capitalize<Rest>>}`
16
+ : S
17
+
18
+ type StripPrefix<S extends string> = S extends `--${infer Name}` ? Name
19
+ : S extends `-${infer Name}` ? Name
20
+ : S
21
+
22
+ type OptionNameToCamelCase<S extends string> = KebabToCamel<StripPrefix<S>>
23
+
24
+ export interface OptionBuilder<A = any, Name extends string = string> {
25
+ readonly _tag: "OptionBuilder"
26
+ readonly name: Name
27
+ readonly long: Name
28
+ readonly short?: string
29
+ readonly description: string
30
+ readonly schema?: Schema.Schema<A, any>
31
+ readonly defaultValue?: A
32
+ }
33
+
34
+ type OptionBuilderWithSchema<A, Name extends string> =
35
+ & Omit<
36
+ OptionBuilder<A, Name>,
37
+ "schema" | "defaultValue"
38
+ >
39
+ & {
40
+ readonly schema: Schema.Schema<A, any>
41
+ readonly defaultValue?: A
42
+ }
43
+
44
+ export const option = <
45
+ const Long extends `--${string}`,
46
+ const Short extends `-${string}` | undefined = undefined,
47
+ >(
48
+ long: Long,
49
+ short?: Short,
50
+ ): {
51
+ // Using exact `string` would reject schemas with literal encoded types,
52
+ // like in case of Schema.BooleanFromString.
53
+ schema<A, I extends string = string>(
54
+ schema: Schema.Schema<A, I>,
55
+ ): OptionBuilderWithSchema<A, Long>
56
+
57
+ description(
58
+ desc: string,
59
+ ): {
60
+ schema<A, I extends string = string>(
61
+ schema: Schema.Schema<A, I>,
62
+ ): OptionBuilderWithSchema<A, Long>
63
+
64
+ default<A>(
65
+ value: A,
66
+ ): {
67
+ schema<A2 extends A, I extends string = string>(
68
+ schema: Schema.Schema<A2, I>,
69
+ ): OptionBuilderWithSchema<A2, Long>
70
+ }
71
+ }
72
+
73
+ default<A>(
74
+ value: A,
75
+ ): {
76
+ schema<A2 extends A, I extends string = string>(
77
+ schema: Schema.Schema<A2, I>,
78
+ ): OptionBuilderWithSchema<A2, Long>
79
+
80
+ description(
81
+ desc: string,
82
+ ): {
83
+ schema<A2 extends A, I extends string = string>(
84
+ schema: Schema.Schema<A2, I>,
85
+ ): OptionBuilderWithSchema<A2, Long>
86
+ }
87
+ }
88
+ } => {
89
+ const longName = long
90
+ const shortName = short ? short.slice(1) : undefined
91
+
92
+ return {
93
+ schema: <A, I extends string = string>(schema: Schema.Schema<A, I>) => ({
94
+ _tag: "OptionBuilder" as const,
95
+ name: longName,
96
+ long: longName,
97
+ short: shortName,
98
+ description: "",
99
+ schema,
100
+ }),
101
+
102
+ description: (desc) => ({
103
+ schema: <A, I extends string = string>(schema: Schema.Schema<A, I>) => ({
104
+ _tag: "OptionBuilder" as const,
105
+ name: longName,
106
+ long: longName,
107
+ short: shortName,
108
+ description: desc,
109
+ schema,
110
+ }),
111
+
112
+ default: <A>(value: A) => ({
113
+ schema: <A2 extends A, I extends string = string>(
114
+ schema: Schema.Schema<A2, I>,
115
+ ) => ({
116
+ _tag: "OptionBuilder" as const,
117
+ name: longName,
118
+ long: longName,
119
+ short: shortName,
120
+ description: desc,
121
+ schema,
122
+ defaultValue: value as A2,
123
+ }),
124
+ }),
125
+ }),
126
+
127
+ default: <A>(value: A) => ({
128
+ schema: <A2 extends A, I extends string = string>(
129
+ schema: Schema.Schema<A2, I>,
130
+ ) => ({
131
+ _tag: "OptionBuilder" as const,
132
+ name: longName,
133
+ long: longName,
134
+ short: shortName,
135
+ description: "",
136
+ schema,
137
+ defaultValue: value as A2,
138
+ }),
139
+
140
+ description: (desc) => ({
141
+ schema: <A2 extends A, I extends string = string>(
142
+ schema: Schema.Schema<A2, I>,
143
+ ) => ({
144
+ _tag: "OptionBuilder" as const,
145
+ name: longName,
146
+ long: longName,
147
+ short: shortName,
148
+ description: desc,
149
+ schema,
150
+ defaultValue: value as A2,
151
+ }),
152
+ }),
153
+ }),
154
+ }
155
+ }
156
+
157
+ export type OptionsMap = Record<string, OptionBuilder<any, any>>
158
+
159
+ type ExtractOptionsFromBuilders<Opts extends OptionsMap> = {
160
+ [
161
+ K in keyof Opts as Opts[K] extends OptionBuilder<any, infer Name>
162
+ ? OptionNameToCamelCase<Name>
163
+ : never
164
+ ]: Opts[K] extends OptionBuilder<infer A, any> ? A : never
165
+ }
166
+
167
+ export type ExtractOptionValues<Opts extends OptionsMap> =
168
+ ExtractOptionsFromBuilders<Opts>
169
+
170
+ export interface SubcommandDef<Handled extends boolean = boolean> {
171
+ readonly _tag: "SubcommandDef"
172
+ readonly command: CommanderSet<any, any, Handled>
173
+ }
174
+
175
+ type CommanderBuilder = {
176
+ option: typeof optionMethod
177
+ optionHelp: typeof optionHelp
178
+ optionVersion: typeof optionVersion
179
+ subcommand: typeof subcommand
180
+ handle: typeof handle
181
+ }
182
+
183
+ export type CommanderSet<
184
+ Opts extends OptionsMap = {},
185
+ Subcommands extends ReadonlyArray<SubcommandDef> = [],
186
+ Handled extends boolean = false,
187
+ > =
188
+ & Pipeable.Pipeable
189
+ & CommanderSet.Instance<Opts, Subcommands, Handled>
190
+ & {
191
+ [TypeId]: typeof TypeId
192
+ }
193
+ & CommanderBuilder
194
+
195
+ export namespace CommanderSet {
196
+ export type Instance<
197
+ Opts extends OptionsMap = {},
198
+ Subcommands extends ReadonlyArray<SubcommandDef> = [],
199
+ Handled extends boolean = false,
200
+ > = {
201
+ readonly name: string
202
+ readonly description?: string
203
+ readonly version?: string
204
+ readonly options: Opts
205
+ readonly subcommands: Subcommands
206
+ readonly handler?: Handled extends true
207
+ ? (args: ExtractOptionValues<Opts>) => Effect.Effect<void>
208
+ : never
209
+ }
210
+
211
+ export type Default = CommanderSet<{}, [], false>
212
+
213
+ export type Proto = {
214
+ [TypeId]: typeof TypeId
215
+ pipe(): any
216
+ } & CommanderBuilder
217
+ }
218
+
219
+ const optionMethod = function<
220
+ S,
221
+ Opt extends OptionBuilder<any, any>,
222
+ >(
223
+ this: S,
224
+ opt: Opt,
225
+ ): S extends CommanderSet<infer Opts, infer Subs, infer _H> ? CommanderSet<
226
+ & Opts
227
+ & {
228
+ [K in Opt["name"] as OptionNameToCamelCase<K>]: Opt
229
+ },
230
+ Subs,
231
+ false
232
+ >
233
+ : CommanderSet<
234
+ {
235
+ [K in Opt["name"] as OptionNameToCamelCase<K>]: Opt
236
+ },
237
+ [],
238
+ false
239
+ >
240
+ {
241
+ const base = this && typeof this === "object" ? this as any : {}
242
+ const baseName = base.name || ""
243
+ const baseOptions: OptionsMap = base.options || {}
244
+ const baseSubcommands = base.subcommands || []
245
+ const baseDescription = base.description
246
+ const baseVersion = base.version
247
+
248
+ const camelKey = kebabToCamel(stripPrefix(opt.long))
249
+
250
+ return makeSet({
251
+ name: baseName,
252
+ description: baseDescription,
253
+ version: baseVersion,
254
+ options: { ...baseOptions, [camelKey]: opt },
255
+ subcommands: baseSubcommands,
256
+ }) as any
257
+ }
258
+
259
+ export const optionHelp = function<S>(
260
+ this: S,
261
+ ): S extends CommanderSet<infer Opts, infer Subs, infer _H> ? CommanderSet<
262
+ & Opts
263
+ & {
264
+ "help": OptionBuilder<boolean, "--help">
265
+ },
266
+ Subs,
267
+ false
268
+ >
269
+ : CommanderSet<
270
+ {
271
+ "help": OptionBuilder<boolean, "--help">
272
+ },
273
+ [],
274
+ false
275
+ >
276
+ {
277
+ const base = this && typeof this === "object" ? this as any : {}
278
+ const baseName = base.name || ""
279
+ const baseOptions: OptionsMap = base.options || {}
280
+ const baseSubcommands = base.subcommands || []
281
+ const baseDescription = base.description
282
+ const baseVersion = base.version
283
+
284
+ const helpOption: OptionBuilder<boolean, "--help"> = {
285
+ _tag: "OptionBuilder",
286
+ name: "--help",
287
+ long: "--help",
288
+ short: "h",
289
+ description: "Show help information",
290
+ defaultValue: false,
291
+ }
292
+
293
+ return makeSet({
294
+ name: baseName,
295
+ description: baseDescription,
296
+ version: baseVersion,
297
+ options: { ...baseOptions, help: helpOption },
298
+ subcommands: baseSubcommands,
299
+ }) as any
300
+ }
301
+
302
+ export const optionVersion = function<S>(
303
+ this: S,
304
+ ): S extends CommanderSet<infer Opts, infer Subs, infer _H> ? CommanderSet<
305
+ & Opts
306
+ & {
307
+ "version": OptionBuilder<boolean, "--version">
308
+ },
309
+ Subs,
310
+ false
311
+ >
312
+ : CommanderSet<
313
+ {
314
+ "version": OptionBuilder<boolean, "--version">
315
+ },
316
+ [],
317
+ false
318
+ >
319
+ {
320
+ const base = this && typeof this === "object" ? this as any : {}
321
+ const baseName = base.name || ""
322
+ const baseOptions: OptionsMap = base.options || {}
323
+ const baseSubcommands = base.subcommands || []
324
+ const baseDescription = base.description
325
+ const baseVersion = base.version
326
+
327
+ const versionOption: OptionBuilder<boolean, "--version"> = {
328
+ _tag: "OptionBuilder",
329
+ name: "--version",
330
+ long: "--version",
331
+ short: "V",
332
+ description: "Show version information",
333
+ defaultValue: false,
334
+ }
335
+
336
+ return makeSet({
337
+ name: baseName,
338
+ description: baseDescription,
339
+ version: baseVersion,
340
+ options: { ...baseOptions, version: versionOption },
341
+ subcommands: baseSubcommands,
342
+ }) as any
343
+ }
344
+
345
+ export const subcommand = function<
346
+ S,
347
+ SubOpts extends OptionsMap,
348
+ SubSubs extends ReadonlyArray<SubcommandDef>,
349
+ SubHandled extends boolean,
350
+ >(
351
+ this: S,
352
+ cmd: CommanderSet<SubOpts, SubSubs, SubHandled>,
353
+ ): S extends CommanderSet<infer Opts, infer Subs, infer _H> ? CommanderSet<
354
+ Opts,
355
+ [...Subs, SubcommandDef<SubHandled>],
356
+ false
357
+ >
358
+ : CommanderSet<
359
+ {},
360
+ [SubcommandDef<SubHandled>],
361
+ false
362
+ >
363
+ {
364
+ const base = this && typeof this === "object" ? this as any : {}
365
+ const baseName = base.name || ""
366
+ const baseOptions = base.options || {}
367
+ const baseSubcommands = base.subcommands || []
368
+ const baseDescription = base.description
369
+ const baseVersion = base.version
370
+
371
+ const subDef: SubcommandDef = {
372
+ _tag: "SubcommandDef",
373
+ command: cmd as any,
374
+ }
375
+
376
+ return makeSet({
377
+ name: baseName,
378
+ description: baseDescription,
379
+ version: baseVersion,
380
+ options: baseOptions,
381
+ subcommands: [...baseSubcommands, subDef] as any,
382
+ }) as any
383
+ }
384
+
385
+ export const handle = function<
386
+ Opts extends OptionsMap,
387
+ Subs extends ReadonlyArray<SubcommandDef>,
388
+ >(
389
+ this: CommanderSet<Opts, Subs, false>,
390
+ handler: (args: ExtractOptionValues<Opts>) => Effect.Effect<void>,
391
+ ): CommanderSet<Opts, Subs, true> {
392
+ const base = this && typeof this === "object" ? this as any : {}
393
+
394
+ return makeSet({
395
+ name: base.name || "",
396
+ description: base.description,
397
+ version: base.version,
398
+ options: base.options || {},
399
+ subcommands: base.subcommands || [],
400
+ handler: handler as any,
401
+ }) as any
402
+ }
403
+
404
+ export const make = <const Name extends string>(config: {
405
+ readonly name: Name
406
+ readonly description?: string
407
+ readonly version?: string
408
+ }): CommanderSet<{}, [], false> =>
409
+ makeSet({
410
+ name: config.name,
411
+ description: config.description,
412
+ version: config.version,
413
+ options: {},
414
+ subcommands: [],
415
+ })
416
+
417
+ const CommanderProto = {
418
+ [TypeId]: TypeId,
419
+
420
+ option: optionMethod,
421
+ optionHelp,
422
+ optionVersion,
423
+ subcommand,
424
+ handle,
425
+
426
+ pipe() {
427
+ return Pipeable.pipeArguments(this, arguments)
428
+ },
429
+ } satisfies CommanderSet.Proto
430
+
431
+ function makeSet<
432
+ Opts extends OptionsMap,
433
+ Subs extends ReadonlyArray<SubcommandDef>,
434
+ Handled extends boolean = false,
435
+ >(config: {
436
+ readonly name: string
437
+ readonly description?: string
438
+ readonly version?: string
439
+ readonly options: Opts
440
+ readonly subcommands: Subs
441
+ readonly handler?: (args: ExtractOptionValues<Opts>) => Effect.Effect<void>
442
+ }): CommanderSet<Opts, Subs, Handled> {
443
+ return Object.assign(
444
+ Object.create(CommanderProto),
445
+ config,
446
+ ) as CommanderSet<Opts, Subs, Handled>
447
+ }
448
+
449
+ export function isCommanderSet(
450
+ input: unknown,
451
+ ): input is CommanderSet<any, any, any> {
452
+ return Predicate.hasProperty(input, TypeId)
453
+ }
454
+
455
+ const kebabToCamel = (str: string): string => {
456
+ return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
457
+ }
458
+
459
+ const stripPrefix = (str: string): string => {
460
+ if (str.startsWith("--")) return str.slice(2)
461
+ if (str.startsWith("-")) return str.slice(1)
462
+ return str
463
+ }
464
+
465
+ interface ParsedArgs {
466
+ readonly flags: Record<string, boolean>
467
+ readonly options: Record<string, string>
468
+ readonly positional: ReadonlyArray<string>
469
+ }
470
+
471
+ const parseRawArgs = (
472
+ args: ReadonlyArray<string>,
473
+ ): Effect.Effect<ParsedArgs, CommanderError> =>
474
+ Effect.gen(function*() {
475
+ const flags: Record<string, boolean> = {}
476
+ const options: Record<string, string> = {}
477
+ const positional: Array<string> = []
478
+
479
+ let i = 0
480
+ while (i < args.length) {
481
+ const arg = args[i]!
482
+
483
+ if (arg === "--") {
484
+ positional.push(...args.slice(i + 1))
485
+ break
486
+ }
487
+
488
+ if (arg.startsWith("--")) {
489
+ const equalIndex = arg.indexOf("=")
490
+ if (equalIndex !== -1) {
491
+ const key = arg.slice(2, equalIndex)
492
+ const value = arg.slice(equalIndex + 1)
493
+ options[key] = value
494
+ } else {
495
+ const key = arg.slice(2)
496
+ if (i + 1 < args.length && !args[i + 1]!.startsWith("-")) {
497
+ options[key] = args[i + 1]!
498
+ i++
499
+ } else {
500
+ flags[key] = true
501
+ }
502
+ }
503
+ } else if (arg.startsWith("-") && arg.length > 1) {
504
+ const chars = arg.slice(1)
505
+ for (let j = 0; j < chars.length; j++) {
506
+ const char = chars[j]!
507
+ if (
508
+ j === chars.length - 1 && i + 1 < args.length && !args[i + 1]!
509
+ .startsWith("-")
510
+ ) {
511
+ options[char] = args[i + 1]!
512
+ i++
513
+ } else {
514
+ flags[char] = true
515
+ }
516
+ }
517
+ } else {
518
+ positional.push(arg)
519
+ }
520
+
521
+ i++
522
+ }
523
+
524
+ return { flags, options, positional }
525
+ })
526
+
527
+ export const parse = <
528
+ Opts extends OptionsMap,
529
+ Subs extends ReadonlyArray<SubcommandDef>,
530
+ Handled extends boolean,
531
+ >(
532
+ cmd: CommanderSet<Opts, Subs, Handled>,
533
+ args: ReadonlyArray<string>,
534
+ ): Effect.Effect<ExtractOptionValues<Opts>, CommanderError> =>
535
+ Effect.gen(function*() {
536
+ const parsed = yield* parseRawArgs(args)
537
+
538
+ const result: Record<string, any> = {}
539
+
540
+ for (const optBuilder of Object.values(cmd.options)) {
541
+ const longName = stripPrefix(optBuilder.long)
542
+ const shortName = optBuilder.short
543
+
544
+ const longMatch = parsed.options[longName] || parsed.flags[longName]
545
+ const shortMatch = shortName
546
+ ? (parsed.options[shortName] || parsed.flags[shortName])
547
+ : undefined
548
+
549
+ const rawValue = longMatch ?? shortMatch
550
+
551
+ const camelKey = kebabToCamel(stripPrefix(optBuilder.long))
552
+
553
+ if (rawValue !== undefined) {
554
+ if (typeof rawValue === "boolean") {
555
+ result[camelKey] = rawValue
556
+ } else if (optBuilder.schema) {
557
+ const decoded = yield* Schema
558
+ .decode(optBuilder.schema)(rawValue)
559
+ .pipe(
560
+ Effect.mapError(
561
+ (error) =>
562
+ new CommanderError({
563
+ message:
564
+ `Invalid value for option ${optBuilder.long}: ${error.message}`,
565
+ cause: error,
566
+ }),
567
+ ),
568
+ )
569
+ result[camelKey] = decoded
570
+ } else {
571
+ result[camelKey] = rawValue
572
+ }
573
+ } else if (optBuilder.defaultValue !== undefined) {
574
+ result[camelKey] = optBuilder.defaultValue
575
+ }
576
+ }
577
+
578
+ return result as ExtractOptionValues<Opts>
579
+ })
580
+
581
+ export const runMain = <
582
+ Opts extends OptionsMap,
583
+ Subs extends ReadonlyArray<SubcommandDef>,
584
+ >(
585
+ cmd: CommanderSet<Opts, Subs, true>,
586
+ ): Effect.Effect<void, CommanderError> =>
587
+ Effect.gen(function*() {
588
+ const args = typeof process !== "undefined" ? process.argv.slice(2) : []
589
+
590
+ const parsedOptions = yield* parse(cmd, args)
591
+
592
+ if ((parsedOptions as any).help) {
593
+ console.log(generateHelp(cmd))
594
+ return
595
+ }
596
+
597
+ if ((parsedOptions as any).version && cmd.version) {
598
+ console.log(`${cmd.name} v${cmd.version}`)
599
+ return
600
+ }
601
+
602
+ if (cmd.handler) {
603
+ yield* cmd.handler(parsedOptions)
604
+ }
605
+ })
606
+
607
+ const generateHelp = <
608
+ Opts extends OptionsMap,
609
+ Subs extends ReadonlyArray<SubcommandDef>,
610
+ Handled extends boolean,
611
+ >(cmd: CommanderSet<Opts, Subs, Handled>): string => {
612
+ const lines: Array<string> = []
613
+
614
+ if (cmd.description) {
615
+ lines.push(cmd.description)
616
+ lines.push("")
617
+ }
618
+
619
+ lines.push(`Usage: ${cmd.name} [options]`)
620
+ lines.push("")
621
+
622
+ const optionsArray = Object.values(cmd.options)
623
+ if (optionsArray.length > 0) {
624
+ lines.push("Options:")
625
+
626
+ for (const opt of optionsArray) {
627
+ const short = opt.short ? `-${opt.short}, ` : " "
628
+ const long = opt.long
629
+ const hasValue = opt.schema !== undefined
630
+ const name = hasValue ? `${long} <value>` : long
631
+ lines.push(` ${short}${name.padEnd(20)} ${opt.description}`)
632
+ }
633
+
634
+ lines.push("")
635
+ }
636
+
637
+ if (cmd.subcommands.length > 0) {
638
+ lines.push("Commands:")
639
+ for (const sub of cmd.subcommands) {
640
+ const subCmd = sub.command
641
+ lines.push(` ${subCmd.name.padEnd(20)} ${subCmd.description || ""}`)
642
+ }
643
+ lines.push("")
644
+ }
645
+
646
+ return lines.join("\n")
647
+ }
648
+
649
+ export const help = <
650
+ Opts extends OptionsMap,
651
+ Subs extends ReadonlyArray<SubcommandDef>,
652
+ Handled extends boolean,
653
+ >(cmd: CommanderSet<Opts, Subs, Handled>): string => generateHelp(cmd)
654
+
655
+ export const NumberFromString = Schema.NumberFromString
656
+
657
+ export const choice = <const Choices extends ReadonlyArray<string>>(
658
+ choices: Choices,
659
+ ): Schema.Schema<Choices[number], string> => Schema.Literal(...choices)
660
+
661
+ export const repeatable = <A>(
662
+ schema: Schema.Schema<A, string>,
663
+ ): Schema.Schema<ReadonlyArray<A>, string> =>
664
+ Schema
665
+ .transform(Schema.String, Schema.Array(Schema.String), {
666
+ strict: true,
667
+ decode: (s) => s.split(",").map((part) => part.trim()),
668
+ encode: (arr) => arr.join(","),
669
+ })
670
+ .pipe(
671
+ Schema.compose(Schema.Array(schema)),
672
+ ) as any