prefix.ts
utils/bash/prefix.ts
No strong subsystem tag
205
Lines
6222
Bytes
2
Exports
4
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 205 lines, 4 detected imports, and 2 detected exports.
Important relationships
Detected exports
getCommandPrefixStaticgetCompoundCommandPrefixesStatic
Keywords
argscommandspeccommandprefixprefixresultsubcommandslengthstringsrecursiondepth
Detected imports
../shell/specPrefix.js./commands.js./parser.js./registry.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import { buildPrefix } from '../shell/specPrefix.js'
import { splitCommand_DEPRECATED } from './commands.js'
import { extractCommandArguments, parseCommand } from './parser.js'
import { getCommandSpec } from './registry.js'
const NUMERIC = /^\d+$/
const ENV_VAR = /^[A-Za-z_][A-Za-z0-9_]*=/
// Wrapper commands with complex option handling that can't be expressed in specs
const WRAPPER_COMMANDS = new Set([
'nice', // command position varies based on options
])
const toArray = <T>(val: T | T[]): T[] => (Array.isArray(val) ? val : [val])
// Check if args[0] matches a known subcommand (disambiguates wrapper commands
// that also have subcommands, e.g. the git spec has isCommand args for aliases).
function isKnownSubcommand(
arg: string,
spec: { subcommands?: { name: string | string[] }[] } | null,
): boolean {
if (!spec?.subcommands?.length) return false
return spec.subcommands.some(sub =>
Array.isArray(sub.name) ? sub.name.includes(arg) : sub.name === arg,
)
}
export async function getCommandPrefixStatic(
command: string,
recursionDepth = 0,
wrapperCount = 0,
): Promise<{ commandPrefix: string | null } | null> {
if (wrapperCount > 2 || recursionDepth > 10) return null
const parsed = await parseCommand(command)
if (!parsed) return null
if (!parsed.commandNode) {
return { commandPrefix: null }
}
const { envVars, commandNode } = parsed
const cmdArgs = extractCommandArguments(commandNode)
const [cmd, ...args] = cmdArgs
if (!cmd) return { commandPrefix: null }
// Check if this is a wrapper command by looking at its spec
const spec = await getCommandSpec(cmd)
// Check if this is a wrapper command
let isWrapper =
WRAPPER_COMMANDS.has(cmd) ||
(spec?.args && toArray(spec.args).some(arg => arg?.isCommand))
// Special case: if the command has subcommands and the first arg matches a subcommand,
// treat it as a regular command, not a wrapper
if (isWrapper && args[0] && isKnownSubcommand(args[0], spec)) {
isWrapper = false
}
const prefix = isWrapper
? await handleWrapper(cmd, args, recursionDepth, wrapperCount)
: await buildPrefix(cmd, args, spec)
if (prefix === null && recursionDepth === 0 && isWrapper) {
return null
}
const envPrefix = envVars.length ? `${envVars.join(' ')} ` : ''
return { commandPrefix: prefix ? envPrefix + prefix : null }
}
async function handleWrapper(
command: string,
args: string[],
recursionDepth: number,
wrapperCount: number,
): Promise<string | null> {
const spec = await getCommandSpec(command)
if (spec?.args) {
const commandArgIndex = toArray(spec.args).findIndex(arg => arg?.isCommand)
if (commandArgIndex !== -1) {
const parts = [command]
for (let i = 0; i < args.length && i <= commandArgIndex; i++) {
if (i === commandArgIndex) {
const result = await getCommandPrefixStatic(
args.slice(i).join(' '),
recursionDepth + 1,
wrapperCount + 1,
)
if (result?.commandPrefix) {
parts.push(...result.commandPrefix.split(' '))
return parts.join(' ')
}
break
} else if (
args[i] &&
!args[i]!.startsWith('-') &&
!ENV_VAR.test(args[i]!)
) {
parts.push(args[i]!)
}
}
}
}
const wrapped = args.find(
arg => !arg.startsWith('-') && !NUMERIC.test(arg) && !ENV_VAR.test(arg),
)
if (!wrapped) return command
const result = await getCommandPrefixStatic(
args.slice(args.indexOf(wrapped)).join(' '),
recursionDepth + 1,
wrapperCount + 1,
)
return !result?.commandPrefix ? null : `${command} ${result.commandPrefix}`
}
/**
* Computes prefixes for a compound command (with && / || / ;).
* For single commands, returns a single-element array with the prefix.
*
* For compound commands, computes per-subcommand prefixes and collapses
* them: subcommands sharing a root (first word) are collapsed via
* word-aligned longest common prefix.
*
* @param excludeSubcommand — optional filter; return true for subcommands
* that should be excluded from the prefix suggestion (e.g. read-only
* commands that are already auto-allowed).
*/
export async function getCompoundCommandPrefixesStatic(
command: string,
excludeSubcommand?: (subcommand: string) => boolean,
): Promise<string[]> {
const subcommands = splitCommand_DEPRECATED(command)
if (subcommands.length <= 1) {
const result = await getCommandPrefixStatic(command)
return result?.commandPrefix ? [result.commandPrefix] : []
}
const prefixes: string[] = []
for (const subcmd of subcommands) {
const trimmed = subcmd.trim()
if (excludeSubcommand?.(trimmed)) continue
const result = await getCommandPrefixStatic(trimmed)
if (result?.commandPrefix) {
prefixes.push(result.commandPrefix)
}
}
if (prefixes.length === 0) return []
// Group prefixes by their first word (root command)
const groups = new Map<string, string[]>()
for (const prefix of prefixes) {
const root = prefix.split(' ')[0]!
const group = groups.get(root)
if (group) {
group.push(prefix)
} else {
groups.set(root, [prefix])
}
}
// Collapse each group via word-aligned LCP
const collapsed: string[] = []
for (const [, group] of groups) {
collapsed.push(longestCommonPrefix(group))
}
return collapsed
}
/**
* Compute the longest common prefix of strings, aligned to word boundaries.
* e.g. ["git fetch", "git worktree"] → "git"
* ["npm run test", "npm run lint"] → "npm run"
*/
function longestCommonPrefix(strings: string[]): string {
if (strings.length === 0) return ''
if (strings.length === 1) return strings[0]!
const first = strings[0]!
const words = first.split(' ')
let commonWords = words.length
for (let i = 1; i < strings.length; i++) {
const otherWords = strings[i]!.split(' ')
let shared = 0
while (
shared < commonWords &&
shared < otherWords.length &&
words[shared] === otherWords[shared]
) {
shared++
}
commonWords = shared
}
return words.slice(0, Math.max(1, commonWords)).join(' ')
}