54 lines
1.4 KiB
TypeScript
54 lines
1.4 KiB
TypeScript
import { memoizeWithLRU } from '../memoize.js'
|
|
import specs from './specs/index.js'
|
|
|
|
export type CommandSpec = {
|
|
name: string
|
|
description?: string
|
|
subcommands?: CommandSpec[]
|
|
args?: Argument | Argument[]
|
|
options?: Option[]
|
|
}
|
|
|
|
export type Argument = {
|
|
name?: string
|
|
description?: string
|
|
isDangerous?: boolean
|
|
isVariadic?: boolean // repeats infinitely e.g. echo hello world
|
|
isOptional?: boolean
|
|
isCommand?: boolean // wrapper commands e.g. timeout, sudo
|
|
isModule?: string | boolean // for python -m and similar module args
|
|
isScript?: boolean // script files e.g. node script.js
|
|
}
|
|
|
|
export type Option = {
|
|
name: string | string[]
|
|
description?: string
|
|
args?: Argument | Argument[]
|
|
isRequired?: boolean
|
|
}
|
|
|
|
export async function loadFigSpec(
|
|
command: string,
|
|
): Promise<CommandSpec | null> {
|
|
if (!command || command.includes('/') || command.includes('\\')) return null
|
|
if (command.includes('..')) return null
|
|
if (command.startsWith('-') && command !== '-') return null
|
|
|
|
try {
|
|
const module = await import(`@withfig/autocomplete/build/${command}.js`)
|
|
return module.default || module
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
export const getCommandSpec = memoizeWithLRU(
|
|
async (command: string): Promise<CommandSpec | null> => {
|
|
const spec =
|
|
specs.find(s => s.name === command) ||
|
|
(await loadFigSpec(command)) ||
|
|
null
|
|
return spec
|
|
},
|
|
(command: string) => command,
|
|
)
|