///|
fn nonnegative_int(value : String, field : String) -> Int32 raise ConfigError {
if value.is_empty() {
raise InvalidValue(field)
}
let mut result = 0
for c in value {
if c < '0' || c > '9' {
raise InvalidValue(field)
}
let digit = c.to_int() - 48
if result > (2147483647 - digit) / 10 {
raise InvalidValue(field)
}
result = result * 10 + digit
}
result
}
///|
fn nonnegative_size(value : String, field : String) -> Int64 raise ConfigError {
if value.is_empty() {
raise InvalidValue(field)
}
let mut result = 0L
for c in value {
if c < '0' || c > '9' {
raise InvalidValue(field)
}
let digit = (c.to_int() - 48).to_int64()
if result > (9007199254740991L - digit) / 10 {
raise InvalidValue(field)
}
result = result * 10 + digit
}
result
}
///|
fn cli_positive_int(value : String, field : String) -> Int32 raise ConfigError {
let result = nonnegative_int(value, field)
if result == 0 {
raise InvalidValue(field)
}
result
}
///|
fn csv_patterns(value : String) -> Array[String] raise ConfigError {
let result : Array[String] = []
let mut start = 0
let mut braces = 0
// 1: class start; 2: after !/^; 3: ordinary class members.
let mut class_state = 0
for i in 0.. 0 {
if c == ']' && class_state == 3 {
class_state = 0
} else if class_state == 1 && (c == '!' || c == '^') {
class_state = 2
} else {
class_state = 3
}
continue
}
match c {
'[' => class_state = 1
'{' => braces += 1
'}' => {
if braces == 0 {
raise InvalidValue("glob")
}
braces -= 1
}
',' if braces == 0 => {
let pattern = value[start:i].to_owned().trim().to_owned()
validate_pattern(pattern)
result.push(pattern)
start = i + 1
}
_ => ()
}
}
if braces != 0 || class_state != 0 {
raise InvalidValue("glob")
}
let pattern = value[start:].to_owned().trim().to_owned()
validate_pattern(pattern)
result.push(pattern)
result
}
///|
fn canonical_option(key : String) -> String {
match key {
"-f" | "--format" => "--style"
"--budget" => "--token-budget"
"--split" => "--split-output"
_ => key
}
}
///|
fn cli_style(value : String) -> Style raise ConfigError {
match value {
"md" => Markdown
"txt" => Plain
_ => parse_style(value)
}
}
///|
fn set_action(current : Action, next : Action) -> Action raise ConfigError {
if (current == Help && next == HelpAll) ||
(current == HelpAll && next == Help) {
return HelpAll
}
if current != Pack && current != next {
raise Conflict("action")
}
next
}
///|
/// Arguments exclude argv[0]. Supports --key=value and -- to end option parsing.
pub fn parse_cli(args : Array[String]) -> Cli raise ConfigError {
let patch = Patch::empty()
let mut directory : String? = None
let mut config_path : String? = None
let mut action = Pack
let mut stdout = false
let mut stdin_mode = StdinMode::Off
let explain_paths : Array[String] = []
let mut report_json = false
let mut mcp = false
let mut webui = false
let mut webui_port = 8765
let mut webui_port_set = false
let webui_other_options : Array[String] = []
let mut mcp_max_tokens = 16000
let mut mcp_max_output_bytes = 16777216
let mut mcp_max_volumes = 64
let mut quiet = false
let mut verbose = false
let mut positional_only = false
let mut index = 0
while index < args.length() {
let arg = args[index]
index += 1
if !positional_only && arg == "--" {
positional_only = true
continue
}
if positional_only || !arg.has_prefix("-") || arg == "-" {
if directory is Some(_) {
raise MultipleDirectories
}
validate_path(arg, "directory")
directory = Some(arg)
continue
}
let (key, inline) = match arg.split_once("=") {
Some((key, value)) => (key.to_owned(), Some(value.to_owned()))
None => (arg, None)
}
let original_key = key
let key = canonical_option(key)
if ![
"--webui", "--webui-port", "--tokenizer-path", "--max-files", "--max-total-size",
"--max-file-size", "-h", "--help", "--help-all", "-v", "--version",
].contains(key) {
webui_other_options.push(key)
}
if key == "--token-count-tree" {
// Like Repomix's optional argument, a following non-option is a threshold.
let threshold = match inline {
Some(value) => nonnegative_int(value, "output.tokenCountTree")
None if index < args.length() && !args[index].has_prefix("-") => {
let value = args[index]
index += 1
nonnegative_int(value, "output.tokenCountTree")
}
None => 0
}
patch.token_count_tree = Some(Enabled(threshold))
continue
}
let takes_value = match key {
"-o"
| "--output"
| "--style"
| "--output-file-path-style"
| "--include"
| "--keep-full"
| "--header-text"
| "--instruction-file-path"
| "-i"
| "--ignore"
| "--token-count-encoding"
| "--token-budget"
| "--split-output"
| "--top-files-len"
| "--include-logs-count"
| "--remote"
| "--remote-branch"
| "--max-git-bytes"
| "--max-files"
| "--max-file-size"
| "--max-total-size"
| "--webui-port"
| "--mcp-max-tokens"
| "--mcp-max-output-bytes"
| "--mcp-max-volumes"
| "--explain-path"
| "-c"
| "--config"
| "--tokenizer-path" => true
_ => false
}
if takes_value {
let value = match inline {
Some(value) => value
None => {
if index >= args.length() ||
(
args[index].has_prefix("-") &&
!((key == "-o" || key == "--output") && args[index] == "-")
) {
raise MissingValue(original_key)
}
let value = args[index]
index += 1
value
}
}
match key {
"-o" | "--output" => {
validate_path(value, "output.filePath")
patch.output = Some(value)
}
"--style" => patch.style = Some(cli_style(value))
"--output-file-path-style" =>
patch.output_file_path_style = Some(parse_path_style(value))
"--include" =>
patch.include_patterns = append_unique(
patch.include_patterns,
csv_patterns(value),
)
"--keep-full" =>
patch.keep_full_patterns = append_unique(
patch.keep_full_patterns,
csv_patterns(value),
)
"--explain-path" => {
validate_path(value, "explain.paths")
if explain_paths.length() >= 256 {
raise InvalidValue("explain.paths")
}
explain_paths.push(value)
}
"--header-text" => patch.header_text = Some(value)
"--instruction-file-path" => patch.instruction_file_path = Some(value)
"-i" | "--ignore" =>
patch.ignore = append_unique(patch.ignore, csv_patterns(value))
"--token-count-encoding" => {
if value != "o200k_base" {
raise InvalidValue("tokenCount.encoding")
}
patch.encoding = Some(value)
}
"--token-budget" =>
patch.token_budget = Some(
cli_positive_int(value, "output.tokenBudget"),
)
"--split-output" =>
patch.split_output = Some(
SplitOutput::Enabled(cli_positive_int(value, "output.splitOutput")),
)
"--top-files-len" =>
patch.top_files_length = Some(
nonnegative_int(value, "output.topFilesLength"),
)
"--include-logs-count" =>
patch.include_logs_count = Some(
cli_positive_int(value, "output.git.includeLogsCount"),
)
"--max-git-bytes" =>
patch.max_git_bytes = Some(
cli_positive_int(value, "input.maxGitBytes"),
)
"--max-file-size" =>
patch.max_file_size = Some(
cli_positive_int(value, "input.maxFileSize"),
)
"--max-files" =>
patch.max_files = Some(nonnegative_int(value, "input.maxFiles"))
"--max-total-size" =>
patch.max_total_size = Some(
nonnegative_size(value, "input.maxTotalSize"),
)
"--webui-port" => {
webui_port = cli_positive_int(value, "webui.port")
webui_port_set = true
if webui_port > 65535 {
raise InvalidValue("webui.port")
}
}
"--mcp-max-tokens" =>
mcp_max_tokens = cli_positive_int(value, "mcp.maxTokens")
"--mcp-max-output-bytes" => {
mcp_max_output_bytes = cli_positive_int(value, "mcp.maxOutputBytes")
if mcp_max_output_bytes > 268435456 {
raise InvalidValue("mcp.maxOutputBytes")
}
}
"--mcp-max-volumes" => {
mcp_max_volumes = cli_positive_int(value, "mcp.maxVolumes")
if mcp_max_volumes > 256 {
raise InvalidValue("mcp.maxVolumes")
}
}
"--remote" => {
validate_path(value, "remote.url")
patch.remote = Some(value)
}
"--remote-branch" => {
validate_path(value, "remote.branch")
patch.remote_branch = Some(value)
}
"-c" | "--config" => {
validate_path(value, "config")
config_path = Some(value)
}
"--tokenizer-path" => {
validate_path(value, "tokenizerPath")
patch.tokenizer_path = Some(value)
}
_ => raise UnknownOption(key)
}
} else {
// Boolean and action flags do not accept an attached value.
if inline is Some(_) {
raise InvalidValue(key)
}
match key {
"--mcp" => mcp = true
"--webui" => webui = true
"--stdout" => stdout = true
"--stdin" => {
if stdin_mode == Nul {
raise Conflict("stdin/stdin0")
}
stdin_mode = Lines
}
"--stdin0" => {
if stdin_mode == Lines {
raise Conflict("stdin/stdin0")
}
stdin_mode = Nul
}
"--report-json" => report_json = true
"--no-report-json" => report_json = false
"--no-split-output" => patch.split_output = Some(SplitOutput::Disabled)
"--quiet" => quiet = true
"--verbose" => verbose = true
"--no-gitignore" => patch.use_gitignore = Some(false)
"--no-dot-ignore" => patch.use_dot_ignore = Some(false)
"--no-default-patterns" => patch.use_default_patterns = Some(false)
"--no-security-check" => patch.security_check = Some(false)
"--compress" => patch.compress = Some(true)
"--no-compress" => patch.compress = Some(false)
"--full-metadata" => patch.full_metadata = Some(true)
"--no-full-metadata" => patch.full_metadata = Some(false)
"--file-summary" => patch.file_summary = Some(true)
"--no-file-summary" => patch.file_summary = Some(false)
"--directory-structure" => patch.directory_structure = Some(true)
"--no-directory-structure" => patch.directory_structure = Some(false)
"--files" => patch.include_files = Some(true)
"--no-files" => patch.include_files = Some(false)
"--git-sections" => patch.git_sections = Some(true)
"--no-git-sections" => patch.git_sections = Some(false)
"--metadata" => patch.metadata = Some(true)
"--no-metadata" => patch.metadata = Some(false)
"--output-show-line-numbers" => patch.show_line_numbers = Some(true)
"--no-output-show-line-numbers" => patch.show_line_numbers = Some(false)
"--include-diffs" => patch.include_diffs = Some(true)
"--no-include-diffs" => patch.include_diffs = Some(false)
"--include-logs" => patch.include_logs = Some(true)
"--no-include-logs" => patch.include_logs = Some(false)
"--parsable-style" => ()
"--init" => action = set_action(action, Init)
"-h" | "--help" => action = set_action(action, Help)
"--help-all" => action = set_action(action, HelpAll)
"-v" | "--version" => action = set_action(action, Version)
_ => raise UnknownOption(key)
}
}
}
if quiet && verbose {
raise Conflict("quiet/verbose")
}
if stdout && patch.output is Some(path) && path != "-" {
raise Conflict("output/stdout")
}
if action == Init && stdin_mode != Off {
raise Conflict("stdin/init")
}
if mcp && (stdin_mode != Off || stdout || report_json || action == Init) {
raise Conflict("mcp")
}
if !explain_paths.is_empty() &&
(
mcp ||
stdin_mode != Off ||
stdout ||
action == Init ||
patch.output is Some(_)
) {
raise Conflict("explain")
}
if webui_port_set && !webui {
raise Conflict("webui.port/webui")
}
if webui && !webui_other_options.is_empty() {
raise Conflict("webui.options")
}
validate_patch(patch)
{
directory: directory.unwrap_or("."),
config_path,
action,
stdout,
stdin_mode,
explain_paths,
report_json,
mcp,
webui,
webui_port,
mcp_max_tokens,
mcp_max_output_bytes,
mcp_max_volumes,
quiet,
verbose,
patch,
}
}