Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
6.2 kB · 204 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205import { buildPrefix } from '../shell/specPrefix.js'import { splitCommand_DEPRECATED } from './commands.js'import { extractCommandArguments, parseCommand } from './parser.js'import { getCommandSpec } from './registry.js'
const NUMERIC = /^\d+$/const ENV_VAR = /^[A-Za-z_][A-Za-z0-9_]*=/
// Wrapper commands with complex option handling that can't be expressed in specsconst WRAPPER_COMMANDS = new Set([ 'nice', // command position varies based on options])
const toArray = <T>(val: T | T[]): T[] => (Array.isArray(val) ? val : [val])
// Check if args[0] matches a known subcommand (disambiguates wrapper commands// that also have subcommands, e.g. the git spec has isCommand args for aliases).function isKnownSubcommand( arg: string, spec: { subcommands?: { name: string | string[] }[] } | null,): boolean { if (!spec?.subcommands?.length) return false return spec.subcommands.some(sub => Array.isArray(sub.name) ? sub.name.includes(arg) : sub.name === arg, )}
export async function getCommandPrefixStatic( command: string, recursionDepth = 0, wrapperCount = 0,): Promise<{ commandPrefix: string | null } | null> { if (wrapperCount > 2 || recursionDepth > 10) return null
const parsed = await parseCommand(command) if (!parsed) return null if (!parsed.commandNode) { return { commandPrefix: null } }
const { envVars, commandNode } = parsed const cmdArgs = extractCommandArguments(commandNode)
const [cmd, ...args] = cmdArgs if (!cmd) return { commandPrefix: null }
// Check if this is a wrapper command by looking at its spec const spec = await getCommandSpec(cmd) // Check if this is a wrapper command let isWrapper = WRAPPER_COMMANDS.has(cmd) || (spec?.args && toArray(spec.args).some(arg => arg?.isCommand))
// Special case: if the command has subcommands and the first arg matches a subcommand, // treat it as a regular command, not a wrapper if (isWrapper && args[0] && isKnownSubcommand(args[0], spec)) { isWrapper = false }
const prefix = isWrapper ? await handleWrapper(cmd, args, recursionDepth, wrapperCount) : await buildPrefix(cmd, args, spec)
if (prefix === null && recursionDepth === 0 && isWrapper) { return null }
const envPrefix = envVars.length ? `${envVars.join(' ')} ` : '' return { commandPrefix: prefix ? envPrefix + prefix : null }}
async function handleWrapper( command: string, args: string[], recursionDepth: number, wrapperCount: number,): Promise<string | null> { const spec = await getCommandSpec(command)
if (spec?.args) { const commandArgIndex = toArray(spec.args).findIndex(arg => arg?.isCommand)
if (commandArgIndex !== -1) { const parts = [command]
for (let i = 0; i < args.length && i <= commandArgIndex; i++) { if (i === commandArgIndex) { const result = await getCommandPrefixStatic( args.slice(i).join(' '), recursionDepth + 1, wrapperCount + 1, ) if (result?.commandPrefix) { parts.push(...result.commandPrefix.split(' ')) return parts.join(' ') } break } else if ( args[i] && !args[i]!.startsWith('-') && !ENV_VAR.test(args[i]!) ) { parts.push(args[i]!) } } } }
const wrapped = args.find( arg => !arg.startsWith('-') && !NUMERIC.test(arg) && !ENV_VAR.test(arg), ) if (!wrapped) return command
const result = await getCommandPrefixStatic( args.slice(args.indexOf(wrapped)).join(' '), recursionDepth + 1, wrapperCount + 1, )
return !result?.commandPrefix ? null : `${command} ${result.commandPrefix}`}
/** * Computes prefixes for a compound command (with && / || / ;). * For single commands, returns a single-element array with the prefix. * * For compound commands, computes per-subcommand prefixes and collapses * them: subcommands sharing a root (first word) are collapsed via * word-aligned longest common prefix. * * @param excludeSubcommand — optional filter; return true for subcommands * that should be excluded from the prefix suggestion (e.g. read-only * commands that are already auto-allowed). */export async function getCompoundCommandPrefixesStatic( command: string, excludeSubcommand?: (subcommand: string) => boolean,): Promise<string[]> { const subcommands = splitCommand_DEPRECATED(command) if (subcommands.length <= 1) { const result = await getCommandPrefixStatic(command) return result?.commandPrefix ? [result.commandPrefix] : [] }
const prefixes: string[] = [] for (const subcmd of subcommands) { const trimmed = subcmd.trim() if (excludeSubcommand?.(trimmed)) continue const result = await getCommandPrefixStatic(trimmed) if (result?.commandPrefix) { prefixes.push(result.commandPrefix) } }
if (prefixes.length === 0) return []
// Group prefixes by their first word (root command) const groups = new Map<string, string[]>() for (const prefix of prefixes) { const root = prefix.split(' ')[0]! const group = groups.get(root) if (group) { group.push(prefix) } else { groups.set(root, [prefix]) } }
// Collapse each group via word-aligned LCP const collapsed: string[] = [] for (const [, group] of groups) { collapsed.push(longestCommonPrefix(group)) } return collapsed}
/** * Compute the longest common prefix of strings, aligned to word boundaries. * e.g. ["git fetch", "git worktree"] → "git" * ["npm run test", "npm run lint"] → "npm run" */function longestCommonPrefix(strings: string[]): string { if (strings.length === 0) return '' if (strings.length === 1) return strings[0]!
const first = strings[0]! const words = first.split(' ') let commonWords = words.length
for (let i = 1; i < strings.length; i++) { const otherWords = strings[i]!.split(' ') let shared = 0 while ( shared < commonWords && shared < otherWords.length && words[shared] === otherWords[shared] ) { shared++ } commonWords = shared }
return words.slice(0, Math.max(1, commonWords)).join(' ')}