1import { assert, describe, expect, it } from "@effect/vitest"
2import { Effect, FileSystem, Layer, Path, Stdio } from "effect"
3import { Argument, CliOutput, Command, Flag } from "effect/cli"
4import { toImpl } from "effect/cli/internal/command"
5import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"
6import { TestConsole } from "effect/testing"
7import * as Cli from "./fixtures/ComprehensiveCli.ts"
8import * as MockTerminal from "./services/MockTerminal.ts"
9import * as TestActions from "./services/TestActions.ts"
10
11const ActionsLayer = TestActions.layer
12const ConsoleLayer = TestConsole.layer
13const FileSystemLayer = FileSystem.layerNoop({})
14const PathLayer = Path.layer
15const TerminalLayer = MockTerminal.layer
16const CliOutputLayer = CliOutput.layer(
17 CliOutput.defaultFormatter({
18 colors: false
19 })
20)
21
22const TestLayer = Layer.mergeAll(
23 ActionsLayer,
24 ConsoleLayer,
25 FileSystemLayer,
26 PathLayer,
27 TerminalLayer,
28 CliOutputLayer,
29 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => Effect.die("Not implemented"))),
30 Stdio.layerTest({})
31)
32
33const runCommand = Effect.fnUntraced(
34 function*(command: ReadonlyArray<string>) {
35 yield* Cli.run(command)
36 const output = yield* TestConsole.logLines
37 return output.join("\n")
38 }
39)
40
41describe("Command help output", () => {
42 it("marks omittable flags as not required in structured help", () => {
43 const command = Command.make("app", {
44 required: Flag.String("required"),
45 optional: Flag.String("optional").pipe(Flag.optional),
46 defaulted: Flag.String("defaulted").pipe(Flag.withDefault("output.txt"))
47 })
48 const help = toImpl(command).buildHelpDoc(["app"])
49
50 assert.deepStrictEqual(help.flags.map((flag) => flag.required), [true, false, false])
51 })
52
53 it("marks omittable arguments as not required in structured help", () => {
54 const requiredVariadic = Command.make("app", {
55 files: Argument.String("files").pipe(Argument.variadic({ min: 1 }))
56 })
57 const optionalVariadic = Command.make("app", {
58 files: Argument.String("files").pipe(Argument.variadic())
59 })
60 const defaulted = Command.make("app", {
61 output: Argument.String("output").pipe(Argument.withDefault("output.txt"))
62 })
63
64 assert.deepStrictEqual(
65 [
66 toImpl(requiredVariadic).buildHelpDoc(["app"]).args![0].required,
67 toImpl(optionalVariadic).buildHelpDoc(["app"]).args![0].required,
68 toImpl(defaulted).buildHelpDoc(["app"]).args![0].required
69 ],
70 [true, false, false]
71 )
72 })
73
74 it.effect("renders root command help", () =>
75 Effect.gen(function*() {
76 const helpText = yield* runCommand(["--help"])
77
78 expect(helpText).toMatchInlineSnapshot(`
79 "DESCRIPTION
80 A comprehensive CLI tool demonstrating all features
81
82 USAGE
83 mycli <subcommand> [flags]
84
85 FLAGS
86 --debug, -d Enable debug logging
87 --config, -c file Path to configuration file
88 --quiet, -q Suppress non-error output
89
90 GLOBAL FLAGS
91 --help, -h Show help information
92 --version, -v Show version information
93 --wizard Start wizard mode for a command
94 --completions <bash|zsh|fish|sh> Print shell completion script (choices: bash, zsh, fish, sh)
95 --log-level <all|trace|debug|info|warn|warning|error|fatal|none> Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
96
97 SUBCOMMANDS
98 admin Administrative commands
99 copy Copy files or directories
100 move Move or rename files
101 remove Remove files or directories
102 build Build the project
103 git Git version control
104 test-required Test command with required option
105 test-failing Test command that always fails
106 app Application management
107 app-nested Application with nested services"
108 `)
109 }).pipe(Effect.provide(TestLayer)))
110
111 it.effect("aligns flag descriptions when flag names are long", () =>
112 Effect.gen(function*() {
113 const command = Command.make("tool", {
114 short: Flag.String("short").pipe(Flag.withDescription("Short flag description")),
115 veryLong: Flag.String("this-is-a-very-very-long-flag-name").pipe(
116 Flag.withDescription("Long flag description")
117 )
118 })
119 const run = Command.runWith(command, { version: "1.0.0" })
120
121 yield* run(["--help"])
122
123 const helpText = (yield* TestConsole.logLines).join("\n")
124 const lines = helpText.split("\n")
125 const shortLine = lines.find((line) => line.includes("--short"))
126 const longLine = lines.find((line) => line.includes("--this-is-a-very-very-long-flag-name"))
127
128 expect(shortLine).toBeDefined()
129 expect(longLine).toBeDefined()
130 expect(shortLine!.indexOf("Short flag description")).toBe(longLine!.indexOf("Long flag description"))
131 }).pipe(Effect.provide(TestLayer)))
132
133 it.effect("aligns flag descriptions when flag names contain wide graphemes", () =>
134 Effect.gen(function*() {
135 const command = Command.make("tool", {
136 file: Flag.Boolean("ファイル").pipe(Flag.withDescription("Wide flag description")),
137 emoji: Flag.Boolean("👩‍💻").pipe(Flag.withDescription("Emoji flag description")),
138 verbose: Flag.Boolean("verbose").pipe(Flag.withDescription("ASCII flag description"))
139 })
140 const run = Command.runWith(command, { version: "1.0.0" })
141
142 yield* run(["--help"])
143
144 const lines = (yield* TestConsole.logLines).join("\n").split("\n")
145 assert.include(lines, " --ファイル Wide flag description")
146 assert.include(lines, " --👩‍💻 Emoji flag description")
147 assert.include(lines, " --verbose ASCII flag description")
148 }).pipe(Effect.provide(TestLayer)))
149
150 it.effect("aligns flag descriptions when flag names contain zero-width code points", () =>
151 Effect.gen(function*() {
152 const command = Command.make("tool", {
153 combining: Flag.Boolean("e\u0301").pipe(Flag.withDescription("Combining flag description")),
154 zeroWidth: Flag.Boolean("a\u200Bb").pipe(Flag.withDescription("Zero-width flag description")),
155 ascii: Flag.Boolean("abcd").pipe(Flag.withDescription("ASCII flag description"))
156 })
157 const run = Command.runWith(command, { version: "1.0.0" })
158
159 yield* run(["--help"])
160
161 const lines = (yield* TestConsole.logLines).join("\n").split("\n")
162 assert.include(lines, " --e\u0301 Combining flag description")
163 assert.include(lines, " --a\u200Bb Zero-width flag description")
164 assert.include(lines, " --abcd ASCII flag description")
165 }).pipe(Effect.provide(TestLayer)))
166
167 it.effect("separates long subcommand and argument names from their descriptions", () =>
168 Effect.gen(function*() {
169 const child = Command.make("account:set-password", {
170 account: Argument.String("existing-account-identifier").pipe(
171 Argument.withDescription("Account to update")
172 )
173 }).pipe(Command.withDescription("Rewrite the credential hash"))
174 const command = Command.make("demo").pipe(Command.withSubcommands([child]))
175 const run = Command.runWith(command, { version: "1.0.0" })
176
177 yield* run(["--help"])
178 const rootHelp = yield* TestConsole.logLines
179
180 yield* run(["account:set-password", "--help"])
181 const allHelp = yield* TestConsole.logLines
182 const childHelp = allHelp.slice(rootHelp.length)
183
184 assert.isTrue(rootHelp.some((line) => String(line).includes("account:set-password Rewrite the credential hash")))
185 assert.isTrue(
186 childHelp.some((line) => String(line).includes("existing-account-identifier string Account to update"))
187 )
188 }).pipe(Effect.provide(TestLayer)))
189
190 it.effect("hides flags marked with withHidden from help output", () =>
191 Effect.gen(function*() {
192 const command = Command.make("tool", {
193 visible: Flag.String("visible").pipe(Flag.withDescription("Visible flag")),
194 secret: Flag.String("experimental-foo").pipe(
195 Flag.withDescription("Should not appear"),
196 Flag.withHidden
197 )
198 })
199 const run = Command.runWith(command, { version: "1.0.0" })
200
201 yield* run(["--help"])
202
203 const helpText = (yield* TestConsole.logLines).join("\n")
204 expect(helpText).toContain("--visible")
205 expect(helpText).not.toContain("--experimental-foo")
206 expect(helpText).not.toContain("Should not appear")
207 }).pipe(Effect.provide(TestLayer)))
208
209 it.effect("hidden flag still parses on the command line", () =>
210 Effect.gen(function*() {
211 let captured: string | undefined
212 const command = Command.make("tool", {
213 secret: Flag.String("experimental-foo").pipe(Flag.withHidden)
214 }, (config) =>
215 Effect.sync(() => {
216 captured = config.secret
217 }))
218 const run = Command.runWith(command, { version: "1.0.0" })
219
220 yield* run(["--experimental-foo", "value"])
221
222 expect(captured).toBe("value")
223 }).pipe(Effect.provide(TestLayer)))
224
225 it.effect("hidden flag name does not leak through unrecognized-flag suggestions", () =>
226 Effect.gen(function*() {
227 const command = Command.make("tool", {
228 secret: Flag.String("experimental-foo").pipe(Flag.withHidden)
229 }, () => Effect.void)
230 const run = Command.runWith(command, { version: "1.0.0" })
231
232 yield* run(["--experimental-fo", "value"]).pipe(
233 Effect.catchTag("ShowHelp", () => Effect.void)
234 )
235
236 const errorText = (yield* TestConsole.errorLines).join("\n")
237 const helpText = (yield* TestConsole.logLines).join("\n")
238 expect(errorText + helpText).not.toContain("experimental-foo")
239 }).pipe(Effect.provide(TestLayer)))
240
241 it.effect("hides unlisted subcommands from help output", () =>
242 Effect.gen(function*() {
243 const visible = Command.make("visible").pipe(
244 Command.withDescription("A visible subcommand")
245 )
246 const secret = Command.make("experimental-foo").pipe(
247 Command.withDescription("Should not appear"),
248 Command.unlisted
249 )
250 const root = Command.make("tool").pipe(
251 Command.withSubcommands([visible, secret])
252 )
253 const run = Command.runWith(root, { version: "1.0.0" })
254
255 yield* run(["--help"])
256
257 const helpText = (yield* TestConsole.logLines).join("\n")
258 expect(helpText).toContain("visible")
259 expect(helpText).not.toContain("experimental-foo")
260 expect(helpText).not.toContain("Should not appear")
261 }).pipe(Effect.provide(TestLayer)))
262
263 it.effect("unlisted subcommand still parses on the command line", () =>
264 Effect.gen(function*() {
265 let invoked = false
266 const secret = Command.make("experimental-foo").pipe(
267 Command.unlisted,
268 Command.withHandler(() =>
269 Effect.sync(() => {
270 invoked = true
271 })
272 )
273 )
274 const root = Command.make("tool").pipe(
275 Command.withSubcommands([secret])
276 )
277 const run = Command.runWith(root, { version: "1.0.0" })
278
279 yield* run(["experimental-foo"])
280
281 expect(invoked).toBe(true)
282 }).pipe(Effect.provide(TestLayer)))
283
284 it.effect("unlisted subcommand name does not leak through unknown-subcommand suggestions", () =>
285 Effect.gen(function*() {
286 const secret = Command.make("experimental-foo").pipe(Command.unlisted)
287 const root = Command.make("tool").pipe(
288 Command.withSubcommands([secret])
289 )
290 const run = Command.runWith(root, { version: "1.0.0" })
291
292 yield* run(["experimental-fo"]).pipe(
293 Effect.catchTag("ShowHelp", () => Effect.void)
294 )
295
296 const errorText = (yield* TestConsole.errorLines).join("\n")
297 const helpText = (yield* TestConsole.logLines).join("\n")
298 expect(errorText + helpText).not.toContain("experimental-foo")
299 }).pipe(Effect.provide(TestLayer)))
300
301 it.effect("subcommand group with only unlisted commands disappears entirely", () =>
302 Effect.gen(function*() {
303 const secret = Command.make("experimental-foo").pipe(Command.unlisted)
304 const root = Command.make("tool").pipe(
305 Command.withSubcommands([secret])
306 )
307 const run = Command.runWith(root, { version: "1.0.0" })
308
309 yield* run(["--help"])
310
311 const helpText = (yield* TestConsole.logLines).join("\n")
312 expect(helpText).not.toContain("SUBCOMMANDS")
313 expect(helpText).not.toContain("<subcommand>")
314 }).pipe(Effect.provide(TestLayer)))
315
316 it.effect("renders command examples", () =>
317 Effect.gen(function*() {
318 const command = Command.make("login").pipe(
319 Command.withDescription("Authenticate with Supabase"),
320 Command.withExamples([
321 { command: "myapp login", description: "Log in with browser OAuth" },
322 { command: "myapp login --token sbp_abc123", description: "Log in with a token" },
323 { command: "myapp login --logout" },
324 { command: "myapp login --logout" },
325 { command: "myapp login", description: "Log in with browser OAuth" }
326 ])
327 )
328 const runLogin = Command.runWith(command, { version: "1.0.0" })
329
330 yield* runLogin(["--help"])
331
332 const output = (yield* TestConsole.logLines).join("\n")
333 expect(output).toMatchInlineSnapshot(`
334 "DESCRIPTION
335 Authenticate with Supabase
336
337 USAGE
338 login [flags]
339
340 GLOBAL FLAGS
341 --help, -h Show help information
342 --version, -v Show version information
343 --wizard Start wizard mode for a command
344 --completions <bash|zsh|fish|sh> Print shell completion script (choices: bash, zsh, fish, sh)
345 --log-level <all|trace|debug|info|warn|warning|error|fatal|none> Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
346
347 EXAMPLES
348 # Log in with browser OAuth
349 myapp login
350
351 # Log in with a token
352 myapp login --token sbp_abc123
353
354 myapp login --logout
355 myapp login --logout
356
357 # Log in with browser OAuth
358 myapp login"
359 `)
360 }).pipe(Effect.provide(TestLayer)))
361
362 it.effect("renders file command positional arguments", () =>
363 Effect.gen(function*() {
364 const helpText = yield* runCommand(["copy", "--help"])
365
366 expect(helpText).toMatchInlineSnapshot(`
367 "DESCRIPTION
368 Copy files or directories
369
370 USAGE
371 mycli copy [flags] <source> <destination>
372
373 ARGUMENTS
374 source file Source file or directory
375 destination file Destination path
376
377 FLAGS
378 --debug, -d Enable debug logging
379 --config, -c file Path to configuration file
380 --quiet, -q Suppress non-error output
381 --recursive, -r Copy directories recursively
382 --force, -f Overwrite existing files
383 --buffer-size integer Buffer size in KB
384
385 GLOBAL FLAGS
386 --help, -h Show help information
387 --version, -v Show version information
388 --wizard Start wizard mode for a command
389 --completions <bash|zsh|fish|sh> Print shell completion script (choices: bash, zsh, fish, sh)
390 --log-level <all|trace|debug|info|warn|warning|error|fatal|none> Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)"
391 `)
392 }).pipe(Effect.provide(TestLayer)))
393
394 it.effect("renders variadic arguments", () =>
395 Effect.gen(function*() {
396 const helpText = yield* runCommand(["remove", "--help"])
397
398 expect(helpText).toMatchInlineSnapshot(`
399 "DESCRIPTION
400 Remove files or directories
401
402 USAGE
403 mycli remove [flags] <files...>
404
405 ARGUMENTS
406 files... string Files to remove
407
408 FLAGS
409 --debug, -d Enable debug logging
410 --config, -c file Path to configuration file
411 --quiet, -q Suppress non-error output
412 --recursive, -r Remove directories and contents
413 --force, -f Force removal without prompts
414 --verbose, -v Explain what is being done
415
416 GLOBAL FLAGS
417 --help, -h Show help information
418 --version, -v Show version information
419 --wizard Start wizard mode for a command
420 --completions <bash|zsh|fish|sh> Print shell completion script (choices: bash, zsh, fish, sh)
421 --log-level <all|trace|debug|info|warn|warning|error|fatal|none> Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)"
422 `)
423 }).pipe(Effect.provide(TestLayer)))
424
425 it.effect("renders deeply nested subcommand help", () =>
426 Effect.gen(function*() {
427 const helpText = yield* runCommand(["admin", "users", "list", "--help"])
428
429 expect(helpText).toMatchInlineSnapshot(`
430 "DESCRIPTION
431 List all users in the system
432
433 USAGE
434 mycli admin users list [flags]
435
436 FLAGS
437 --debug, -d Enable debug logging
438 --config, -c file Path to configuration file
439 --quiet, -q Suppress non-error output
440 --sudo Run with elevated privileges
441 --format string Output format (json, table, csv)
442 --active Show only active users
443 --verbose, -v Show detailed information
444
445 GLOBAL FLAGS
446 --help, -h Show help information
447 --version, -v Show version information
448 --wizard Start wizard mode for a command
449 --completions <bash|zsh|fish|sh> Print shell completion script (choices: bash, zsh, fish, sh)
450 --log-level <all|trace|debug|info|warn|warning|error|fatal|none> Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)"
451 `)
452 }).pipe(Effect.provide(TestLayer)))
453
454 it.effect("renders mixed required and optional positional arguments", () =>
455 Effect.gen(function*() {
456 const helpText = yield* runCommand(["admin", "users", "create", "--help"])
457
458 expect(helpText).toMatchInlineSnapshot(`
459 "DESCRIPTION
460 Create a new user account
461
462 USAGE
463 mycli admin users create [flags] <username> [<email>]
464
465 ARGUMENTS
466 username string Username for the new user
467 email string Email address (optional) (optional)
468
469 FLAGS
470 --debug, -d Enable debug logging
471 --config, -c file Path to configuration file
472 --quiet, -q Suppress non-error output
473 --sudo Run with elevated privileges
474 --role string User role (admin, user, guest)
475 --notify, -n Send notification email
476
477 GLOBAL FLAGS
478 --help, -h Show help information
479 --version, -v Show version information
480 --wizard Start wizard mode for a command
481 --completions <bash|zsh|fish|sh> Print shell completion script (choices: bash, zsh, fish, sh)
482 --log-level <all|trace|debug|info|warn|warning|error|fatal|none> Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)"
483 `)
484 }).pipe(Effect.provide(TestLayer)))
485
486 it.effect("renders intermediate subcommand shared flags and children", () =>
487 Effect.gen(function*() {
488 const helpText = yield* runCommand(["admin", "config", "--help"])
489
490 expect(helpText).toMatchInlineSnapshot(`
491 "DESCRIPTION
492 Manage application configuration
493
494 USAGE
495 mycli admin config <subcommand> [flags]
496
497 FLAGS
498 --debug, -d Enable debug logging
499 --config, -c file Path to configuration file
500 --quiet, -q Suppress non-error output
501 --sudo Run with elevated privileges
502 --profile, -p string Configuration profile to use
503
504 GLOBAL FLAGS
505 --help, -h Show help information
506 --version, -v Show version information
507 --wizard Start wizard mode for a command
508 --completions <bash|zsh|fish|sh> Print shell completion script (choices: bash, zsh, fish, sh)
509 --log-level <all|trace|debug|info|warn|warning|error|fatal|none> Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
510
511 SUBCOMMANDS
512 set Set configuration values
513 get Get configuration value"
514 `)
515 }).pipe(Effect.provide(TestLayer)))
516
517 it.effect("renders variadic arguments with a minimum count", () =>
518 Effect.gen(function*() {
519 const helpText = yield* runCommand(["admin", "config", "set", "--help"])
520
521 expect(helpText).toMatchInlineSnapshot(`
522 "DESCRIPTION
523 Set configuration values
524
525 USAGE
526 mycli admin config set [flags] <key=value...>
527
528 ARGUMENTS
529 key=value... string Configuration key-value pairs
530
531 FLAGS
532 --debug, -d Enable debug logging
533 --config, -c file Path to configuration file
534 --quiet, -q Suppress non-error output
535 --sudo Run with elevated privileges
536 --profile, -p string Configuration profile to use
537 --config-file, -f file Write to specific config file
538
539 GLOBAL FLAGS
540 --help, -h Show help information
541 --version, -v Show version information
542 --wizard Start wizard mode for a command
543 --completions <bash|zsh|fish|sh> Print shell completion script (choices: bash, zsh, fish, sh)
544 --log-level <all|trace|debug|info|warn|warning|error|fatal|none> Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)"
545 `)
546 }).pipe(Effect.provide(TestLayer)))
547
548 it.effect("shared flags are visible in subcommand help while local flags stay local", () =>
549 Effect.gen(function*() {
550 const root = Command.make("tool", {
551 workspace: Flag.String("workspace")
552 }).pipe(
553 Command.withSharedFlags({
554 model: Flag.String("model")
555 }),
556 Command.withSubcommands([
557 Command.make("chat", {
558 topic: Flag.String("topic")
559 })
560 ])
561 )
562
563 const runRoot = Command.runWith(root, { version: "1.0.0" })
564
565 yield* runRoot(["--help"])
566 const rootHelp = yield* TestConsole.logLines
567 expect(rootHelp.some((line) => String(line).includes("--workspace"))).toBe(true)
568 expect(rootHelp.some((line) => String(line).includes("--model"))).toBe(true)
569
570 yield* runRoot(["chat", "--help"])
571 const allHelp = yield* TestConsole.logLines
572 const chatHelp = allHelp.slice(rootHelp.length)
573 expect(chatHelp.some((line) => String(line).includes("--workspace"))).toBe(false)
574 expect(chatHelp.some((line) => String(line).includes("--model"))).toBe(true)
575 expect(chatHelp.some((line) => String(line).includes("--topic"))).toBe(true)
576 }).pipe(Effect.provide(TestLayer)))
577
578 it.effect("includes inherited shared flags in subcommand completions", () =>
579 Effect.gen(function*() {
580 const root = Command.make("tool").pipe(
581 Command.withSharedFlags({
582 workspace: Flag.String("workspace").pipe(Flag.withAlias("w"))
583 }),
584 Command.withSubcommands([
585 Command.make("chat", {
586 topic: Flag.String("topic").pipe(Flag.withAlias("t"))
587 })
588 ])
589 )
590 const runRoot = Command.runWith(root, { version: "1.0.0" })
591
592 yield* runRoot(["--completions", "bash"])
593
594 const completionScript = (yield* TestConsole.logLines).join("\n")
595 const chatCompletions = completionScript.slice(completionScript.indexOf("_tool_chat()"))
596 expect(chatCompletions).toContain(`_filtered_flags+=" --topic -t"`)
597 expect(chatCompletions).toContain(`_filtered_flags+=" --workspace -w"`)
598 }).pipe(Effect.provide(TestLayer)))
599
600 it.effect("renders grouped subcommands", () =>
601 Effect.gen(function*() {
602 const ungrouped = Command.make("ungrouped").pipe(
603 Command.withDescription("This command is not in a group")
604 )
605 const init = Command.make("init").pipe(Command.withDescription("Create a new project"))
606 const login = Command.make("login").pipe(Command.withDescription("Authenticate with the platform"))
607 const start = Command.make("start").pipe(Command.withDescription("Start local services"))
608 const stop = Command.make("stop").pipe(Command.withDescription("Stop local services"))
609 const db = Command.make("db").pipe(Command.withDescription("Manage local database"))
610 const projects = Command.make("projects").pipe(Command.withDescription("Manage cloud projects"))
611 const functions = Command.make("functions").pipe(Command.withDescription("Manage edge functions"))
612
613 const grouped = Command.make("tool").pipe(
614 Command.withSubcommands([
615 {
616 group: "Quick Start",
617 commands: [init, login]
618 },
619 {
620 group: "Local Development",
621 commands: [start, stop, db]
622 },
623 {
624 group: "Management APIs",
625 commands: [projects, functions]
626 },
627 ungrouped
628 ])
629 )
630
631 const runGrouped = Command.runWith(grouped, { version: "1.0.0" })
632 yield* runGrouped(["--help"])
633
634 const helpText = (yield* TestConsole.logLines).join("\n")
635
636 expect(helpText).toMatchInlineSnapshot(`
637 "USAGE
638 tool <subcommand> [flags]
639
640 GLOBAL FLAGS
641 --help, -h Show help information
642 --version, -v Show version information
643 --wizard Start wizard mode for a command
644 --completions <bash|zsh|fish|sh> Print shell completion script (choices: bash, zsh, fish, sh)
645 --log-level <all|trace|debug|info|warn|warning|error|fatal|none> Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
646
647 SUBCOMMANDS
648 ungrouped This command is not in a group
649
650 Quick Start:
651 init Create a new project
652 login Authenticate with the platform
653
654 Local Development:
655 start Start local services
656 stop Stop local services
657 db Manage local database
658
659 Management APIs:
660 projects Manage cloud projects
661 functions Manage edge functions"
662 `)
663 }).pipe(Effect.provide(TestLayer)))
664
665 it.effect("renders subcommand aliases in listings", () =>
666 Effect.gen(function*() {
667 const plan = Command.make("plan").pipe(
668 Command.withAlias("p"),
669 Command.withDescription("Draft a plan in your editor")
670 )
671
672 const root = Command.make("tool").pipe(Command.withSubcommands([plan]))
673 const runRoot = Command.runWith(root, { version: "1.0.0" })
674
675 yield* runRoot(["--help"])
676
677 const helpText = (yield* TestConsole.logLines).join("\n")
678
679 expect(helpText).toMatchInlineSnapshot(`
680 "USAGE
681 tool <subcommand> [flags]
682
683 GLOBAL FLAGS
684 --help, -h Show help information
685 --version, -v Show version information
686 --wizard Start wizard mode for a command
687 --completions <bash|zsh|fish|sh> Print shell completion script (choices: bash, zsh, fish, sh)
688 --log-level <all|trace|debug|info|warn|warning|error|fatal|none> Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
689
690 SUBCOMMANDS
691 plan, p Draft a plan in your editor"
692 `)
693 }).pipe(Effect.provide(TestLayer)))
694
695 it.effect("keeps subcommand flags in aliased completion contexts", () =>
696 Effect.gen(function*() {
697 const list = Command.make("list", {
698 format: Flag.Literals("format", ["json", "text"])
699 }).pipe(Command.withAlias("ls"))
700 const root = Command.make("ctl").pipe(Command.withSubcommands([list]))
701 const runRoot = Command.runWith(root, { version: "1.0.0" })
702
703 yield* runRoot(["--completions", "bash"])
704
705 const completion = (yield* TestConsole.logLines).join("\n")
706 const aliasContext = completion.split("_ctl_ls()")[1]?.split("\n}")[0] ?? ""
707 expect(aliasContext).toContain("--format")
708 }).pipe(Effect.provide(TestLayer)))
709})