4// Tier 1: lightweight flag/arg helpers 6export const cliActionArg = () => getProcArgv().slice(3).find(a => !a.startsWith('-')) 8const ensureDashPrefix = (name: string) => name.startsWith('--') ? name : `--${name}` 10export const cliFlag = (name: string, alias?: string) => 13export const cliArg = (name: string, defaultVal?: string) => { 14 const fullName = ensureDashPrefix(name) 15 const prefix = fullName.endsWith('=') ? fullName : fullName + '=' 17 return found ? found.slice(prefix.length) : defaultVal 20export const cliArgInt = (name: string, defaultVal: number) => 21 parseInt(cliArg(name, String(defaultVal))!, 10) 23export const cliHasArg = (name: string) => { 24 const fullName = ensureDashPrefix(name) 25 const prefix = fullName.endsWith('=') ? fullName : fullName + '=' 29// Tier 2: declarative schema -> parsed args + auto help text 31interface CliFlag { flag: true; hidden?: true; desc?: string; alias?: string } 32interface CliString { type: 'string'; default?: string; enum?: readonly string[]; desc?: string; alias?: string; hint?: string } 33interface CliInt { type: 'int'; default: number; desc?: string; alias?: string } 34interface CliCsv { type: 'csv'; default?: string[]; desc?: string; alias?: string } 35type CliSpec = CliFlag | CliString | CliInt | CliCsv 36export type CliSchema = Record<string, CliSpec> 37export type CliActions = Record<string, { desc: string }> 39type ParsedCli<S extends CliSchema> = { 40 [K in keyof S]: S[K] extends CliFlag ? boolean 41 : S[K] extends CliInt ? number 42 : S[K] extends CliCsv ? string[] | undefined 44} & { _action?: string } 46export const parseCli = <S extends CliSchema>(schema: S, actions?: CliActions): ParsedCli<S> => { 47 const result: Record<string, string | number | boolean | string[] | undefined> = {} 48 const schemaKeys = new Set(Object.keys(schema)) 49 schemaKeys.add('help') 50 const aliasToKey = new Map<string, string>() 51 aliasToKey.set('h', 'help') 52 for (const [key, spec] of Object.entries(schema)) { 53 if (spec.alias) aliasToKey.set(spec.alias, key) 57 const _action = rawAction && !rawAction.startsWith('-') ? rawAction : undefined 59 // Detect unknown args (both --unknown flags and stray positional args) 61 if (a.startsWith('--')) return !schemaKeys.has(a.replace(/^--/, '').split('=')[0]) 62 if (a.startsWith('-')) return !aliasToKey.has(a) 63 return true // stray arg 66 if (cliFlag('--help', '-h')) { 68 console.log(cliHelpText(actionName, schema, actions)) 72 if (unknownArgs.length) { 74 const displayKeys = [...schemaKeys].filter(k => k !== 'help' && !(schema[k] as CliFlag | undefined)?.hidden) 75 console.error(`Valid flags: ${displayKeys.map(k => '--' + k).join(', ')}`) 79 for (const [name, spec] of Object.entries(schema)) { 81 result[name] = cliFlag(`--${name}`, spec.alias ? `-${spec.alias}` : undefined) 82 } else if (spec.type === 'csv') { 83 const raw = cliArg(`--${name}`) 84 result[name] = raw ? raw.split(',').filter(Boolean) : spec.default 85 } else if (spec.type === 'int') { 86 result[name] = cliArgInt(`--${name}`, spec.default) 88 result[name] = cliArg(`--${name}`, spec.default) 89 // Validate enum if present 90 if (spec.enum && result[name] && !spec.enum.includes(result[name])) { 91 console.error(`Invalid value for --${name}: '${result[name]}'`) 92 console.error(`Valid values: ${spec.enum.join(', ')}`) 97 result._action = _action 98 return result as ParsedCli<S> 101export const cliSchemaFlags = (schema: CliSchema) => 102 Object.entries(schema).filter(([, spec]) => !('hidden' in spec && spec.hidden)).map(([name, spec]) => { 103 const aliasPart = spec.alias ? `[-${spec.alias}] ` : '' 104 if ('flag' in spec) return `${aliasPart}[--${name}]` 105 const defaultVal = 'default' in spec ? spec.default : undefined 106 if (defaultVal != null) { 107 const enumVals = 'enum' in spec ? spec.enum : null 108 const shown = enumVals ? enumVals.join('|') : (Array.isArray(defaultVal) ? defaultVal.join(',') : defaultVal) 109 return `${aliasPart}[--${name}=${shown}]` 111 const valHint = ('hint' in spec && spec.hint) || spec.desc || (spec.type === 'int' ? 'N' : 'X') 112 return `${aliasPart}[--${name}=<${valHint}>]` 115export const cliHelpText = (actionName: string, schema: CliSchema, actions?: CliActions) => { 116 const flagLine = `${actionName} ${cliSchemaFlags(schema)}` 117 const descs = Object.entries(schema) 118 .filter(([, spec]) => !('hidden' in spec && spec.hidden) && spec.desc) 119 .map(([name, spec]) => { 120 const alias = spec.alias ? `, -${spec.alias}` : '' 121 return ` --${name}${alias} ${spec.desc}` 123 const actionDescs = actions ? Object.entries(actions).map(([name, { desc }]) => ` ${name} ${desc}`) : [] 124 const allDescs = [...descs, ...actionDescs] 125 return allDescs.length ? `${flagLine}\n${allDescs.join('\n')}` : flagLine