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,1378 @@
1
+ import * as t from "bun:test"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Schema from "effect/Schema"
4
+ import * as Commander from "./Commander.ts"
5
+
6
+ t.describe("make", () => {
7
+ t.it("should create a basic command", () => {
8
+ const cmd = Commander.make({
9
+ name: "test-app",
10
+ description: "A test application",
11
+ })
12
+ t.expect(cmd.name).toBe("test-app")
13
+ t.expect(cmd.description).toBe("A test application")
14
+ })
15
+ })
16
+
17
+ t.describe("option - nested builder API", () => {
18
+ t.it("should add an option with schema", () => {
19
+ const cmd = Commander
20
+ .make({ name: "app" })
21
+ .option(
22
+ Commander
23
+ .option("--output", "-o")
24
+ .schema(Schema.String),
25
+ )
26
+
27
+ t.expect(cmd.options.output).toBeDefined()
28
+ t.expect(cmd.options.output.long).toBe("--output")
29
+ t.expect(cmd.options.output.short).toBe("o")
30
+ })
31
+
32
+ t.it("should add option with description", () => {
33
+ const cmd = Commander
34
+ .make({ name: "app" })
35
+ .option(
36
+ Commander
37
+ .option("--output", "-o")
38
+ .description("Output file")
39
+ .schema(Schema.String),
40
+ )
41
+
42
+ t.expect(cmd.options.output.description).toBe("Output file")
43
+ })
44
+
45
+ t.it("should add option with default value", () => {
46
+ const cmd = Commander
47
+ .make({ name: "app" })
48
+ .option(
49
+ Commander
50
+ .option("--count", "-c")
51
+ .default(10)
52
+ .schema(Commander.NumberFromString),
53
+ )
54
+
55
+ t.expect(cmd.options.count.defaultValue).toBe(10)
56
+ })
57
+
58
+ t.it("should chain multiple options", () => {
59
+ const cmd = Commander
60
+ .make({ name: "app" })
61
+ .option(
62
+ Commander
63
+ .option("--input", "-i")
64
+ .schema(Schema.String),
65
+ )
66
+ .option(
67
+ Commander
68
+ .option("--output", "-o")
69
+ .schema(Schema.String),
70
+ )
71
+
72
+ t.expect(cmd.options.input).toBeDefined()
73
+ t.expect(cmd.options.output).toBeDefined()
74
+ t.expect(cmd.options.input.long).toBe("--input")
75
+ t.expect(cmd.options.output.long).toBe("--output")
76
+ })
77
+ })
78
+
79
+ t.describe("parse - kebab-to-camel conversion", () => {
80
+ t.it("should convert kebab-case to camelCase", async () => {
81
+ const cmd = Commander
82
+ .make({ name: "app" })
83
+ .option(
84
+ Commander
85
+ .option("--input-file")
86
+ .schema(Schema.String),
87
+ )
88
+
89
+ const result = await Effect.runPromise(
90
+ Commander.parse(cmd, ["--input-file", "test.txt"]),
91
+ )
92
+
93
+ t.expect(result.inputFile).toBe("test.txt")
94
+ })
95
+
96
+ t.it("should handle single word options", async () => {
97
+ const cmd = Commander
98
+ .make({ name: "app" })
99
+ .option(
100
+ Commander
101
+ .option("--port")
102
+ .schema(Commander.NumberFromString),
103
+ )
104
+
105
+ const result = await Effect.runPromise(
106
+ Commander.parse(cmd, ["--port", "3000"]),
107
+ )
108
+
109
+ t.expect(result.port).toBe(3000)
110
+ })
111
+
112
+ t.it("should parse short options", async () => {
113
+ const cmd = Commander
114
+ .make({ name: "app" })
115
+ .option(
116
+ Commander
117
+ .option("--port", "-p")
118
+ .schema(Commander.NumberFromString),
119
+ )
120
+
121
+ const result = await Effect.runPromise(
122
+ Commander.parse(cmd, ["-p", "8080"]),
123
+ )
124
+
125
+ t.expect(result.port).toBe(8080)
126
+ })
127
+
128
+ t.it("should parse multiple options", async () => {
129
+ const cmd = Commander
130
+ .make({ name: "app" })
131
+ .option(
132
+ Commander
133
+ .option("--host", "-h")
134
+ .schema(Schema.String),
135
+ )
136
+ .option(
137
+ Commander
138
+ .option("--port", "-p")
139
+ .schema(Commander.NumberFromString),
140
+ )
141
+
142
+ const result = await Effect.runPromise(
143
+ Commander.parse(cmd, ["--host", "localhost", "--port", "3000"]),
144
+ )
145
+
146
+ t.expect(result.host).toBe("localhost")
147
+ t.expect(result.port).toBe(3000)
148
+ })
149
+
150
+ t.it("should handle options with equals syntax", async () => {
151
+ const cmd = Commander
152
+ .make({ name: "app" })
153
+ .option(
154
+ Commander
155
+ .option("--port")
156
+ .schema(Commander.NumberFromString),
157
+ )
158
+
159
+ const result = await Effect.runPromise(
160
+ Commander.parse(cmd, ["--port=3000"]),
161
+ )
162
+
163
+ t.expect(result.port).toBe(3000)
164
+ })
165
+
166
+ t.it("should use default value when option not provided", async () => {
167
+ const cmd = Commander
168
+ .make({ name: "app" })
169
+ .option(
170
+ Commander
171
+ .option("--port", "-p")
172
+ .default(3000)
173
+ .schema(Commander.NumberFromString),
174
+ )
175
+
176
+ const result = await Effect.runPromise(
177
+ Commander.parse(cmd, []),
178
+ )
179
+
180
+ t.expect(result.port).toBe(3000)
181
+ })
182
+
183
+ t.it("should override default when option specified", async () => {
184
+ const cmd = Commander
185
+ .make({ name: "app" })
186
+ .option(
187
+ Commander
188
+ .option("--port", "-p")
189
+ .default(3000)
190
+ .schema(Commander.NumberFromString),
191
+ )
192
+
193
+ const result = await Effect.runPromise(
194
+ Commander.parse(cmd, ["--port", "8080"]),
195
+ )
196
+
197
+ t.expect(result.port).toBe(8080)
198
+ })
199
+ })
200
+
201
+ t.describe("optionHelp", () => {
202
+ t.it("should add help option", () => {
203
+ const cmd = Commander
204
+ .make({ name: "app" })
205
+ .optionHelp()
206
+
207
+ t.expect(cmd.options.help).toBeDefined()
208
+ t.expect(cmd.options.help.long).toBe("--help")
209
+ t.expect(cmd.options.help.short).toBe("h")
210
+ })
211
+ })
212
+
213
+ t.describe("optionVersion", () => {
214
+ t.it("should add version option", () => {
215
+ const cmd = Commander
216
+ .make({ name: "app", version: "1.0.0" })
217
+ .optionVersion()
218
+
219
+ t.expect(cmd.options.version).toBeDefined()
220
+ t.expect(cmd.options.version.long).toBe("--version")
221
+ t.expect(cmd.options.version.short).toBe("V")
222
+ })
223
+ })
224
+
225
+ t.describe("handle", () => {
226
+ t.it("should mark command as handled", () => {
227
+ const handled = Commander
228
+ .make({ name: "app" })
229
+ .option(
230
+ Commander
231
+ .option("--name", "-n")
232
+ .schema(Schema.String),
233
+ )
234
+ .handle((opts) => Effect.void)
235
+
236
+ const unhandled = Commander
237
+ .make({ name: "app2" })
238
+ .option(
239
+ Commander
240
+ .option("--name", "-n")
241
+ .schema(Schema.String),
242
+ )
243
+
244
+ t.expect(handled.handler).toBeDefined()
245
+ t.expect(unhandled.handler).toBeUndefined()
246
+ })
247
+ })
248
+
249
+ t.describe("subcommand", () => {
250
+ t.it("should add subcommand", () => {
251
+ const subCmd = Commander
252
+ .make({ name: "format" })
253
+ .option(
254
+ Commander
255
+ .option("--style")
256
+ .schema(Schema.String),
257
+ )
258
+ .handle((opts) => Effect.void)
259
+
260
+ const main = Commander
261
+ .make({ name: "main" })
262
+ .subcommand(subCmd)
263
+
264
+ t.expect(main.subcommands.length).toBe(1)
265
+ t.expect(main.subcommands[0]!.command.name).toBe("format")
266
+ })
267
+ })
268
+
269
+ t.describe("help", () => {
270
+ t.it("should generate help text", () => {
271
+ const cmd = Commander
272
+ .make({
273
+ name: "myapp",
274
+ description: "My awesome application",
275
+ version: "1.0.0",
276
+ })
277
+ .option(
278
+ Commander
279
+ .option("--output", "-o")
280
+ .description("Output file")
281
+ .schema(Schema.String),
282
+ )
283
+ .optionHelp()
284
+
285
+ const helpText = Commander.help(cmd)
286
+
287
+ t.expect(helpText).toContain("My awesome application")
288
+ t.expect(helpText).toContain("Usage: myapp [options]")
289
+ t.expect(helpText).toContain("--output")
290
+ t.expect(helpText).toContain("-o,")
291
+ t.expect(helpText).toContain("Output file")
292
+ t.expect(helpText).toContain("--help")
293
+ })
294
+ })
295
+
296
+ t.describe("BooleanFromString", () => {
297
+ t.it("should decode true value", async () => {
298
+ const result = await Effect.runPromise(
299
+ Schema.decode(Schema.BooleanFromString)("true"),
300
+ )
301
+ t.expect(result).toBe(true)
302
+ })
303
+
304
+ t.it("should decode false value", async () => {
305
+ const result = await Effect.runPromise(
306
+ Schema.decode(Schema.BooleanFromString)("false"),
307
+ )
308
+ t.expect(result).toBe(false)
309
+ })
310
+ })
311
+
312
+ t.describe("choice", () => {
313
+ t.it("should accept valid choice", async () => {
314
+ const ColorSchema = Schema.compose(
315
+ Schema.String,
316
+ Schema.Literal("red", "green", "blue"),
317
+ )
318
+
319
+ const result = await Effect.runPromise(
320
+ Schema.decode(ColorSchema)("red"),
321
+ )
322
+
323
+ t.expect(result).toBe("red")
324
+ })
325
+
326
+ t.it("should fail on invalid choice", async () => {
327
+ const ColorSchema = Schema.compose(
328
+ Schema.String,
329
+ Schema.Literal("red", "green", "blue"),
330
+ )
331
+
332
+ const result = await Effect.runPromise(
333
+ Effect.either(Schema.decode(ColorSchema)("yellow")),
334
+ )
335
+
336
+ t.expect(result._tag).toBe("Left")
337
+ })
338
+ })
339
+
340
+ t.describe("repeatable", () => {
341
+ t.it("should parse comma-separated values", async () => {
342
+ const schema = Commander.repeatable(Schema.String)
343
+
344
+ const result = await Effect.runPromise(
345
+ Schema.decode(schema)("foo,bar,baz"),
346
+ )
347
+
348
+ t.expect(result).toEqual(["foo", "bar", "baz"])
349
+ })
350
+
351
+ t.it("should parse comma-separated numbers", async () => {
352
+ const schema = Commander.repeatable(Commander.NumberFromString)
353
+
354
+ const result = await Effect.runPromise(
355
+ Schema.decode(schema)("1,2,3,4,5"),
356
+ )
357
+
358
+ t.expect(result).toEqual([1, 2, 3, 4, 5])
359
+ })
360
+
361
+ t.it("should trim whitespace", async () => {
362
+ const schema = Commander.repeatable(Schema.String)
363
+
364
+ const result = await Effect.runPromise(
365
+ Schema.decode(schema)("foo, bar , baz"),
366
+ )
367
+
368
+ t.expect(result).toEqual(["foo", "bar", "baz"])
369
+ })
370
+
371
+ t.it("should encode back to string", async () => {
372
+ const schema = Commander.repeatable(Schema.String)
373
+
374
+ const result = await Effect.runPromise(
375
+ Schema.encode(schema)(["foo", "bar", "baz"]),
376
+ )
377
+
378
+ t.expect(result).toBe("foo,bar,baz")
379
+ })
380
+ })
381
+
382
+ t.describe("integration", () => {
383
+ t.it("should work with builder pattern", async () => {
384
+ const cmd = Commander
385
+ .make({
386
+ name: "converter",
387
+ description: "Convert files",
388
+ version: "2.1.0",
389
+ })
390
+ .option(
391
+ Commander
392
+ .option("--input", "-i")
393
+ .description("Input file")
394
+ .schema(Schema.String),
395
+ )
396
+ .option(
397
+ Commander
398
+ .option("--output", "-o")
399
+ .description("Output file")
400
+ .default("output.txt")
401
+ .schema(Schema.String),
402
+ )
403
+ .option(
404
+ Commander
405
+ .option("--format", "-f")
406
+ .default("json")
407
+ .schema(
408
+ Schema.compose(
409
+ Schema.String,
410
+ Schema.Literal("json", "xml", "yaml"),
411
+ ),
412
+ ),
413
+ )
414
+ .optionHelp()
415
+
416
+ const result = await Effect.runPromise(
417
+ Commander.parse(cmd, [
418
+ "--input",
419
+ "input.txt",
420
+ "-f",
421
+ "yaml",
422
+ ]),
423
+ )
424
+
425
+ t.expect(result.input).toBe("input.txt")
426
+ t.expect(result.output).toBe("output.txt")
427
+ t.expect(result.format).toBe("yaml")
428
+ t.expect(result.help).toBe(false)
429
+ })
430
+
431
+ t.it("should handle kebab-case option names", async () => {
432
+ const cmd = Commander
433
+ .make({ name: "app" })
434
+ .option(
435
+ Commander
436
+ .option("--dry-run")
437
+ .default(false)
438
+ .schema(Schema.BooleanFromString),
439
+ )
440
+ .option(
441
+ Commander
442
+ .option("--cache-dir")
443
+ .schema(Schema.String),
444
+ )
445
+
446
+ const result = await Effect.runPromise(
447
+ Commander.parse(cmd, ["--dry-run", "true", "--cache-dir", "/tmp/cache"]),
448
+ )
449
+
450
+ t.expect(result.dryRun).toBe(true)
451
+ t.expect(result.cacheDir).toBe("/tmp/cache")
452
+ })
453
+ })
454
+
455
+ t.describe("parse - comprehensive", () => {
456
+ t.it("should parse with explicit args", async () => {
457
+ const cmd = Commander
458
+ .make({ name: "app" })
459
+ .option(
460
+ Commander
461
+ .option("--port", "-p")
462
+ .schema(Commander.NumberFromString),
463
+ )
464
+
465
+ const result = await Effect.runPromise(
466
+ Commander.parse(cmd, ["--port", "3000"]),
467
+ )
468
+
469
+ t.expect(result.port).toBe(3000)
470
+ })
471
+
472
+ t.it("should parse short options", async () => {
473
+ const cmd = Commander
474
+ .make({ name: "app" })
475
+ .option(
476
+ Commander
477
+ .option("--port", "-p")
478
+ .schema(Commander.NumberFromString),
479
+ )
480
+
481
+ const result = await Effect.runPromise(
482
+ Commander.parse(cmd, ["-p", "8080"]),
483
+ )
484
+
485
+ t.expect(result.port).toBe(8080)
486
+ })
487
+
488
+ t.it("should parse multiple options", async () => {
489
+ const cmd = Commander
490
+ .make({ name: "app" })
491
+ .option(
492
+ Commander
493
+ .option("--host", "-h")
494
+ .schema(Schema.String),
495
+ )
496
+ .option(
497
+ Commander
498
+ .option("--port", "-p")
499
+ .schema(Commander.NumberFromString),
500
+ )
501
+
502
+ const result = await Effect.runPromise(
503
+ Commander.parse(cmd, ["--host", "localhost", "--port", "3000"]),
504
+ )
505
+
506
+ t.expect(result.host).toBe("localhost")
507
+ t.expect(result.port).toBe(3000)
508
+ })
509
+
510
+ t.it("should handle options with equals syntax", async () => {
511
+ const cmd = Commander
512
+ .make({ name: "app" })
513
+ .option(
514
+ Commander
515
+ .option("--port")
516
+ .schema(Commander.NumberFromString),
517
+ )
518
+
519
+ const result = await Effect.runPromise(
520
+ Commander.parse(cmd, ["--port=3000"]),
521
+ )
522
+
523
+ t.expect(result.port).toBe(3000)
524
+ })
525
+
526
+ t.it("should parse combined short flags", async () => {
527
+ const cmd = Commander
528
+ .make({ name: "app" })
529
+ .option(
530
+ Commander
531
+ .option("--verbose", "-v")
532
+ .default(false)
533
+ .schema(Schema.BooleanFromString),
534
+ )
535
+ .option(
536
+ Commander
537
+ .option("--debug", "-d")
538
+ .default(false)
539
+ .schema(Schema.BooleanFromString),
540
+ )
541
+
542
+ const result = await Effect.runPromise(
543
+ Commander.parse(cmd, []),
544
+ )
545
+
546
+ t.expect(result.verbose).toBe(false)
547
+ t.expect(result.debug).toBe(false)
548
+ })
549
+ })
550
+
551
+ t.describe("boolean options", () => {
552
+ t.it("should return false for boolean flag when not specified", async () => {
553
+ const cmd = Commander
554
+ .make({ name: "app" })
555
+ .option(
556
+ Commander
557
+ .option("--verbose", "-v")
558
+ .default(false)
559
+ .schema(Schema.BooleanFromString),
560
+ )
561
+
562
+ const result = await Effect.runPromise(
563
+ Commander.parse(cmd, []),
564
+ )
565
+
566
+ t.expect(result.verbose).toBe(false)
567
+ })
568
+
569
+ t.it("should return true for boolean flag when specified", async () => {
570
+ const cmd = Commander
571
+ .make({ name: "app" })
572
+ .option(
573
+ Commander
574
+ .option("--verbose", "-v")
575
+ .default(false)
576
+ .schema(Schema.BooleanFromString),
577
+ )
578
+
579
+ const result = await Effect.runPromise(
580
+ Commander.parse(cmd, ["--verbose", "true"]),
581
+ )
582
+
583
+ t.expect(result.verbose).toBe(true)
584
+ })
585
+
586
+ t.it("should handle boolean with custom default", async () => {
587
+ const cmd = Commander
588
+ .make({ name: "app" })
589
+ .option(
590
+ Commander
591
+ .option("--color")
592
+ .default("auto")
593
+ .schema(Schema.String),
594
+ )
595
+
596
+ const result = await Effect.runPromise(
597
+ Commander.parse(cmd, []),
598
+ )
599
+
600
+ t.expect(result.color).toBe("auto")
601
+ })
602
+ })
603
+
604
+ t.describe("options with choices", () => {
605
+ t.it("should accept valid choice", async () => {
606
+ const cmd = Commander
607
+ .make({ name: "app" })
608
+ .option(
609
+ Commander
610
+ .option("--color", "-c")
611
+ .schema(
612
+ Schema.compose(
613
+ Schema.String,
614
+ Schema.Literal("red", "green", "blue"),
615
+ ),
616
+ ),
617
+ )
618
+
619
+ const result = await Effect.runPromise(
620
+ Commander.parse(cmd, ["--color", "red"]),
621
+ )
622
+
623
+ t.expect(result.color).toBe("red")
624
+ })
625
+
626
+ t.it("should reject invalid choice", async () => {
627
+ const cmd = Commander
628
+ .make({ name: "app" })
629
+ .option(
630
+ Commander
631
+ .option("--color", "-c")
632
+ .schema(
633
+ Schema.compose(
634
+ Schema.String,
635
+ Schema.Literal("red", "green", "blue"),
636
+ ),
637
+ ),
638
+ )
639
+
640
+ const result = await Effect.runPromise(
641
+ Effect.either(Commander.parse(cmd, ["--color", "yellow"])),
642
+ )
643
+
644
+ t.expect(result._tag).toBe("Left")
645
+ })
646
+
647
+ t.it("should handle multiple choice options", async () => {
648
+ const cmd = Commander
649
+ .make({ name: "app" })
650
+ .option(
651
+ Commander
652
+ .option("--format", "-f")
653
+ .default("json")
654
+ .schema(
655
+ Schema.compose(
656
+ Schema.String,
657
+ Schema.Literal("json", "xml", "yaml"),
658
+ ),
659
+ ),
660
+ )
661
+ .option(
662
+ Commander
663
+ .option("--level", "-l")
664
+ .default("info")
665
+ .schema(
666
+ Schema.compose(
667
+ Schema.String,
668
+ Schema.Literal("debug", "info", "warn", "error"),
669
+ ),
670
+ ),
671
+ )
672
+
673
+ const result = await Effect.runPromise(
674
+ Commander.parse(cmd, ["--format", "xml", "--level", "debug"]),
675
+ )
676
+
677
+ t.expect(result.format).toBe("xml")
678
+ t.expect(result.level).toBe("debug")
679
+ })
680
+ })
681
+
682
+ t.describe("options with defaults", () => {
683
+ t.it("should use default when option not specified", async () => {
684
+ const cmd = Commander
685
+ .make({ name: "app" })
686
+ .option(
687
+ Commander
688
+ .option("--port", "-p")
689
+ .default(3000)
690
+ .schema(Commander.NumberFromString),
691
+ )
692
+
693
+ const result = await Effect.runPromise(
694
+ Commander.parse(cmd, []),
695
+ )
696
+
697
+ t.expect(result.port).toBe(3000)
698
+ })
699
+
700
+ t.it("should override default when option specified", async () => {
701
+ const cmd = Commander
702
+ .make({ name: "app" })
703
+ .option(
704
+ Commander
705
+ .option("--port", "-p")
706
+ .default(3000)
707
+ .schema(Commander.NumberFromString),
708
+ )
709
+
710
+ const result = await Effect.runPromise(
711
+ Commander.parse(cmd, ["--port", "8080"]),
712
+ )
713
+
714
+ t.expect(result.port).toBe(8080)
715
+ })
716
+
717
+ t.it("should handle multiple defaults", async () => {
718
+ const cmd = Commander
719
+ .make({ name: "app" })
720
+ .option(
721
+ Commander
722
+ .option("--host")
723
+ .default("localhost")
724
+ .schema(Schema.String),
725
+ )
726
+ .option(
727
+ Commander
728
+ .option("--port")
729
+ .default(3000)
730
+ .schema(Commander.NumberFromString),
731
+ )
732
+ .option(
733
+ Commander
734
+ .option("--debug")
735
+ .default(false)
736
+ .schema(Schema.BooleanFromString),
737
+ )
738
+
739
+ const result = await Effect.runPromise(
740
+ Commander.parse(cmd, []),
741
+ )
742
+
743
+ t.expect(result.host).toBe("localhost")
744
+ t.expect(result.port).toBe(3000)
745
+ t.expect(result.debug).toBe(false)
746
+ })
747
+
748
+ t.it("should use default for string option", async () => {
749
+ const cmd = Commander
750
+ .make({ name: "app" })
751
+ .option(
752
+ Commander
753
+ .option("--output", "-o")
754
+ .default("output.txt")
755
+ .schema(Schema.String),
756
+ )
757
+
758
+ const result = await Effect.runPromise(
759
+ Commander.parse(cmd, []),
760
+ )
761
+
762
+ t.expect(result.output).toBe("output.txt")
763
+ })
764
+ })
765
+
766
+ t.describe("action/handler", () => {
767
+ t.it("should invoke handler with parsed options", async () => {
768
+ let capturedOptions: any = null
769
+
770
+ const cmd = Commander
771
+ .make({ name: "app" })
772
+ .option(
773
+ Commander
774
+ .option("--name", "-n")
775
+ .schema(Schema.String),
776
+ )
777
+ .handle((opts) =>
778
+ Effect.sync(() => {
779
+ capturedOptions = opts
780
+ })
781
+ )
782
+
783
+ const parsed = await Effect.runPromise(
784
+ Commander.parse(cmd, ["--name", "test"]),
785
+ )
786
+
787
+ t.expect(parsed.name).toBe("test")
788
+ })
789
+
790
+ t.it("should support async handlers", async () => {
791
+ let executed = false
792
+
793
+ const cmd = Commander
794
+ .make({ name: "app" })
795
+ .option(
796
+ Commander
797
+ .option("--delay")
798
+ .default(0)
799
+ .schema(Commander.NumberFromString),
800
+ )
801
+ .handle((opts) =>
802
+ Effect.gen(function*() {
803
+ yield* Effect.sleep(opts.delay)
804
+ executed = true
805
+ })
806
+ )
807
+
808
+ await Effect.runPromise(
809
+ Commander.parse(cmd, ["--delay", "10"]),
810
+ )
811
+
812
+ t.expect(executed).toBe(false)
813
+ })
814
+
815
+ t.it("should pass all options to handler", async () => {
816
+ let capturedOpts: any = null
817
+
818
+ const cmd = Commander
819
+ .make({ name: "app" })
820
+ .option(
821
+ Commander
822
+ .option("--input", "-i")
823
+ .schema(Schema.String),
824
+ )
825
+ .option(
826
+ Commander
827
+ .option("--output", "-o")
828
+ .schema(Schema.String),
829
+ )
830
+ .option(
831
+ Commander
832
+ .option("--verbose", "-v")
833
+ .default(false)
834
+ .schema(Schema.BooleanFromString),
835
+ )
836
+ .handle((opts) =>
837
+ Effect.sync(() => {
838
+ capturedOpts = opts
839
+ })
840
+ )
841
+
842
+ await Effect.runPromise(
843
+ Commander.parse(cmd, ["-i", "in.txt", "-o", "out.txt", "-v", "true"]),
844
+ )
845
+ })
846
+ })
847
+
848
+ t.describe("version", () => {
849
+ t.it("should include version in command definition", () => {
850
+ const cmd = Commander
851
+ .make({ name: "app", version: "1.0.0" })
852
+ .optionVersion()
853
+
854
+ t.expect(cmd.version).toBe("1.0.0")
855
+ t.expect(cmd.options.version).toBeDefined()
856
+ })
857
+
858
+ t.it("should handle version without version option", () => {
859
+ const cmd = Commander
860
+ .make({ name: "app", version: "2.0.0" })
861
+
862
+ t.expect(cmd.version).toBe("2.0.0")
863
+ t.expect((cmd.options as any).version).toBeUndefined()
864
+ })
865
+
866
+ t.it("should include version option in help", () => {
867
+ const cmd = Commander
868
+ .make({ name: "app", version: "1.0.0" })
869
+ .optionVersion()
870
+
871
+ const help = Commander.help(cmd)
872
+
873
+ t.expect(help).toContain("--version")
874
+ t.expect(help).toContain("-V")
875
+ })
876
+ })
877
+
878
+ t.describe("help - comprehensive", () => {
879
+ t.it("should generate help with description", () => {
880
+ const cmd = Commander
881
+ .make({
882
+ name: "myapp",
883
+ description: "A test application",
884
+ })
885
+ .optionHelp()
886
+
887
+ const help = Commander.help(cmd)
888
+
889
+ t.expect(help).toContain("A test application")
890
+ t.expect(help).toContain("Usage: myapp [options]")
891
+ })
892
+
893
+ t.it("should include all options in help", () => {
894
+ const cmd = Commander
895
+ .make({ name: "app" })
896
+ .option(
897
+ Commander
898
+ .option("--input", "-i")
899
+ .description("Input file")
900
+ .schema(Schema.String),
901
+ )
902
+ .option(
903
+ Commander
904
+ .option("--output", "-o")
905
+ .description("Output file")
906
+ .schema(Schema.String),
907
+ )
908
+ .optionHelp()
909
+
910
+ const help = Commander.help(cmd)
911
+
912
+ t.expect(help).toContain("--input")
913
+ t.expect(help).toContain("-i,")
914
+ t.expect(help).toContain("Input file")
915
+ t.expect(help).toContain("--output")
916
+ t.expect(help).toContain("-o,")
917
+ t.expect(help).toContain("Output file")
918
+ })
919
+
920
+ t.it("should show subcommands in help", () => {
921
+ const subCmd = Commander
922
+ .make({ name: "init", description: "Initialize project" })
923
+ .handle(() => Effect.void)
924
+
925
+ const cmd = Commander
926
+ .make({ name: "app" })
927
+ .subcommand(subCmd)
928
+ .optionHelp()
929
+
930
+ const help = Commander.help(cmd)
931
+
932
+ t.expect(help).toContain("Commands:")
933
+ t.expect(help).toContain("init")
934
+ t.expect(help).toContain("Initialize project")
935
+ })
936
+
937
+ t.it("should format option descriptions properly", () => {
938
+ const cmd = Commander
939
+ .make({ name: "app" })
940
+ .option(
941
+ Commander
942
+ .option("--config", "-c")
943
+ .description("Config file path")
944
+ .schema(Schema.String),
945
+ )
946
+
947
+ const help = Commander.help(cmd)
948
+
949
+ t.expect(help).toContain("-c, --config")
950
+ t.expect(help).toContain("Config file path")
951
+ })
952
+ })
953
+
954
+ t.describe("subcommands - comprehensive", () => {
955
+ t.it("should add subcommand", () => {
956
+ const subCmd = Commander
957
+ .make({ name: "build" })
958
+ .option(
959
+ Commander
960
+ .option("--watch", "-w")
961
+ .default(false)
962
+ .schema(Schema.BooleanFromString),
963
+ )
964
+ .handle(() => Effect.void)
965
+
966
+ const cmd = Commander
967
+ .make({ name: "app" })
968
+ .subcommand(subCmd)
969
+
970
+ t.expect(cmd.subcommands.length).toBe(1)
971
+ t.expect(cmd.subcommands[0]!.command.name).toBe("build")
972
+ })
973
+
974
+ t.it("should add multiple subcommands", () => {
975
+ const build = Commander
976
+ .make({ name: "build" })
977
+ .handle(() => Effect.void)
978
+
979
+ const test = Commander
980
+ .make({ name: "test" })
981
+ .handle(() => Effect.void)
982
+
983
+ const cmd = Commander
984
+ .make({ name: "app" })
985
+ .subcommand(build)
986
+ .subcommand(test)
987
+
988
+ t.expect(cmd.subcommands.length).toBe(2)
989
+ t.expect(cmd.subcommands[0]!.command.name).toBe("build")
990
+ t.expect(cmd.subcommands[1]!.command.name).toBe("test")
991
+ })
992
+
993
+ t.it("should nest subcommands", () => {
994
+ const deploy = Commander
995
+ .make({ name: "deploy" })
996
+ .handle(() => Effect.void)
997
+
998
+ const build = Commander
999
+ .make({ name: "build" })
1000
+ .subcommand(deploy)
1001
+ .handle(() => Effect.void)
1002
+
1003
+ const cmd = Commander
1004
+ .make({ name: "app" })
1005
+ .subcommand(build)
1006
+
1007
+ t.expect(cmd.subcommands[0]!.command.subcommands.length).toBe(1)
1008
+ t.expect(cmd.subcommands[0]!.command.subcommands[0]!.command.name).toBe(
1009
+ "deploy",
1010
+ )
1011
+ })
1012
+ })
1013
+
1014
+ t.describe("option types", () => {
1015
+ t.it("should parse string option", async () => {
1016
+ const cmd = Commander
1017
+ .make({ name: "app" })
1018
+ .option(
1019
+ Commander
1020
+ .option("--name")
1021
+ .schema(Schema.String),
1022
+ )
1023
+
1024
+ const result = await Effect.runPromise(
1025
+ Commander.parse(cmd, ["--name", "test"]),
1026
+ )
1027
+
1028
+ t.expect(result.name).toBe("test")
1029
+ t.expect(typeof result.name).toBe("string")
1030
+ })
1031
+
1032
+ t.it("should parse number option", async () => {
1033
+ const cmd = Commander
1034
+ .make({ name: "app" })
1035
+ .option(
1036
+ Commander
1037
+ .option("--count")
1038
+ .schema(Commander.NumberFromString),
1039
+ )
1040
+
1041
+ const result = await Effect.runPromise(
1042
+ Commander.parse(cmd, ["--count", "42"]),
1043
+ )
1044
+
1045
+ t.expect(result.count).toBe(42)
1046
+ t.expect(typeof result.count).toBe("number")
1047
+ })
1048
+
1049
+ t.it("should parse boolean option", async () => {
1050
+ const cmd = Commander
1051
+ .make({ name: "app" })
1052
+ .option(
1053
+ Commander
1054
+ .option("--enabled")
1055
+ .schema(Schema.BooleanFromString),
1056
+ )
1057
+
1058
+ const result = await Effect.runPromise(
1059
+ Commander.parse(cmd, ["--enabled", "true"]),
1060
+ )
1061
+
1062
+ t.expect(result.enabled).toBe(true)
1063
+ t.expect(typeof result.enabled).toBe("boolean")
1064
+ })
1065
+
1066
+ t.it("should fail on invalid number", async () => {
1067
+ const cmd = Commander
1068
+ .make({ name: "app" })
1069
+ .option(
1070
+ Commander
1071
+ .option("--count")
1072
+ .schema(Commander.NumberFromString),
1073
+ )
1074
+
1075
+ const result = await Effect.runPromise(
1076
+ Effect.either(Commander.parse(cmd, ["--count", "not-a-number"])),
1077
+ )
1078
+
1079
+ t.expect(result._tag).toBe("Left")
1080
+ })
1081
+ })
1082
+
1083
+ t.describe("complex scenarios", () => {
1084
+ t.it("should handle mixed option types", async () => {
1085
+ const cmd = Commander
1086
+ .make({ name: "server" })
1087
+ .option(
1088
+ Commander
1089
+ .option("--host", "-h")
1090
+ .default("localhost")
1091
+ .schema(Schema.String),
1092
+ )
1093
+ .option(
1094
+ Commander
1095
+ .option("--port", "-p")
1096
+ .default(3000)
1097
+ .schema(Commander.NumberFromString),
1098
+ )
1099
+ .option(
1100
+ Commander
1101
+ .option("--ssl")
1102
+ .default(false)
1103
+ .schema(Schema.BooleanFromString),
1104
+ )
1105
+ .option(
1106
+ Commander
1107
+ .option("--env", "-e")
1108
+ .default("development")
1109
+ .schema(
1110
+ Schema.compose(
1111
+ Schema.String,
1112
+ Schema.Literal("development", "production", "test"),
1113
+ ),
1114
+ ),
1115
+ )
1116
+
1117
+ const result = await Effect.runPromise(
1118
+ Commander.parse(cmd, [
1119
+ "--host",
1120
+ "0.0.0.0",
1121
+ "-p",
1122
+ "8080",
1123
+ "--ssl",
1124
+ "true",
1125
+ "-e",
1126
+ "production",
1127
+ ]),
1128
+ )
1129
+
1130
+ t.expect(result.host).toBe("0.0.0.0")
1131
+ t.expect(result.port).toBe(8080)
1132
+ t.expect(result.ssl).toBe(true)
1133
+ t.expect(result.env).toBe("production")
1134
+ })
1135
+
1136
+ t.it("should handle repeatable options", async () => {
1137
+ const cmd = Commander
1138
+ .make({ name: "app" })
1139
+ .option(
1140
+ Commander
1141
+ .option("--tags", "-t")
1142
+ .schema(Commander.repeatable(Schema.String)),
1143
+ )
1144
+
1145
+ const result = await Effect.runPromise(
1146
+ Commander.parse(cmd, ["--tags", "foo,bar,baz"]),
1147
+ )
1148
+
1149
+ t.expect(result.tags).toEqual(["foo", "bar", "baz"])
1150
+ })
1151
+
1152
+ t.it("should preserve option order independence", async () => {
1153
+ const cmd = Commander
1154
+ .make({ name: "app" })
1155
+ .option(
1156
+ Commander
1157
+ .option("--first")
1158
+ .schema(Schema.String),
1159
+ )
1160
+ .option(
1161
+ Commander
1162
+ .option("--second")
1163
+ .schema(Schema.String),
1164
+ )
1165
+
1166
+ const result1 = await Effect.runPromise(
1167
+ Commander.parse(cmd, ["--first", "1", "--second", "2"]),
1168
+ )
1169
+
1170
+ const result2 = await Effect.runPromise(
1171
+ Commander.parse(cmd, ["--second", "2", "--first", "1"]),
1172
+ )
1173
+
1174
+ t.expect(result1.first).toBe("1")
1175
+ t.expect(result1.second).toBe("2")
1176
+ t.expect(result2.first).toBe("1")
1177
+ t.expect(result2.second).toBe("2")
1178
+ })
1179
+
1180
+ t.it("should handle options with hyphens in names", async () => {
1181
+ const cmd = Commander
1182
+ .make({ name: "app" })
1183
+ .option(
1184
+ Commander
1185
+ .option("--dry-run")
1186
+ .default(false)
1187
+ .schema(Schema.BooleanFromString),
1188
+ )
1189
+ .option(
1190
+ Commander
1191
+ .option("--no-cache")
1192
+ .default(false)
1193
+ .schema(Schema.BooleanFromString),
1194
+ )
1195
+
1196
+ const result = await Effect.runPromise(
1197
+ Commander.parse(cmd, ["--dry-run", "true", "--no-cache", "true"]),
1198
+ )
1199
+
1200
+ t.expect(result.dryRun).toBe(true)
1201
+ t.expect(result.noCache).toBe(true)
1202
+ })
1203
+ })
1204
+
1205
+ t.describe("error handling", () => {
1206
+ t.it("should fail gracefully on invalid option value", async () => {
1207
+ const cmd = Commander
1208
+ .make({ name: "app" })
1209
+ .option(
1210
+ Commander
1211
+ .option("--port")
1212
+ .schema(Commander.NumberFromString),
1213
+ )
1214
+
1215
+ const result = await Effect.runPromise(
1216
+ Effect.either(Commander.parse(cmd, ["--port", "invalid"])),
1217
+ )
1218
+
1219
+ t.expect(result._tag).toBe("Left")
1220
+ if (result._tag === "Left") {
1221
+ t.expect(result.left.message).toContain("Invalid value")
1222
+ }
1223
+ })
1224
+
1225
+ t.it("should fail on invalid choice", async () => {
1226
+ const cmd = Commander
1227
+ .make({ name: "app" })
1228
+ .option(
1229
+ Commander
1230
+ .option("--mode")
1231
+ .schema(Schema.compose(Schema.String, Schema.Literal("dev", "prod"))),
1232
+ )
1233
+
1234
+ const result = await Effect.runPromise(
1235
+ Effect.either(Commander.parse(cmd, ["--mode", "staging"])),
1236
+ )
1237
+
1238
+ t.expect(result._tag).toBe("Left")
1239
+ })
1240
+ })
1241
+
1242
+ t.describe("builder pattern", () => {
1243
+ t.it("should chain option definitions fluently", () => {
1244
+ const cmd = Commander
1245
+ .make({ name: "app" })
1246
+ .option(
1247
+ Commander
1248
+ .option("--input", "-i")
1249
+ .description("Input file")
1250
+ .schema(Schema.String),
1251
+ )
1252
+ .option(
1253
+ Commander
1254
+ .option("--output", "-o")
1255
+ .description("Output file")
1256
+ .default("out.txt")
1257
+ .schema(Schema.String),
1258
+ )
1259
+
1260
+ t.expect(cmd.options.input.description).toBe("Input file")
1261
+ t.expect(cmd.options.output.description).toBe("Output file")
1262
+ t.expect(cmd.options.output.defaultValue).toBe("out.txt")
1263
+ })
1264
+
1265
+ t.it("should chain description and default in any order", () => {
1266
+ const cmd1 = Commander
1267
+ .make({ name: "app" })
1268
+ .option(
1269
+ Commander
1270
+ .option("--port")
1271
+ .description("Port number")
1272
+ .default(3000)
1273
+ .schema(Commander.NumberFromString),
1274
+ )
1275
+
1276
+ const cmd2 = Commander
1277
+ .make({ name: "app" })
1278
+ .option(
1279
+ Commander
1280
+ .option("--port")
1281
+ .default(3000)
1282
+ .description("Port number")
1283
+ .schema(Commander.NumberFromString),
1284
+ )
1285
+
1286
+ t.expect(cmd1.options.port.description).toBe("Port number")
1287
+ t.expect(cmd1.options.port.defaultValue).toBe(3000)
1288
+ t.expect(cmd2.options.port.description).toBe("Port number")
1289
+ t.expect(cmd2.options.port.defaultValue).toBe(3000)
1290
+ })
1291
+
1292
+ t.it("should support method chaining with subcommands", () => {
1293
+ const sub1 = Commander
1294
+ .make({ name: "sub1" })
1295
+ .handle(() => Effect.void)
1296
+
1297
+ const sub2 = Commander
1298
+ .make({ name: "sub2" })
1299
+ .handle(() => Effect.void)
1300
+
1301
+ const cmd = Commander
1302
+ .make({ name: "app" })
1303
+ .option(
1304
+ Commander
1305
+ .option("--global")
1306
+ .schema(Schema.String),
1307
+ )
1308
+ .subcommand(sub1)
1309
+ .subcommand(sub2)
1310
+ .optionHelp()
1311
+
1312
+ t.expect(cmd.options.global).toBeDefined()
1313
+ t.expect(cmd.options.help).toBeDefined()
1314
+ t.expect(cmd.subcommands.length).toBe(2)
1315
+ })
1316
+ })
1317
+
1318
+ t.describe("example scenario", () => {
1319
+ t.it("should handle main command with subcommand", async () => {
1320
+ const unhandledFormat = Commander.make({
1321
+ name: "format",
1322
+ description: "Format source files",
1323
+ })
1324
+
1325
+ const handledFormat = unhandledFormat
1326
+ .option(
1327
+ Commander
1328
+ .option("--style", "-s")
1329
+ .description("Code style to use")
1330
+ .default("standard")
1331
+ .schema(
1332
+ Schema.compose(
1333
+ Schema.String,
1334
+ Schema.Literal("standard", "prettier", "biome"),
1335
+ ),
1336
+ ),
1337
+ )
1338
+ .handle((opts) => Effect.sync(() => ({ style: opts.style })))
1339
+
1340
+ const main = Commander
1341
+ .make({
1342
+ name: "main",
1343
+ description: "this is doing that",
1344
+ version: "1.0.0",
1345
+ })
1346
+ .option(
1347
+ Commander
1348
+ .option("--source", "-s")
1349
+ .schema(Schema.String),
1350
+ )
1351
+ .option(
1352
+ Commander
1353
+ .option("--verbose", "-v")
1354
+ .description("Enable verbose output")
1355
+ .default(false)
1356
+ .schema(Schema.BooleanFromString),
1357
+ )
1358
+ .optionHelp()
1359
+ .subcommand(handledFormat)
1360
+ .handle((opts) => Effect.sync(() => opts))
1361
+
1362
+ const resultMain = await Effect.runPromise(
1363
+ Commander.parse(main, ["--source", "test.ts", "--verbose", "true"]),
1364
+ )
1365
+
1366
+ t.expect(resultMain.source).toBe("test.ts")
1367
+ t.expect(resultMain.verbose).toBe(true)
1368
+ t.expect(resultMain.help).toBe(false)
1369
+
1370
+ t.expect(main.subcommands.length).toBe(1)
1371
+ t.expect(main.subcommands[0]!.command.name).toBe("format")
1372
+
1373
+ t.expect(main.subcommands[0]!.command.options.style).toBeDefined()
1374
+ t.expect(main.subcommands[0]!.command.options.style.defaultValue).toBe(
1375
+ "standard",
1376
+ )
1377
+ })
1378
+ })