feat(cli): add machine-readable CLI help, recommendation, and configuration flows
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
import { getCliConfigValue } from "./helpers.smartconfig.js";
|
||||
|
||||
export type TCliOutputMode = "human" | "plain" | "json";
|
||||
|
||||
export interface ICliMode {
|
||||
output: TCliOutputMode;
|
||||
interactive: boolean;
|
||||
json: boolean;
|
||||
plain: boolean;
|
||||
quiet: boolean;
|
||||
yes: boolean;
|
||||
help: boolean;
|
||||
agent: boolean;
|
||||
checkUpdates: boolean;
|
||||
isTty: boolean;
|
||||
command?: string;
|
||||
}
|
||||
|
||||
interface ICliConfigSettings {
|
||||
interactive?: boolean;
|
||||
output?: TCliOutputMode;
|
||||
checkUpdates?: boolean;
|
||||
}
|
||||
|
||||
type TArgSource = Record<string, any> & { _?: string[] };
|
||||
|
||||
const camelCase = (value: string): string => {
|
||||
return value.replace(/-([a-z])/g, (_match, group: string) =>
|
||||
group.toUpperCase(),
|
||||
);
|
||||
};
|
||||
|
||||
const getArgValue = (argvArg: TArgSource, key: string): any => {
|
||||
const keyVariants = [key, camelCase(key), key.replace(/-/g, "")];
|
||||
for (const keyVariant of keyVariants) {
|
||||
if (argvArg[keyVariant] !== undefined) {
|
||||
return argvArg[keyVariant];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const parseRawArgv = (argv: string[]): TArgSource => {
|
||||
const parsedArgv: TArgSource = { _: [] };
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const currentArg = argv[i];
|
||||
|
||||
if (currentArg.startsWith("--no-")) {
|
||||
const key = currentArg.slice(5);
|
||||
parsedArgv[key] = false;
|
||||
parsedArgv[camelCase(key)] = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentArg.startsWith("--")) {
|
||||
const withoutPrefix = currentArg.slice(2);
|
||||
const [rawKey, inlineValue] = withoutPrefix.split("=", 2);
|
||||
if (inlineValue !== undefined) {
|
||||
parsedArgv[rawKey] = inlineValue;
|
||||
parsedArgv[camelCase(rawKey)] = inlineValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextArg = argv[i + 1];
|
||||
if (nextArg && !nextArg.startsWith("-")) {
|
||||
parsedArgv[rawKey] = nextArg;
|
||||
parsedArgv[camelCase(rawKey)] = nextArg;
|
||||
i++;
|
||||
} else {
|
||||
parsedArgv[rawKey] = true;
|
||||
parsedArgv[camelCase(rawKey)] = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentArg.startsWith("-") && currentArg.length > 1) {
|
||||
for (const shortFlag of currentArg.slice(1).split("")) {
|
||||
parsedArgv[shortFlag] = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
parsedArgv._ = parsedArgv._ || [];
|
||||
parsedArgv._.push(currentArg);
|
||||
}
|
||||
|
||||
return parsedArgv;
|
||||
};
|
||||
|
||||
const normalizeOutputMode = (value: unknown): TCliOutputMode | undefined => {
|
||||
if (value === "human" || value === "plain" || value === "json") {
|
||||
return value;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const resolveCliMode = (
|
||||
argvArg: TArgSource,
|
||||
cliConfig: ICliConfigSettings,
|
||||
): ICliMode => {
|
||||
const isTty = Boolean(process.stdout?.isTTY && process.stdin?.isTTY);
|
||||
const agentMode = Boolean(getArgValue(argvArg, "agent"));
|
||||
const outputOverride = normalizeOutputMode(getArgValue(argvArg, "output"));
|
||||
|
||||
let output: TCliOutputMode =
|
||||
normalizeOutputMode(cliConfig.output) || (isTty ? "human" : "plain");
|
||||
if (agentMode || getArgValue(argvArg, "json")) {
|
||||
output = "json";
|
||||
} else if (getArgValue(argvArg, "plain")) {
|
||||
output = "plain";
|
||||
} else if (outputOverride) {
|
||||
output = outputOverride;
|
||||
}
|
||||
|
||||
const interactiveSetting = getArgValue(argvArg, "interactive");
|
||||
let interactive = cliConfig.interactive ?? isTty;
|
||||
if (interactiveSetting === true) {
|
||||
interactive = true;
|
||||
} else if (interactiveSetting === false) {
|
||||
interactive = false;
|
||||
}
|
||||
if (!isTty || output !== "human" || agentMode) {
|
||||
interactive = false;
|
||||
}
|
||||
|
||||
const checkUpdatesSetting = getArgValue(argvArg, "check-updates");
|
||||
let checkUpdates = cliConfig.checkUpdates ?? output === "human";
|
||||
if (checkUpdatesSetting === true) {
|
||||
checkUpdates = true;
|
||||
} else if (checkUpdatesSetting === false) {
|
||||
checkUpdates = false;
|
||||
}
|
||||
if (output !== "human" || agentMode) {
|
||||
checkUpdates = false;
|
||||
}
|
||||
|
||||
return {
|
||||
output,
|
||||
interactive,
|
||||
json: output === "json",
|
||||
plain: output === "plain",
|
||||
quiet: Boolean(
|
||||
getArgValue(argvArg, "quiet") ||
|
||||
getArgValue(argvArg, "q") ||
|
||||
output === "json",
|
||||
),
|
||||
yes: Boolean(getArgValue(argvArg, "yes") || getArgValue(argvArg, "y")),
|
||||
help: Boolean(
|
||||
getArgValue(argvArg, "help") ||
|
||||
getArgValue(argvArg, "h") ||
|
||||
argvArg._?.[0] === "help",
|
||||
),
|
||||
agent: agentMode,
|
||||
checkUpdates,
|
||||
isTty,
|
||||
command: argvArg._?.[0],
|
||||
};
|
||||
};
|
||||
|
||||
const getCliModeConfig = async (): Promise<ICliConfigSettings> => {
|
||||
return await getCliConfigValue<ICliConfigSettings>("cli", {});
|
||||
};
|
||||
|
||||
export const getCliMode = async (
|
||||
argvArg: TArgSource = {},
|
||||
): Promise<ICliMode> => {
|
||||
const cliConfig = await getCliModeConfig();
|
||||
return resolveCliMode(argvArg, cliConfig);
|
||||
};
|
||||
|
||||
export const getRawCliMode = async (): Promise<ICliMode> => {
|
||||
const cliConfig = await getCliModeConfig();
|
||||
const rawArgv = parseRawArgv(process.argv.slice(2));
|
||||
return resolveCliMode(rawArgv, cliConfig);
|
||||
};
|
||||
|
||||
export const printJson = (data: unknown): void => {
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
};
|
||||
|
||||
export const runWithSuppressedOutput = async <T>(
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> => {
|
||||
const originalConsole = {
|
||||
log: console.log,
|
||||
info: console.info,
|
||||
warn: console.warn,
|
||||
error: console.error,
|
||||
};
|
||||
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
||||
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
||||
const noop = () => undefined;
|
||||
|
||||
console.log = noop;
|
||||
console.info = noop;
|
||||
console.warn = noop;
|
||||
console.error = noop;
|
||||
process.stdout.write = (() => true) as typeof process.stdout.write;
|
||||
process.stderr.write = (() => true) as typeof process.stderr.write;
|
||||
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
console.log = originalConsole.log;
|
||||
console.info = originalConsole.info;
|
||||
console.warn = originalConsole.warn;
|
||||
console.error = originalConsole.error;
|
||||
process.stdout.write = originalStdoutWrite;
|
||||
process.stderr.write = originalStderrWrite;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user