Skip to content

Commit 9ee2fbe

Browse files
committed
feat(@probitas/probitas): add unknown CLI argument detection with hints
Detect unknown options in `run` and `list` commands using parseArgs unknown callback. Provides contextual hints for common mistakes like --tag, --name, --filter, and suggests similar options for typos using Levenshtein distance.
1 parent eabe5c4 commit 9ee2fbe

6 files changed

Lines changed: 709 additions & 63 deletions

File tree

deno.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@
106106
"@std/testing/bdd": "jsr:@std/testing@^1.0.16/bdd",
107107
"@std/testing/mock": "jsr:@std/testing@^1.0.16/mock",
108108
"@std/testing/time": "jsr:@std/testing@^1.0.16/time",
109+
"@std/text": "jsr:@std/text@^1.0.17",
110+
"@std/text/closest-string": "jsr:@std/text@^1.0.17/closest-string",
111+
"@std/text/levenshtein-distance": "jsr:@std/text@^1.0.17/levenshtein-distance",
109112
"jsr:@probitas/probitas@^0": "./src/mod.ts"
110113
}
111114
}

deno.lock

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

src/cli/commands/list.ts

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import { fromErrorObject, isErrorObject } from "@core/errorutil/error-object";
1212
import { unreachable } from "@core/errorutil/unreachable";
1313
import { EXIT_CODE } from "../constants.ts";
1414
import { findProbitasConfigFile, loadConfig } from "../config.ts";
15+
import {
16+
createUnknownArgHandler,
17+
extractKnownOptions,
18+
formatUnknownArgError,
19+
} from "../unknown_args.ts";
1520
import { createDiscoveryProgress, writeStatus } from "../progress.ts";
1621
import {
1722
configureLogging,
@@ -29,6 +34,36 @@ import {
2934

3035
const logger = getLogger(["probitas", "cli", "list"]);
3136

37+
/**
38+
* parseArgs configuration for the list command
39+
*/
40+
const PARSE_ARGS_CONFIG = {
41+
string: ["config", "include", "exclude", "selector", "env"],
42+
boolean: [
43+
"help",
44+
"json",
45+
"no-env",
46+
"reload",
47+
"quiet",
48+
"verbose",
49+
"debug",
50+
],
51+
collect: ["include", "exclude", "selector"],
52+
alias: {
53+
h: "help",
54+
s: "selector",
55+
r: "reload",
56+
v: "verbose",
57+
q: "quiet",
58+
d: "debug",
59+
},
60+
default: {
61+
include: undefined,
62+
exclude: undefined,
63+
selector: undefined,
64+
},
65+
} as const;
66+
3267
/**
3368
* Execute the list command
3469
*
@@ -46,34 +81,33 @@ export async function listCommand(
4681
// Extract deno options first (before parseArgs)
4782
const { denoArgs, remainingArgs } = extractDenoOptions(args);
4883

84+
// Setup unknown argument handler
85+
const knownOptions = extractKnownOptions(PARSE_ARGS_CONFIG);
86+
const { handler: unknownHandler, result: unknownResult } =
87+
createUnknownArgHandler({
88+
knownOptions,
89+
commandName: "list",
90+
});
91+
4992
// Parse command-line arguments
5093
const parsed = parseArgs(remainingArgs, {
51-
string: ["config", "include", "exclude", "selector", "env"],
52-
boolean: [
53-
"help",
54-
"json",
55-
"no-env",
56-
"reload",
57-
"quiet",
58-
"verbose",
59-
"debug",
60-
],
61-
collect: ["include", "exclude", "selector"],
62-
alias: {
63-
h: "help",
64-
s: "selector",
65-
r: "reload",
66-
v: "verbose",
67-
q: "quiet",
68-
d: "debug",
69-
},
70-
default: {
71-
include: undefined,
72-
exclude: undefined,
73-
selector: undefined,
74-
},
94+
...PARSE_ARGS_CONFIG,
95+
unknown: unknownHandler,
7596
});
7697

98+
// Check for unknown arguments before showing help
99+
if (unknownResult.hasErrors) {
100+
for (const unknown of unknownResult.unknownArgs) {
101+
console.error(
102+
formatUnknownArgError(unknown, {
103+
knownOptions,
104+
commandName: "list",
105+
}),
106+
);
107+
}
108+
return EXIT_CODE.USAGE_ERROR;
109+
}
110+
77111
// Show help if requested
78112
if (parsed.help) {
79113
try {

src/cli/commands/run.ts

Lines changed: 73 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import { unreachable } from "@core/errorutil/unreachable";
1010
import { getLogger, type LogLevel } from "@logtape/logtape";
1111
import { DEFAULT_TIMEOUT, EXIT_CODE } from "../constants.ts";
1212
import { findProbitasConfigFile, loadConfig } from "../config.ts";
13+
import {
14+
createUnknownArgHandler,
15+
extractKnownOptions,
16+
formatUnknownArgError,
17+
} from "../unknown_args.ts";
1318
import { discoverScenarioFiles } from "@probitas/discover";
1419
import type { StepOptions } from "@probitas/core";
1520
import type { Reporter, RunResult } from "@probitas/runner";
@@ -36,6 +41,51 @@ import {
3641

3742
const logger = getLogger(["probitas", "cli", "run"]);
3843

44+
/**
45+
* parseArgs configuration for the run command
46+
*/
47+
const PARSE_ARGS_CONFIG = {
48+
string: [
49+
"reporter",
50+
"config",
51+
"max-concurrency",
52+
"max-failures",
53+
"include",
54+
"exclude",
55+
"selector",
56+
"timeout",
57+
"env",
58+
],
59+
boolean: [
60+
"help",
61+
"no-color",
62+
"no-timeout",
63+
"no-env",
64+
"reload",
65+
"quiet",
66+
"verbose",
67+
"debug",
68+
"sequential",
69+
"fail-fast",
70+
],
71+
collect: ["include", "exclude", "selector"],
72+
alias: {
73+
h: "help",
74+
s: "selector",
75+
S: "sequential",
76+
f: "fail-fast",
77+
v: "verbose",
78+
q: "quiet",
79+
d: "debug",
80+
r: "reload",
81+
},
82+
default: {
83+
include: undefined,
84+
exclude: undefined,
85+
selector: undefined,
86+
},
87+
} as const;
88+
3989
/**
4090
* Execute the run command
4191
*
@@ -55,49 +105,33 @@ export async function runCommand(
55105
// Extract deno options first (before parseArgs)
56106
const { denoArgs, remainingArgs } = extractDenoOptions(args);
57107

108+
// Setup unknown argument handler
109+
const knownOptions = extractKnownOptions(PARSE_ARGS_CONFIG);
110+
const { handler: unknownHandler, result: unknownResult } =
111+
createUnknownArgHandler({
112+
knownOptions,
113+
commandName: "run",
114+
});
115+
58116
// Parse command-line arguments
59117
const parsed = parseArgs(remainingArgs, {
60-
string: [
61-
"reporter",
62-
"config",
63-
"max-concurrency",
64-
"max-failures",
65-
"include",
66-
"exclude",
67-
"selector",
68-
"timeout",
69-
"env",
70-
],
71-
boolean: [
72-
"help",
73-
"no-color",
74-
"no-timeout",
75-
"no-env",
76-
"reload",
77-
"quiet",
78-
"verbose",
79-
"debug",
80-
"sequential",
81-
"fail-fast",
82-
],
83-
collect: ["include", "exclude", "selector"],
84-
alias: {
85-
h: "help",
86-
s: "selector",
87-
S: "sequential",
88-
f: "fail-fast",
89-
v: "verbose",
90-
q: "quiet",
91-
d: "debug",
92-
r: "reload",
93-
},
94-
default: {
95-
include: undefined,
96-
exclude: undefined,
97-
selector: undefined,
98-
},
118+
...PARSE_ARGS_CONFIG,
119+
unknown: unknownHandler,
99120
});
100121

122+
// Check for unknown arguments before showing help
123+
if (unknownResult.hasErrors) {
124+
for (const unknown of unknownResult.unknownArgs) {
125+
console.error(
126+
formatUnknownArgError(unknown, {
127+
knownOptions,
128+
commandName: "run",
129+
}),
130+
);
131+
}
132+
return EXIT_CODE.USAGE_ERROR;
133+
}
134+
101135
// Show help if requested
102136
if (parsed.help) {
103137
try {

0 commit comments

Comments
 (0)