refactor(cli): normalize route boundaries

This commit is contained in:
Peter Steinberger
2026-04-06 15:37:34 +01:00
parent e4fa414ed0
commit ff7fe37d17
7 changed files with 95 additions and 38 deletions

View File

@@ -29,6 +29,14 @@ describe("command-path-matches", () => {
).toBe(false);
});
it("treats structured rules without exact as prefix matches", () => {
expect(
matchesCommandPathRule(["plugins", "update", "now"], {
pattern: ["plugins", "update"],
}),
).toBe(true);
});
it("matches any command path from a rule set", () => {
expect(
matchesAnyCommandPath(

View File

@@ -5,12 +5,24 @@ export type StructuredCommandPathMatchRule = {
export type CommandPathMatchRule = readonly string[] | StructuredCommandPathMatchRule;
type NormalizedCommandPathMatchRule = {
pattern: readonly string[];
exact: boolean;
};
function isStructuredCommandPathMatchRule(
rule: CommandPathMatchRule,
): rule is StructuredCommandPathMatchRule {
return !Array.isArray(rule);
}
function normalizeCommandPathMatchRule(rule: CommandPathMatchRule): NormalizedCommandPathMatchRule {
if (!isStructuredCommandPathMatchRule(rule)) {
return { pattern: rule, exact: false };
}
return { pattern: rule.pattern, exact: rule.exact ?? false };
}
export function matchesCommandPath(
commandPath: string[],
pattern: readonly string[],
@@ -23,10 +35,10 @@ export function matchesCommandPath(
}
export function matchesCommandPathRule(commandPath: string[], rule: CommandPathMatchRule): boolean {
if (!isStructuredCommandPathMatchRule(rule)) {
return matchesCommandPath(commandPath, rule);
}
return matchesCommandPath(commandPath, rule.pattern, { exact: rule.exact });
const normalizedRule = normalizeCommandPathMatchRule(rule);
return matchesCommandPath(commandPath, normalizedRule.pattern, {
exact: normalizedRule.exact,
});
}
export function matchesAnyCommandPath(

View File

@@ -4,7 +4,7 @@ import { matchesCommandPath } from "../command-path-matches.js";
import { resolveCliCommandPathPolicy } from "../command-path-policy.js";
import {
routedCommandDefinitions,
type RoutedCommandDefinition,
type AnyRoutedCommandDefinition,
} from "./routed-command-definitions.js";
export type RouteSpec = {
@@ -22,7 +22,7 @@ function createCommandLoadPlugins(commandPath: readonly string[]): (argv: string
function createParsedRoute(params: {
entry: CliCommandCatalogEntry;
definition: RoutedCommandDefinition;
definition: AnyRoutedCommandDefinition;
}): RouteSpec {
return {
match: (path) =>
@@ -51,6 +51,6 @@ export const routedCommands: RouteSpec[] = cliCommandCatalog
.map((entry) =>
createParsedRoute({
entry,
definition: routedCommandDefinitions[entry.route.id] as RoutedCommandDefinition,
definition: routedCommandDefinitions[entry.route.id],
}),
);

View File

@@ -11,16 +11,21 @@ import {
parseStatusRouteArgs,
} from "./route-args.js";
export type RoutedCommandDefinition<TArgs = unknown> = {
parseArgs: (argv: string[]) => TArgs | null;
runParsedArgs: (args: TArgs) => Promise<void>;
type RouteArgParser<TArgs> = (argv: string[]) => TArgs | null;
type ParsedRouteArgs<TParse extends RouteArgParser<unknown>> = Exclude<ReturnType<TParse>, null>;
export type RoutedCommandDefinition<TParse extends RouteArgParser<unknown>> = {
parseArgs: TParse;
runParsedArgs: (args: ParsedRouteArgs<TParse>) => Promise<void>;
};
function defineRoutedCommand<TParse extends (argv: string[]) => unknown>(definition: {
parseArgs: TParse;
runParsedArgs: (args: Exclude<ReturnType<TParse>, null>) => Promise<void>;
}): RoutedCommandDefinition<Exclude<ReturnType<TParse>, null>> {
return definition as RoutedCommandDefinition<Exclude<ReturnType<TParse>, null>>;
export type AnyRoutedCommandDefinition = RoutedCommandDefinition<RouteArgParser<unknown>>;
function defineRoutedCommand<TParse extends RouteArgParser<unknown>>(
definition: RoutedCommandDefinition<TParse>,
): RoutedCommandDefinition<TParse> {
return definition;
}
export const routedCommandDefinitions = {