Skip to content

Commit 6c9bc7d

Browse files
committed
Make interactive recursive
1 parent 22312f9 commit 6c9bc7d

File tree

10 files changed

+237
-149
lines changed

10 files changed

+237
-149
lines changed

README.md

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,39 +32,47 @@ npm install --save interactive-commander
3232
```typescript
3333
import { InteractiveCommand, InteractiveOption } from "interactive-commander";
3434

35-
const program = new InteractiveCommand()
36-
// Registers -i, --interactive flags
37-
.interactive("-i, --interactive", "interactive mode");
35+
const program = new InteractiveCommand();
3836

3937
program
4038
.command("pizza")
41-
42-
// .option adds a regular (non-interactive) option to the command
39+
// Detached options are interactive by default
4340
.option("-d, --drink", "drink")
4441

45-
// To create an interactive option, use addOption and pass in a new instance
46-
// of InteractiveOption
42+
// Missing mandatory options won't throw an error in interactive mode
43+
.requiredOption("-o, --olive-oil", "olive oil")
44+
45+
// Boolean InteractiveOptions will show a confirmation prompt by default
46+
.option("-c, --cheese", "cheese")
47+
.option("-C, --no-cheese", "no cheese")
48+
4749
.addOption(
4850
new InteractiveOption("-n, --count <number>", "number of pizzas")
4951
// InteractiveOption supports all the methods of Commander.js' Option
5052
.argParser(Number)
5153
.default(1),
5254
)
5355

54-
// Boolean InteractiveOptions will show a confirmation prompt by default
55-
.addOption(new InteractiveOption("-c, --cheese", "cheese"))
56-
.addOption(new InteractiveOption("-C, --no-cheese", "no cheese"))
56+
.addOption(
57+
new InteractiveOption(
58+
"--non-interactive-option <value>",
59+
"non-interactive option",
60+
)
61+
// Passing in undefined to prompt will disable interactive mode for this option
62+
.prompt(undefined)
63+
.default("default value"),
64+
)
5765

5866
// InteractiveOptions with choices will show a select prompt by default
5967
.addOption(
6068
new InteractiveOption("-s, --size <size>", "size")
6169
.choices(["small", "medium", "large"])
62-
// Missing mandatory options won't throw an error in interactive mode
70+
.default("medium")
6371
.makeOptionMandatory(),
6472
)
6573

6674
.addOption(
67-
new InteractiveOption("-n, --name <string>", "your name")
75+
new InteractiveOption("-m, --name <string>", "your name")
6876
// You can use the prompt method to implement your own prompt logic
6977
.prompt(async (currentValue, option, command) => {
7078
// TODO: Implement your own prompt logic here
@@ -76,18 +84,24 @@ program
7684
.makeOptionMandatory(),
7785
)
7886

79-
.action((_options, cmd: Command) => {
87+
.action((_options, cmd: InteractiveCommand) => {
8088
console.log("Options: %o", cmd.opts());
8189
});
8290

83-
await program.parseAsync(process.argv);
91+
await program
92+
// Enables interactive mode (when -i or --interactive is passed in)
93+
// This should almost always be called on the root command right before
94+
// calling parseAsync
95+
.interactive("-i, --interactive", "interactive mode")
96+
.parseAsync(process.argv);
8497

8598
// Try the following commands:
8699
// command-name pizza
87100
// command-name pizza -i
88101
// command-name pizza -i --count 2
89102
// command-name pizza -i --count 2 --no-cheese
90103
// command-name pizza -i --name "John Doe"
104+
// command-name pizza -i --name "John Doe" --non-interactive-option abc
91105
```
92106

93107
More examples can be found in the [examples](/examples/) directory.

examples/custom-prompt.ts

Lines changed: 0 additions & 35 deletions
This file was deleted.

examples/order-pizza.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/* eslint-disable no-warning-comments, unicorn/no-useless-undefined */
2+
import { InteractiveCommand, InteractiveOption } from "../src/index.ts";
3+
4+
const program = new InteractiveCommand();
5+
6+
program
7+
.command("pizza")
8+
// Detached options are interactive by default
9+
.option("-d, --drink", "drink")
10+
11+
// Missing mandatory options won't throw an error in interactive mode
12+
.requiredOption("-o, --olive-oil", "olive oil")
13+
14+
// Boolean InteractiveOptions will show a confirmation prompt by default
15+
.option("-c, --cheese", "cheese")
16+
.option("-C, --no-cheese", "no cheese")
17+
18+
.addOption(
19+
new InteractiveOption("-n, --count <number>", "number of pizzas")
20+
// InteractiveOption supports all the methods of Commander.js' Option
21+
.argParser(Number)
22+
.default(1),
23+
)
24+
25+
.addOption(
26+
new InteractiveOption(
27+
"--non-interactive-option <value>",
28+
"non-interactive option",
29+
)
30+
// Passing in undefined to prompt will disable interactive mode for this option
31+
.prompt(undefined)
32+
.default("default value"),
33+
)
34+
35+
// InteractiveOptions with choices will show a select prompt by default
36+
.addOption(
37+
new InteractiveOption("-s, --size <size>", "size")
38+
.choices(["small", "medium", "large"])
39+
.default("medium")
40+
.makeOptionMandatory(),
41+
)
42+
43+
.addOption(
44+
new InteractiveOption("-m, --name <string>", "your name")
45+
// You can use the prompt method to implement your own prompt logic
46+
.prompt(async (currentValue, option, command) => {
47+
// TODO: Implement your own prompt logic here
48+
const answer = "world";
49+
50+
// Return the answer
51+
return answer;
52+
})
53+
.makeOptionMandatory(),
54+
)
55+
56+
.action((_options, cmd: InteractiveCommand) => {
57+
console.log("Options: %o", cmd.opts());
58+
});
59+
60+
await program
61+
// Enables interactive mode (when -i or --interactive is passed in)
62+
// This should almost always be called on the root command right before
63+
// calling parseAsync
64+
.interactive("-i, --interactive", "interactive mode")
65+
.parseAsync(process.argv);
66+
67+
// Try the following commands:
68+
// command-name pizza
69+
// command-name pizza -i
70+
// command-name pizza -i --count 2
71+
// command-name pizza -i --count 2 --no-cheese
72+
// command-name pizza -i --name "John Doe"
73+
// command-name pizza -i --name "John Doe" --non-interactive-option abc

examples/simple.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "interactive-commander",
3-
"version": "0.1.5",
3+
"version": "0.2.0",
44
"description": "Commander.js with integrated interactive prompts",
55
"keywords": [
66
"commander",

src/interactive-command.test.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { Command, Option } from "commander";
44
import assert from "node:assert";
55
import { test } from "node:test";
66

7-
await test("interactive flags", async (t) => {
8-
await t.test("unset", async () => {
7+
await test("interactive", async (t) => {
8+
await t.test("unset flags", async () => {
99
const interactiveCommand = new InteractiveCommand();
1010

1111
assert.deepStrictEqual(interactiveCommand.options, []);
@@ -30,20 +30,42 @@ await test("interactive flags", async (t) => {
3030
]);
3131
});
3232

33-
await t.test("inheritance", async () => {
34-
const parentCommand = new InteractiveCommand().interactive(
35-
"-c, --custom",
36-
"custom option",
37-
);
33+
await t.test("nested commands", async () => {
34+
const rootCommand = new InteractiveCommand();
3835

39-
const subCommand = parentCommand.command("sub");
36+
const subCommand = rootCommand.command("sub");
4037

41-
for (const command of [parentCommand, subCommand]) {
38+
rootCommand.interactive("-c, --custom", "custom option");
39+
40+
for (const command of [rootCommand, subCommand]) {
4241
assert.deepStrictEqual(command.options, [
4342
new Option("-c, --custom", "custom option"),
4443
]);
4544
}
4645
});
46+
47+
await t.test("multiple invokations", async (t) => {
48+
const readFunction = t.mock.fn(async () => "value");
49+
50+
const rootCommand = new InteractiveCommand();
51+
52+
const subCommand = rootCommand
53+
.command("sub")
54+
.addOption(
55+
new InteractiveOption("-c, --custom <value>", "custom option").prompt(
56+
readFunction,
57+
),
58+
);
59+
60+
rootCommand.interactive();
61+
rootCommand.interactive();
62+
63+
await rootCommand.parseAsync(["node", "test", "sub", "-i"]);
64+
65+
assert.strictEqual(readFunction.mock.calls.length, 1);
66+
assert.strictEqual(rootCommand.options.length, 1);
67+
assert.strictEqual(subCommand.options.length, 2);
68+
});
4769
});
4870

4971
await test("parse", async (t) => {
@@ -59,10 +81,15 @@ await test("parseAsync", async (t) => {
5981
let subCommand: InteractiveCommand;
6082

6183
t.beforeEach(() => {
62-
rootCommand = new InteractiveCommand().interactive();
63-
rootCommand.exitOverride();
84+
rootCommand = new InteractiveCommand().configureOutput({
85+
// eslint-disable-next-line @typescript-eslint/no-empty-function
86+
outputError() {},
87+
});
6488

89+
rootCommand.exitOverride();
6590
subCommand = rootCommand.command("sub");
91+
92+
rootCommand.interactive();
6693
});
6794

6895
await t.test("interactive sub-command - non-interactive mode", async (t) => {
@@ -80,6 +107,8 @@ await test("parseAsync", async (t) => {
80107
const nonInteractiveCommand = new Command("non-interactive");
81108
rootCommand.addCommand(nonInteractiveCommand);
82109

110+
rootCommand.interactive();
111+
83112
assert.doesNotThrow(async () => {
84113
await rootCommand.parseAsync(["node", "test", "non-interactive", "-i"]);
85114
});

0 commit comments

Comments
 (0)