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