Skip to content

Commit 40d6e12

Browse files
ryanbas21claude
andcommitted
feat(cli): add Command.runWithWizard for programmatic wizard mode
Adds a new API to programmatically invoke wizard mode based on custom conditions. This allows CLI authors to enable wizard mode in scenarios beyond just the --wizard flag. Key features: - Command.runWithWizard() accepts a shouldRunWizard predicate function - When the predicate returns true, wizard mode is automatically invoked - Common use case: run wizard when bare command is executed (no args) - Fully backwards compatible with existing --wizard flag behavior - Comprehensive test coverage Example usage: ```typescript const cli = Command.runWithWizard(command, { name: "MyApp", version: "1.0.0", shouldRunWizard: (args) => args.length <= 1 }) ``` Fixes #5699 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 0d78615 commit 40d6e12

File tree

4 files changed

+169
-0
lines changed

4 files changed

+169
-0
lines changed

.changeset/soft-oranges-arrive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@effect/cli": minor
3+
---
4+
5+
Add runWithWizard for a programmatic wizard mode

packages/cli/src/Command.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,3 +441,51 @@ export const run: {
441441
config: Omit<CliApp.ConstructorArgs<never>, "command">
442442
): (args: ReadonlyArray<string>) => Effect<void, E | ValidationError, R | CliApp.Environment>
443443
} = Internal.run
444+
445+
/**
446+
* Creates a CLI application from a `Command` with optional automatic wizard mode fallback.
447+
*
448+
* This function is similar to `run`, but allows you to specify when wizard mode should
449+
* be automatically invoked. This is useful when you want to provide an interactive
450+
* experience for users who run your CLI without arguments or in specific scenarios.
451+
*
452+
* @example
453+
* ```typescript
454+
* import { Command, Options } from "@effect/cli"
455+
* import { Effect } from "effect"
456+
*
457+
* const command = Command.make("greet", {
458+
* name: Options.text("name")
459+
* }, ({ name }) =>
460+
* Effect.log(`Hello, ${name}!`)
461+
* )
462+
*
463+
* // Run wizard mode when no arguments are provided (besides the command name)
464+
* const cli = Command.runWithWizard(command, {
465+
* name: "MyApp",
466+
* version: "1.0.0",
467+
* shouldRunWizard: (args) => args.length <= 1
468+
* })
469+
*
470+
* // Running `mycli` (no args) will start wizard mode
471+
* // Running `mycli --name John` will execute normally
472+
* ```
473+
*
474+
* @since 1.0.0
475+
* @category conversions
476+
*/
477+
export const runWithWizard: {
478+
(
479+
config: Omit<CliApp.ConstructorArgs<never>, "command"> & {
480+
readonly shouldRunWizard?: (args: ReadonlyArray<string>) => boolean
481+
}
482+
): <Name extends string, R, E, A>(
483+
self: Command<Name, R, E, A>
484+
) => (args: ReadonlyArray<string>) => Effect<void, E | ValidationError | QuitException, R | CliApp.Environment>
485+
<Name extends string, R, E, A>(
486+
self: Command<Name, R, E, A>,
487+
config: Omit<CliApp.ConstructorArgs<never>, "command"> & {
488+
readonly shouldRunWizard?: (args: ReadonlyArray<string>) => boolean
489+
}
490+
): (args: ReadonlyArray<string>) => Effect<void, E | ValidationError | QuitException, R | CliApp.Environment>
491+
} = Internal.runWithWizard

packages/cli/src/internal/command.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type * as Usage from "../Usage.js"
2525
import * as ValidationError from "../ValidationError.js"
2626
import * as InternalArgs from "./args.js"
2727
import * as InternalCliApp from "./cliApp.js"
28+
import * as InternalCliConfig from "./cliConfig.js"
2829
import * as InternalDescriptor from "./commandDescriptor.js"
2930
import * as InternalOptions from "./options.js"
3031

@@ -558,3 +559,45 @@ export const run = dual<
558559
const handler = (args: any) => self.transform(self.handler(args), args)
559560
return (args) => InternalCliApp.run(app, args, handler)
560561
})
562+
563+
/** @internal */
564+
export const runWithWizard = dual<
565+
(
566+
config: Omit<CliApp.CliApp.ConstructorArgs<never>, "command"> & {
567+
readonly shouldRunWizard?: (args: ReadonlyArray<string>) => boolean
568+
}
569+
) => <Name extends string, R, E, A>(
570+
self: Command.Command<Name, R, E, A>
571+
) => (
572+
args: ReadonlyArray<string>
573+
) => Effect.Effect<void, E | ValidationError.ValidationError | Terminal.QuitException, R | CliApp.CliApp.Environment>,
574+
<Name extends string, R, E, A>(
575+
self: Command.Command<Name, R, E, A>,
576+
config: Omit<CliApp.CliApp.ConstructorArgs<never>, "command"> & {
577+
readonly shouldRunWizard?: (args: ReadonlyArray<string>) => boolean
578+
}
579+
) => (
580+
args: ReadonlyArray<string>
581+
) => Effect.Effect<void, E | ValidationError.ValidationError | Terminal.QuitException, R | CliApp.CliApp.Environment>
582+
>(2, (self, config) => {
583+
const { shouldRunWizard = () => false, ...restConfig } = config
584+
const app = InternalCliApp.make({
585+
...restConfig,
586+
command: self.descriptor
587+
})
588+
registeredDescriptors.set(self.tag, self.descriptor)
589+
const handler = (args: any) => self.transform(self.handler(args), args)
590+
const normalRun = (args: ReadonlyArray<string>) => InternalCliApp.run(app, args, handler)
591+
592+
return (args) => {
593+
// If shouldRunWizard returns true, run wizard mode first
594+
if (shouldRunWizard(args)) {
595+
const prefix = Arr.take(args, 1) // Get the command name from args
596+
const cliConfig = InternalCliConfig.defaultConfig
597+
return wizard(self, prefix, cliConfig).pipe(
598+
Effect.flatMap((wizardArgs) => normalRun(wizardArgs))
599+
)
600+
}
601+
return normalRun(args)
602+
}
603+
})

packages/cli/test/Wizard.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type * as CliApp from "@effect/cli/CliApp"
2+
import * as Args from "@effect/cli/Args"
23
import * as Command from "@effect/cli/Command"
34
import * as Options from "@effect/cli/Options"
45
import { NodeFileSystem, NodePath } from "@effect/platform-node"
@@ -41,4 +42,76 @@ describe("Wizard", () => {
4142
const result = Array.some(lines, (line) => line.includes("Quitting wizard mode..."))
4243
expect(result).toBe(true)
4344
}).pipe(runEffect))
45+
46+
describe("runWithWizard", () => {
47+
it("should skip wizard mode when shouldRunWizard returns false", () =>
48+
Effect.gen(function*() {
49+
let executedWithName: string | undefined
50+
const command = Command.make("greet", {
51+
name: Options.text("name")
52+
}, ({ name }) => Effect.sync(() => { executedWithName = name })).pipe(
53+
Command.withDescription("Greet someone")
54+
)
55+
56+
const cli = Command.runWithWizard(command, {
57+
name: "Test",
58+
version: "1.0.0",
59+
shouldRunWizard: (args) => args.length <= 1
60+
})
61+
62+
// Simulate running with args (should NOT trigger wizard)
63+
const args = Array.make("node", "greet", "--name", "Bob")
64+
yield* cli(args)
65+
66+
// Verify the command was executed with the provided value
67+
expect(executedWithName).toBe("Bob")
68+
69+
const lines = yield* MockConsole.getLines({ stripAnsi: true })
70+
const wizardStarted = Array.some(lines, (line) =>
71+
line.includes("Wizard Mode") || line.includes("wizard")
72+
)
73+
// Wizard should NOT have been started
74+
expect(wizardStarted).toBe(false)
75+
}).pipe(runEffect))
76+
77+
it("should allow default shouldRunWizard behavior (always false)", () =>
78+
Effect.gen(function*() {
79+
let executedWithName: string | undefined
80+
const command = Command.make("greet", {
81+
name: Options.text("name")
82+
}, ({ name }) => Effect.sync(() => { executedWithName = name }))
83+
84+
// Don't provide shouldRunWizard - should default to always false
85+
const cli = Command.runWithWizard(command, {
86+
name: "Test",
87+
version: "1.0.0"
88+
})
89+
90+
const args = Array.make("node", "greet", "--name", "Charlie")
91+
yield* cli(args)
92+
93+
expect(executedWithName).toBe("Charlie")
94+
}).pipe(runEffect))
95+
96+
it("should use standard wizard flag when shouldRunWizard is not triggered", () =>
97+
Effect.gen(function*() {
98+
const command = Command.make("foo", { message: Options.text("message") })
99+
100+
const cli = Command.runWithWizard(command, {
101+
name: "Test",
102+
version: "1.0.0",
103+
shouldRunWizard: (args) => args.length <= 1
104+
})
105+
106+
// Using --wizard flag explicitly (shouldRunWizard returns false because args.length > 1)
107+
const args = Array.make("node", "test", "--wizard")
108+
const fiber = yield* Effect.fork(cli(args))
109+
yield* MockTerminal.inputKey("c", { ctrl: true })
110+
yield* Fiber.join(fiber)
111+
112+
const lines = yield* MockConsole.getLines({ stripAnsi: true })
113+
const result = Array.some(lines, (line) => line.includes("Quitting wizard mode..."))
114+
expect(result).toBe(true)
115+
}).pipe(runEffect))
116+
})
44117
})

0 commit comments

Comments
 (0)