[READ-ONLY] Mirror of https://github.com/bombshell-dev/tab. shell autocompletions for javascript CLIs
4

Configure Feed

Select the types of activity you want to include in your feed.

feat: add yarn handler (#52)

* add yarn handler

* fix cd

authored by

AmirHossein Sakhravi and committed by
GitHub
(Sep 21, 2025, 8:39 PM +0330) da063c06 a942235f

+154 -3
+153 -1
bin/handlers/yarn-handler.ts
··· 1 1 import type { PackageManagerCompletion } from '../package-manager-completion.js'; 2 + import { stripAnsiEscapes, type ParsedOption } from '../utils/text-utils.js'; 3 + import { 4 + LazyCommand, 5 + OptionHandlers, 6 + commonOptionHandlers, 7 + setupLazyOptionLoading, 8 + setupCommandArguments, 9 + safeExec, 10 + safeExecSync, 11 + } from '../utils/shared.js'; 12 + 13 + const OPTIONS_SECTION_RE = /^\s*Options:\s*$/i; 14 + const COMMANDS_SECTION_RE = /^\s*Commands:\s*$/i; 15 + const SECTION_END_RE = /^(Run `yarn help|Visit https:\/\/)/i; 16 + const LINE_SPLIT_RE = /\r?\n/; 17 + const YARN_OPTION_RE = 18 + /^\s*(?:-([a-zA-Z]),?\s*)?--([a-z][a-z0-9-]*)(?:\s+<[^>]+>|\s+\[[^\]]+\])?\s+(.+)$/; 19 + const YARN_COMMAND_RE = /^\s*-\s+([a-z][a-z0-9-]*(?:\s*\/\s*[a-z][a-zA-Z]*)*)/; 20 + 21 + function toLines(text: string): string[] { 22 + return stripAnsiEscapes(text).split(LINE_SPLIT_RE); 23 + } 24 + 25 + function findSectionStart(lines: string[], header: RegExp): number { 26 + for (let i = 0; i < lines.length; i++) { 27 + if (header.test(lines[i].trim())) return i + 1; 28 + } 29 + return -1; 30 + } 31 + 32 + const yarnOptionHandlers: OptionHandlers = { 33 + ...commonOptionHandlers, 34 + 35 + emoji(complete) { 36 + complete('true', ' '); 37 + complete('false', ' '); 38 + }, 39 + 40 + production(complete) { 41 + complete('true', ' '); 42 + complete('false', ' '); 43 + }, 44 + 45 + 'scripts-prepend-node-path'(complete) { 46 + complete('true', ' '); 47 + complete('false', ' '); 48 + }, 49 + }; 50 + 51 + export function parseYarnHelp(helpText: string): Record<string, string> { 52 + const lines = toLines(helpText); 53 + const commands: Record<string, string> = {}; 54 + 55 + const startIndex = findSectionStart(lines, COMMANDS_SECTION_RE); 56 + if (startIndex === -1) return commands; 57 + 58 + for (let i = startIndex; i < lines.length; i++) { 59 + const line = lines[i]; 60 + 61 + // Stop at section end 62 + if (SECTION_END_RE.test(line)) break; 63 + 64 + if (!line.trim()) continue; 65 + 66 + const match = line.match(YARN_COMMAND_RE); 67 + if (match) { 68 + const [, commandWithAliases] = match; 69 + // handle commands with aliases like "generate-lock-entry / generateLockEntry" 70 + const commands_parts = commandWithAliases.split(/\s*\/\s*/); 71 + const mainCommand = commands_parts[0].trim(); 72 + 73 + if (mainCommand) { 74 + // yarn doesn't provide descriptions in main help, use empty string 75 + commands[mainCommand] = ''; 76 + 77 + // add aliases if they exist 78 + for (let j = 1; j < commands_parts.length; j++) { 79 + const alias = commands_parts[j].trim(); 80 + if (alias) { 81 + commands[alias] = ''; 82 + } 83 + } 84 + } 85 + } 86 + } 87 + 88 + return commands; 89 + } 90 + 91 + export async function getYarnCommandsFromMainHelp(): Promise< 92 + Record<string, string> 93 + > { 94 + const output = await safeExec('yarn --help'); 95 + return output ? parseYarnHelp(output) : {}; 96 + } 97 + 98 + export function parseYarnOptions( 99 + helpText: string, 100 + { flagsOnly = true }: { flagsOnly?: boolean } = {} 101 + ): ParsedOption[] { 102 + const lines = toLines(helpText); 103 + const out: ParsedOption[] = []; 104 + 105 + const start = findSectionStart(lines, OPTIONS_SECTION_RE); 106 + if (start === -1) return out; 107 + 108 + for (let i = start; i < lines.length; i++) { 109 + const line = lines[i]; 110 + if (SECTION_END_RE.test(line.trim())) break; 111 + 112 + const m = line.match(YARN_OPTION_RE); 113 + if (!m) continue; 114 + 115 + const [, short, long, desc] = m; 116 + const takesValue = line.includes('<') || line.includes('['); 117 + if (flagsOnly && takesValue) continue; 118 + 119 + out.push({ 120 + short: short || undefined, 121 + long, 122 + desc: desc.trim(), 123 + }); 124 + } 125 + 126 + return out; 127 + } 128 + 129 + function loadYarnOptionsSync(cmd: LazyCommand, command: string): void { 130 + const output = safeExecSync(`yarn ${command} --help`); 131 + if (!output) return; 132 + 133 + const options = parseYarnOptions(output, { flagsOnly: false }); 134 + 135 + for (const { long, short, desc } of options) { 136 + const exists = cmd.optionsRaw?.get?.(long); 137 + if (exists) continue; 138 + 139 + const handler = yarnOptionHandlers[long]; 140 + if (handler) cmd.option(long, desc, handler, short); 141 + else cmd.option(long, desc, short); 142 + } 143 + } 2 144 3 145 export async function setupYarnCompletions( 4 146 completion: PackageManagerCompletion 5 - ): Promise<void> {} 147 + ): Promise<void> { 148 + try { 149 + const commands = await getYarnCommandsFromMainHelp(); 150 + 151 + for (const [command, description] of Object.entries(commands)) { 152 + const c = completion.command(command, description); 153 + setupCommandArguments(c, command, 'yarn'); 154 + setupLazyOptionLoading(c, command, 'yarn', loadYarnOptionsSync); 155 + } 156 + } catch {} 157 + }
+1 -2
package.json
··· 77 77 "import": "./dist/commander.js", 78 78 "require": "./dist/commander.cjs" 79 79 } 80 - }, 81 - "packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b" 80 + } 82 81 }