🌳
pt0/serverF/cliArgAI.mts
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) =>
11 getProcArgv().includes(ensureDashPrefix(name)) || (alias ? getProcArgv().includes(alias) : false)
13export const cliArg = (name: string, defaultVal?: string) => {
14 const fullName = ensureDashPrefix(name)
15 const prefix = fullName.endsWith('=') ? fullName : fullName + '='
16 const found = getProcArgv().find(a => a.startsWith(prefix))
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 + '='
26 return getProcArgv().some(a => a.startsWith(prefix))
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
43 : 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)
54 }
56 const rawAction = getProcArgv()[2]
57 const _action = rawAction && !rawAction.startsWith('-') ? rawAction : undefined
59 // Detect unknown args (both --unknown flags and stray positional args)
60 const unknownArgs = getProcArgv().slice(3).filter(a => {
61 if (a.startsWith('--')) return !schemaKeys.has(a.replace(/^--/, '').split('=')[0])
62 if (a.startsWith('-')) return !aliasToKey.has(a)
63 return true // stray arg
64 })
66 if (cliFlag('--help', '-h')) {
67 const actionName = getProcArgv()[2] || ''
68 console.log(cliHelpText(actionName, schema, actions))
69 process.exit(0)
70 }
72 if (unknownArgs.length) {
73 console.error(`${unknownArgsPrefix} ${unknownArgs.join(', ')}`)
74 const displayKeys = [...schemaKeys].filter(k => k !== 'help' && !(schema[k] as CliFlag | undefined)?.hidden)
75 console.error(`Valid flags: ${displayKeys.map(k => '--' + k).join(', ')}`)
76 process.exit(1)
77 }
79 for (const [name, spec] of Object.entries(schema)) {
80 if ('flag' in spec) {
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)
87 } else {
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(', ')}`)
93 process.exit(1)
94 }
95 }
96 }
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}]`
110 }
111 const valHint = ('hint' in spec && spec.hint) || spec.desc || (spec.type === 'int' ? 'N' : 'X')
112 return `${aliasPart}[--${name}=<${valHint}>]`
113 }).join(' ')
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}`
122 })
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