source dump of claude code
at main 145 lines 5.1 kB view raw
1/** 2 * Utility for substituting $ARGUMENTS placeholders in skill/command prompts. 3 * 4 * Supports: 5 * - $ARGUMENTS - replaced with the full arguments string 6 * - $ARGUMENTS[0], $ARGUMENTS[1], etc. - replaced with individual indexed arguments 7 * - $0, $1, etc. - shorthand for $ARGUMENTS[0], $ARGUMENTS[1] 8 * - Named arguments (e.g., $foo, $bar) - when argument names are defined in frontmatter 9 * 10 * Arguments are parsed using shell-quote for proper shell argument handling. 11 */ 12 13import { tryParseShellCommand } from './bash/shellQuote.js' 14 15/** 16 * Parse an arguments string into an array of individual arguments. 17 * Uses shell-quote for proper shell argument parsing including quoted strings. 18 * 19 * Examples: 20 * - "foo bar baz" => ["foo", "bar", "baz"] 21 * - 'foo "hello world" baz' => ["foo", "hello world", "baz"] 22 * - "foo 'hello world' baz" => ["foo", "hello world", "baz"] 23 */ 24export function parseArguments(args: string): string[] { 25 if (!args || !args.trim()) { 26 return [] 27 } 28 29 // Return $KEY to preserve variable syntax literally (don't expand variables) 30 const result = tryParseShellCommand(args, key => `$${key}`) 31 if (!result.success) { 32 // Fall back to simple whitespace split if parsing fails 33 return args.split(/\s+/).filter(Boolean) 34 } 35 36 // Filter to only string tokens (ignore shell operators, etc.) 37 return result.tokens.filter( 38 (token): token is string => typeof token === 'string', 39 ) 40} 41 42/** 43 * Parse argument names from the frontmatter 'arguments' field. 44 * Accepts either a space-separated string or an array of strings. 45 * 46 * Examples: 47 * - "foo bar baz" => ["foo", "bar", "baz"] 48 * - ["foo", "bar", "baz"] => ["foo", "bar", "baz"] 49 */ 50export function parseArgumentNames( 51 argumentNames: string | string[] | undefined, 52): string[] { 53 if (!argumentNames) { 54 return [] 55 } 56 57 // Filter out empty strings and numeric-only names (which conflict with $0, $1 shorthand) 58 const isValidName = (name: string): boolean => 59 typeof name === 'string' && name.trim() !== '' && !/^\d+$/.test(name) 60 61 if (Array.isArray(argumentNames)) { 62 return argumentNames.filter(isValidName) 63 } 64 if (typeof argumentNames === 'string') { 65 return argumentNames.split(/\s+/).filter(isValidName) 66 } 67 return [] 68} 69 70/** 71 * Generate argument hint showing remaining unfilled args. 72 * @param argNames - Array of argument names from frontmatter 73 * @param typedArgs - Arguments the user has typed so far 74 * @returns Hint string like "[arg2] [arg3]" or undefined if all filled 75 */ 76export function generateProgressiveArgumentHint( 77 argNames: string[], 78 typedArgs: string[], 79): string | undefined { 80 const remaining = argNames.slice(typedArgs.length) 81 if (remaining.length === 0) return undefined 82 return remaining.map(name => `[${name}]`).join(' ') 83} 84 85/** 86 * Substitute $ARGUMENTS placeholders in content with actual argument values. 87 * 88 * @param content - The content containing placeholders 89 * @param args - The raw arguments string (may be undefined/null) 90 * @param appendIfNoPlaceholder - If true and no placeholders are found, appends "ARGUMENTS: {args}" to content 91 * @param argumentNames - Optional array of named arguments (e.g., ["foo", "bar"]) that map to indexed positions 92 * @returns The content with placeholders substituted 93 */ 94export function substituteArguments( 95 content: string, 96 args: string | undefined, 97 appendIfNoPlaceholder = true, 98 argumentNames: string[] = [], 99): string { 100 // undefined/null means no args provided - return content unchanged 101 // empty string is a valid input that should replace placeholders with empty 102 if (args === undefined || args === null) { 103 return content 104 } 105 106 const parsedArgs = parseArguments(args) 107 const originalContent = content 108 109 // Replace named arguments (e.g., $foo, $bar) with their values 110 // Named arguments map to positions: argumentNames[0] -> parsedArgs[0], etc. 111 for (let i = 0; i < argumentNames.length; i++) { 112 const name = argumentNames[i] 113 if (!name) continue 114 115 // Match $name but not $name[...] or $nameXxx (word chars) 116 // Also ensure we match word boundaries to avoid partial matches 117 content = content.replace( 118 new RegExp(`\\$${name}(?![\\[\\w])`, 'g'), 119 parsedArgs[i] ?? '', 120 ) 121 } 122 123 // Replace indexed arguments ($ARGUMENTS[0], $ARGUMENTS[1], etc.) 124 content = content.replace(/\$ARGUMENTS\[(\d+)\]/g, (_, indexStr: string) => { 125 const index = parseInt(indexStr, 10) 126 return parsedArgs[index] ?? '' 127 }) 128 129 // Replace shorthand indexed arguments ($0, $1, etc.) 130 content = content.replace(/\$(\d+)(?!\w)/g, (_, indexStr: string) => { 131 const index = parseInt(indexStr, 10) 132 return parsedArgs[index] ?? '' 133 }) 134 135 // Replace $ARGUMENTS with the full arguments string 136 content = content.replaceAll('$ARGUMENTS', args) 137 138 // If no placeholders were found and appendIfNoPlaceholder is true, append 139 // But only if args is non-empty (empty string means command invoked with no args) 140 if (content === originalContent && appendIfNoPlaceholder && args) { 141 content = content + `\n\nARGUMENTS: ${args}` 142 } 143 144 return content 145}