import { describe, expect, it } from "@effect/vitest" import { Effect, FileSystem, Layer, Path, Stdio } from "effect" import { TestConsole } from "effect/testing" import { CliOutput, Command, Flag } from "effect/unstable/cli" import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner" import * as Cli from "./fixtures/ComprehensiveCli.ts" import * as MockTerminal from "./services/MockTerminal.ts" import * as TestActions from "./services/TestActions.ts" const ActionsLayer = TestActions.layer const ConsoleLayer = TestConsole.layer const FileSystemLayer = FileSystem.layerNoop({}) const PathLayer = Path.layer const TerminalLayer = MockTerminal.layer const CliOutputLayer = CliOutput.layer( CliOutput.defaultFormatter({ colors: false }) ) const TestLayer = Layer.mergeAll( ActionsLayer, ConsoleLayer, FileSystemLayer, PathLayer, TerminalLayer, CliOutputLayer, Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => Effect.die("Not implemented"))), Stdio.layerTest({}) ) const runCommand = Effect.fnUntraced( function*(command: ReadonlyArray) { yield* Cli.run(command) const output = yield* TestConsole.logLines return output.join("\n") } ) describe("Command help output", () => { it.effect("renders root command help", () => Effect.gen(function*() { const helpText = yield* runCommand(["--help"]) expect(helpText).toMatchInlineSnapshot(` "DESCRIPTION A comprehensive CLI tool demonstrating all features USAGE mycli [flags] FLAGS --debug, -d Enable debug logging --config, -c file Path to configuration file --quiet, -q Suppress non-error output GLOBAL FLAGS --help, -h Show help information --version, -v Show version information --completions Print shell completion script (choices: bash, zsh, fish, sh) --log-level Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none) SUBCOMMANDS admin Administrative commands copy Copy files or directories move Move or rename files remove Remove files or directories build Build the project git Git version control test-required Test command with required option test-failing Test command that always fails app Application management app-nested Application with nested services" `) }).pipe(Effect.provide(TestLayer))) it.effect("aligns flag descriptions when flag names are long", () => Effect.gen(function*() { const command = Command.make("tool", { short: Flag.string("short").pipe(Flag.withDescription("Short flag description")), veryLong: Flag.string("this-is-a-very-very-long-flag-name").pipe( Flag.withDescription("Long flag description") ) }) const run = Command.runWith(command, { version: "1.0.0" }) yield* run(["--help"]) const helpText = (yield* TestConsole.logLines).join("\n") const lines = helpText.split("\n") const shortLine = lines.find((line) => line.includes("--short")) const longLine = lines.find((line) => line.includes("--this-is-a-very-very-long-flag-name")) expect(shortLine).toBeDefined() expect(longLine).toBeDefined() expect(shortLine!.indexOf("Short flag description")).toBe(longLine!.indexOf("Long flag description")) }).pipe(Effect.provide(TestLayer))) it.effect("hides flags marked with withHidden from help output", () => Effect.gen(function*() { const command = Command.make("tool", { visible: Flag.string("visible").pipe(Flag.withDescription("Visible flag")), secret: Flag.string("experimental-foo").pipe( Flag.withDescription("Should not appear"), Flag.withHidden ) }) const run = Command.runWith(command, { version: "1.0.0" }) yield* run(["--help"]) const helpText = (yield* TestConsole.logLines).join("\n") expect(helpText).toContain("--visible") expect(helpText).not.toContain("--experimental-foo") expect(helpText).not.toContain("Should not appear") }).pipe(Effect.provide(TestLayer))) it.effect("hidden flag still parses on the command line", () => Effect.gen(function*() { let captured: string | undefined const command = Command.make("tool", { secret: Flag.string("experimental-foo").pipe(Flag.withHidden) }, (config) => Effect.sync(() => { captured = config.secret })) const run = Command.runWith(command, { version: "1.0.0" }) yield* run(["--experimental-foo", "value"]) expect(captured).toBe("value") }).pipe(Effect.provide(TestLayer))) it.effect("hidden flag name does not leak through unrecognized-flag suggestions", () => Effect.gen(function*() { const command = Command.make("tool", { secret: Flag.string("experimental-foo").pipe(Flag.withHidden) }, () => Effect.void) const run = Command.runWith(command, { version: "1.0.0" }) yield* run(["--experimental-fo", "value"]).pipe( Effect.catchTag("ShowHelp", () => Effect.void) ) const errorText = (yield* TestConsole.errorLines).join("\n") const helpText = (yield* TestConsole.logLines).join("\n") expect(errorText + helpText).not.toContain("experimental-foo") }).pipe(Effect.provide(TestLayer))) it.effect("hides subcommands marked with withHidden from help output", () => Effect.gen(function*() { const visible = Command.make("visible").pipe( Command.withDescription("A visible subcommand") ) const secret = Command.make("experimental-foo").pipe( Command.withDescription("Should not appear"), Command.withHidden ) const root = Command.make("tool").pipe( Command.withSubcommands([visible, secret]) ) const run = Command.runWith(root, { version: "1.0.0" }) yield* run(["--help"]) const helpText = (yield* TestConsole.logLines).join("\n") expect(helpText).toContain("visible") expect(helpText).not.toContain("experimental-foo") expect(helpText).not.toContain("Should not appear") }).pipe(Effect.provide(TestLayer))) it.effect("hidden subcommand still parses on the command line", () => Effect.gen(function*() { let invoked = false const secret = Command.make("experimental-foo").pipe( Command.withHidden, Command.withHandler(() => Effect.sync(() => { invoked = true }) ) ) const root = Command.make("tool").pipe( Command.withSubcommands([secret]) ) const run = Command.runWith(root, { version: "1.0.0" }) yield* run(["experimental-foo"]) expect(invoked).toBe(true) }).pipe(Effect.provide(TestLayer))) it.effect("hidden subcommand name does not leak through unknown-subcommand suggestions", () => Effect.gen(function*() { const secret = Command.make("experimental-foo").pipe(Command.withHidden) const root = Command.make("tool").pipe( Command.withSubcommands([secret]) ) const run = Command.runWith(root, { version: "1.0.0" }) yield* run(["experimental-fo"]).pipe( Effect.catchTag("ShowHelp", () => Effect.void) ) const errorText = (yield* TestConsole.errorLines).join("\n") const helpText = (yield* TestConsole.logLines).join("\n") expect(errorText + helpText).not.toContain("experimental-foo") }).pipe(Effect.provide(TestLayer))) it.effect("subcommand group with only hidden commands disappears entirely", () => Effect.gen(function*() { const secret = Command.make("experimental-foo").pipe(Command.withHidden) const root = Command.make("tool").pipe( Command.withSubcommands([secret]) ) const run = Command.runWith(root, { version: "1.0.0" }) yield* run(["--help"]) const helpText = (yield* TestConsole.logLines).join("\n") expect(helpText).not.toContain("SUBCOMMANDS") expect(helpText).not.toContain("") }).pipe(Effect.provide(TestLayer))) it.effect("renders command examples", () => Effect.gen(function*() { const command = Command.make("login").pipe( Command.withDescription("Authenticate with Supabase"), Command.withExamples([ { command: "myapp login", description: "Log in with browser OAuth" }, { command: "myapp login --token sbp_abc123", description: "Log in with a token" }, { command: "myapp login --logout" }, { command: "myapp login --logout" }, { command: "myapp login", description: "Log in with browser OAuth" } ]) ) const runLogin = Command.runWith(command, { version: "1.0.0" }) yield* runLogin(["--help"]) const output = (yield* TestConsole.logLines).join("\n") expect(output).toMatchInlineSnapshot(` "DESCRIPTION Authenticate with Supabase USAGE login [flags] GLOBAL FLAGS --help, -h Show help information --version, -v Show version information --completions Print shell completion script (choices: bash, zsh, fish, sh) --log-level Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none) EXAMPLES # Log in with browser OAuth myapp login # Log in with a token myapp login --token sbp_abc123 myapp login --logout myapp login --logout # Log in with browser OAuth myapp login" `) }).pipe(Effect.provide(TestLayer))) it.effect("renders file command positional arguments", () => Effect.gen(function*() { const helpText = yield* runCommand(["copy", "--help"]) expect(helpText).toMatchInlineSnapshot(` "DESCRIPTION Copy files or directories USAGE mycli copy [flags] ARGUMENTS source file Source file or directory destination file Destination path FLAGS --debug, -d Enable debug logging --config, -c file Path to configuration file --quiet, -q Suppress non-error output --recursive, -r Copy directories recursively --force, -f Overwrite existing files --buffer-size integer Buffer size in KB GLOBAL FLAGS --help, -h Show help information --version, -v Show version information --completions Print shell completion script (choices: bash, zsh, fish, sh) --log-level Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)" `) }).pipe(Effect.provide(TestLayer))) it.effect("renders variadic arguments", () => Effect.gen(function*() { const helpText = yield* runCommand(["remove", "--help"]) expect(helpText).toMatchInlineSnapshot(` "DESCRIPTION Remove files or directories USAGE mycli remove [flags] ARGUMENTS files... string Files to remove FLAGS --debug, -d Enable debug logging --config, -c file Path to configuration file --quiet, -q Suppress non-error output --recursive, -r Remove directories and contents --force, -f Force removal without prompts --verbose, -v Explain what is being done GLOBAL FLAGS --help, -h Show help information --version, -v Show version information --completions Print shell completion script (choices: bash, zsh, fish, sh) --log-level Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)" `) }).pipe(Effect.provide(TestLayer))) it.effect("renders deeply nested subcommand help", () => Effect.gen(function*() { const helpText = yield* runCommand(["admin", "users", "list", "--help"]) expect(helpText).toMatchInlineSnapshot(` "DESCRIPTION List all users in the system USAGE mycli admin users list [flags] FLAGS --debug, -d Enable debug logging --config, -c file Path to configuration file --quiet, -q Suppress non-error output --sudo Run with elevated privileges --format string Output format (json, table, csv) --active Show only active users --verbose, -v Show detailed information GLOBAL FLAGS --help, -h Show help information --version, -v Show version information --completions Print shell completion script (choices: bash, zsh, fish, sh) --log-level Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)" `) }).pipe(Effect.provide(TestLayer))) it.effect("renders mixed required and optional positional arguments", () => Effect.gen(function*() { const helpText = yield* runCommand(["admin", "users", "create", "--help"]) expect(helpText).toMatchInlineSnapshot(` "DESCRIPTION Create a new user account USAGE mycli admin users create [flags] [] ARGUMENTS username string Username for the new user email string Email address (optional) (optional) FLAGS --debug, -d Enable debug logging --config, -c file Path to configuration file --quiet, -q Suppress non-error output --sudo Run with elevated privileges --role string User role (admin, user, guest) --notify, -n Send notification email GLOBAL FLAGS --help, -h Show help information --version, -v Show version information --completions Print shell completion script (choices: bash, zsh, fish, sh) --log-level Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)" `) }).pipe(Effect.provide(TestLayer))) it.effect("renders intermediate subcommand shared flags and children", () => Effect.gen(function*() { const helpText = yield* runCommand(["admin", "config", "--help"]) expect(helpText).toMatchInlineSnapshot(` "DESCRIPTION Manage application configuration USAGE mycli admin config [flags] FLAGS --debug, -d Enable debug logging --config, -c file Path to configuration file --quiet, -q Suppress non-error output --sudo Run with elevated privileges --profile, -p string Configuration profile to use GLOBAL FLAGS --help, -h Show help information --version, -v Show version information --completions Print shell completion script (choices: bash, zsh, fish, sh) --log-level Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none) SUBCOMMANDS set Set configuration values get Get configuration value" `) }).pipe(Effect.provide(TestLayer))) it.effect("renders variadic arguments with a minimum count", () => Effect.gen(function*() { const helpText = yield* runCommand(["admin", "config", "set", "--help"]) expect(helpText).toMatchInlineSnapshot(` "DESCRIPTION Set configuration values USAGE mycli admin config set [flags] ARGUMENTS key=value... string Configuration key-value pairs FLAGS --debug, -d Enable debug logging --config, -c file Path to configuration file --quiet, -q Suppress non-error output --sudo Run with elevated privileges --profile, -p string Configuration profile to use --config-file, -f file Write to specific config file GLOBAL FLAGS --help, -h Show help information --version, -v Show version information --completions Print shell completion script (choices: bash, zsh, fish, sh) --log-level Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)" `) }).pipe(Effect.provide(TestLayer))) it.effect("shared flags are visible in subcommand help while local flags stay local", () => Effect.gen(function*() { const root = Command.make("tool", { workspace: Flag.string("workspace") }).pipe( Command.withSharedFlags({ model: Flag.string("model") }), Command.withSubcommands([ Command.make("chat", { topic: Flag.string("topic") }) ]) ) const runRoot = Command.runWith(root, { version: "1.0.0" }) yield* runRoot(["--help"]) const rootHelp = yield* TestConsole.logLines expect(rootHelp.some((line) => String(line).includes("--workspace"))).toBe(true) expect(rootHelp.some((line) => String(line).includes("--model"))).toBe(true) yield* runRoot(["chat", "--help"]) const allHelp = yield* TestConsole.logLines const chatHelp = allHelp.slice(rootHelp.length) expect(chatHelp.some((line) => String(line).includes("--workspace"))).toBe(false) expect(chatHelp.some((line) => String(line).includes("--model"))).toBe(true) expect(chatHelp.some((line) => String(line).includes("--topic"))).toBe(true) }).pipe(Effect.provide(TestLayer))) it.effect("renders grouped subcommands", () => Effect.gen(function*() { const ungrouped = Command.make("ungrouped").pipe( Command.withDescription("This command is not in a group") ) const init = Command.make("init").pipe(Command.withDescription("Create a new project")) const login = Command.make("login").pipe(Command.withDescription("Authenticate with the platform")) const start = Command.make("start").pipe(Command.withDescription("Start local services")) const stop = Command.make("stop").pipe(Command.withDescription("Stop local services")) const db = Command.make("db").pipe(Command.withDescription("Manage local database")) const projects = Command.make("projects").pipe(Command.withDescription("Manage cloud projects")) const functions = Command.make("functions").pipe(Command.withDescription("Manage edge functions")) const grouped = Command.make("tool").pipe( Command.withSubcommands([ { group: "Quick Start", commands: [init, login] }, { group: "Local Development", commands: [start, stop, db] }, { group: "Management APIs", commands: [projects, functions] }, ungrouped ]) ) const runGrouped = Command.runWith(grouped, { version: "1.0.0" }) yield* runGrouped(["--help"]) const helpText = (yield* TestConsole.logLines).join("\n") expect(helpText).toMatchInlineSnapshot(` "USAGE tool [flags] GLOBAL FLAGS --help, -h Show help information --version, -v Show version information --completions Print shell completion script (choices: bash, zsh, fish, sh) --log-level Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none) SUBCOMMANDS ungrouped This command is not in a group Quick Start: init Create a new project login Authenticate with the platform Local Development: start Start local services stop Stop local services db Manage local database Management APIs: projects Manage cloud projects functions Manage edge functions" `) }).pipe(Effect.provide(TestLayer))) it.effect("renders subcommand aliases in listings", () => Effect.gen(function*() { const plan = Command.make("plan").pipe( Command.withAlias("p"), Command.withDescription("Draft a plan in your editor") ) const root = Command.make("tool").pipe(Command.withSubcommands([plan])) const runRoot = Command.runWith(root, { version: "1.0.0" }) yield* runRoot(["--help"]) const helpText = (yield* TestConsole.logLines).join("\n") expect(helpText).toMatchInlineSnapshot(` "USAGE tool [flags] GLOBAL FLAGS --help, -h Show help information --version, -v Show version information --completions Print shell completion script (choices: bash, zsh, fish, sh) --log-level Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none) SUBCOMMANDS plan, p Draft a plan in your editor" `) }).pipe(Effect.provide(TestLayer))) })