Download src/utils/shell/specPrefix.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 7.91 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/utils/shell/specPrefix.ts
- Command line
-
hf download hf://chenbhao/codev/src/utils/shell/specPrefix.ts
-
curl -L -o specPrefix.ts https://huggingface.co/chenbhao/codev/resolve/main/src/utils/shell/specPrefix.ts
7.91 kB
| /** | |
| * Fig-spec-driven command prefix extraction. | |
| * | |
| * Given a command name + args array + its @withfig/autocomplete spec, walks | |
| * the spec to find how deep into the args a meaningful prefix extends. | |
| * `git -C /repo status --short` → `git status` (spec says -C takes a value, | |
| * skip it, find `status` as a known subcommand). | |
| * | |
| * Pure over (string, string[], CommandSpec) — no parser dependency. Extracted | |
| * from src/utils/bash/prefix.ts so PowerShell's extractor can reuse it; | |
| * external CLIs (git, npm, kubectl) are shell-agnostic. | |
| */ | |
| import type { CommandSpec } from '../bash/registry.js' | |
| const URL_PROTOCOLS = ['http://', 'https://', 'ftp://'] | |
| // Overrides for commands whose fig specs aren't available at runtime | |
| // (dynamic imports don't work in native/node builds). Without these, | |
| // calculateDepth falls back to 2, producing overly broad prefixes. | |
| export const DEPTH_RULES: Record<string, number> = { | |
| rg: 2, // pattern argument is required despite variadic paths | |
| 'pre-commit': 2, | |
| // CLI tools with deep subcommand trees (e.g. gcloud scheduler jobs list) | |
| gcloud: 4, | |
| 'gcloud compute': 6, | |
| 'gcloud beta': 6, | |
| aws: 4, | |
| az: 4, | |
| kubectl: 3, | |
| docker: 3, | |
| dotnet: 3, | |
| 'git push': 2, | |
| } | |
| const toArray = <T>(val: T | T[]): T[] => (Array.isArray(val) ? val : [val]) | |
| // Check if an argument matches a known subcommand (case-insensitive: PS | |
| // callers pass original-cased args; fig spec names are lowercase) | |
| function isKnownSubcommand(arg: string, spec: CommandSpec | null): boolean { | |
| if (!spec?.subcommands?.length) return false | |
| const argLower = arg.toLowerCase() | |
| return spec.subcommands.some(sub => | |
| Array.isArray(sub.name) | |
| ? sub.name.some(n => n.toLowerCase() === argLower) | |
| : sub.name.toLowerCase() === argLower, | |
| ) | |
| } | |
| // Check if a flag takes an argument based on spec, or use heuristic | |
| function flagTakesArg( | |
| flag: string, | |
| nextArg: string | undefined, | |
| spec: CommandSpec | null, | |
| ): boolean { | |
| // Check if flag is in spec.options | |
| if (spec?.options) { | |
| const option = spec.options.find(opt => | |
| Array.isArray(opt.name) ? opt.name.includes(flag) : opt.name === flag, | |
| ) | |
| if (option) return !!option.args | |
| } | |
| // Heuristic: if next arg isn't a flag and isn't a known subcommand, assume it's a flag value | |
| if (spec?.subcommands?.length && nextArg && !nextArg.startsWith('-')) { | |
| return !isKnownSubcommand(nextArg, spec) | |
| } | |
| return false | |
| } | |
| // Find the first subcommand by skipping flags and their values | |
| function findFirstSubcommand( | |
| args: string[], | |
| spec: CommandSpec | null, | |
| ): string | undefined { | |
| for (let i = 0; i < args.length; i++) { | |
| const arg = args[i] | |
| if (!arg) continue | |
| if (arg.startsWith('-')) { | |
| if (flagTakesArg(arg, args[i + 1], spec)) i++ | |
| continue | |
| } | |
| if (!spec?.subcommands?.length) return arg | |
| if (isKnownSubcommand(arg, spec)) return arg | |
| } | |
| return undefined | |
| } | |
| export async function buildPrefix( | |
| command: string, | |
| args: string[], | |
| spec: CommandSpec | null, | |
| ): Promise<string> { | |
| const maxDepth = await calculateDepth(command, args, spec) | |
| const parts = [command] | |
| const hasSubcommands = !!spec?.subcommands?.length | |
| let foundSubcommand = false | |
| for (let i = 0; i < args.length; i++) { | |
| const arg = args[i] | |
| if (!arg || parts.length >= maxDepth) break | |
| if (arg.startsWith('-')) { | |
| // Special case: python -c should stop after -c | |
| if (arg === '-c' && ['python', 'python3'].includes(command.toLowerCase())) | |
| break | |
| // Check for isCommand/isModule flags that should be included in prefix | |
| if (spec?.options) { | |
| const option = spec.options.find(opt => | |
| Array.isArray(opt.name) ? opt.name.includes(arg) : opt.name === arg, | |
| ) | |
| if ( | |
| option?.args && | |
| toArray(option.args).some(a => a?.isCommand || a?.isModule) | |
| ) { | |
| parts.push(arg) | |
| continue | |
| } | |
| } | |
| // For commands with subcommands, skip global flags to find the subcommand | |
| if (hasSubcommands && !foundSubcommand) { | |
| if (flagTakesArg(arg, args[i + 1], spec)) i++ | |
| continue | |
| } | |
| break // Stop at flags (original behavior) | |
| } | |
| if (await shouldStopAtArg(arg, args.slice(0, i), spec)) break | |
| if (hasSubcommands && !foundSubcommand) { | |
| foundSubcommand = isKnownSubcommand(arg, spec) | |
| } | |
| parts.push(arg) | |
| } | |
| return parts.join(' ') | |
| } | |
| async function calculateDepth( | |
| command: string, | |
| args: string[], | |
| spec: CommandSpec | null, | |
| ): Promise<number> { | |
| // Find first subcommand by skipping flags and their values | |
| const firstSubcommand = findFirstSubcommand(args, spec) | |
| const commandLower = command.toLowerCase() | |
| const key = firstSubcommand | |
| ? `${commandLower} ${firstSubcommand.toLowerCase()}` | |
| : commandLower | |
| if (DEPTH_RULES[key]) return DEPTH_RULES[key] | |
| if (DEPTH_RULES[commandLower]) return DEPTH_RULES[commandLower] | |
| if (!spec) return 2 | |
| if (spec.options && args.some(arg => arg?.startsWith('-'))) { | |
| for (const arg of args) { | |
| if (!arg?.startsWith('-')) continue | |
| const option = spec.options.find(opt => | |
| Array.isArray(opt.name) ? opt.name.includes(arg) : opt.name === arg, | |
| ) | |
| if ( | |
| option?.args && | |
| toArray(option.args).some(arg => arg?.isCommand || arg?.isModule) | |
| ) | |
| return 3 | |
| } | |
| } | |
| // Find subcommand spec using the already-found firstSubcommand | |
| if (firstSubcommand && spec.subcommands?.length) { | |
| const firstSubLower = firstSubcommand.toLowerCase() | |
| const subcommand = spec.subcommands.find(sub => | |
| Array.isArray(sub.name) | |
| ? sub.name.some(n => n.toLowerCase() === firstSubLower) | |
| : sub.name.toLowerCase() === firstSubLower, | |
| ) | |
| if (subcommand) { | |
| if (subcommand.args) { | |
| const subArgs = toArray(subcommand.args) | |
| if (subArgs.some(arg => arg?.isCommand)) return 3 | |
| if (subArgs.some(arg => arg?.isVariadic)) return 2 | |
| } | |
| if (subcommand.subcommands?.length) return 4 | |
| // Leaf subcommand with NO args declared (git show, git log, git tag): | |
| // the 3rd word is transient (SHA, ref, tag name) → dead over-specific | |
| // rule like PowerShell(git show 81210f8:*). NOT the isOptional case — | |
| // `git fetch` declares optional remote/branch and `git fetch origin` | |
| // is tested (bash/prefix.test.ts:912) as intentional remote scoping. | |
| if (!subcommand.args) return 2 | |
| return 3 | |
| } | |
| } | |
| if (spec.args) { | |
| const argsArray = toArray(spec.args) | |
| if (argsArray.some(arg => arg?.isCommand)) { | |
| return !Array.isArray(spec.args) && spec.args.isCommand | |
| ? 2 | |
| : Math.min(2 + argsArray.findIndex(arg => arg?.isCommand), 3) | |
| } | |
| if (!spec.subcommands?.length) { | |
| if (argsArray.some(arg => arg?.isVariadic)) return 1 | |
| if (argsArray[0] && !argsArray[0].isOptional) return 2 | |
| } | |
| } | |
| return spec.args && toArray(spec.args).some(arg => arg?.isDangerous) ? 3 : 2 | |
| } | |
| async function shouldStopAtArg( | |
| arg: string, | |
| args: string[], | |
| spec: CommandSpec | null, | |
| ): Promise<boolean> { | |
| if (arg.startsWith('-')) return true | |
| const dotIndex = arg.lastIndexOf('.') | |
| const hasExtension = | |
| dotIndex > 0 && | |
| dotIndex < arg.length - 1 && | |
| !arg.substring(dotIndex + 1).includes(':') | |
| const hasFile = arg.includes('/') || hasExtension | |
| const hasUrl = URL_PROTOCOLS.some(proto => arg.startsWith(proto)) | |
| if (!hasFile && !hasUrl) return false | |
| // Check if we're after a -m flag for python modules | |
| if (spec?.options && args.length > 0 && args[args.length - 1] === '-m') { | |
| const option = spec.options.find(opt => | |
| Array.isArray(opt.name) ? opt.name.includes('-m') : opt.name === '-m', | |
| ) | |
| if (option?.args && toArray(option.args).some(arg => arg?.isModule)) { | |
| return false // Don't stop at module names | |
| } | |
| } | |
| // For actual files/URLs, always stop regardless of context | |
| return true | |
| } | |