From 4239c732fbd2c6573005764d37bf223b58b8d680 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Thu, 11 Sep 2025 20:16:36 +0330 Subject: [PATCH 01/10] add npm handler --- bin/handlers/npm-handler.ts | 296 ++++++++++++++++++++++++++++++++++- bin/handlers/pnpm-handler.ts | 12 +- 2 files changed, 301 insertions(+), 7 deletions(-) diff --git a/bin/handlers/npm-handler.ts b/bin/handlers/npm-handler.ts index e4528e9..7202fc0 100644 --- a/bin/handlers/npm-handler.ts +++ b/bin/handlers/npm-handler.ts @@ -1,5 +1,299 @@ +import { promisify } from 'node:util'; +import child_process from 'node:child_process'; + +const exec = promisify(child_process.exec); +const { execSync } = child_process; + import type { PackageManagerCompletion } from '../package-manager-completion.js'; +import { Command, Option } from '../../src/t.js'; +import { + packageJsonScriptCompletion, + packageJsonDependencyCompletion, +} from '../completions/completion-producers.js'; +import { getWorkspacePatterns } from '../utils/filesystem-utils.js'; +import { stripAnsiEscapes, type ParsedOption } from '../utils/text-utils.js'; + +interface LazyCommand extends Command { + _lazyCommand?: string; + _optionsLoaded?: boolean; + optionsRaw?: Map; +} + +// regex patterns to avoid recompilation in loops +const ALL_COMMANDS_RE = /^All commands:\s*$/i; +const OPTIONS_SECTION_RE = /^Options:\s*$/i; +const SECTION_END_RE = /^(aliases|run|more)/i; +const COMMAND_VALIDATION_RE = /^[a-z][a-z0-9-]*$/; +const NPM_OPTION_RE = + /(?:\[)?(?:-([a-z])\|)?--([a-z][a-z0-9-]+)(?:\s+<[^>]+>)?(?:\])?/gi; +const OPTION_VALUE_RE = /<[^>]+>/; +const NON_INDENTED_LINE_RE = /^\s/; + +// completion handlers for npm options that take values +const npmOptionHandlers = { + loglevel: function (complete: (value: string, description: string) => void) { + // npm log levels from documentation + [ + 'silent', + 'error', + 'warn', + 'notice', + 'http', + 'info', + 'verbose', + 'silly', + ].forEach((level) => complete(level, `Log level: ${level}`)); + }, + + registry: function (complete: (value: string, description: string) => void) { + complete('https://registry.npmjs.org/', 'Official npm registry'); + complete('https://registry.npmmirror.com/', 'npm China mirror'); + }, + + 'install-strategy': function ( + complete: (value: string, description: string) => void + ) { + // From npm help: hoisted|nested|shallow|linked + complete('hoisted', 'Hoist all dependencies to top level'); + complete('nested', 'Create nested node_modules structure'); + complete('shallow', 'Shallow dependency installation'); + complete('linked', 'Use linked dependencies'); + }, + + workspace: function (complete: (value: string, description: string) => void) { + // Get workspace patterns from package.json workspaces or pnpm-workspace.yaml + const workspacePatterns = getWorkspacePatterns(); + workspacePatterns.forEach((pattern) => { + complete(pattern, `Workspace pattern: ${pattern}`); + }); + + // Common workspace patterns + complete('packages/*', 'All packages in packages directory'); + complete('apps/*', 'All apps in apps directory'); + }, + + omit: function (complete: (value: string, description: string) => void) { + // From npm help: dev|optional|peer + complete('dev', 'Omit devDependencies'); + complete('optional', 'Omit optionalDependencies'); + complete('peer', 'Omit peerDependencies'); + }, + + include: function (complete: (value: string, description: string) => void) { + // From npm help: prod|dev|optional|peer + complete('prod', 'Include production dependencies'); + complete('dev', 'Include devDependencies'); + complete('optional', 'Include optionalDependencies'); + complete('peer', 'Include peerDependencies'); + }, +}; + +// parse npm help text to extract commands and their descriptions +export function parseNpmHelp(helpText: string): Record { + const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/); + + // find "All commands:" section + let startIndex = -1; + for (let i = 0; i < helpLines.length; i++) { + if (ALL_COMMANDS_RE.test(helpLines[i].trim())) { + startIndex = i + 1; + break; + } + } + + if (startIndex === -1) return {}; + + const commands: Record = {}; + let commandsText = ''; + + // collect all lines that are part of the commands section + for (let i = startIndex; i < helpLines.length; i++) { + const line = helpLines[i]; + + // stop if we hit a non-indented line that starts a new section + if (!NON_INDENTED_LINE_RE.test(line) && line.trim() && !line.includes(',')) + break; + + // add this line to our commands text + if (NON_INDENTED_LINE_RE.test(line)) { + commandsText += ' ' + line.trim(); + } + } + + // parse the comma-separated command list + const commandList = commandsText + .split(',') + .map((cmd) => cmd.trim()) + .filter((cmd) => cmd && COMMAND_VALIDATION_RE.test(cmd)); + + // npm does not ptrovide descriptions in the main help. + commandList.forEach((cmd) => { + commands[cmd] = ' '; + }); + + // this is the most common used aliase that isn't in the main list + commands['run'] = ' '; + + return commands; +} + +// Get npm commands from the main help output +export async function getNpmCommandsFromMainHelp(): Promise< + Record +> { + try { + const { stdout } = await exec('npm --help', { + encoding: 'utf8', + timeout: 500, + maxBuffer: 4 * 1024 * 1024, + }); + return parseNpmHelp(stdout); + } catch (error: any) { + // npm --help exits with status 1 but still provides output + if (error.stdout) { + return parseNpmHelp(error.stdout); + } + return {}; + } +} + +// Parse npm options from help text (npm has a different format than pnpm) +export function parseNpmOptions( + helpText: string, + { flagsOnly = true }: { flagsOnly?: boolean } = {} +): ParsedOption[] { + const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/); + const optionsOut: ParsedOption[] = []; + + // Find the Options: section + let optionsStartIndex = -1; + for (let i = 0; i < helpLines.length; i++) { + if (OPTIONS_SECTION_RE.test(helpLines[i].trim())) { + optionsStartIndex = i + 1; + break; + } + } + + if (optionsStartIndex === -1) return []; + + // Parse the compact npm option format: [-S|--save|--no-save] etc. + for (let i = optionsStartIndex; i < helpLines.length; i++) { + const line = helpLines[i]; + + // Stop at aliases or other sections + if (SECTION_END_RE.test(line.trim())) break; + + // Parse option patterns like [-S|--save] or [--loglevel ] + const optionMatches = line.matchAll(NPM_OPTION_RE); + + for (const match of optionMatches) { + const short = match[1] || undefined; + const long = match[2]; + + // Check if this option takes a value + const takesValue = OPTION_VALUE_RE.test(match[0]); + + if (flagsOnly && takesValue) continue; + + optionsOut.push({ + short, + long, + desc: `npm ${long} option`, + }); + } + } + + return optionsOut; +} + +// Load dynamic options synchronously when requested +export function loadDynamicOptionsSync( + cmd: LazyCommand, + command: string +): void { + try { + const stdout = execSync(`npm ${command} --help`, { + encoding: 'utf8', + timeout: 500, + }); + + const allOptions = parseNpmOptions(stdout, { flagsOnly: false }); + + for (const { long, short, desc } of allOptions) { + const alreadyDefined = cmd.optionsRaw?.get?.(long); + if (!alreadyDefined) { + const handler = + npmOptionHandlers[long as keyof typeof npmOptionHandlers]; + if (handler) { + cmd.option(long, desc, handler, short); + } else { + cmd.option(long, desc, short); + } + } + } + } catch (error: unknown) { + // npm help commands may exit with status 1 but still provide output + if (error instanceof Error && 'stdout' in error) { + try { + const allOptions = parseNpmOptions(error.stdout as string, { + flagsOnly: false, + }); + for (const { long, short, desc } of allOptions) { + const alreadyDefined = cmd.optionsRaw?.get?.(long); + if (!alreadyDefined) { + const handler = + npmOptionHandlers[long as keyof typeof npmOptionHandlers]; + if (handler) { + cmd.option(long, desc, handler, short); + } else { + cmd.option(long, desc, short); + } + } + } + } catch {} + } + } +} + +// Setup lazy option loading for a command +function setupLazyOptionLoading(cmd: LazyCommand, command: string): void { + cmd._lazyCommand = command; + cmd._optionsLoaded = false; + + const optionsStore = cmd.options; + cmd.optionsRaw = optionsStore; + + Object.defineProperty(cmd, 'options', { + get() { + if (!this._optionsLoaded) { + this._optionsLoaded = true; + loadDynamicOptionsSync(this, this._lazyCommand); + } + return optionsStore; + }, + configurable: true, + }); +} export async function setupNpmCompletions( completion: PackageManagerCompletion -): Promise {} +): Promise { + try { + const commandsWithDescriptions = await getNpmCommandsFromMainHelp(); + + for (const [command, description] of Object.entries( + commandsWithDescriptions + )) { + const cmd = completion.command(command, description); + + if (['remove', 'rm', 'uninstall', 'un'].includes(command)) { + cmd.argument('package', packageJsonDependencyCompletion); + } + if (['run', 'run-script'].includes(command)) { + cmd.argument('script', packageJsonScriptCompletion, true); + } + + setupLazyOptionLoading(cmd, command); + } + } catch (_err) {} +} diff --git a/bin/handlers/pnpm-handler.ts b/bin/handlers/pnpm-handler.ts index 770d173..ddbbc0e 100644 --- a/bin/handlers/pnpm-handler.ts +++ b/bin/handlers/pnpm-handler.ts @@ -27,8 +27,10 @@ import { type ParsedOption, } from '../utils/text-utils.js'; -// regex to detect options section in help text +// regex patterns to avoid recompilation in loops const OPTIONS_SECTION_RE = /^\s*Options:/i; +const LEVEL_MATCH_RE = /(?:levels?|options?|values?)[^:]*:\s*([^.]+)/i; +const REPORTER_MATCH_RE = /--reporter\s+(\w+)/g; function extractValidValuesFromHelp( helpText: string, @@ -42,9 +44,7 @@ function extractValidValuesFromHelp( for (let j = i; j < Math.min(i + 3, lines.length); j++) { const searchLine = lines[j]; - const levelMatch = searchLine.match( - /(?:levels?|options?|values?)[^:]*:\s*([^.]+)/i - ); + const levelMatch = searchLine.match(LEVEL_MATCH_RE); if (levelMatch) { return levelMatch[1] .split(/[,\s]+/) @@ -53,11 +53,11 @@ function extractValidValuesFromHelp( } if (optionName === 'reporter') { - const reporterMatch = searchLine.match(/--reporter\s+(\w+)/); + const reporterMatch = searchLine.match(REPORTER_MATCH_RE); if (reporterMatch) { const reporterValues = new Set(); for (const helpLine of lines) { - const matches = helpLine.matchAll(/--reporter\s+(\w+)/g); + const matches = helpLine.matchAll(REPORTER_MATCH_RE); for (const match of matches) { reporterValues.add(match[1]); } From ee4a0eb810724af0a52bc09fed9c18401e9dc554 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Thu, 11 Sep 2025 23:07:29 +0330 Subject: [PATCH 02/10] big update --- bin/handlers/bun-handler.ts | 238 +++++++++++++++++++++++++++++- bin/handlers/npm-handler.ts | 190 ++++++------------------ bin/handlers/pnpm-handler.ts | 219 +++++++++++++-------------- bin/utils/package-manager-base.ts | 122 +++++++++++++++ 4 files changed, 507 insertions(+), 262 deletions(-) create mode 100644 bin/utils/package-manager-base.ts diff --git a/bin/handlers/bun-handler.ts b/bin/handlers/bun-handler.ts index 225b049..baf1ac3 100644 --- a/bin/handlers/bun-handler.ts +++ b/bin/handlers/bun-handler.ts @@ -1,5 +1,241 @@ 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, + // createLogLevelHandler, +} from '../utils/package-manager-base.js'; + +// regex patterns to avoid recompilation in loops +const COMMANDS_SECTION_RE = /^Commands:\s*$/i; +const FLAGS_SECTION_RE = /^Flags:\s*$/i; +const SECTION_END_RE = /^(Examples|Full documentation|Learn more)/i; +const COMMAND_VALIDATION_RE = /^[a-z][a-z0-9-]*$/; +const BUN_OPTION_RE = + /^\s*(?:-([a-zA-Z]),?\s*)?--([a-z][a-z0-9-]*)(?:=<[^>]+>)?\s+(.+)$/; +// const NON_INDENTED_LINE_RE = /^\s/; + +// bun-specific completion handlers +const bunOptionHandlers: OptionHandlers = { + // Use common handlers + ...commonOptionHandlers, + + // bun doesn't have traditional log levels, but has verbose/silent + silent: function (complete) { + complete('true', 'Enable silent mode'); + complete('false', 'Disable silent mode'); + }, + + backend: function (complete) { + // From bun help: "clonefile" (default), "hardlink", "symlink", "copyfile" + complete('clonefile', 'Clone files (default, fastest)'); + complete('hardlink', 'Use hard links'); + complete('symlink', 'Use symbolic links'); + complete('copyfile', 'Copy files'); + }, + + linker: function (complete) { + // From bun help: "isolated" or "hoisted" + complete('isolated', 'Isolated linker strategy'); + complete('hoisted', 'Hoisted linker strategy'); + }, + + omit: function (complete) { + // From bun help: 'dev', 'optional', or 'peer' + complete('dev', 'Omit devDependencies'); + complete('optional', 'Omit optionalDependencies'); + complete('peer', 'Omit peerDependencies'); + }, + + shell: function (complete) { + // From bun help: 'bun' or 'system' + complete('bun', 'Use Bun shell'); + complete('system', 'Use system shell'); + }, + + 'unhandled-rejections': function (complete) { + // From bun help: "strict", "throw", "warn", "none", or "warn-with-error-code" + complete('strict', 'Strict unhandled rejection handling'); + complete('throw', 'Throw on unhandled rejections'); + complete('warn', 'Warn on unhandled rejections'); + complete('none', 'Ignore unhandled rejections'); + complete('warn-with-error-code', 'Warn with error code'); + }, +}; + +// Parse bun help text to extract commands and their descriptions +export function parseBunHelp(helpText: string): Record { + const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/); + + // Find "Commands:" section + let startIndex = -1; + for (let i = 0; i < helpLines.length; i++) { + if (COMMANDS_SECTION_RE.test(helpLines[i].trim())) { + startIndex = i + 1; + break; + } + } + + if (startIndex === -1) return {}; + + const commands: Record = {}; + + // Parse bun's unique command format + for (let i = startIndex; i < helpLines.length; i++) { + const line = helpLines[i]; + + // Stop when we hit Flags section or empty line followed by non-command content + if ( + FLAGS_SECTION_RE.test(line.trim()) || + (line.trim() === '' && + i + 1 < helpLines.length && + !helpLines[i + 1].match(/^\s+[a-z]/)) + ) + break; + + // Skip empty lines + if (line.trim() === '') continue; + + // Handle different bun command formats: + // Format 1: " run ./my-script.ts Execute a file with Bun" + // Format 2: " lint Run a package.json script" (continuation) + // Format 3: " install Install dependencies for a package.json (bun i)" + + // Try to match command at start of line (2 spaces) + const mainCommandMatch = line.match(/^ ([a-z][a-z0-9-]*)\s+(.+)$/); + if (mainCommandMatch) { + const [, command, rest] = mainCommandMatch; + if (COMMAND_VALIDATION_RE.test(command)) { + // Extract description - find the last part that looks like a description + // Split by multiple spaces and take the last part that contains letters + const parts = rest.split(/\s{2,}/); + let description = parts[parts.length - 1]; + + // If the last part starts with a capital letter, it's likely the description + if (description && /^[A-Z]/.test(description)) { + commands[command] = description.trim(); + } else if (parts.length > 1) { + // Otherwise, look for the first part that starts with a capital + for (const part of parts) { + if (/^[A-Z]/.test(part)) { + commands[command] = part.trim(); + break; + } + } + } + } + } + + // Handle continuation lines (12+ spaces) + const continuationMatch = line.match(/^\s{12,}([a-z][a-z0-9-]*)\s+(.+)$/); + if (continuationMatch) { + const [, command, description] = continuationMatch; + if (COMMAND_VALIDATION_RE.test(command)) { + commands[command] = description.trim(); + } + } + } + + return commands; +} + +// Get bun commands from the main help output +export async function getBunCommandsFromMainHelp(): Promise< + Record +> { + const output = await safeExec('bun --help'); + return output ? parseBunHelp(output) : {}; +} + +// Parse bun options from help text +export function parseBunOptions( + helpText: string, + { flagsOnly = true }: { flagsOnly?: boolean } = {} +): ParsedOption[] { + const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/); + const optionsOut: ParsedOption[] = []; + + // Find the Flags: section + let optionsStartIndex = -1; + for (let i = 0; i < helpLines.length; i++) { + if (FLAGS_SECTION_RE.test(helpLines[i].trim())) { + optionsStartIndex = i + 1; + break; + } + } + + if (optionsStartIndex === -1) return []; + + // Parse bun's flag format + for (let i = optionsStartIndex; i < helpLines.length; i++) { + const line = helpLines[i]; + + // Stop at examples or other sections + if (SECTION_END_RE.test(line.trim())) break; + + // Parse option lines like: " -c, --config= Specify path to config file" + const optionMatch = line.match(BUN_OPTION_RE); + + if (optionMatch) { + const [, short, long, desc] = optionMatch; + + // Check if this option takes a value (has =) + const takesValue = line.includes('=<'); + + if (flagsOnly && takesValue) continue; + + optionsOut.push({ + short: short || undefined, + long, + desc: desc.trim(), + }); + } + } + + return optionsOut; +} + +// Load dynamic options synchronously when requested +function loadBunOptionsSync(cmd: LazyCommand, command: string): void { + const output = safeExecSync(`bun ${command} --help`); + if (!output) return; + + const allOptions = parseBunOptions(output, { flagsOnly: false }); + + for (const { long, short, desc } of allOptions) { + const alreadyDefined = cmd.optionsRaw?.get?.(long); + if (!alreadyDefined) { + const handler = bunOptionHandlers[long]; + if (handler) { + cmd.option(long, desc, handler, short); + } else { + cmd.option(long, desc, short); + } + } + } +} export async function setupBunCompletions( completion: PackageManagerCompletion -): Promise {} +): Promise { + try { + const commandsWithDescriptions = await getBunCommandsFromMainHelp(); + + for (const [command, description] of Object.entries( + commandsWithDescriptions + )) { + const cmd = completion.command(command, description); + + // Setup common argument patterns + setupCommandArguments(cmd, command, 'bun'); + + // Setup lazy option loading + setupLazyOptionLoading(cmd, command, 'bun', loadBunOptionsSync); + } + } catch (_err) {} +} diff --git a/bin/handlers/npm-handler.ts b/bin/handlers/npm-handler.ts index 7202fc0..9859f39 100644 --- a/bin/handlers/npm-handler.ts +++ b/bin/handlers/npm-handler.ts @@ -1,23 +1,15 @@ -import { promisify } from 'node:util'; -import child_process from 'node:child_process'; - -const exec = promisify(child_process.exec); -const { execSync } = child_process; - import type { PackageManagerCompletion } from '../package-manager-completion.js'; -import { Command, Option } from '../../src/t.js'; -import { - packageJsonScriptCompletion, - packageJsonDependencyCompletion, -} from '../completions/completion-producers.js'; -import { getWorkspacePatterns } from '../utils/filesystem-utils.js'; import { stripAnsiEscapes, type ParsedOption } from '../utils/text-utils.js'; - -interface LazyCommand extends Command { - _lazyCommand?: string; - _optionsLoaded?: boolean; - optionsRaw?: Map; -} +import { + LazyCommand, + OptionHandlers, + commonOptionHandlers, + setupLazyOptionLoading, + setupCommandArguments, + safeExec, + safeExecSync, + createLogLevelHandler, +} from '../utils/package-manager-base.js'; // regex patterns to avoid recompilation in loops const ALL_COMMANDS_RE = /^All commands:\s*$/i; @@ -29,30 +21,22 @@ const NPM_OPTION_RE = const OPTION_VALUE_RE = /<[^>]+>/; const NON_INDENTED_LINE_RE = /^\s/; -// completion handlers for npm options that take values -const npmOptionHandlers = { - loglevel: function (complete: (value: string, description: string) => void) { - // npm log levels from documentation - [ - 'silent', - 'error', - 'warn', - 'notice', - 'http', - 'info', - 'verbose', - 'silly', - ].forEach((level) => complete(level, `Log level: ${level}`)); - }, - - registry: function (complete: (value: string, description: string) => void) { - complete('https://registry.npmjs.org/', 'Official npm registry'); - complete('https://registry.npmmirror.com/', 'npm China mirror'); - }, - - 'install-strategy': function ( - complete: (value: string, description: string) => void - ) { +const npmOptionHandlers: OptionHandlers = { + ...commonOptionHandlers, + + // npm log levels + loglevel: createLogLevelHandler([ + 'silent', + 'error', + 'warn', + 'notice', + 'http', + 'info', + 'verbose', + 'silly', + ]), + + 'install-strategy': function (complete) { // From npm help: hoisted|nested|shallow|linked complete('hoisted', 'Hoist all dependencies to top level'); complete('nested', 'Create nested node_modules structure'); @@ -60,26 +44,14 @@ const npmOptionHandlers = { complete('linked', 'Use linked dependencies'); }, - workspace: function (complete: (value: string, description: string) => void) { - // Get workspace patterns from package.json workspaces or pnpm-workspace.yaml - const workspacePatterns = getWorkspacePatterns(); - workspacePatterns.forEach((pattern) => { - complete(pattern, `Workspace pattern: ${pattern}`); - }); - - // Common workspace patterns - complete('packages/*', 'All packages in packages directory'); - complete('apps/*', 'All apps in apps directory'); - }, - - omit: function (complete: (value: string, description: string) => void) { + omit: function (complete) { // From npm help: dev|optional|peer complete('dev', 'Omit devDependencies'); complete('optional', 'Omit optionalDependencies'); complete('peer', 'Omit peerDependencies'); }, - include: function (complete: (value: string, description: string) => void) { + include: function (complete) { // From npm help: prod|dev|optional|peer complete('prod', 'Include production dependencies'); complete('dev', 'Include devDependencies'); @@ -141,20 +113,8 @@ export function parseNpmHelp(helpText: string): Record { export async function getNpmCommandsFromMainHelp(): Promise< Record > { - try { - const { stdout } = await exec('npm --help', { - encoding: 'utf8', - timeout: 500, - maxBuffer: 4 * 1024 * 1024, - }); - return parseNpmHelp(stdout); - } catch (error: any) { - // npm --help exits with status 1 but still provides output - if (error.stdout) { - return parseNpmHelp(error.stdout); - } - return {}; - } + const output = await safeExec('npm --help'); + return output ? parseNpmHelp(output) : {}; } // Parse npm options from help text (npm has a different format than pnpm) @@ -198,7 +158,7 @@ export function parseNpmOptions( optionsOut.push({ short, long, - desc: `npm ${long} option`, + desc: ' ', }); } } @@ -207,74 +167,25 @@ export function parseNpmOptions( } // Load dynamic options synchronously when requested -export function loadDynamicOptionsSync( - cmd: LazyCommand, - command: string -): void { - try { - const stdout = execSync(`npm ${command} --help`, { - encoding: 'utf8', - timeout: 500, - }); - - const allOptions = parseNpmOptions(stdout, { flagsOnly: false }); - - for (const { long, short, desc } of allOptions) { - const alreadyDefined = cmd.optionsRaw?.get?.(long); - if (!alreadyDefined) { - const handler = - npmOptionHandlers[long as keyof typeof npmOptionHandlers]; - if (handler) { - cmd.option(long, desc, handler, short); - } else { - cmd.option(long, desc, short); - } +function loadNpmOptionsSync(cmd: LazyCommand, command: string): void { + const output = safeExecSync(`npm ${command} --help`); + if (!output) return; + + const allOptions = parseNpmOptions(output, { flagsOnly: false }); + + for (const { long, short, desc } of allOptions) { + const alreadyDefined = cmd.optionsRaw?.get?.(long); + if (!alreadyDefined) { + const handler = npmOptionHandlers[long]; + if (handler) { + cmd.option(long, desc, handler, short); + } else { + cmd.option(long, desc, short); } } - } catch (error: unknown) { - // npm help commands may exit with status 1 but still provide output - if (error instanceof Error && 'stdout' in error) { - try { - const allOptions = parseNpmOptions(error.stdout as string, { - flagsOnly: false, - }); - for (const { long, short, desc } of allOptions) { - const alreadyDefined = cmd.optionsRaw?.get?.(long); - if (!alreadyDefined) { - const handler = - npmOptionHandlers[long as keyof typeof npmOptionHandlers]; - if (handler) { - cmd.option(long, desc, handler, short); - } else { - cmd.option(long, desc, short); - } - } - } - } catch {} - } } } -// Setup lazy option loading for a command -function setupLazyOptionLoading(cmd: LazyCommand, command: string): void { - cmd._lazyCommand = command; - cmd._optionsLoaded = false; - - const optionsStore = cmd.options; - cmd.optionsRaw = optionsStore; - - Object.defineProperty(cmd, 'options', { - get() { - if (!this._optionsLoaded) { - this._optionsLoaded = true; - loadDynamicOptionsSync(this, this._lazyCommand); - } - return optionsStore; - }, - configurable: true, - }); -} - export async function setupNpmCompletions( completion: PackageManagerCompletion ): Promise { @@ -286,14 +197,11 @@ export async function setupNpmCompletions( )) { const cmd = completion.command(command, description); - if (['remove', 'rm', 'uninstall', 'un'].includes(command)) { - cmd.argument('package', packageJsonDependencyCompletion); - } - if (['run', 'run-script'].includes(command)) { - cmd.argument('script', packageJsonScriptCompletion, true); - } + // Setup common argument patterns + setupCommandArguments(cmd, command, 'npm'); - setupLazyOptionLoading(cmd, command); + // Setup lazy option loading + setupLazyOptionLoading(cmd, command, 'npm', loadNpmOptionsSync); } } catch (_err) {} } diff --git a/bin/handlers/pnpm-handler.ts b/bin/handlers/pnpm-handler.ts index ddbbc0e..26e02ef 100644 --- a/bin/handlers/pnpm-handler.ts +++ b/bin/handlers/pnpm-handler.ts @@ -1,22 +1,15 @@ -import { promisify } from 'node:util'; -import child_process from 'node:child_process'; - -const exec = promisify(child_process.exec); -const { execSync } = child_process; import type { PackageManagerCompletion } from '../package-manager-completion.js'; -import { Command, Option } from '../../src/t.js'; - -interface LazyCommand extends Command { - _lazyCommand?: string; - _optionsLoaded?: boolean; - optionsRaw?: Map; -} - -import { - packageJsonScriptCompletion, - packageJsonDependencyCompletion, -} from '../completions/completion-producers.js'; import { getWorkspacePatterns } from '../utils/filesystem-utils.js'; +import { + LazyCommand, + OptionHandlers, + commonOptionHandlers, + setupLazyOptionLoading, + setupCommandArguments, + safeExec, + safeExecSync, + createLogLevelHandler, +} from '../utils/package-manager-base.js'; import { stripAnsiEscapes, measureIndent, @@ -30,14 +23,45 @@ import { // regex patterns to avoid recompilation in loops const OPTIONS_SECTION_RE = /^\s*Options:/i; const LEVEL_MATCH_RE = /(?:levels?|options?|values?)[^:]*:\s*([^.]+)/i; -const REPORTER_MATCH_RE = /--reporter\s+(\w+)/g; function extractValidValuesFromHelp( helpText: string, optionName: string -): string[] { +): Array<{ value: string; desc: string }> { const lines = stripAnsiEscapes(helpText).split(/\r?\n/); + // Handle reporter option specially + if (optionName === 'reporter') { + const reporterValues: Array<{ value: string; desc: string }> = []; + + // Simple approach: look for lines with "--reporter " + for (const helpLine of lines) { + if ( + helpLine.includes('--reporter') && + helpLine.match(/^\s*--reporter\s+\w/) + ) { + const match = helpLine.match(/^\s*--reporter\s+(\w+|\w+-\w+)\s+(.+)$/); + if (match) { + const [, value, desc] = match; + reporterValues.push({ value, desc: desc.trim() }); + } + } + + // Handle special case: "-s, --silent, --reporter silent" + const silentMatch = helpLine.match( + /-s,\s*--silent,\s*--reporter\s+silent\s+(.+)/ + ); + if (silentMatch && !reporterValues.some((r) => r.value === 'silent')) { + reporterValues.push({ value: 'silent', desc: silentMatch[1].trim() }); + } + } + + if (reporterValues.length > 0) { + return reporterValues; + } + } + + // Handle other options for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.includes(`--${optionName}`) || line.includes(`${optionName}:`)) { @@ -49,21 +73,8 @@ function extractValidValuesFromHelp( return levelMatch[1] .split(/[,\s]+/) .map((v) => v.trim()) - .filter((v) => v && !v.includes('(') && !v.includes(')')); - } - - if (optionName === 'reporter') { - const reporterMatch = searchLine.match(REPORTER_MATCH_RE); - if (reporterMatch) { - const reporterValues = new Set(); - for (const helpLine of lines) { - const matches = helpLine.matchAll(REPORTER_MATCH_RE); - for (const match of matches) { - reporterValues.add(match[1]); - } - } - return Array.from(reporterValues); - } + .filter((v) => v && !v.includes('(') && !v.includes(')')) + .map((value) => ({ value, desc: `Log level: ${value}` })); } } } @@ -72,44 +83,44 @@ function extractValidValuesFromHelp( return []; } -// completion handlers for pnpm options that take values -const pnpmOptionHandlers = { - // Let shell handle directory completions - it's much better at it - // dir, modules-dir, store-dir, lockfile-dir, virtual-store-dir removed +// pnpm-specific completion handlers +const pnpmOptionHandlers: OptionHandlers = { + // Use common handlers + ...commonOptionHandlers, - loglevel: function (complete: (value: string, description: string) => void) { - // Try to get values from help, fall back to known values + // pnpm log levels with dynamic extraction fallback + loglevel: function (complete) { const helpValues = extractValidValuesFromHelp( - execSync('pnpm install --help', { encoding: 'utf8', timeout: 500 }), + safeExecSync('pnpm install --help'), 'loglevel' ); if (helpValues.length > 0) { - helpValues.forEach((value) => complete(value, `Log level: ${value}`)); + helpValues.forEach(({ value, desc }) => complete(value, desc)); } else { - // Fallback based on documented values - ['debug', 'info', 'warn', 'error', 'silent'].forEach((level) => - complete(level, `Log level: ${level}`) + // Fallback to known pnpm levels + createLogLevelHandler(['debug', 'info', 'warn', 'error', 'silent'])( + complete ); } }, - reporter: function (complete: (value: string, description: string) => void) { - // valid values from pnpm help - const reporters = [ - { value: 'default', desc: 'Default reporter when stdout is TTY' }, - { - value: 'append-only', - desc: 'Output always appended, no cursor manipulation', - }, - { value: 'ndjson', desc: 'Most verbose reporter in NDJSON format' }, - { value: 'silent', desc: 'No output logged to console' }, - ]; - - reporters.forEach(({ value, desc }) => complete(value, desc)); + reporter: function (complete) { + // Extract reporter values dynamically from install help + const helpOutput = safeExecSync('pnpm install --help'); + const reporterValues = extractValidValuesFromHelp(helpOutput, 'reporter'); + + if (reporterValues.length > 0) { + reporterValues.forEach(({ value, desc }) => complete(value, desc)); + } else { + // Fallback to common reporter types if extraction fails + createLogLevelHandler(['default', 'append-only', 'ndjson', 'silent'])( + complete + ); + } }, - filter: function (complete: (value: string, description: string) => void) { + filter: function (complete) { // Based on pnpm documentation complete('.', 'Current working directory'); complete('!', 'Exclude packages matching selector'); @@ -191,16 +202,8 @@ export function parsePnpmHelp(helpText: string): Record { export async function getPnpmCommandsFromMainHelp(): Promise< Record > { - try { - const { stdout } = await exec('pnpm --help', { - encoding: 'utf8', - timeout: 500, - maxBuffer: 4 * 1024 * 1024, - }); - return parsePnpmHelp(stdout); - } catch { - return {}; - } + const output = await safeExec('pnpm --help'); + return output ? parsePnpmHelp(output) : {}; } // here we parse the pnpm options from the help text @@ -267,53 +270,32 @@ export function parsePnpmOptions( return optionsOut; } -// we load the dynamic options synchronously when requested ( separated from the command loading ) -export function loadDynamicOptionsSync( - cmd: LazyCommand, - command: string -): void { - try { - const stdout = execSync(`pnpm ${command} --help`, { - encoding: 'utf8', - timeout: 500, - }); - - const allOptions = parsePnpmOptions(stdout, { flagsOnly: false }); - - for (const { long, short, desc } of allOptions) { - const alreadyDefined = cmd.optionsRaw?.get?.(long); - if (!alreadyDefined) { - const handler = - pnpmOptionHandlers[long as keyof typeof pnpmOptionHandlers]; - if (handler) { - cmd.option(long, desc, handler, short); - } else { - cmd.option(long, desc, short); - } +// we load the dynamic options synchronously when requested +function loadPnpmOptionsSync(cmd: LazyCommand, command: string): void { + const output = safeExecSync(`pnpm ${command} --help`); + if (!output) return; + + const allOptions = parsePnpmOptions(output, { flagsOnly: false }); + + for (const { long, short, desc } of allOptions) { + const alreadyDefined = cmd.optionsRaw?.get?.(long); + if (!alreadyDefined) { + const handler = pnpmOptionHandlers[long]; + if (handler) { + cmd.option(long, desc, handler, short); + } else { + cmd.option(long, desc, short); } } - } catch (_err) {} -} - -// we setup the lazy option loading for a command - -function setupLazyOptionLoading(cmd: LazyCommand, command: string): void { - cmd._lazyCommand = command; - cmd._optionsLoaded = false; - - const optionsStore = cmd.options; - cmd.optionsRaw = optionsStore; + } - Object.defineProperty(cmd, 'options', { - get() { - if (!this._optionsLoaded) { - this._optionsLoaded = true; - loadDynamicOptionsSync(this, this._lazyCommand); // block until filled - } - return optionsStore; - }, - configurable: true, - }); + // Special case: add reporter option manually since it doesn't match standard pattern + if (output.includes('--reporter') && !cmd.optionsRaw?.get?.('reporter')) { + const handler = pnpmOptionHandlers['reporter']; + if (handler) { + cmd.option('reporter', 'Output reporter for pnpm commands', handler); + } + } } export async function setupPnpmCompletions( @@ -327,14 +309,11 @@ export async function setupPnpmCompletions( )) { const cmd = completion.command(command, description); - if (['remove', 'rm', 'update', 'up'].includes(command)) { - cmd.argument('package', packageJsonDependencyCompletion); - } - if (command === 'run') { - cmd.argument('script', packageJsonScriptCompletion, true); - } + // Setup common argument patterns + setupCommandArguments(cmd, command, 'pnpm'); - setupLazyOptionLoading(cmd, command); + // Setup lazy option loading + setupLazyOptionLoading(cmd, command, 'pnpm', loadPnpmOptionsSync); } } catch (_err) {} } diff --git a/bin/utils/package-manager-base.ts b/bin/utils/package-manager-base.ts new file mode 100644 index 0000000..b3d9e6b --- /dev/null +++ b/bin/utils/package-manager-base.ts @@ -0,0 +1,122 @@ +import { promisify } from 'node:util'; +import child_process from 'node:child_process'; + +export const exec = promisify(child_process.exec); +export const { execSync } = child_process; + +import { Command, Option } from '../../src/t.js'; +import { + packageJsonScriptCompletion, + packageJsonDependencyCompletion, +} from '../completions/completion-producers.js'; +import { getWorkspacePatterns } from './filesystem-utils.js'; + +export interface LazyCommand extends Command { + _lazyCommand?: string; + _optionsLoaded?: boolean; + optionsRaw?: Map; +} + +export type CompletionHandler = ( + complete: (value: string, description: string) => void +) => void; +export type OptionHandlers = Record; + +export const commonOptionHandlers = { + workspace: function (complete: (value: string, description: string) => void) { + const workspacePatterns = getWorkspacePatterns(); + workspacePatterns.forEach((pattern) => { + complete(pattern, `Workspace pattern: ${pattern}`); + }); + + complete('packages/*', 'All packages in packages directory'); + complete('apps/*', 'All apps in apps directory'); + }, + + registry: function (complete: (value: string, description: string) => void) { + complete('https://registry.npmjs.org/', 'Official npm registry'); + complete('https://registry.npmmirror.com/', 'npm China mirror'); + }, +}; + +export function setupLazyOptionLoading( + cmd: LazyCommand, + command: string, + packageManager: string, + loadOptionsSync: (cmd: LazyCommand, command: string) => void +): void { + cmd._lazyCommand = command; + cmd._optionsLoaded = false; + + const optionsStore = cmd.options; + cmd.optionsRaw = optionsStore; + + Object.defineProperty(cmd, 'options', { + get() { + if (!this._optionsLoaded) { + this._optionsLoaded = true; + loadOptionsSync(this, this._lazyCommand); + } + return optionsStore; + }, + configurable: true, + }); +} + +export function setupCommandArguments( + cmd: LazyCommand, + command: string, + packageManager: string +): void { + // Package removal commands + if (['remove', 'rm', 'uninstall', 'un', 'update', 'up'].includes(command)) { + cmd.argument('package', packageJsonDependencyCompletion); + } + + // Script running commands + if (['run', 'run-script'].includes(command)) { + cmd.argument('script', packageJsonScriptCompletion, true); + } +} + +export async function safeExec( + command: string, + options: any = {} +): Promise { + try { + const { stdout } = await exec(command, { + encoding: 'utf8' as const, + timeout: 500, + maxBuffer: 4 * 1024 * 1024, + ...options, + }); + return stdout as unknown as string; + } catch (error) { + // Many package managers exit with non-zero but still provide useful output + if (error instanceof Error && 'stdout' in error) { + return error.stdout as unknown as string; + } + return ''; + } +} +export function safeExecSync(command: string, options: any = {}): string { + try { + return execSync(command, { + encoding: 'utf8' as const, + timeout: 500, + ...options, + }) as unknown as string; + } catch (error: any) { + // Handle non-zero exit codes that still provide output + if (error.stdout) { + return error.stdout as unknown as string; + } + return ''; + } +} + +export function createLogLevelHandler(levels: string[]): CompletionHandler { + return function (complete: (value: string, description: string) => void) { + levels.forEach((level) => complete(level, ' ')); + }; +} From 72df3a8491640c83f60231593a97b0eb7bbb34ad Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Thu, 11 Sep 2025 23:58:33 +0330 Subject: [PATCH 03/10] clean npm handler --- bin/handlers/bun-handler.ts | 50 ++-- bin/handlers/npm-handler.ts | 220 ++++++++---------- bin/handlers/pnpm-handler.ts | 2 +- .../{package-manager-base.ts => shared.ts} | 0 4 files changed, 117 insertions(+), 155 deletions(-) rename bin/utils/{package-manager-base.ts => shared.ts} (100%) diff --git a/bin/handlers/bun-handler.ts b/bin/handlers/bun-handler.ts index baf1ac3..75e8251 100644 --- a/bin/handlers/bun-handler.ts +++ b/bin/handlers/bun-handler.ts @@ -9,30 +9,29 @@ import { safeExec, safeExecSync, // createLogLevelHandler, -} from '../utils/package-manager-base.js'; +} from '../utils/shared.js'; -// regex patterns to avoid recompilation in loops const COMMANDS_SECTION_RE = /^Commands:\s*$/i; const FLAGS_SECTION_RE = /^Flags:\s*$/i; const SECTION_END_RE = /^(Examples|Full documentation|Learn more)/i; const COMMAND_VALIDATION_RE = /^[a-z][a-z0-9-]*$/; const BUN_OPTION_RE = /^\s*(?:-([a-zA-Z]),?\s*)?--([a-z][a-z0-9-]*)(?:=<[^>]+>)?\s+(.+)$/; -// const NON_INDENTED_LINE_RE = /^\s/; +const MAIN_COMMAND_RE = /^ ([a-z][a-z0-9-]*)\s+(.+)$/; +const CONTINUATION_COMMAND_RE = /^\s{12,}([a-z][a-z0-9-]*)\s+(.+)$/; +const EMPTY_LINE_FOLLOWED_BY_NON_COMMAND_RE = /^\s+[a-z]/; +const DESCRIPTION_SPLIT_RE = /\s{2,}/; +const CAPITAL_LETTER_START_RE = /^[A-Z]/; -// bun-specific completion handlers const bunOptionHandlers: OptionHandlers = { - // Use common handlers ...commonOptionHandlers, - // bun doesn't have traditional log levels, but has verbose/silent silent: function (complete) { complete('true', 'Enable silent mode'); complete('false', 'Disable silent mode'); }, backend: function (complete) { - // From bun help: "clonefile" (default), "hardlink", "symlink", "copyfile" complete('clonefile', 'Clone files (default, fastest)'); complete('hardlink', 'Use hard links'); complete('symlink', 'Use symbolic links'); @@ -40,26 +39,22 @@ const bunOptionHandlers: OptionHandlers = { }, linker: function (complete) { - // From bun help: "isolated" or "hoisted" complete('isolated', 'Isolated linker strategy'); complete('hoisted', 'Hoisted linker strategy'); }, omit: function (complete) { - // From bun help: 'dev', 'optional', or 'peer' complete('dev', 'Omit devDependencies'); complete('optional', 'Omit optionalDependencies'); complete('peer', 'Omit peerDependencies'); }, shell: function (complete) { - // From bun help: 'bun' or 'system' complete('bun', 'Use Bun shell'); complete('system', 'Use system shell'); }, 'unhandled-rejections': function (complete) { - // From bun help: "strict", "throw", "warn", "none", or "warn-with-error-code" complete('strict', 'Strict unhandled rejection handling'); complete('throw', 'Throw on unhandled rejections'); complete('warn', 'Warn on unhandled rejections'); @@ -68,11 +63,9 @@ const bunOptionHandlers: OptionHandlers = { }, }; -// Parse bun help text to extract commands and their descriptions export function parseBunHelp(helpText: string): Record { const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/); - // Find "Commands:" section let startIndex = -1; for (let i = 0; i < helpLines.length; i++) { if (COMMANDS_SECTION_RE.test(helpLines[i].trim())) { @@ -85,44 +78,35 @@ export function parseBunHelp(helpText: string): Record { const commands: Record = {}; - // Parse bun's unique command format + // parse bun's unique command format for (let i = startIndex; i < helpLines.length; i++) { const line = helpLines[i]; - // Stop when we hit Flags section or empty line followed by non-command content + // stop when we hit Flags section or empty line followed by non-command content if ( FLAGS_SECTION_RE.test(line.trim()) || (line.trim() === '' && i + 1 < helpLines.length && - !helpLines[i + 1].match(/^\s+[a-z]/)) + !helpLines[i + 1].match(EMPTY_LINE_FOLLOWED_BY_NON_COMMAND_RE)) ) break; // Skip empty lines if (line.trim() === '') continue; - // Handle different bun command formats: - // Format 1: " run ./my-script.ts Execute a file with Bun" - // Format 2: " lint Run a package.json script" (continuation) - // Format 3: " install Install dependencies for a package.json (bun i)" - - // Try to match command at start of line (2 spaces) - const mainCommandMatch = line.match(/^ ([a-z][a-z0-9-]*)\s+(.+)$/); + const mainCommandMatch = line.match(MAIN_COMMAND_RE); if (mainCommandMatch) { const [, command, rest] = mainCommandMatch; if (COMMAND_VALIDATION_RE.test(command)) { - // Extract description - find the last part that looks like a description - // Split by multiple spaces and take the last part that contains letters - const parts = rest.split(/\s{2,}/); + const parts = rest.split(DESCRIPTION_SPLIT_RE); let description = parts[parts.length - 1]; - // If the last part starts with a capital letter, it's likely the description - if (description && /^[A-Z]/.test(description)) { + // If the last part starts with capital letter, it's likely the description + if (description && CAPITAL_LETTER_START_RE.test(description)) { commands[command] = description.trim(); } else if (parts.length > 1) { - // Otherwise, look for the first part that starts with a capital for (const part of parts) { - if (/^[A-Z]/.test(part)) { + if (CAPITAL_LETTER_START_RE.test(part)) { commands[command] = part.trim(); break; } @@ -131,8 +115,7 @@ export function parseBunHelp(helpText: string): Record { } } - // Handle continuation lines (12+ spaces) - const continuationMatch = line.match(/^\s{12,}([a-z][a-z0-9-]*)\s+(.+)$/); + const continuationMatch = line.match(CONTINUATION_COMMAND_RE); if (continuationMatch) { const [, command, description] = continuationMatch; if (COMMAND_VALIDATION_RE.test(command)) { @@ -144,7 +127,6 @@ export function parseBunHelp(helpText: string): Record { return commands; } -// Get bun commands from the main help output export async function getBunCommandsFromMainHelp(): Promise< Record > { @@ -200,7 +182,7 @@ export function parseBunOptions( return optionsOut; } -// Load dynamic options synchronously when requested +// load dynamic options synchronously when requested function loadBunOptionsSync(cmd: LazyCommand, command: string): void { const output = safeExecSync(`bun ${command} --help`); if (!output) return; diff --git a/bin/handlers/npm-handler.ts b/bin/handlers/npm-handler.ts index 9859f39..e2ed05b 100644 --- a/bin/handlers/npm-handler.ts +++ b/bin/handlers/npm-handler.ts @@ -9,22 +9,43 @@ import { safeExec, safeExecSync, createLogLevelHandler, -} from '../utils/package-manager-base.js'; +} from '../utils/shared.js'; -// regex patterns to avoid recompilation in loops const ALL_COMMANDS_RE = /^All commands:\s*$/i; const OPTIONS_SECTION_RE = /^Options:\s*$/i; -const SECTION_END_RE = /^(aliases|run|more)/i; +const SECTION_END_RE = /^(aliases|run|more)/i; // marks end of Options: block const COMMAND_VALIDATION_RE = /^[a-z][a-z0-9-]*$/; const NPM_OPTION_RE = /(?:\[)?(?:-([a-z])\|)?--([a-z][a-z0-9-]+)(?:\s+<[^>]+>)?(?:\])?/gi; -const OPTION_VALUE_RE = /<[^>]+>/; -const NON_INDENTED_LINE_RE = /^\s/; +const ANGLE_VALUE_RE = /<[^>]+>/; +const INDENTED_LINE_RE = /^\s/; + +function toLines(helpText: string): string[] { + return stripAnsiEscapes(helpText).split(/\r?\n/); +} + +function readIndentedBlockAfter(lines: string[], headerRe: RegExp): string { + const start = lines.findIndex((l) => headerRe.test(l.trim())); + if (start === -1) return ''; + + let buf = ''; + for (let i = start + 1; i < lines.length; i++) { + const line = lines[i]; + if (!INDENTED_LINE_RE.test(line) && line.trim() && !line.includes(',')) + break; + if (INDENTED_LINE_RE.test(line)) buf += ' ' + line.trim(); + } + return buf; +} + +const listHandler = + (values: string[], describe: (v: string) => string = () => ' ') => + (complete: (value: string, description: string) => void) => + values.forEach((v) => complete(v, describe(v))); const npmOptionHandlers: OptionHandlers = { ...commonOptionHandlers, - // npm log levels loglevel: createLogLevelHandler([ 'silent', 'error', @@ -36,72 +57,60 @@ const npmOptionHandlers: OptionHandlers = { 'silly', ]), - 'install-strategy': function (complete) { - // From npm help: hoisted|nested|shallow|linked - complete('hoisted', 'Hoist all dependencies to top level'); - complete('nested', 'Create nested node_modules structure'); - complete('shallow', 'Shallow dependency installation'); - complete('linked', 'Use linked dependencies'); - }, - - omit: function (complete) { - // From npm help: dev|optional|peer - complete('dev', 'Omit devDependencies'); - complete('optional', 'Omit optionalDependencies'); - complete('peer', 'Omit peerDependencies'); - }, - - include: function (complete) { - // From npm help: prod|dev|optional|peer - complete('prod', 'Include production dependencies'); - complete('dev', 'Include devDependencies'); - complete('optional', 'Include optionalDependencies'); - complete('peer', 'Include peerDependencies'); - }, + 'install-strategy': listHandler( + ['hoisted', 'nested', 'shallow', 'linked'], + (v) => + ( + ({ + hoisted: 'Hoist all dependencies to top level', + nested: 'Nested node_modules structure', + shallow: 'Shallow dependency installation', + linked: 'Use linked dependencies', + }) as Record + )[v] ?? ' ' + ), + + omit: listHandler( + ['dev', 'optional', 'peer'], + (v) => + ( + ({ + dev: 'Omit devDependencies', + optional: 'Omit optionalDependencies', + peer: 'Omit peerDependencies', + }) as Record + )[v] ?? ' ' + ), + + include: listHandler( + ['prod', 'dev', 'optional', 'peer'], + (v) => + ( + ({ + prod: 'Include production deps', + dev: 'Include dev deps', + optional: 'Include optional deps', + peer: 'Include peer deps', + }) as Record + )[v] ?? ' ' + ), }; -// parse npm help text to extract commands and their descriptions export function parseNpmHelp(helpText: string): Record { - const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/); - - // find "All commands:" section - let startIndex = -1; - for (let i = 0; i < helpLines.length; i++) { - if (ALL_COMMANDS_RE.test(helpLines[i].trim())) { - startIndex = i + 1; - break; - } - } - - if (startIndex === -1) return {}; + const lines = toLines(helpText); + const commandsBlob = readIndentedBlockAfter(lines, ALL_COMMANDS_RE); + if (!commandsBlob) return {}; const commands: Record = {}; - let commandsText = ''; - - // collect all lines that are part of the commands section - for (let i = startIndex; i < helpLines.length; i++) { - const line = helpLines[i]; - // stop if we hit a non-indented line that starts a new section - if (!NON_INDENTED_LINE_RE.test(line) && line.trim() && !line.includes(',')) - break; - - // add this line to our commands text - if (NON_INDENTED_LINE_RE.test(line)) { - commandsText += ' ' + line.trim(); - } - } - - // parse the comma-separated command list - const commandList = commandsText + commandsBlob .split(',') - .map((cmd) => cmd.trim()) - .filter((cmd) => cmd && COMMAND_VALIDATION_RE.test(cmd)); - - // npm does not ptrovide descriptions in the main help. - commandList.forEach((cmd) => { - commands[cmd] = ' '; - }); + .map((c) => c.trim()) + .filter((c) => c && COMMAND_VALIDATION_RE.test(c)) + .forEach((cmd) => { + // npm main help has no per-command descriptions + commands[cmd] = ' '; + }); // this is the most common used aliase that isn't in the main list commands['run'] = ' '; @@ -117,56 +126,35 @@ export async function getNpmCommandsFromMainHelp(): Promise< return output ? parseNpmHelp(output) : {}; } -// Parse npm options from help text (npm has a different format than pnpm) export function parseNpmOptions( helpText: string, { flagsOnly = true }: { flagsOnly?: boolean } = {} ): ParsedOption[] { - const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/); - const optionsOut: ParsedOption[] = []; - - // Find the Options: section - let optionsStartIndex = -1; - for (let i = 0; i < helpLines.length; i++) { - if (OPTIONS_SECTION_RE.test(helpLines[i].trim())) { - optionsStartIndex = i + 1; - break; - } - } - - if (optionsStartIndex === -1) return []; - - // Parse the compact npm option format: [-S|--save|--no-save] etc. - for (let i = optionsStartIndex; i < helpLines.length; i++) { - const line = helpLines[i]; - - // Stop at aliases or other sections - if (SECTION_END_RE.test(line.trim())) break; + const lines = toLines(helpText); - // Parse option patterns like [-S|--save] or [--loglevel ] - const optionMatches = line.matchAll(NPM_OPTION_RE); + const start = lines.findIndex((l) => OPTIONS_SECTION_RE.test(l.trim())); + if (start === -1) return []; - for (const match of optionMatches) { - const short = match[1] || undefined; - const long = match[2]; + const out: ParsedOption[] = []; - // Check if this option takes a value - const takesValue = OPTION_VALUE_RE.test(match[0]); + for (const line of lines.slice(start + 1)) { + const trimmed = line.trim(); + if (SECTION_END_RE.test(trimmed)) break; + const matches = line.matchAll(NPM_OPTION_RE); + for (const m of matches) { + const short = m[1] || undefined; + const long = m[2]; + const takesValue = ANGLE_VALUE_RE.test(m[0]); if (flagsOnly && takesValue) continue; - optionsOut.push({ - short, - long, - desc: ' ', - }); + out.push({ short, long, desc: ' ' }); } } - return optionsOut; + return out; } -// Load dynamic options synchronously when requested function loadNpmOptionsSync(cmd: LazyCommand, command: string): void { const output = safeExecSync(`npm ${command} --help`); if (!output) return; @@ -174,15 +162,12 @@ function loadNpmOptionsSync(cmd: LazyCommand, command: string): void { const allOptions = parseNpmOptions(output, { flagsOnly: false }); for (const { long, short, desc } of allOptions) { - const alreadyDefined = cmd.optionsRaw?.get?.(long); - if (!alreadyDefined) { - const handler = npmOptionHandlers[long]; - if (handler) { - cmd.option(long, desc, handler, short); - } else { - cmd.option(long, desc, short); - } - } + const exists = cmd.optionsRaw?.get?.(long); + if (exists) continue; + + const handler = npmOptionHandlers[long]; + if (handler) cmd.option(long, desc, handler, short); + else cmd.option(long, desc, short); } } @@ -190,18 +175,13 @@ export async function setupNpmCompletions( completion: PackageManagerCompletion ): Promise { try { - const commandsWithDescriptions = await getNpmCommandsFromMainHelp(); - - for (const [command, description] of Object.entries( - commandsWithDescriptions - )) { - const cmd = completion.command(command, description); + const commands = await getNpmCommandsFromMainHelp(); + for (const [command, description] of Object.entries(commands)) { + const c = completion.command(command, description); - // Setup common argument patterns - setupCommandArguments(cmd, command, 'npm'); + setupCommandArguments(c, command, 'npm'); - // Setup lazy option loading - setupLazyOptionLoading(cmd, command, 'npm', loadNpmOptionsSync); + setupLazyOptionLoading(c, command, 'npm', loadNpmOptionsSync); } - } catch (_err) {} + } catch {} } diff --git a/bin/handlers/pnpm-handler.ts b/bin/handlers/pnpm-handler.ts index 26e02ef..fda991e 100644 --- a/bin/handlers/pnpm-handler.ts +++ b/bin/handlers/pnpm-handler.ts @@ -9,7 +9,7 @@ import { safeExec, safeExecSync, createLogLevelHandler, -} from '../utils/package-manager-base.js'; +} from '../utils/shared.js'; import { stripAnsiEscapes, measureIndent, diff --git a/bin/utils/package-manager-base.ts b/bin/utils/shared.ts similarity index 100% rename from bin/utils/package-manager-base.ts rename to bin/utils/shared.ts From 7d3a4d70fd0535705e0ece3a127561260cdbbdd0 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Fri, 12 Sep 2025 00:07:23 +0330 Subject: [PATCH 04/10] clean shared --- bin/utils/shared.ts | 39 ++++++++++++++------------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/bin/utils/shared.ts b/bin/utils/shared.ts index b3d9e6b..981ace4 100644 --- a/bin/utils/shared.ts +++ b/bin/utils/shared.ts @@ -22,18 +22,15 @@ export type CompletionHandler = ( ) => void; export type OptionHandlers = Record; -export const commonOptionHandlers = { - workspace: function (complete: (value: string, description: string) => void) { - const workspacePatterns = getWorkspacePatterns(); - workspacePatterns.forEach((pattern) => { - complete(pattern, `Workspace pattern: ${pattern}`); - }); - +export const commonOptionHandlers: OptionHandlers = { + workspace(complete) { + const patterns = getWorkspacePatterns(); + patterns.forEach((p) => complete(p, `Workspace pattern: ${p}`)); complete('packages/*', 'All packages in packages directory'); complete('apps/*', 'All apps in apps directory'); }, - registry: function (complete: (value: string, description: string) => void) { + registry(complete) { complete('https://registry.npmjs.org/', 'Official npm registry'); complete('https://registry.npmmirror.com/', 'npm China mirror'); }, @@ -42,14 +39,14 @@ export const commonOptionHandlers = { export function setupLazyOptionLoading( cmd: LazyCommand, command: string, - packageManager: string, + _packageManager: string, loadOptionsSync: (cmd: LazyCommand, command: string) => void ): void { cmd._lazyCommand = command; cmd._optionsLoaded = false; - const optionsStore = cmd.options; - cmd.optionsRaw = optionsStore; + const store = cmd.options; + cmd.optionsRaw = store; Object.defineProperty(cmd, 'options', { get() { @@ -57,7 +54,7 @@ export function setupLazyOptionLoading( this._optionsLoaded = true; loadOptionsSync(this, this._lazyCommand); } - return optionsStore; + return store; }, configurable: true, }); @@ -66,14 +63,12 @@ export function setupLazyOptionLoading( export function setupCommandArguments( cmd: LazyCommand, command: string, - packageManager: string + _packageManager: string ): void { - // Package removal commands if (['remove', 'rm', 'uninstall', 'un', 'update', 'up'].includes(command)) { cmd.argument('package', packageJsonDependencyCompletion); } - // Script running commands if (['run', 'run-script'].includes(command)) { cmd.argument('script', packageJsonScriptCompletion, true); } @@ -92,13 +87,13 @@ export async function safeExec( }); return stdout as unknown as string; } catch (error) { - // Many package managers exit with non-zero but still provide useful output if (error instanceof Error && 'stdout' in error) { - return error.stdout as unknown as string; + return (error as any).stdout as string; } return ''; } } + export function safeExecSync(command: string, options: any = {}): string { try { return execSync(command, { @@ -107,16 +102,10 @@ export function safeExecSync(command: string, options: any = {}): string { ...options, }) as unknown as string; } catch (error: any) { - // Handle non-zero exit codes that still provide output - if (error.stdout) { - return error.stdout as unknown as string; - } - return ''; + return error?.stdout ? (error.stdout as string) : ''; } } export function createLogLevelHandler(levels: string[]): CompletionHandler { - return function (complete: (value: string, description: string) => void) { - levels.forEach((level) => complete(level, ' ')); - }; + return (complete) => levels.forEach((lvl) => complete(lvl, ' ')); } From 43e32779a4e49e036deac6443672eebe47cccabc Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Fri, 12 Sep 2025 00:19:18 +0330 Subject: [PATCH 05/10] clean pnpm handler --- bin/handlers/pnpm-handler.ts | 315 ++++++++++++++++------------------- 1 file changed, 139 insertions(+), 176 deletions(-) diff --git a/bin/handlers/pnpm-handler.ts b/bin/handlers/pnpm-handler.ts index fda991e..53a2a45 100644 --- a/bin/handlers/pnpm-handler.ts +++ b/bin/handlers/pnpm-handler.ts @@ -20,58 +20,78 @@ import { type ParsedOption, } from '../utils/text-utils.js'; -// regex patterns to avoid recompilation in loops const OPTIONS_SECTION_RE = /^\s*Options:/i; const LEVEL_MATCH_RE = /(?:levels?|options?|values?)[^:]*:\s*([^.]+)/i; +const LINE_SPLIT_RE = /\r?\n/; +const REPORTER_LINE_RE = /^\s*--reporter\s+\w/; +const REPORTER_MATCH_RE = /^\s*--reporter\s+(\w+(?:-\w+)*)\s+(.+)$/; +const SILENT_REPORTER_RE = /-s,\s*--silent,\s*--reporter\s+silent\s+(.+)/; +const COMMA_SPACE_SPLIT_RE = /[,\s]+/; + +function toLines(text: string): string[] { + return stripAnsiEscapes(text).split(LINE_SPLIT_RE); +} + +function findCommandDescColumn(lines: string[]): number { + let col = Number.POSITIVE_INFINITY; + for (const line of lines) { + const m = line.match(COMMAND_ROW_RE); + if (!m) continue; + const idx = line.indexOf(m[2]); + if (idx >= 0 && idx < col) col = idx; + } + return col; +} + +function findOptionDescColumn(lines: string[], flagsOnly: boolean): number { + let col = Number.POSITIVE_INFINITY; + for (const line of lines) { + const m = line.match(OPTION_ROW_RE); + if (!m) continue; + if (flagsOnly && m.groups?.val) continue; // skip value-taking options in flagsOnly mode + const idx = line.indexOf(m.groups!.desc); + if (idx >= 0 && idx < col) col = idx; + } + return col; +} function extractValidValuesFromHelp( helpText: string, optionName: string ): Array<{ value: string; desc: string }> { - const lines = stripAnsiEscapes(helpText).split(/\r?\n/); + const lines = toLines(helpText); - // Handle reporter option specially + // edge case: reporter often appears as multiple lines if (optionName === 'reporter') { - const reporterValues: Array<{ value: string; desc: string }> = []; - - // Simple approach: look for lines with "--reporter " - for (const helpLine of lines) { - if ( - helpLine.includes('--reporter') && - helpLine.match(/^\s*--reporter\s+\w/) - ) { - const match = helpLine.match(/^\s*--reporter\s+(\w+|\w+-\w+)\s+(.+)$/); + const out: Array<{ value: string; desc: string }> = []; + + for (const line of lines) { + if (line.includes('--reporter') && REPORTER_LINE_RE.test(line)) { + const match = line.match(REPORTER_MATCH_RE); if (match) { const [, value, desc] = match; - reporterValues.push({ value, desc: desc.trim() }); + out.push({ value, desc: desc.trim() }); } } - // Handle special case: "-s, --silent, --reporter silent" - const silentMatch = helpLine.match( - /-s,\s*--silent,\s*--reporter\s+silent\s+(.+)/ - ); - if (silentMatch && !reporterValues.some((r) => r.value === 'silent')) { - reporterValues.push({ value: 'silent', desc: silentMatch[1].trim() }); + const silent = line.match(SILENT_REPORTER_RE); + if (silent && !out.some((r) => r.value === 'silent')) { + out.push({ value: 'silent', desc: silent[1].trim() }); } } - if (reporterValues.length > 0) { - return reporterValues; - } + if (out.length) return out; } - // Handle other options for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.includes(`--${optionName}`) || line.includes(`${optionName}:`)) { + const ln = lines[i]; + if (ln.includes(`--${optionName}`) || ln.includes(`${optionName}:`)) { for (let j = i; j < Math.min(i + 3, lines.length); j++) { - const searchLine = lines[j]; - - const levelMatch = searchLine.match(LEVEL_MATCH_RE); - if (levelMatch) { - return levelMatch[1] - .split(/[,\s]+/) + const probe = lines[j]; + const m = probe.match(LEVEL_MATCH_RE); + if (m) { + return m[1] + .split(COMMA_SPACE_SPLIT_RE) .map((v) => v.trim()) .filter((v) => v && !v.includes('(') && !v.includes(')')) .map((value) => ({ value, desc: `Log level: ${value}` })); @@ -79,126 +99,99 @@ function extractValidValuesFromHelp( } } } - return []; } -// pnpm-specific completion handlers const pnpmOptionHandlers: OptionHandlers = { - // Use common handlers ...commonOptionHandlers, - // pnpm log levels with dynamic extraction fallback - loglevel: function (complete) { - const helpValues = extractValidValuesFromHelp( + loglevel(complete) { + const fromHelp = extractValidValuesFromHelp( safeExecSync('pnpm install --help'), 'loglevel' ); - - if (helpValues.length > 0) { - helpValues.forEach(({ value, desc }) => complete(value, desc)); + if (fromHelp.length) { + fromHelp.forEach(({ value, desc }) => complete(value, desc)); } else { - // Fallback to known pnpm levels createLogLevelHandler(['debug', 'info', 'warn', 'error', 'silent'])( complete ); } }, - reporter: function (complete) { - // Extract reporter values dynamically from install help - const helpOutput = safeExecSync('pnpm install --help'); - const reporterValues = extractValidValuesFromHelp(helpOutput, 'reporter'); - - if (reporterValues.length > 0) { - reporterValues.forEach(({ value, desc }) => complete(value, desc)); + reporter(complete) { + const out = extractValidValuesFromHelp( + safeExecSync('pnpm install --help'), + 'reporter' + ); + if (out.length) { + out.forEach(({ value, desc }) => complete(value, desc)); } else { - // Fallback to common reporter types if extraction fails createLogLevelHandler(['default', 'append-only', 'ndjson', 'silent'])( complete ); } }, - filter: function (complete) { - // Based on pnpm documentation + filter(complete) { complete('.', 'Current working directory'); complete('!', 'Exclude packages matching selector'); - // Get actual workspace patterns from pnpm-workspace.yaml - const workspacePatterns = getWorkspacePatterns(); - workspacePatterns.forEach((pattern) => { - complete(pattern, `Workspace pattern: ${pattern}`); - complete(`${pattern}...`, `Include dependencies of ${pattern}`); + const patterns = getWorkspacePatterns(); + patterns.forEach((p) => { + complete(p, `Workspace pattern: ${p}`); + complete(`${p}...`, `Include dependencies of ${p}`); }); - // Common scope patterns complete('@*/*', 'All scoped packages'); complete('...', 'Include dependencies of pattern'); complete('...', 'Include dependents of pattern'); }, }; -// we parse the pnpm help text to extract commands and their descriptions! export function parsePnpmHelp(helpText: string): Record { - const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/); - - // we find the earliest description column across command rows. - let descColumnIndex = Number.POSITIVE_INFINITY; - for (const line of helpLines) { - const rowMatch = line.match(COMMAND_ROW_RE); - if (!rowMatch) continue; - const descColumnIndexOnThisLine = line.indexOf(rowMatch[2]); - if ( - descColumnIndexOnThisLine >= 0 && - descColumnIndexOnThisLine < descColumnIndex - ) { - descColumnIndex = descColumnIndexOnThisLine; - } - } - if (!Number.isFinite(descColumnIndex)) return {}; - - // we fold rows, and join continuation lines aligned to descColumnIndex or deeper. - type PendingRow = { names: string[]; desc: string } | null; - let pendingRow: PendingRow = null; - - const commandMap = new Map(); - const flushPendingRow = () => { - if (!pendingRow) return; - const desc = pendingRow.desc.trim(); - for (const name of pendingRow.names) commandMap.set(name, desc); - pendingRow = null; + const lines = toLines(helpText); + + const descCol = findCommandDescColumn(lines); + if (!Number.isFinite(descCol)) return {}; + + type Pending = { names: string[]; desc: string } | null; + let pending: Pending = null; + + const out = new Map(); + + const flush = () => { + if (!pending) return; + const desc = pending.desc.trim(); + for (const n of pending.names) out.set(n, desc); + pending = null; }; - for (const line of helpLines) { - if (OPTIONS_SECTION_RE.test(line)) break; // we stop at options + for (const line of lines) { + if (OPTIONS_SECTION_RE.test(line)) break; // end of commands section - // we match the command row - const rowMatch = line.match(COMMAND_ROW_RE); - if (rowMatch) { - flushPendingRow(); - pendingRow = { - names: parseAliasList(rowMatch[1]), - desc: rowMatch[2].trim(), + const row = line.match(COMMAND_ROW_RE); + if (row) { + flush(); + pending = { + names: parseAliasList(row[1]), + desc: row[2].trim(), }; continue; } - // we join continuation lines aligned to descColumnIndex or deeper - if (pendingRow) { - const indentWidth = measureIndent(line); - if (indentWidth >= descColumnIndex && line.trim()) { - pendingRow.desc += ' ' + line.trim(); + if (pending) { + const indent = measureIndent(line); + if (indent >= descCol && line.trim()) { + pending.desc += ' ' + line.trim(); } } } - // we flush the pending row and return the command map - flushPendingRow(); + flush(); - return Object.fromEntries(commandMap); + return Object.fromEntries(out); } -// now we get the pnpm commands from the main help output export async function getPnpmCommandsFromMainHelp(): Promise< Record > { @@ -206,95 +199,71 @@ export async function getPnpmCommandsFromMainHelp(): Promise< return output ? parsePnpmHelp(output) : {}; } -// here we parse the pnpm options from the help text export function parsePnpmOptions( helpText: string, { flagsOnly = true }: { flagsOnly?: boolean } = {} ): ParsedOption[] { - // we strip the ANSI escapes from the help text - const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/); - - // we find the earliest description column among option rows we care about - let descColumnIndex = Number.POSITIVE_INFINITY; - for (const line of helpLines) { - const optionMatch = line.match(OPTION_ROW_RE); - if (!optionMatch) continue; - if (flagsOnly && optionMatch.groups?.val) continue; - const descColumnIndexOnThisLine = line.indexOf(optionMatch.groups!.desc); - if ( - descColumnIndexOnThisLine >= 0 && - descColumnIndexOnThisLine < descColumnIndex - ) { - descColumnIndex = descColumnIndexOnThisLine; - } - } - if (!Number.isFinite(descColumnIndex)) return []; + const lines = toLines(helpText); - // we fold the option rows and join the continuations - const optionsOut: ParsedOption[] = []; - let pendingOption: ParsedOption | null = null; + const descCol = findOptionDescColumn(lines, flagsOnly); + if (!Number.isFinite(descCol)) return []; - const flushPendingOption = () => { - if (!pendingOption) return; - pendingOption.desc = pendingOption.desc.trim(); - optionsOut.push(pendingOption); - pendingOption = null; + const out: ParsedOption[] = []; + let pending: ParsedOption | null = null; + + const flush = () => { + if (!pending) return; + pending.desc = pending.desc.trim(); + out.push(pending); + pending = null; }; - // we match the option row - for (const line of helpLines) { - const optionMatch = line.match(OPTION_ROW_RE); - if (optionMatch) { - if (flagsOnly && optionMatch.groups?.val) continue; - flushPendingOption(); - pendingOption = { - short: optionMatch.groups?.short || undefined, - long: optionMatch.groups!.long, - desc: optionMatch.groups!.desc.trim(), + for (const line of lines) { + const m = line.match(OPTION_ROW_RE); + if (m) { + if (flagsOnly && m.groups?.val) continue; + flush(); + pending = { + short: m.groups?.short || undefined, + long: m.groups!.long, + desc: m.groups!.desc.trim(), }; continue; } - // we join the continuations - if (pendingOption) { - const indentWidth = measureIndent(line); - const startsNewOption = OPTION_HEAD_RE.test(line); - if (indentWidth >= descColumnIndex && line.trim() && !startsNewOption) { - pendingOption.desc += ' ' + line.trim(); + if (pending) { + const indent = measureIndent(line); + const startsNew = OPTION_HEAD_RE.test(line); + if (indent >= descCol && line.trim() && !startsNew) { + pending.desc += ' ' + line.trim(); } } } - // we flush the pending option - flushPendingOption(); + flush(); - return optionsOut; + return out; } -// we load the dynamic options synchronously when requested function loadPnpmOptionsSync(cmd: LazyCommand, command: string): void { const output = safeExecSync(`pnpm ${command} --help`); if (!output) return; - const allOptions = parsePnpmOptions(output, { flagsOnly: false }); + const options = parsePnpmOptions(output, { flagsOnly: false }); - for (const { long, short, desc } of allOptions) { - const alreadyDefined = cmd.optionsRaw?.get?.(long); - if (!alreadyDefined) { - const handler = pnpmOptionHandlers[long]; - if (handler) { - cmd.option(long, desc, handler, short); - } else { - cmd.option(long, desc, short); - } - } + for (const { long, short, desc } of options) { + const exists = cmd.optionsRaw?.get?.(long); + if (exists) continue; + + const handler = pnpmOptionHandlers[long]; + if (handler) cmd.option(long, desc, handler, short); + else cmd.option(long, desc, short); } - // Special case: add reporter option manually since it doesn't match standard pattern + // edge case: reporter sometimes doesn’t match standard row pattern if (output.includes('--reporter') && !cmd.optionsRaw?.get?.('reporter')) { const handler = pnpmOptionHandlers['reporter']; - if (handler) { + if (handler) cmd.option('reporter', 'Output reporter for pnpm commands', handler); - } } } @@ -302,18 +271,12 @@ export async function setupPnpmCompletions( completion: PackageManagerCompletion ): Promise { try { - const commandsWithDescriptions = await getPnpmCommandsFromMainHelp(); - - for (const [command, description] of Object.entries( - commandsWithDescriptions - )) { - const cmd = completion.command(command, description); - - // Setup common argument patterns - setupCommandArguments(cmd, command, 'pnpm'); + const commands = await getPnpmCommandsFromMainHelp(); - // Setup lazy option loading - setupLazyOptionLoading(cmd, command, 'pnpm', loadPnpmOptionsSync); + for (const [command, description] of Object.entries(commands)) { + const c = completion.command(command, description); + setupCommandArguments(c, command, 'pnpm'); + setupLazyOptionLoading(c, command, 'pnpm', loadPnpmOptionsSync); } - } catch (_err) {} + } catch {} } From 26ed7774782185bdf09cbe1a1a60113133c566af Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Fri, 12 Sep 2025 00:25:53 +0330 Subject: [PATCH 06/10] clean bun handler --- bin/handlers/bun-handler.ts | 167 ++++++++++++++++-------------------- 1 file changed, 72 insertions(+), 95 deletions(-) diff --git a/bin/handlers/bun-handler.ts b/bin/handlers/bun-handler.ts index 75e8251..673cf92 100644 --- a/bin/handlers/bun-handler.ts +++ b/bin/handlers/bun-handler.ts @@ -8,7 +8,6 @@ import { setupCommandArguments, safeExec, safeExecSync, - // createLogLevelHandler, } from '../utils/shared.js'; const COMMANDS_SECTION_RE = /^Commands:\s*$/i; @@ -22,39 +21,51 @@ const CONTINUATION_COMMAND_RE = /^\s{12,}([a-z][a-z0-9-]*)\s+(.+)$/; const EMPTY_LINE_FOLLOWED_BY_NON_COMMAND_RE = /^\s+[a-z]/; const DESCRIPTION_SPLIT_RE = /\s{2,}/; const CAPITAL_LETTER_START_RE = /^[A-Z]/; +const LINE_SPLIT_RE = /\r?\n/; + +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 bunOptionHandlers: OptionHandlers = { ...commonOptionHandlers, - silent: function (complete) { + silent(complete) { complete('true', 'Enable silent mode'); complete('false', 'Disable silent mode'); }, - backend: function (complete) { + backend(complete) { complete('clonefile', 'Clone files (default, fastest)'); complete('hardlink', 'Use hard links'); complete('symlink', 'Use symbolic links'); complete('copyfile', 'Copy files'); }, - linker: function (complete) { + linker(complete) { complete('isolated', 'Isolated linker strategy'); complete('hoisted', 'Hoisted linker strategy'); }, - omit: function (complete) { + omit(complete) { complete('dev', 'Omit devDependencies'); complete('optional', 'Omit optionalDependencies'); complete('peer', 'Omit peerDependencies'); }, - shell: function (complete) { + shell(complete) { complete('bun', 'Use Bun shell'); complete('system', 'Use system shell'); }, - 'unhandled-rejections': function (complete) { + 'unhandled-rejections'(complete) { complete('strict', 'Strict unhandled rejection handling'); complete('throw', 'Throw on unhandled rejections'); complete('warn', 'Warn on unhandled rejections'); @@ -63,51 +74,44 @@ const bunOptionHandlers: OptionHandlers = { }, }; +/** ---------- Commands ---------- */ export function parseBunHelp(helpText: string): Record { - const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/); - - let startIndex = -1; - for (let i = 0; i < helpLines.length; i++) { - if (COMMANDS_SECTION_RE.test(helpLines[i].trim())) { - startIndex = i + 1; - break; - } - } + const lines = toLines(helpText); + const startIndex = findSectionStart(lines, COMMANDS_SECTION_RE); if (startIndex === -1) return {}; const commands: Record = {}; - // parse bun's unique command format - for (let i = startIndex; i < helpLines.length; i++) { - const line = helpLines[i]; + for (let i = startIndex; i < lines.length; i++) { + const line = lines[i]; // stop when we hit Flags section or empty line followed by non-command content if ( FLAGS_SECTION_RE.test(line.trim()) || (line.trim() === '' && - i + 1 < helpLines.length && - !helpLines[i + 1].match(EMPTY_LINE_FOLLOWED_BY_NON_COMMAND_RE)) - ) + i + 1 < lines.length && + !lines[i + 1].match(EMPTY_LINE_FOLLOWED_BY_NON_COMMAND_RE)) + ) { break; + } - // Skip empty lines - if (line.trim() === '') continue; + if (!line.trim()) continue; - const mainCommandMatch = line.match(MAIN_COMMAND_RE); - if (mainCommandMatch) { - const [, command, rest] = mainCommandMatch; + // main command row + const main = line.match(MAIN_COMMAND_RE); + if (main) { + const [, command, rest] = main; if (COMMAND_VALIDATION_RE.test(command)) { const parts = rest.split(DESCRIPTION_SPLIT_RE); - let description = parts[parts.length - 1]; + let desc = parts[parts.length - 1]; - // If the last part starts with capital letter, it's likely the description - if (description && CAPITAL_LETTER_START_RE.test(description)) { - commands[command] = description.trim(); + if (desc && CAPITAL_LETTER_START_RE.test(desc)) { + commands[command] = desc.trim(); } else if (parts.length > 1) { - for (const part of parts) { - if (CAPITAL_LETTER_START_RE.test(part)) { - commands[command] = part.trim(); + for (const p of parts) { + if (CAPITAL_LETTER_START_RE.test(p)) { + commands[command] = p.trim(); break; } } @@ -115,9 +119,9 @@ export function parseBunHelp(helpText: string): Record { } } - const continuationMatch = line.match(CONTINUATION_COMMAND_RE); - if (continuationMatch) { - const [, command, description] = continuationMatch; + const cont = line.match(CONTINUATION_COMMAND_RE); + if (cont) { + const [, command, description] = cont; if (COMMAND_VALIDATION_RE.test(command)) { commands[command] = description.trim(); } @@ -134,71 +138,50 @@ export async function getBunCommandsFromMainHelp(): Promise< return output ? parseBunHelp(output) : {}; } -// Parse bun options from help text export function parseBunOptions( helpText: string, { flagsOnly = true }: { flagsOnly?: boolean } = {} ): ParsedOption[] { - const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/); - const optionsOut: ParsedOption[] = []; - - // Find the Flags: section - let optionsStartIndex = -1; - for (let i = 0; i < helpLines.length; i++) { - if (FLAGS_SECTION_RE.test(helpLines[i].trim())) { - optionsStartIndex = i + 1; - break; - } - } - - if (optionsStartIndex === -1) return []; + const lines = toLines(helpText); + const out: ParsedOption[] = []; - // Parse bun's flag format - for (let i = optionsStartIndex; i < helpLines.length; i++) { - const line = helpLines[i]; + const start = findSectionStart(lines, FLAGS_SECTION_RE); + if (start === -1) return out; - // Stop at examples or other sections + for (let i = start; i < lines.length; i++) { + const line = lines[i]; if (SECTION_END_RE.test(line.trim())) break; - // Parse option lines like: " -c, --config= Specify path to config file" - const optionMatch = line.match(BUN_OPTION_RE); + const m = line.match(BUN_OPTION_RE); + if (!m) continue; - if (optionMatch) { - const [, short, long, desc] = optionMatch; + const [, short, long, desc] = m; + const takesValue = line.includes('=<'); // bun shows value as --opt= + if (flagsOnly && takesValue) continue; - // Check if this option takes a value (has =) - const takesValue = line.includes('=<'); - - if (flagsOnly && takesValue) continue; - - optionsOut.push({ - short: short || undefined, - long, - desc: desc.trim(), - }); - } + out.push({ + short: short || undefined, + long, + desc: desc.trim(), + }); } - return optionsOut; + return out; } -// load dynamic options synchronously when requested function loadBunOptionsSync(cmd: LazyCommand, command: string): void { const output = safeExecSync(`bun ${command} --help`); if (!output) return; - const allOptions = parseBunOptions(output, { flagsOnly: false }); + const options = parseBunOptions(output, { flagsOnly: false }); - for (const { long, short, desc } of allOptions) { - const alreadyDefined = cmd.optionsRaw?.get?.(long); - if (!alreadyDefined) { - const handler = bunOptionHandlers[long]; - if (handler) { - cmd.option(long, desc, handler, short); - } else { - cmd.option(long, desc, short); - } - } + for (const { long, short, desc } of options) { + const exists = cmd.optionsRaw?.get?.(long); + if (exists) continue; + + const handler = bunOptionHandlers[long]; + if (handler) cmd.option(long, desc, handler, short); + else cmd.option(long, desc, short); } } @@ -206,18 +189,12 @@ export async function setupBunCompletions( completion: PackageManagerCompletion ): Promise { try { - const commandsWithDescriptions = await getBunCommandsFromMainHelp(); - - for (const [command, description] of Object.entries( - commandsWithDescriptions - )) { - const cmd = completion.command(command, description); - - // Setup common argument patterns - setupCommandArguments(cmd, command, 'bun'); + const commands = await getBunCommandsFromMainHelp(); - // Setup lazy option loading - setupLazyOptionLoading(cmd, command, 'bun', loadBunOptionsSync); + for (const [command, description] of Object.entries(commands)) { + const c = completion.command(command, description); + setupCommandArguments(c, command, 'bun'); + setupLazyOptionLoading(c, command, 'bun', loadBunOptionsSync); } - } catch (_err) {} + } catch {} } From 58c492433b9c35fc9ec6e1cbd6b596a165067826 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Sat, 13 Sep 2025 13:07:58 +0330 Subject: [PATCH 07/10] update --- bin/handlers/bun-handler.ts | 197 +----------------------------------- 1 file changed, 1 insertion(+), 196 deletions(-) diff --git a/bin/handlers/bun-handler.ts b/bin/handlers/bun-handler.ts index 673cf92..a0614b8 100644 --- a/bin/handlers/bun-handler.ts +++ b/bin/handlers/bun-handler.ts @@ -1,200 +1,5 @@ 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 COMMANDS_SECTION_RE = /^Commands:\s*$/i; -const FLAGS_SECTION_RE = /^Flags:\s*$/i; -const SECTION_END_RE = /^(Examples|Full documentation|Learn more)/i; -const COMMAND_VALIDATION_RE = /^[a-z][a-z0-9-]*$/; -const BUN_OPTION_RE = - /^\s*(?:-([a-zA-Z]),?\s*)?--([a-z][a-z0-9-]*)(?:=<[^>]+>)?\s+(.+)$/; -const MAIN_COMMAND_RE = /^ ([a-z][a-z0-9-]*)\s+(.+)$/; -const CONTINUATION_COMMAND_RE = /^\s{12,}([a-z][a-z0-9-]*)\s+(.+)$/; -const EMPTY_LINE_FOLLOWED_BY_NON_COMMAND_RE = /^\s+[a-z]/; -const DESCRIPTION_SPLIT_RE = /\s{2,}/; -const CAPITAL_LETTER_START_RE = /^[A-Z]/; -const LINE_SPLIT_RE = /\r?\n/; - -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 bunOptionHandlers: OptionHandlers = { - ...commonOptionHandlers, - - silent(complete) { - complete('true', 'Enable silent mode'); - complete('false', 'Disable silent mode'); - }, - - backend(complete) { - complete('clonefile', 'Clone files (default, fastest)'); - complete('hardlink', 'Use hard links'); - complete('symlink', 'Use symbolic links'); - complete('copyfile', 'Copy files'); - }, - - linker(complete) { - complete('isolated', 'Isolated linker strategy'); - complete('hoisted', 'Hoisted linker strategy'); - }, - - omit(complete) { - complete('dev', 'Omit devDependencies'); - complete('optional', 'Omit optionalDependencies'); - complete('peer', 'Omit peerDependencies'); - }, - - shell(complete) { - complete('bun', 'Use Bun shell'); - complete('system', 'Use system shell'); - }, - - 'unhandled-rejections'(complete) { - complete('strict', 'Strict unhandled rejection handling'); - complete('throw', 'Throw on unhandled rejections'); - complete('warn', 'Warn on unhandled rejections'); - complete('none', 'Ignore unhandled rejections'); - complete('warn-with-error-code', 'Warn with error code'); - }, -}; - -/** ---------- Commands ---------- */ -export function parseBunHelp(helpText: string): Record { - const lines = toLines(helpText); - - const startIndex = findSectionStart(lines, COMMANDS_SECTION_RE); - if (startIndex === -1) return {}; - - const commands: Record = {}; - - for (let i = startIndex; i < lines.length; i++) { - const line = lines[i]; - - // stop when we hit Flags section or empty line followed by non-command content - if ( - FLAGS_SECTION_RE.test(line.trim()) || - (line.trim() === '' && - i + 1 < lines.length && - !lines[i + 1].match(EMPTY_LINE_FOLLOWED_BY_NON_COMMAND_RE)) - ) { - break; - } - - if (!line.trim()) continue; - - // main command row - const main = line.match(MAIN_COMMAND_RE); - if (main) { - const [, command, rest] = main; - if (COMMAND_VALIDATION_RE.test(command)) { - const parts = rest.split(DESCRIPTION_SPLIT_RE); - let desc = parts[parts.length - 1]; - - if (desc && CAPITAL_LETTER_START_RE.test(desc)) { - commands[command] = desc.trim(); - } else if (parts.length > 1) { - for (const p of parts) { - if (CAPITAL_LETTER_START_RE.test(p)) { - commands[command] = p.trim(); - break; - } - } - } - } - } - - const cont = line.match(CONTINUATION_COMMAND_RE); - if (cont) { - const [, command, description] = cont; - if (COMMAND_VALIDATION_RE.test(command)) { - commands[command] = description.trim(); - } - } - } - - return commands; -} - -export async function getBunCommandsFromMainHelp(): Promise< - Record -> { - const output = await safeExec('bun --help'); - return output ? parseBunHelp(output) : {}; -} - -export function parseBunOptions( - helpText: string, - { flagsOnly = true }: { flagsOnly?: boolean } = {} -): ParsedOption[] { - const lines = toLines(helpText); - const out: ParsedOption[] = []; - - const start = findSectionStart(lines, FLAGS_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(BUN_OPTION_RE); - if (!m) continue; - - const [, short, long, desc] = m; - const takesValue = line.includes('=<'); // bun shows value as --opt= - if (flagsOnly && takesValue) continue; - - out.push({ - short: short || undefined, - long, - desc: desc.trim(), - }); - } - - return out; -} - -function loadBunOptionsSync(cmd: LazyCommand, command: string): void { - const output = safeExecSync(`bun ${command} --help`); - if (!output) return; - - const options = parseBunOptions(output, { flagsOnly: false }); - - for (const { long, short, desc } of options) { - const exists = cmd.optionsRaw?.get?.(long); - if (exists) continue; - - const handler = bunOptionHandlers[long]; - if (handler) cmd.option(long, desc, handler, short); - else cmd.option(long, desc, short); - } -} export async function setupBunCompletions( completion: PackageManagerCompletion -): Promise { - try { - const commands = await getBunCommandsFromMainHelp(); - - for (const [command, description] of Object.entries(commands)) { - const c = completion.command(command, description); - setupCommandArguments(c, command, 'bun'); - setupLazyOptionLoading(c, command, 'bun', loadBunOptionsSync); - } - } catch {} -} +): Promise { } \ No newline at end of file From a5e22a220dbf89e5d78e9c07b12302a39bdad2c5 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Sat, 13 Sep 2025 13:09:45 +0330 Subject: [PATCH 08/10] update --- bin/handlers/bun-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/handlers/bun-handler.ts b/bin/handlers/bun-handler.ts index a0614b8..225b049 100644 --- a/bin/handlers/bun-handler.ts +++ b/bin/handlers/bun-handler.ts @@ -2,4 +2,4 @@ import type { PackageManagerCompletion } from '../package-manager-completion.js' export async function setupBunCompletions( completion: PackageManagerCompletion -): Promise { } \ No newline at end of file +): Promise {} From b0501531da4c72cd7a15aa9d5ede62134cfeb1a3 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Sat, 13 Sep 2025 13:13:07 +0330 Subject: [PATCH 09/10] ci From fab13cabbcfefb6534e060936677e0de77157dae Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Sat, 13 Sep 2025 13:59:32 +0330 Subject: [PATCH 10/10] update --- bin/handlers/npm-handler.ts | 35 +- bin/handlers/pnpm-handler.ts | 97 ++++-- bin/utils/shared.ts | 7 - completion-debug.log | 652 +++++++++++++++++++++++++++++++++++ 4 files changed, 729 insertions(+), 62 deletions(-) create mode 100644 completion-debug.log diff --git a/bin/handlers/npm-handler.ts b/bin/handlers/npm-handler.ts index e2ed05b..2c1f6ab 100644 --- a/bin/handlers/npm-handler.ts +++ b/bin/handlers/npm-handler.ts @@ -59,41 +59,12 @@ const npmOptionHandlers: OptionHandlers = { 'install-strategy': listHandler( ['hoisted', 'nested', 'shallow', 'linked'], - (v) => - ( - ({ - hoisted: 'Hoist all dependencies to top level', - nested: 'Nested node_modules structure', - shallow: 'Shallow dependency installation', - linked: 'Use linked dependencies', - }) as Record - )[v] ?? ' ' + () => ' ' ), - omit: listHandler( - ['dev', 'optional', 'peer'], - (v) => - ( - ({ - dev: 'Omit devDependencies', - optional: 'Omit optionalDependencies', - peer: 'Omit peerDependencies', - }) as Record - )[v] ?? ' ' - ), + omit: listHandler(['dev', 'optional', 'peer'], () => ' '), - include: listHandler( - ['prod', 'dev', 'optional', 'peer'], - (v) => - ( - ({ - prod: 'Include production deps', - dev: 'Include dev deps', - optional: 'Include optional deps', - peer: 'Include peer deps', - }) as Record - )[v] ?? ' ' - ), + include: listHandler(['prod', 'dev', 'optional', 'peer'], () => ' '), }; export function parseNpmHelp(helpText: string): Record { diff --git a/bin/handlers/pnpm-handler.ts b/bin/handlers/pnpm-handler.ts index 53a2a45..b3502bf 100644 --- a/bin/handlers/pnpm-handler.ts +++ b/bin/handlers/pnpm-handler.ts @@ -23,10 +23,13 @@ import { const OPTIONS_SECTION_RE = /^\s*Options:/i; const LEVEL_MATCH_RE = /(?:levels?|options?|values?)[^:]*:\s*([^.]+)/i; const LINE_SPLIT_RE = /\r?\n/; -const REPORTER_LINE_RE = /^\s*--reporter\s+\w/; -const REPORTER_MATCH_RE = /^\s*--reporter\s+(\w+(?:-\w+)*)\s+(.+)$/; -const SILENT_REPORTER_RE = /-s,\s*--silent,\s*--reporter\s+silent\s+(.+)/; const COMMA_SPACE_SPLIT_RE = /[,\s]+/; +const OPTION_WITH_VALUE_RE = + /^\s*(?:-\w,?\s*)?--(\w+(?:-\w+)*)\s+(\w+(?:-\w+)*)\s+(.+)$/; +const OPTION_ALIAS_RE = + /^\s*-\w,?\s*--\w+(?:,\s*--(\w+(?:-\w+)*)\s+(\w+(?:-\w+)*))?\s+(.+)$/; +const CONTINUATION_LINE_RE = /^\s{20,}/; +const SECTION_HEADER_RE = /^\s*[A-Z][^:]*:\s*$/; function toLines(text: string): string[] { return stripAnsiEscapes(text).split(LINE_SPLIT_RE); @@ -60,29 +63,74 @@ function extractValidValuesFromHelp( optionName: string ): Array<{ value: string; desc: string }> { const lines = toLines(helpText); + const results: Array<{ value: string; desc: string }> = []; - // edge case: reporter often appears as multiple lines - if (optionName === 'reporter') { - const out: Array<{ value: string; desc: string }> = []; - - for (const line of lines) { - if (line.includes('--reporter') && REPORTER_LINE_RE.test(line)) { - const match = line.match(REPORTER_MATCH_RE); - if (match) { - const [, value, desc] = match; - out.push({ value, desc: desc.trim() }); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Look for options with values in any section + const optionMatch = line.match(OPTION_WITH_VALUE_RE); + if (optionMatch) { + const [, option, value, initialDesc] = optionMatch; + if (option === optionName) { + // capture continuation lines for complete description + let fullDesc = initialDesc.trim(); + let j = i + 1; + + // Look ahead for continuation lines (indented lines that don't start new options) + while (j < lines.length) { + const nextLine = lines[j]; + const isIndented = CONTINUATION_LINE_RE.test(nextLine); + const isNewOption = + OPTION_WITH_VALUE_RE.test(nextLine) || + OPTION_ALIAS_RE.test(nextLine); + const isEmptyOrSection = + !nextLine.trim() || SECTION_HEADER_RE.test(nextLine); + + if (isIndented && !isNewOption && !isEmptyOrSection) { + fullDesc += ' ' + nextLine.trim(); + j++; + } else { + break; + } } - } - const silent = line.match(SILENT_REPORTER_RE); - if (silent && !out.some((r) => r.value === 'silent')) { - out.push({ value: 'silent', desc: silent[1].trim() }); + results.push({ value, desc: fullDesc }); } } - if (out.length) return out; + const aliasMatch = line.match(OPTION_ALIAS_RE); + if (aliasMatch) { + const [, option, value, initialDesc] = aliasMatch; + if (option === optionName && value) { + // capture continuation lines for alias descriptions too + let fullDesc = initialDesc.trim(); + let j = i + 1; + + while (j < lines.length) { + const nextLine = lines[j]; + const isIndented = CONTINUATION_LINE_RE.test(nextLine); + const isNewOption = + OPTION_WITH_VALUE_RE.test(nextLine) || + OPTION_ALIAS_RE.test(nextLine); + const isEmptyOrSection = + !nextLine.trim() || SECTION_HEADER_RE.test(nextLine); + + if (isIndented && !isNewOption && !isEmptyOrSection) { + fullDesc += ' ' + nextLine.trim(); + j++; + } else { + break; + } + } + + results.push({ value, desc: fullDesc }); + } + } } + if (results.length) return results; + for (let i = 0; i < lines.length; i++) { const ln = lines[i]; if (ln.includes(`--${optionName}`) || ln.includes(`${optionName}:`)) { @@ -259,11 +307,14 @@ function loadPnpmOptionsSync(cmd: LazyCommand, command: string): void { else cmd.option(long, desc, short); } - // edge case: reporter sometimes doesn’t match standard row pattern - if (output.includes('--reporter') && !cmd.optionsRaw?.get?.('reporter')) { - const handler = pnpmOptionHandlers['reporter']; - if (handler) - cmd.option('reporter', 'Output reporter for pnpm commands', handler); + // Register options found by general algorithm but not in standard parsing + for (const [optionName, handler] of Object.entries(pnpmOptionHandlers)) { + if (!cmd.optionsRaw?.get?.(optionName)) { + const values = extractValidValuesFromHelp(output, optionName); + if (values.length > 0) { + cmd.option(optionName, ' ', handler); + } + } } } diff --git a/bin/utils/shared.ts b/bin/utils/shared.ts index 981ace4..30f749a 100644 --- a/bin/utils/shared.ts +++ b/bin/utils/shared.ts @@ -26,13 +26,6 @@ export const commonOptionHandlers: OptionHandlers = { workspace(complete) { const patterns = getWorkspacePatterns(); patterns.forEach((p) => complete(p, `Workspace pattern: ${p}`)); - complete('packages/*', 'All packages in packages directory'); - complete('apps/*', 'All apps in apps directory'); - }, - - registry(complete) { - complete('https://registry.npmjs.org/', 'Official npm registry'); - complete('https://registry.npmmirror.com/', 'npm China mirror'); }, }; diff --git a/completion-debug.log b/completion-debug.log new file mode 100644 index 0000000..7d5988a --- /dev/null +++ b/completion-debug.log @@ -0,0 +1,652 @@ + +========= starting completion logic ========== +CURRENT: 2, words[*]: pnpm +Truncated words[*]: pnpm , +lastParam: , lastChar: +Adding extra empty parameter +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- '' '' +completion output: add Installs a package and any packages that it depends on. By default, any new package is installed as a prod dependency +import Generates a pnpm-lock.yaml from an npm package-lock.json (or npm-shrinkwrap.json) file +i Install all dependencies for a project +install Install all dependencies for a project +it Runs a pnpm install followed immediately by a pnpm test +install-test Runs a pnpm install followed immediately by a pnpm test +ln Connect the local project to another one +link Connect the local project to another one +prune Removes extraneous packages +rb Rebuild a package +rebuild Rebuild a package +rm Removes packages from node_modules and from the project's package.json +remove Removes packages from node_modules and from the project's package.json +unlink Unlinks a package. Like yarn unlink but pnpm re-installs the dependency after removing the external link +up Updates packages to their latest version based on the specified range +update Updates packages to their latest version based on the specified range +audit Checks for known security issues with the installed packages +licenses Check licenses in consumed packages +ls Print all the versions of packages that are installed, as well as their dependencies, in a tree-structure +list Print all the versions of packages that are installed, as well as their dependencies, in a tree-structure +outdated Check for outdated packages +exec Executes a shell command in scope of a project +run Runs a defined package script +start Runs an arbitrary command specified in the package's "start" property of its "scripts" object +t Runs a package's "test" script, if one was provided +test Runs a package's "test" script, if one was provided +cat-file Prints the contents of a file based on the hash value stored in the index file +cat-index Prints the index file of a specific package from the store +find-hash Experimental! Lists the packages that include the file with the specified hash. +pack Create a tarball from a package +publish Publishes a package to the registry +root Prints the effective modules directory +store Adds new packages to the pnpm store directly. Does not modify any projects or files outside the store +:4 +last line: :4 +directive: 4 +completions: add Installs a package and any packages that it depends on. By default, any new package is installed as a prod dependency +import Generates a pnpm-lock.yaml from an npm package-lock.json (or npm-shrinkwrap.json) file +i Install all dependencies for a project +install Install all dependencies for a project +it Runs a pnpm install followed immediately by a pnpm test +install-test Runs a pnpm install followed immediately by a pnpm test +ln Connect the local project to another one +link Connect the local project to another one +prune Removes extraneous packages +rb Rebuild a package +rebuild Rebuild a package +rm Removes packages from node_modules and from the project's package.json +remove Removes packages from node_modules and from the project's package.json +unlink Unlinks a package. Like yarn unlink but pnpm re-installs the dependency after removing the external link +up Updates packages to their latest version based on the specified range +update Updates packages to their latest version based on the specified range +audit Checks for known security issues with the installed packages +licenses Check licenses in consumed packages +ls Print all the versions of packages that are installed, as well as their dependencies, in a tree-structure +list Print all the versions of packages that are installed, as well as their dependencies, in a tree-structure +outdated Check for outdated packages +exec Executes a shell command in scope of a project +run Runs a defined package script +start Runs an arbitrary command specified in the package's "start" property of its "scripts" object +t Runs a package's "test" script, if one was provided +test Runs a package's "test" script, if one was provided +cat-file Prints the contents of a file based on the hash value stored in the index file +cat-index Prints the index file of a specific package from the store +find-hash Experimental! Lists the packages that include the file with the specified hash. +pack Create a tarball from a package +publish Publishes a package to the registry +root Prints the effective modules directory +store Adds new packages to the pnpm store directly. Does not modify any projects or files outside the store +flagPrefix: +Adding completion: add:Installs a package and any packages that it depends on. By default, any new package is installed as a prod dependency +Adding completion: import:Generates a pnpm-lock.yaml from an npm package-lock.json (or npm-shrinkwrap.json) file +Adding completion: i:Install all dependencies for a project +Adding completion: install:Install all dependencies for a project +Adding completion: it:Runs a pnpm install followed immediately by a pnpm test +Adding completion: install-test:Runs a pnpm install followed immediately by a pnpm test +Adding completion: ln:Connect the local project to another one +Adding completion: link:Connect the local project to another one +Adding completion: prune:Removes extraneous packages +Adding completion: rb:Rebuild a package +Adding completion: rebuild:Rebuild a package +Adding completion: rm:Removes packages from node_modules and from the project's package.json +Adding completion: remove:Removes packages from node_modules and from the project's package.json +Adding completion: unlink:Unlinks a package. Like yarn unlink but pnpm re-installs the dependency after removing the external link +Adding completion: up:Updates packages to their latest version based on the specified range +Adding completion: update:Updates packages to their latest version based on the specified range +Adding completion: audit:Checks for known security issues with the installed packages +Adding completion: licenses:Check licenses in consumed packages +Adding completion: ls:Print all the versions of packages that are installed, as well as their dependencies, in a tree-structure +Adding completion: list:Print all the versions of packages that are installed, as well as their dependencies, in a tree-structure +Adding completion: outdated:Check for outdated packages +Adding completion: exec:Executes a shell command in scope of a project +Adding completion: run:Runs a defined package script +Adding completion: start:Runs an arbitrary command specified in the package's "start" property of its "scripts" object +Adding completion: t:Runs a package's "test" script, if one was provided +Adding completion: test:Runs a package's "test" script, if one was provided +Adding completion: cat-file:Prints the contents of a file based on the hash value stored in the index file +Adding completion: cat-index:Prints the index file of a specific package from the store +Adding completion: find-hash:Experimental! Lists the packages that include the file with the specified hash. +Adding completion: pack:Create a tarball from a package +Adding completion: publish:Publishes a package to the registry +Adding completion: root:Prints the effective modules directory +Adding completion: store:Adds new packages to the pnpm store directly. Does not modify any projects or files outside the store +Calling _describe +_describe found some completions + +========= starting completion logic ========== +CURRENT: 2, words[*]: pnpm ins +Truncated words[*]: pnpm ins, +lastParam: ins, lastChar: s +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- ins +completion output: install Install all dependencies for a project +install-test Runs a pnpm install followed immediately by a pnpm test +:4 +last line: :4 +directive: 4 +completions: install Install all dependencies for a project +install-test Runs a pnpm install followed immediately by a pnpm test +flagPrefix: +Adding completion: install:Install all dependencies for a project +Adding completion: install-test:Runs a pnpm install followed immediately by a pnpm test +Calling _describe +_describe found some completions + +========= starting completion logic ========== +CURRENT: 3, words[*]: pnpm install -- +Truncated words[*]: pnpm install --, +lastParam: --, lastChar: - +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install -- +completion output: --aggregate-output Aggregate output from child processes that are run in parallel, and only print output when child process is finished. It makes reading large logs after running `pnpm recursive` with `--parallel` or with `--workspace-concurrency` much easier (especially on CI). Only `--reporter=append-only` is supported. +--child-concurrency Controls the number of child processes run parallelly to build node modules +--dev Only `devDependencies` are installed +--dir Change to directory (default: /Users/amir/Desktop/projects/tab) +--fix-lockfile Fix broken lockfile entries automatically +--force Force reinstall dependencies: refetch packages modified in store, recreate a lockfile and/or modules directory created by a non-compatible version of pnpm. Install all optionalDependencies even they don't satisfy the current environment(cpu, os, arch) +--global-dir Specify a custom directory to store global packages +--help Output usage information +--hoist-pattern Hoist all dependencies matching the pattern to `node_modules/.pnpm/node_modules`. The default pattern is * and matches everything. Hoisted packages can be required by any dependencies, so it is an emulation of a flat node_modules +--ignore-pnpmfile Disable pnpm hooks defined in .pnpmfile.cjs +--ignore-scripts Don't run lifecycle scripts +--ignore-workspace Ignore pnpm-workspace.yaml if exists in the parent directory, and treat the installation as normal non-workspace installation. +--lockfile-dir The directory in which the pnpm-lock.yaml of the package will be created. Several projects may share a single lockfile. +--lockfile-only Dependencies are not downloaded. Only `pnpm-lock.yaml` is updated +--loglevel What level of logs to report. Any logs at or higher than the given level will be shown. Levels (lowest to highest): debug, info, warn, error. Or use "--silent" to turn off all logging. +--merge-git-branch-lockfiles Merge lockfiles were generated on git branch +--modules-dir The directory in which dependencies will be installed (instead of node_modules) +--network-concurrency Maximum number of concurrent network requests +--no-hoist Dependencies inside the modules directory will have access only to their listed dependencies +--no-lockfile Don't read or generate a `pnpm-lock.yaml` file +--no-optional `optionalDependencies` are not installed +--offline Trigger an error if any required dependencies are not available in local store +--optimistic-repeat-install Skip reinstall if the workspace state is up-to-date selected method depends from the file system the store +--prefer-frozen-lockfile If the available `pnpm-lock.yaml` satisfies the `package.json` then perform a headless installation +--prefer-offline Skip staleness checks for cached data, but request missing data from the server +--prod Packages in `devDependencies` won't be installed +--public-hoist-pattern Hoist all dependencies matching the pattern to the root of the modules directory +--recursive Run installation recursively in every package found in subdirectories. For options that may be used with `-r`, see "pnpm help recursive" +--resolution-only Re-runs resolution: useful for printing out peer dependency issues +--shamefully-hoist All the subdeps will be hoisted into the root node_modules. Your code will have access to them +--side-effects-cache Use or cache the results of (pre/post)install hooks +--side-effects-cache-readonly Only use the side effects cache if present, do not create it for new packages +--store-dir The directory in which all the packages are saved on the disk +--stream Stream output from child processes immediately, prefixed with the originating package directory. This allows output from different packages to be interleaved. +--strict-peer-dependencies Fail on missing or invalid peer dependencies +--use-running-store-server Only allows installation with a store server. If no store server is running, installation will fail +--use-stderr Divert all output to stderr +--use-store-server Starts a store server in the background. The store server will keep running after installation is done. To stop the store server, run `pnpm server stop` +--virtual-store-dir The directory with links to the store (default is node_modules/.pnpm). All direct and indirect dependencies of the project are linked into this directory +--workspace-root Run the command on the root workspace project +--changed-files-ignore-pattern Defines files to ignore when filtering for changed projects since the specified commit/branch. Usage example: pnpm pattern="**/README.md" build +--fail-if-no-match If no projects are matched by the command, exit with exit code 1 (fail) \! in zsh), it means the packages matching the selector must be excluded. E.g., "pnpm packages except "foo" under the current working directory indirect dependents of the matched packages without including the matched packages themselves. ^ must be doubled at the Windows Command Prompt. E.g.: ...^foo (...^^foo in Command Prompt) dependents of the matched packages. E.g.: ...foo, "...@bar/*" inside a given subdirectory. E.g.: ./components since the specified commit/branch. E.g.: "[master]", "[HEAD~2]". It may be used together with "...". So, for instance, "...[HEAD~1]" selects all packages changed in the last commit and their dependents under the specified directory. It may be used with "..." to select dependents/dependencies as well. It also may be combined with "[]". For instance, all changed projects inside a directory: "{packages}[origin/master]" +--filter Restricts the scope to package names matching the given pattern. E.g.: foo, "@bar/*" dependencies of the matched packages. E.g.: foo... indirect dependencies of the matched packages without including the matched packages themselves. ^ must be doubled at the Windows Command Prompt. E.g.: foo^... (foo^^... in Command Prompt) +--filter-prod Restricts the scope to package names matching the given pattern similar to --filter, but it ignores devDependencies when searching for dependencies and dependents. +--test-pattern Defines files related to tests. Useful with the changed since filter. When selecting only changed packages and their dependent packages, the dependent packages will be ignored in case a package has changes only in tests. Usage example: pnpm +--reporter Output reporter for pnpm commands +:4 +last line: :4 +directive: 4 +completions: --aggregate-output Aggregate output from child processes that are run in parallel, and only print output when child process is finished. It makes reading large logs after running `pnpm recursive` with `--parallel` or with `--workspace-concurrency` much easier (especially on CI). Only `--reporter=append-only` is supported. +--child-concurrency Controls the number of child processes run parallelly to build node modules +--dev Only `devDependencies` are installed +--dir Change to directory (default: /Users/amir/Desktop/projects/tab) +--fix-lockfile Fix broken lockfile entries automatically +--force Force reinstall dependencies: refetch packages modified in store, recreate a lockfile and/or modules directory created by a non-compatible version of pnpm. Install all optionalDependencies even they don't satisfy the current environment(cpu, os, arch) +--global-dir Specify a custom directory to store global packages +--help Output usage information +--hoist-pattern Hoist all dependencies matching the pattern to `node_modules/.pnpm/node_modules`. The default pattern is * and matches everything. Hoisted packages can be required by any dependencies, so it is an emulation of a flat node_modules +--ignore-pnpmfile Disable pnpm hooks defined in .pnpmfile.cjs +--ignore-scripts Don't run lifecycle scripts +--ignore-workspace Ignore pnpm-workspace.yaml if exists in the parent directory, and treat the installation as normal non-workspace installation. +--lockfile-dir The directory in which the pnpm-lock.yaml of the package will be created. Several projects may share a single lockfile. +--lockfile-only Dependencies are not downloaded. Only `pnpm-lock.yaml` is updated +--loglevel What level of logs to report. Any logs at or higher than the given level will be shown. Levels (lowest to highest): debug, info, warn, error. Or use "--silent" to turn off all logging. +--merge-git-branch-lockfiles Merge lockfiles were generated on git branch +--modules-dir The directory in which dependencies will be installed (instead of node_modules) +--network-concurrency Maximum number of concurrent network requests +--no-hoist Dependencies inside the modules directory will have access only to their listed dependencies +--no-lockfile Don't read or generate a `pnpm-lock.yaml` file +--no-optional `optionalDependencies` are not installed +--offline Trigger an error if any required dependencies are not available in local store +--optimistic-repeat-install Skip reinstall if the workspace state is up-to-date selected method depends from the file system the store +--prefer-frozen-lockfile If the available `pnpm-lock.yaml` satisfies the `package.json` then perform a headless installation +--prefer-offline Skip staleness checks for cached data, but request missing data from the server +--prod Packages in `devDependencies` won't be installed +--public-hoist-pattern Hoist all dependencies matching the pattern to the root of the modules directory +--recursive Run installation recursively in every package found in subdirectories. For options that may be used with `-r`, see "pnpm help recursive" +--resolution-only Re-runs resolution: useful for printing out peer dependency issues +--shamefully-hoist All the subdeps will be hoisted into the root node_modules. Your code will have access to them +--side-effects-cache Use or cache the results of (pre/post)install hooks +--side-effects-cache-readonly Only use the side effects cache if present, do not create it for new packages +--store-dir The directory in which all the packages are saved on the disk +--stream Stream output from child processes immediately, prefixed with the originating package directory. This allows output from different packages to be interleaved. +--strict-peer-dependencies Fail on missing or invalid peer dependencies +--use-running-store-server Only allows installation with a store server. If no store server is running, installation will fail +--use-stderr Divert all output to stderr +--use-store-server Starts a store server in the background. The store server will keep running after installation is done. To stop the store server, run `pnpm server stop` +--virtual-store-dir The directory with links to the store (default is node_modules/.pnpm). All direct and indirect dependencies of the project are linked into this directory +--workspace-root Run the command on the root workspace project +--changed-files-ignore-pattern Defines files to ignore when filtering for changed projects since the specified commit/branch. Usage example: pnpm pattern="**/README.md" build +--fail-if-no-match If no projects are matched by the command, exit with exit code 1 (fail) \! in zsh), it means the packages matching the selector must be excluded. E.g., "pnpm packages except "foo" under the current working directory indirect dependents of the matched packages without including the matched packages themselves. ^ must be doubled at the Windows Command Prompt. E.g.: ...^foo (...^^foo in Command Prompt) dependents of the matched packages. E.g.: ...foo, "...@bar/*" inside a given subdirectory. E.g.: ./components since the specified commit/branch. E.g.: "[master]", "[HEAD~2]". It may be used together with "...". So, for instance, "...[HEAD~1]" selects all packages changed in the last commit and their dependents under the specified directory. It may be used with "..." to select dependents/dependencies as well. It also may be combined with "[]". For instance, all changed projects inside a directory: "{packages}[origin/master]" +--filter Restricts the scope to package names matching the given pattern. E.g.: foo, "@bar/*" dependencies of the matched packages. E.g.: foo... indirect dependencies of the matched packages without including the matched packages themselves. ^ must be doubled at the Windows Command Prompt. E.g.: foo^... (foo^^... in Command Prompt) +--filter-prod Restricts the scope to package names matching the given pattern similar to --filter, but it ignores devDependencies when searching for dependencies and dependents. +--test-pattern Defines files related to tests. Useful with the changed since filter. When selecting only changed packages and their dependent packages, the dependent packages will be ignored in case a package has changes only in tests. Usage example: pnpm +--reporter Output reporter for pnpm commands +flagPrefix: +Adding completion: --aggregate-output:Aggregate output from child processes that are run in parallel, and only print output when child process is finished. It makes reading large logs after running `pnpm recursive` with `--parallel` or with `--workspace-concurrency` much easier (especially on CI). Only `--reporter=append-only` is supported. +Adding completion: --child-concurrency:Controls the number of child processes run parallelly to build node modules +Adding completion: --dev:Only `devDependencies` are installed +Adding completion: --dir:Change to directory (default: /Users/amir/Desktop/projects/tab) +Adding completion: --fix-lockfile:Fix broken lockfile entries automatically +Adding completion: --force:Force reinstall dependencies: refetch packages modified in store, recreate a lockfile and/or modules directory created by a non-compatible version of pnpm. Install all optionalDependencies even they don't satisfy the current environment(cpu, os, arch) +Adding completion: --global-dir:Specify a custom directory to store global packages +Adding completion: --help:Output usage information +Adding completion: --hoist-pattern:Hoist all dependencies matching the pattern to `node_modules/.pnpm/node_modules`. The default pattern is * and matches everything. Hoisted packages can be required by any dependencies, so it is an emulation of a flat node_modules +Adding completion: --ignore-pnpmfile:Disable pnpm hooks defined in .pnpmfile.cjs +Adding completion: --ignore-scripts:Don't run lifecycle scripts +Adding completion: --ignore-workspace:Ignore pnpm-workspace.yaml if exists in the parent directory, and treat the installation as normal non-workspace installation. +Adding completion: --lockfile-dir:The directory in which the pnpm-lock.yaml of the package will be created. Several projects may share a single lockfile. +Adding completion: --lockfile-only:Dependencies are not downloaded. Only `pnpm-lock.yaml` is updated +Adding completion: --loglevel:What level of logs to report. Any logs at or higher than the given level will be shown. Levels (lowest to highest): debug, info, warn, error. Or use "--silent" to turn off all logging. +Adding completion: --merge-git-branch-lockfiles:Merge lockfiles were generated on git branch +Adding completion: --modules-dir:The directory in which dependencies will be installed (instead of node_modules) +Adding completion: --network-concurrency:Maximum number of concurrent network requests +Adding completion: --no-hoist:Dependencies inside the modules directory will have access only to their listed dependencies +Adding completion: --no-lockfile:Don't read or generate a `pnpm-lock.yaml` file +Adding completion: --no-optional:`optionalDependencies` are not installed +Adding completion: --offline:Trigger an error if any required dependencies are not available in local store +Adding completion: --optimistic-repeat-install:Skip reinstall if the workspace state is up-to-date selected method depends from the file system the store +Adding completion: --prefer-frozen-lockfile:If the available `pnpm-lock.yaml` satisfies the `package.json` then perform a headless installation +Adding completion: --prefer-offline:Skip staleness checks for cached data, but request missing data from the server +Adding completion: --prod:Packages in `devDependencies` won't be installed +Adding completion: --public-hoist-pattern:Hoist all dependencies matching the pattern to the root of the modules directory +Adding completion: --recursive:Run installation recursively in every package found in subdirectories. For options that may be used with `-r`, see "pnpm help recursive" +Adding completion: --resolution-only:Re-runs resolution: useful for printing out peer dependency issues +Adding completion: --shamefully-hoist:All the subdeps will be hoisted into the root node_modules. Your code will have access to them +Adding completion: --side-effects-cache:Use or cache the results of (pre/post)install hooks +Adding completion: --side-effects-cache-readonly:Only use the side effects cache if present, do not create it for new packages +Adding completion: --store-dir:The directory in which all the packages are saved on the disk +Adding completion: --stream:Stream output from child processes immediately, prefixed with the originating package directory. This allows output from different packages to be interleaved. +Adding completion: --strict-peer-dependencies:Fail on missing or invalid peer dependencies +Adding completion: --use-running-store-server:Only allows installation with a store server. If no store server is running, installation will fail +Adding completion: --use-stderr:Divert all output to stderr +Adding completion: --use-store-server:Starts a store server in the background. The store server will keep running after installation is done. To stop the store server, run `pnpm server stop` +Adding completion: --virtual-store-dir:The directory with links to the store (default is node_modules/.pnpm). All direct and indirect dependencies of the project are linked into this directory +Adding completion: --workspace-root:Run the command on the root workspace project +Adding completion: --changed-files-ignore-pattern:Defines files to ignore when filtering for changed projects since the specified commit/branch. Usage example: pnpm pattern="**/README.md" build +Adding completion: --fail-if-no-match:If no projects are matched by the command, exit with exit code 1 (fail) \! in zsh), it means the packages matching the selector must be excluded. E.g., "pnpm packages except "foo" under the current working directory indirect dependents of the matched packages without including the matched packages themselves. ^ must be doubled at the Windows Command Prompt. E.g.: ...^foo (...^^foo in Command Prompt) dependents of the matched packages. E.g.: ...foo, "...@bar/*" inside a given subdirectory. E.g.: ./components since the specified commit/branch. E.g.: "[master]", "[HEAD~2]". It may be used together with "...". So, for instance, "...[HEAD~1]" selects all packages changed in the last commit and their dependents under the specified directory. It may be used with "..." to select dependents/dependencies as well. It also may be combined with "[]". For instance, all changed projects inside a directory: "{packages}[origin/master]" +Adding completion: --filter:Restricts the scope to package names matching the given pattern. E.g.: foo, "@bar/*" dependencies of the matched packages. E.g.: foo... indirect dependencies of the matched packages without including the matched packages themselves. ^ must be doubled at the Windows Command Prompt. E.g.: foo^... (foo^^... in Command Prompt) +Adding completion: --filter-prod:Restricts the scope to package names matching the given pattern similar to --filter, but it ignores devDependencies when searching for dependencies and dependents. +Adding completion: --test-pattern:Defines files related to tests. Useful with the changed since filter. When selecting only changed packages and their dependent packages, the dependent packages will be ignored in case a package has changes only in tests. Usage example: pnpm +Adding completion: --reporter:Output reporter for pnpm commands +Calling _describe +_describe found some completions + +========= starting completion logic ========== +CURRENT: 3, words[*]: pnpm install --h +Truncated words[*]: pnpm install --h, +lastParam: --h, lastChar: h +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --h +completion output: --help Output usage information +--hoist-pattern Hoist all dependencies matching the pattern to `node_modules/.pnpm/node_modules`. The default pattern is * and matches everything. Hoisted packages can be required by any dependencies, so it is an emulation of a flat node_modules +:4 +last line: :4 +directive: 4 +completions: --help Output usage information +--hoist-pattern Hoist all dependencies matching the pattern to `node_modules/.pnpm/node_modules`. The default pattern is * and matches everything. Hoisted packages can be required by any dependencies, so it is an emulation of a flat node_modules +flagPrefix: +Adding completion: --help:Output usage information +Adding completion: --hoist-pattern:Hoist all dependencies matching the pattern to `node_modules/.pnpm/node_modules`. The default pattern is * and matches everything. Hoisted packages can be required by any dependencies, so it is an emulation of a flat node_modules +Calling _describe +_describe found some completions + +========= starting completion logic ========== +CURRENT: 3, words[*]: pnpm install --h +Truncated words[*]: pnpm install --h, +lastParam: --h, lastChar: h +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --h +completion output: --help Output usage information +--hoist-pattern Hoist all dependencies matching the pattern to `node_modules/.pnpm/node_modules`. The default pattern is * and matches everything. Hoisted packages can be required by any dependencies, so it is an emulation of a flat node_modules +:4 +last line: :4 +directive: 4 +completions: --help Output usage information +--hoist-pattern Hoist all dependencies matching the pattern to `node_modules/.pnpm/node_modules`. The default pattern is * and matches everything. Hoisted packages can be required by any dependencies, so it is an emulation of a flat node_modules +flagPrefix: +Adding completion: --help:Output usage information +Adding completion: --hoist-pattern:Hoist all dependencies matching the pattern to `node_modules/.pnpm/node_modules`. The default pattern is * and matches everything. Hoisted packages can be required by any dependencies, so it is an emulation of a flat node_modules +Calling _describe +_describe found some completions + +========= starting completion logic ========== +CURRENT: 4, words[*]: pnpm install --hoist-pattern +Truncated words[*]: pnpm install --hoist-pattern , +lastParam: , lastChar: +Adding extra empty parameter +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --hoist-pattern '' '' +completion output: :4 +last line: :4 +directive: 4 +completions: +flagPrefix: +Calling _describe +_describe did not find completions. +Checking if we should do file completion. +deactivating file completion + +========= starting completion logic ========== +CURRENT: 4, words[*]: pnpm install --hoist-pattern +Truncated words[*]: pnpm install --hoist-pattern , +lastParam: , lastChar: +Adding extra empty parameter +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --hoist-pattern '' '' +completion output: :4 +last line: :4 +directive: 4 +completions: +flagPrefix: +Calling _describe +_describe did not find completions. +Checking if we should do file completion. +deactivating file completion + +========= starting completion logic ========== +CURRENT: 4, words[*]: pnpm install --hoist-pattern +Truncated words[*]: pnpm install --hoist-pattern , +lastParam: , lastChar: +Adding extra empty parameter +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --hoist-pattern '' '' +completion output: :4 +last line: :4 +directive: 4 +completions: +flagPrefix: +Calling _describe +_describe did not find completions. +Checking if we should do file completion. +deactivating file completion + +========= starting completion logic ========== +CURRENT: 4, words[*]: pnpm install --hoist-pattern +Truncated words[*]: pnpm install --hoist-pattern , +lastParam: , lastChar: +Adding extra empty parameter +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --hoist-pattern '' '' +completion output: :4 +last line: :4 +directive: 4 +completions: +flagPrefix: +Calling _describe +_describe did not find completions. +Checking if we should do file completion. +deactivating file completion + +========= starting completion logic ========== +CURRENT: 4, words[*]: pnpm install --hoist-pattern +Truncated words[*]: pnpm install --hoist-pattern , +lastParam: , lastChar: +Adding extra empty parameter +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --hoist-pattern '' '' +completion output: :4 +last line: :4 +directive: 4 +completions: +flagPrefix: +Calling _describe +_describe did not find completions. +Checking if we should do file completion. +deactivating file completion + +========= starting completion logic ========== +CURRENT: 4, words[*]: pnpm install --hoist-pattern +Truncated words[*]: pnpm install --hoist-pattern , +lastParam: , lastChar: +Adding extra empty parameter +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --hoist-pattern '' '' +completion output: :4 +last line: :4 +directive: 4 +completions: +flagPrefix: +Calling _describe +_describe did not find completions. +Checking if we should do file completion. +deactivating file completion + +========= starting completion logic ========== +CURRENT: 4, words[*]: pnpm install --hoist-pattern +Truncated words[*]: pnpm install --hoist-pattern , +lastParam: , lastChar: +Adding extra empty parameter +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --hoist-pattern '' '' +completion output: :4 +last line: :4 +directive: 4 +completions: +flagPrefix: +Calling _describe +_describe did not find completions. +Checking if we should do file completion. +deactivating file completion + +========= starting completion logic ========== +CURRENT: 4, words[*]: pnpm install --hoist-pattern +Truncated words[*]: pnpm install --hoist-pattern , +lastParam: , lastChar: +Adding extra empty parameter +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --hoist-pattern '' '' +completion output: :4 +last line: :4 +directive: 4 +completions: +flagPrefix: +Calling _describe +_describe did not find completions. +Checking if we should do file completion. +deactivating file completion + +========= starting completion logic ========== +CURRENT: 4, words[*]: pnpm install --hoist-pattern +Truncated words[*]: pnpm install --hoist-pattern , +lastParam: , lastChar: +Adding extra empty parameter +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --hoist-pattern '' '' +completion output: :4 +last line: :4 +directive: 4 +completions: +flagPrefix: +Calling _describe +_describe did not find completions. +Checking if we should do file completion. +deactivating file completion + +========= starting completion logic ========== +CURRENT: 4, words[*]: pnpm install --hoist-pattern +Truncated words[*]: pnpm install --hoist-pattern , +lastParam: , lastChar: +Adding extra empty parameter +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --hoist-pattern '' '' +completion output: :4 +last line: :4 +directive: 4 +completions: +flagPrefix: +Calling _describe +_describe did not find completions. +Checking if we should do file completion. +deactivating file completion + +========= starting completion logic ========== +CURRENT: 4, words[*]: pnpm install --hoist-pattern +Truncated words[*]: pnpm install --hoist-pattern , +lastParam: , lastChar: +Adding extra empty parameter +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --hoist-pattern '' '' +completion output: :4 +last line: :4 +directive: 4 +completions: +flagPrefix: +Calling _describe +_describe did not find completions. +Checking if we should do file completion. +deactivating file completion + +========= starting completion logic ========== +CURRENT: 4, words[*]: pnpm install --hoist-pattern +Truncated words[*]: pnpm install --hoist-pattern , +lastParam: , lastChar: +Adding extra empty parameter +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --hoist-pattern '' '' +completion output: :4 +last line: :4 +directive: 4 +completions: +flagPrefix: +Calling _describe +_describe did not find completions. +Checking if we should do file completion. +deactivating file completion + +========= starting completion logic ========== +CURRENT: 3, words[*]: pnpm install --hoist-pattern= +Truncated words[*]: pnpm install --hoist-pattern=, +lastParam: --hoist-pattern=, lastChar: = +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --hoist-pattern= +completion output: :4 +last line: :4 +directive: 4 +completions: +flagPrefix: -P --hoist-pattern= +Calling _describe +_describe did not find completions. +Checking if we should do file completion. +deactivating file completion + +========= starting completion logic ========== +CURRENT: 3, words[*]: pnpm install --hoist-pattern= +Truncated words[*]: pnpm install --hoist-pattern=, +lastParam: --hoist-pattern=, lastChar: = +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --hoist-pattern= +completion output: :4 +last line: :4 +directive: 4 +completions: +flagPrefix: -P --hoist-pattern= +Calling _describe +_describe did not find completions. +Checking if we should do file completion. +deactivating file completion + +========= starting completion logic ========== +CURRENT: 3, words[*]: pnpm install --hoist-pattern= +Truncated words[*]: pnpm install --hoist-pattern=, +lastParam: --hoist-pattern=, lastChar: = +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --hoist-pattern= +completion output: :4 +last line: :4 +directive: 4 +completions: +flagPrefix: -P --hoist-pattern= +Calling _describe +_describe did not find completions. +Checking if we should do file completion. +deactivating file completion + +========= starting completion logic ========== +CURRENT: 3, words[*]: pnpm install --r +Truncated words[*]: pnpm install --r, +lastParam: --r, lastChar: r +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --r +completion output: --recursive Run installation recursively in every package found in subdirectories. For options that may be used with `-r`, see "pnpm help recursive" +--resolution-only Re-runs resolution: useful for printing out peer dependency issues +--reporter Output reporter for pnpm commands +:4 +last line: :4 +directive: 4 +completions: --recursive Run installation recursively in every package found in subdirectories. For options that may be used with `-r`, see "pnpm help recursive" +--resolution-only Re-runs resolution: useful for printing out peer dependency issues +--reporter Output reporter for pnpm commands +flagPrefix: +Adding completion: --recursive:Run installation recursively in every package found in subdirectories. For options that may be used with `-r`, see "pnpm help recursive" +Adding completion: --resolution-only:Re-runs resolution: useful for printing out peer dependency issues +Adding completion: --reporter:Output reporter for pnpm commands +Calling _describe +_describe found some completions + +========= starting completion logic ========== +CURRENT: 3, words[*]: pnpm install --re +Truncated words[*]: pnpm install --re, +lastParam: --re, lastChar: e +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --re +completion output: --recursive Run installation recursively in every package found in subdirectories. For options that may be used with `-r`, see "pnpm help recursive" +--resolution-only Re-runs resolution: useful for printing out peer dependency issues +--reporter Output reporter for pnpm commands +:4 +last line: :4 +directive: 4 +completions: --recursive Run installation recursively in every package found in subdirectories. For options that may be used with `-r`, see "pnpm help recursive" +--resolution-only Re-runs resolution: useful for printing out peer dependency issues +--reporter Output reporter for pnpm commands +flagPrefix: +Adding completion: --recursive:Run installation recursively in every package found in subdirectories. For options that may be used with `-r`, see "pnpm help recursive" +Adding completion: --resolution-only:Re-runs resolution: useful for printing out peer dependency issues +Adding completion: --reporter:Output reporter for pnpm commands +Calling _describe +_describe found some completions + +========= starting completion logic ========== +CURRENT: 3, words[*]: pnpm install --re +Truncated words[*]: pnpm install --re, +lastParam: --re, lastChar: e +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- install --re +completion output: --recursive Run installation recursively in every package found in subdirectories. For options that may be used with `-r`, see "pnpm help recursive" +--resolution-only Re-runs resolution: useful for printing out peer dependency issues +--reporter Output reporter for pnpm commands +:4 +last line: :4 +directive: 4 +completions: --recursive Run installation recursively in every package found in subdirectories. For options that may be used with `-r`, see "pnpm help recursive" +--resolution-only Re-runs resolution: useful for printing out peer dependency issues +--reporter Output reporter for pnpm commands +flagPrefix: +Adding completion: --recursive:Run installation recursively in every package found in subdirectories. For options that may be used with `-r`, see "pnpm help recursive" +Adding completion: --resolution-only:Re-runs resolution: useful for printing out peer dependency issues +Adding completion: --reporter:Output reporter for pnpm commands +Calling _describe +_describe found some completions + +========= starting completion logic ========== +CURRENT: 3, words[*]: pnpm add --r +Truncated words[*]: pnpm add --r, +lastParam: --r, lastChar: r +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- add --r +completion output: --recursive Run installation recursively in every package found in subdirectories or in every workspace package, when executed inside a workspace. For options that may be used with `-r`, see "pnpm help recursive" +--reporter Output reporter for pnpm commands +:4 +last line: :4 +directive: 4 +completions: --recursive Run installation recursively in every package found in subdirectories or in every workspace package, when executed inside a workspace. For options that may be used with `-r`, see "pnpm help recursive" +--reporter Output reporter for pnpm commands +flagPrefix: +Adding completion: --recursive:Run installation recursively in every package found in subdirectories or in every workspace package, when executed inside a workspace. For options that may be used with `-r`, see "pnpm help recursive" +Adding completion: --reporter:Output reporter for pnpm commands +Calling _describe +_describe found some completions + +========= starting completion logic ========== +CURRENT: 3, words[*]: pnpm add --re +Truncated words[*]: pnpm add --re, +lastParam: --re, lastChar: e +About to call: eval node /Users/amir/Desktop/projects/tab/dist/bin/cli.js pnpm complete -- add --re +completion output: --recursive Run installation recursively in every package found in subdirectories or in every workspace package, when executed inside a workspace. For options that may be used with `-r`, see "pnpm help recursive" +--reporter Output reporter for pnpm commands +:4 +last line: :4 +directive: 4 +completions: --recursive Run installation recursively in every package found in subdirectories or in every workspace package, when executed inside a workspace. For options that may be used with `-r`, see "pnpm help recursive" +--reporter Output reporter for pnpm commands +flagPrefix: +Adding completion: --recursive:Run installation recursively in every package found in subdirectories or in every workspace package, when executed inside a workspace. For options that may be used with `-r`, see "pnpm help recursive" +Adding completion: --reporter:Output reporter for pnpm commands +Calling _describe +_describe found some completions