From 9905d55061784f005967e7dac06a6bbbe1f6a694 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Thu, 18 Sep 2025 16:19:14 +0330 Subject: [PATCH 1/2] add yarn handler --- bin/handlers/yarn-handler.ts | 155 ++++++++++++++++++++++++++++++++++- 1 file changed, 154 insertions(+), 1 deletion(-) diff --git a/bin/handlers/yarn-handler.ts b/bin/handlers/yarn-handler.ts index 5827efe..2b2beac 100644 --- a/bin/handlers/yarn-handler.ts +++ b/bin/handlers/yarn-handler.ts @@ -1,5 +1,158 @@ import type { PackageManagerCompletion } from '../package-manager-completion.js'; +import { stripAnsiEscapes, type ParsedOption } from '../utils/text-utils.js'; +import { + LazyCommand, + OptionHandlers, + commonOptionHandlers, + setupLazyOptionLoading, + setupCommandArguments, + safeExec, + safeExecSync, +} from '../utils/shared.js'; + +const OPTIONS_SECTION_RE = /^\s*Options:\s*$/i; +const COMMANDS_SECTION_RE = /^\s*Commands:\s*$/i; +const SECTION_END_RE = /^(Run `yarn help|Visit https:\/\/)/i; +const LINE_SPLIT_RE = /\r?\n/; +const YARN_OPTION_RE = + /^\s*(?:-([a-zA-Z]),?\s*)?--([a-z][a-z0-9-]*)(?:\s+<[^>]+>|\s+\[[^\]]+\])?\s+(.+)$/; +const YARN_COMMAND_RE = /^\s*-\s+([a-z][a-z0-9-]*(?:\s*\/\s*[a-z][a-zA-Z]*)*)/; + +function toLines(text: string): string[] { + return stripAnsiEscapes(text).split(LINE_SPLIT_RE); +} + +function findSectionStart(lines: string[], header: RegExp): number { + for (let i = 0; i < lines.length; i++) { + if (header.test(lines[i].trim())) return i + 1; + } + return -1; +} + +const yarnOptionHandlers: OptionHandlers = { + ...commonOptionHandlers, + + emoji(complete) { + complete('true', ' '); + complete('false', ' '); + }, + + production(complete) { + complete('true', ' '); + complete('false', ' '); + }, + + 'scripts-prepend-node-path'(complete) { + complete('true', ' '); + complete('false', ' '); + }, +}; + +export function parseYarnHelp(helpText: string): Record { + const lines = toLines(helpText); + const commands: Record = {}; + + const startIndex = findSectionStart(lines, COMMANDS_SECTION_RE); + if (startIndex === -1) return commands; + + for (let i = startIndex; i < lines.length; i++) { + const line = lines[i]; + + // Stop at section end + if (SECTION_END_RE.test(line)) break; + + if (!line.trim()) continue; + + const match = line.match(YARN_COMMAND_RE); + if (match) { + const [, commandWithAliases] = match; + // handle commands with aliases like "generate-lock-entry / generateLockEntry" + const commands_parts = commandWithAliases.split(/\s*\/\s*/); + const mainCommand = commands_parts[0].trim(); + + if (mainCommand) { + // yarn doesn't provide descriptions in main help, use empty string + commands[mainCommand] = ''; + + // add aliases if they exist + for (let j = 1; j < commands_parts.length; j++) { + const alias = commands_parts[j].trim(); + if (alias) { + commands[alias] = ''; + } + } + } + } + } + + return commands; +} + +export async function getYarnCommandsFromMainHelp(): Promise< + Record +> { + const output = await safeExec('cd /tmp && yarn --help'); + return output ? parseYarnHelp(output) : {}; +} + +export function parseYarnOptions( + helpText: string, + { flagsOnly = true }: { flagsOnly?: boolean } = {} +): ParsedOption[] { + const lines = toLines(helpText); + const out: ParsedOption[] = []; + + const start = findSectionStart(lines, OPTIONS_SECTION_RE); + if (start === -1) return out; + + for (let i = start; i < lines.length; i++) { + const line = lines[i]; + if (SECTION_END_RE.test(line.trim())) break; + + const m = line.match(YARN_OPTION_RE); + if (!m) continue; + + const [, short, long, desc] = m; + const takesValue = line.includes('<') || line.includes('['); + if (flagsOnly && takesValue) continue; + + out.push({ + short: short || undefined, + long, + desc: desc.trim(), + }); + } + + return out; +} + +function loadYarnOptionsSync(cmd: LazyCommand, command: string): void { + // Use cd /tmp to avoid packageManager constraints + const output = safeExecSync(`cd /tmp && yarn ${command} --help`); + if (!output) return; + + const options = parseYarnOptions(output, { flagsOnly: false }); + + for (const { long, short, desc } of options) { + const exists = cmd.optionsRaw?.get?.(long); + if (exists) continue; + + const handler = yarnOptionHandlers[long]; + if (handler) cmd.option(long, desc, handler, short); + else cmd.option(long, desc, short); + } +} export async function setupYarnCompletions( completion: PackageManagerCompletion -): Promise {} +): Promise { + try { + const commands = await getYarnCommandsFromMainHelp(); + + for (const [command, description] of Object.entries(commands)) { + const c = completion.command(command, description); + setupCommandArguments(c, command, 'yarn'); + setupLazyOptionLoading(c, command, 'yarn', loadYarnOptionsSync); + } + } catch {} +} From cb7f4be3f04954f70dcce27135b5113c8f3fa582 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Sun, 21 Sep 2025 12:27:07 +0330 Subject: [PATCH 2/2] fix cd --- bin/handlers/yarn-handler.ts | 5 ++--- package.json | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/bin/handlers/yarn-handler.ts b/bin/handlers/yarn-handler.ts index 2b2beac..b75bef4 100644 --- a/bin/handlers/yarn-handler.ts +++ b/bin/handlers/yarn-handler.ts @@ -91,7 +91,7 @@ export function parseYarnHelp(helpText: string): Record { export async function getYarnCommandsFromMainHelp(): Promise< Record > { - const output = await safeExec('cd /tmp && yarn --help'); + const output = await safeExec('yarn --help'); return output ? parseYarnHelp(output) : {}; } @@ -127,8 +127,7 @@ export function parseYarnOptions( } function loadYarnOptionsSync(cmd: LazyCommand, command: string): void { - // Use cd /tmp to avoid packageManager constraints - const output = safeExecSync(`cd /tmp && yarn ${command} --help`); + const output = safeExecSync(`yarn ${command} --help`); if (!output) return; const options = parseYarnOptions(output, { flagsOnly: false }); diff --git a/package.json b/package.json index 0ea55d3..b1c8ae1 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,5 @@ "import": "./dist/commander.js", "require": "./dist/commander.cjs" } - }, - "packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b" + } }