1use std::collections::BTreeMap;
2use std::sync::LazyLock;
3use std::sync::RwLock;
4
5const PERMISSION_MODES: &[&str] = &["safe", "auto", "yolo", "plan"];
6const PROMPT_MODES: &[&str] = &["default", "prepend", "append"];
7const OUTPUT_MODES: &[&str] = &[
8 "text",
9 "stream-text",
10 "json",
11 "stream-json",
12 "formatted",
13 "stream-formatted",
14 "pass-text",
15 "stream-pass-text",
16 "pass-json",
17 "stream-pass-json",
18];
19const KIMI_OUTPUT_MODES: &[&str] = &[
20 "text",
21 "stream-text",
22 "stream-json",
23 "formatted",
24 "stream-formatted",
25 "pass-text",
26 "stream-pass-text",
27 "pass-json",
28 "stream-pass-json",
29];
30const TEXT_OUTPUT_MODES: &[&str] = &["text", "stream-text"];
31
32#[derive(Clone, Debug)]
33pub struct RunnerInfo {
34 pub binary: String,
35 pub extra_args: Vec<String>,
36 pub no_persist_flags: Vec<String>,
37 pub thinking_flags: BTreeMap<i32, Vec<String>>,
38 pub show_thinking_flags: BTreeMap<bool, Vec<String>>,
39 pub default_thinking: Option<i32>,
40 pub yolo_flags: Vec<String>,
41 pub fast_flags: BTreeMap<bool, Vec<String>>,
42 pub provider_flag: String,
43 pub model_flag: String,
44 pub agent_flag: String,
45 pub prompt_flag: String,
46}
47
48#[derive(Clone, Debug, Default)]
49pub struct ParsedArgs {
50 pub runner: Option<String>,
51 pub thinking: Option<i32>,
52 pub show_thinking: Option<bool>,
53 pub print_config: bool,
54 pub sanitize_osc: Option<bool>,
55 pub output_log_path: Option<bool>,
56 pub output_mode: Option<String>,
57 pub forward_unknown_json: bool,
58 pub save_session: bool,
59 pub cleanup_session: bool,
60 pub yolo: bool,
61 pub fast: Option<bool>,
62 pub permission_mode: Option<String>,
63 pub provider: Option<String>,
64 pub model: Option<String>,
65 pub alias: Option<String>,
66 pub prompt: String,
67 pub prompt_supplied: bool,
68 pub timeout_secs: Option<u64>,
69 pub runner_args: Vec<String>,
70}
71
72#[derive(Clone, Debug)]
73pub struct AliasDef {
74 pub runner: Option<String>,
75 pub thinking: Option<i32>,
76 pub show_thinking: Option<bool>,
77 pub sanitize_osc: Option<bool>,
78 pub output_mode: Option<String>,
79 pub provider: Option<String>,
80 pub model: Option<String>,
81 pub agent: Option<String>,
82 pub prompt: Option<String>,
83 pub prompt_mode: Option<String>,
84}
85
86impl Default for AliasDef {
87 fn default() -> Self {
88 Self {
89 runner: None,
90 thinking: None,
91 show_thinking: None,
92 sanitize_osc: None,
93 output_mode: None,
94 provider: None,
95 model: None,
96 agent: None,
97 prompt: None,
98 prompt_mode: None,
99 }
100 }
101}
102
103#[derive(Clone, Debug)]
104pub struct CccConfig {
105 pub default_runner: String,
106 pub default_provider: String,
107 pub default_model: String,
108 pub default_thinking: Option<i32>,
109 pub default_show_thinking: bool,
110 pub default_sanitize_osc: Option<bool>,
111 pub default_output_mode: String,
112 pub update_check: bool,
113 pub auto_update: bool,
114 pub update_interval_hours: u64,
115 pub aliases: BTreeMap<String, AliasDef>,
116 pub abbreviations: BTreeMap<String, String>,
117}
118
119impl Default for CccConfig {
120 fn default() -> Self {
121 Self {
122 default_runner: "oc".to_string(),
123 default_provider: String::new(),
124 default_model: String::new(),
125 default_thinking: Some(1),
126 default_show_thinking: true,
127 default_sanitize_osc: None,
128 default_output_mode: "text".to_string(),
129 update_check: true,
130 auto_update: false,
131 update_interval_hours: 24,
132 aliases: BTreeMap::new(),
133 abbreviations: BTreeMap::new(),
134 }
135 }
136}
137
138#[derive(Clone, Debug, PartialEq, Eq)]
139pub struct OutputPlan {
140 pub runner_name: String,
141 pub mode: String,
142 pub stream: bool,
143 pub formatted: bool,
144 pub schema: Option<String>,
145 pub argv_flags: Vec<String>,
146 pub warnings: Vec<String>,
147}
148
149pub static RUNNER_REGISTRY: LazyLock<RwLock<BTreeMap<String, RunnerInfo>>> = LazyLock::new(|| {
150 let mut m = BTreeMap::new();
151 let opencode = RunnerInfo {
152 binary: "opencode".into(),
153 default_thinking: None,
154 fast_flags: BTreeMap::new(),
155 extra_args: vec!["--pure".into(), "run".into()],
156 no_persist_flags: vec![],
157 thinking_flags: BTreeMap::new(),
158 show_thinking_flags: {
159 let mut tf = BTreeMap::new();
160 tf.insert(true, vec!["--thinking".into()]);
161 tf
162 },
163 yolo_flags: vec![],
164 provider_flag: String::new(),
165 model_flag: "--model".into(),
166 agent_flag: "--agent".into(),
167 prompt_flag: String::new(),
168 };
169 let claude = RunnerInfo {
170 binary: "claude".into(),
171 default_thinking: None,
172 fast_flags: BTreeMap::new(),
173 extra_args: vec!["-p".into()],
174 no_persist_flags: vec!["--no-session-persistence".into()],
175 thinking_flags: {
176 let mut tf = BTreeMap::new();
177 tf.insert(0, vec!["--thinking".into(), "disabled".into()]);
178 tf.insert(
179 1,
180 vec![
181 "--thinking".into(),
182 "enabled".into(),
183 "--effort".into(),
184 "low".into(),
185 ],
186 );
187 tf.insert(
188 2,
189 vec![
190 "--thinking".into(),
191 "enabled".into(),
192 "--effort".into(),
193 "medium".into(),
194 ],
195 );
196 tf.insert(
197 3,
198 vec![
199 "--thinking".into(),
200 "enabled".into(),
201 "--effort".into(),
202 "high".into(),
203 ],
204 );
205 tf.insert(
206 4,
207 vec![
208 "--thinking".into(),
209 "enabled".into(),
210 "--effort".into(),
211 "max".into(),
212 ],
213 );
214 tf
215 },
216 show_thinking_flags: {
217 let mut tf = BTreeMap::new();
218 tf.insert(
219 true,
220 vec![
221 "--thinking".into(),
222 "enabled".into(),
223 "--effort".into(),
224 "low".into(),
225 ],
226 );
227 tf
228 },
229 yolo_flags: vec!["--dangerously-skip-permissions".into()],
230 provider_flag: String::new(),
231 model_flag: "--model".into(),
232 agent_flag: "--agent".into(),
233 prompt_flag: String::new(),
234 };
235 let kimi = RunnerInfo {
236 binary: "kimi".into(),
237 default_thinking: None,
238 fast_flags: BTreeMap::new(),
239 extra_args: vec![],
240 no_persist_flags: vec![],
241 thinking_flags: {
242 let mut tf = BTreeMap::new();
243 tf.insert(0, vec!["--no-thinking".into()]);
244 tf.insert(1, vec!["--thinking".into()]);
245 tf.insert(2, vec!["--thinking".into()]);
246 tf.insert(3, vec!["--thinking".into()]);
247 tf.insert(4, vec!["--thinking".into()]);
248 tf
249 },
250 show_thinking_flags: {
251 let mut tf = BTreeMap::new();
252 tf.insert(true, vec!["--thinking".into()]);
253 tf
254 },
255 yolo_flags: vec!["--yolo".into()],
256 provider_flag: String::new(),
257 model_flag: "--model".into(),
258 agent_flag: "--agent".into(),
259 prompt_flag: "--prompt".into(),
260 };
261 let codex = RunnerInfo {
262 binary: "codex".into(),
263 default_thinking: Some(2),
264 fast_flags: {
265 let mut ff = BTreeMap::new();
266 ff.insert(true, vec!["--enable".into(), "fast_mode".into()]);
267 ff.insert(false, vec!["--disable".into(), "fast_mode".into()]);
268 ff
269 },
270 extra_args: vec!["exec".into()],
271 no_persist_flags: vec!["--ephemeral".into()],
272 thinking_flags: {
273 let mut tf = BTreeMap::new();
274 tf.insert(0, vec!["-c".into(), "model_reasoning_effort=none".into()]);
275 tf.insert(1, vec!["-c".into(), "model_reasoning_effort=low".into()]);
276 tf.insert(2, vec!["-c".into(), "model_reasoning_effort=medium".into()]);
277 tf.insert(3, vec!["-c".into(), "model_reasoning_effort=high".into()]);
278 tf.insert(4, vec!["-c".into(), "model_reasoning_effort=xhigh".into()]);
279 tf.insert(5, vec!["-c".into(), "model_reasoning_effort=max".into()]);
280 tf
281 },
282 show_thinking_flags: BTreeMap::new(),
283 yolo_flags: vec!["--dangerously-bypass-approvals-and-sandbox".into()],
284 provider_flag: String::new(),
285 model_flag: "--model".into(),
286 agent_flag: String::new(),
287 prompt_flag: String::new(),
288 };
289 let roocode = RunnerInfo {
290 binary: "roocode".into(),
291 default_thinking: None,
292 fast_flags: BTreeMap::new(),
293 extra_args: vec![],
294 no_persist_flags: vec![],
295 thinking_flags: BTreeMap::new(),
296 show_thinking_flags: BTreeMap::new(),
297 yolo_flags: vec![],
298 provider_flag: String::new(),
299 model_flag: String::new(),
300 agent_flag: String::new(),
301 prompt_flag: String::new(),
302 };
303 let crush = RunnerInfo {
304 binary: "crush".into(),
305 default_thinking: None,
306 fast_flags: BTreeMap::new(),
307 extra_args: vec!["run".into()],
308 no_persist_flags: vec![],
309 thinking_flags: BTreeMap::new(),
310 show_thinking_flags: BTreeMap::new(),
311 yolo_flags: vec![],
312 provider_flag: String::new(),
313 model_flag: String::new(),
314 agent_flag: String::new(),
315 prompt_flag: String::new(),
316 };
317 let cursor = RunnerInfo {
318 binary: "cursor-agent".into(),
319 default_thinking: None,
320 fast_flags: BTreeMap::new(),
321 extra_args: vec!["--print".into(), "--trust".into()],
322 no_persist_flags: vec![],
323 thinking_flags: BTreeMap::new(),
324 show_thinking_flags: BTreeMap::new(),
325 yolo_flags: vec!["--yolo".into()],
326 provider_flag: String::new(),
327 model_flag: "--model".into(),
328 agent_flag: String::new(),
329 prompt_flag: String::new(),
330 };
331 let gemini = RunnerInfo {
332 binary: "gemini".into(),
333 default_thinking: None,
334 fast_flags: BTreeMap::new(),
335 extra_args: vec![],
336 no_persist_flags: vec![],
337 thinking_flags: BTreeMap::new(),
338 show_thinking_flags: BTreeMap::new(),
339 yolo_flags: vec![],
340 provider_flag: String::new(),
341 model_flag: "--model".into(),
342 agent_flag: String::new(),
343 prompt_flag: "--prompt".into(),
344 };
345 let pi = RunnerInfo {
346 binary: "pi".into(),
347 default_thinking: None,
348 fast_flags: BTreeMap::new(),
349 extra_args: vec!["-p".into()],
350 no_persist_flags: vec!["--no-session".into()],
351 thinking_flags: {
352 let mut tf = BTreeMap::new();
353 tf.insert(0, vec!["--thinking".into(), "off".into()]);
354 tf.insert(1, vec!["--thinking".into(), "low".into()]);
355 tf.insert(2, vec!["--thinking".into(), "medium".into()]);
356 tf.insert(3, vec!["--thinking".into(), "high".into()]);
357 tf.insert(4, vec!["--thinking".into(), "xhigh".into()]);
358 tf
359 },
360 show_thinking_flags: {
361 let mut tf = BTreeMap::new();
362 tf.insert(true, vec!["--thinking".into(), "low".into()]);
363 tf
364 },
365 yolo_flags: vec![],
366 provider_flag: "--provider".into(),
367 model_flag: "--model".into(),
368 agent_flag: String::new(),
369 prompt_flag: String::new(),
370 };
371 let grok = RunnerInfo {
372 binary: "grok".into(),
373 default_thinking: None,
374 fast_flags: BTreeMap::new(),
375 extra_args: vec!["--no-auto-update".into()],
376 no_persist_flags: vec![],
377 thinking_flags: {
378 let mut tf = BTreeMap::new();
379 tf.insert(0, vec!["--reasoning-effort".into(), "minimal".into()]);
380 tf.insert(1, vec!["--reasoning-effort".into(), "low".into()]);
381 tf.insert(2, vec!["--reasoning-effort".into(), "medium".into()]);
382 tf.insert(3, vec!["--reasoning-effort".into(), "high".into()]);
383 tf.insert(4, vec!["--reasoning-effort".into(), "xhigh".into()]);
384 tf
385 },
386 show_thinking_flags: {
387 let mut tf = BTreeMap::new();
388 tf.insert(true, vec!["--reasoning-effort".into(), "low".into()]);
389 tf
390 },
391 yolo_flags: vec!["--always-approve".into()],
392 provider_flag: String::new(),
393 model_flag: "--model".into(),
394 agent_flag: "--agent".into(),
395 prompt_flag: "-p".into(),
396 };
397
398 let claude_clone = claude.clone();
399 let kimi_clone = kimi.clone();
400 let opencode_clone = opencode.clone();
401
402 let codex_clone = codex.clone();
403 let roocode_clone = roocode.clone();
404 let crush_clone = crush.clone();
405 let cursor_clone = cursor.clone();
406 let gemini_clone = gemini.clone();
407 let pi_clone = pi.clone();
408 let grok_clone = grok.clone();
409
410 m.insert("opencode".into(), opencode);
411 m.insert("claude".into(), claude);
412 m.insert("kimi".into(), kimi);
413 m.insert("codex".into(), codex);
414 m.insert("roocode".into(), roocode);
415 m.insert("crush".into(), crush);
416 m.insert("cursor".into(), cursor);
417 m.insert("gemini".into(), gemini);
418 m.insert("pi".into(), pi);
419 m.insert("grok".into(), grok);
420
421 m.insert("oc".into(), opencode_clone);
422 m.insert("cc".into(), claude_clone.clone());
423 m.insert("c".into(), codex_clone.clone());
424 m.insert("cx".into(), codex_clone);
425 m.insert("k".into(), kimi_clone);
426 m.insert("rc".into(), roocode_clone.clone());
427 m.insert("cr".into(), crush_clone);
428 m.insert("cu".into(), cursor_clone);
429 m.insert("g".into(), gemini_clone);
430 m.insert("p".into(), pi_clone);
431 m.insert("gb".into(), grok_clone);
432
433 RwLock::new(m)
434});
435
436static RUNNER_SELECTOR_STRS: &[&str] = &[
437 "oc", "cc", "c", "cx", "k", "rc", "cr", "cu", "g", "p", "pi", "gb", "grok", "codex", "claude",
438 "opencode", "kimi", "roocode", "crush", "cursor", "gemini",
439];
440
441fn is_runner_selector(s: &str) -> bool {
442 RUNNER_SELECTOR_STRS
443 .iter()
444 .any(|&sel| sel.eq_ignore_ascii_case(s))
445}
446
447fn parse_thinking(s: &str) -> Option<i32> {
448 let rest = s.strip_prefix('+')?;
449 match rest.to_ascii_lowercase().as_str() {
450 "0" | "none" => Some(0),
451 "1" | "low" => Some(1),
452 "2" | "med" | "mid" | "medium" => Some(2),
453 "3" | "high" => Some(3),
454 "4" | "xhigh" => Some(4),
455 "5" | "max" => Some(5),
456 _ => None,
457 }
458}
459
460fn parse_provider_model(s: &str) -> Option<(&str, &str)> {
461 let rest = s.strip_prefix(':')?;
462 let parts: Vec<&str> = rest.splitn(2, ':').collect();
463 if parts.len() == 2 {
464 Some((parts[0], parts[1]))
465 } else {
466 None
467 }
468}
469
470fn parse_model_only(s: &str) -> Option<&str> {
471 let rest = s.strip_prefix(':')?;
472 if rest.contains(':') {
473 None
474 } else {
475 Some(rest)
476 }
477}
478
479fn parse_alias(s: &str) -> Option<&str> {
480 let rest = s.strip_prefix('@')?;
481 if rest
482 .chars()
483 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
484 {
485 Some(rest)
486 } else {
487 None
488 }
489}
490
491fn parse_output_mode(s: &str) -> Option<String> {
492 let mode = s.to_ascii_lowercase();
493 if OUTPUT_MODES.iter().any(|known| *known == mode) {
494 Some(mode)
495 } else {
496 None
497 }
498}
499
500fn parse_output_mode_sugar(s: &str) -> Option<String> {
501 match s.to_ascii_lowercase().as_str() {
502 ".text" => Some("text".to_string()),
503 "..text" => Some("stream-text".to_string()),
504 ".json" => Some("json".to_string()),
505 "..json" => Some("stream-json".to_string()),
506 ".fmt" => Some("formatted".to_string()),
507 "..fmt" => Some("stream-formatted".to_string()),
508 ".pt" => Some("pass-text".to_string()),
509 "..pt" => Some("stream-pass-text".to_string()),
510 ".pj" => Some("pass-json".to_string()),
511 "..pj" => Some("stream-pass-json".to_string()),
512 _ => None,
513 }
514}
515
516pub fn parse_args(argv: &[String]) -> ParsedArgs {
517 let mut parsed = ParsedArgs::default();
518 let mut positional: Vec<String> = Vec::new();
519 let mut force_prompt = false;
520 let mut index = 0;
521
522 while index < argv.len() {
523 let token = &argv[index];
524 if force_prompt || !positional.is_empty() {
525 positional.push(token.clone());
526 } else if token == "--" {
527 force_prompt = true;
528 index += 1;
529 continue;
530 } else if is_runner_selector(token) {
531 parsed.runner = Some(token.to_lowercase());
532 } else if let Some(level) = parse_thinking(token) {
533 parsed.thinking = Some(level);
534 } else if token == "--show-thinking" || token == "--no-show-thinking" {
535 parsed.show_thinking = Some(token == "--show-thinking");
536 } else if token == "--print-config" {
537 parsed.print_config = true;
538 } else if token == "--sanitize-osc" || token == "--no-sanitize-osc" {
539 parsed.sanitize_osc = Some(token == "--sanitize-osc");
540 } else if token == "--output-log-path" || token == "--no-output-log-path" {
541 parsed.output_log_path = Some(token == "--output-log-path");
542 } else if token == "--output-mode" || token == "-o" {
543 if index + 1 >= argv.len() {
544 parsed.output_mode = Some(String::new());
545 } else {
546 parsed.output_mode = Some(argv[index + 1].to_ascii_lowercase());
547 index += 1;
548 }
549 } else if token == "--forward-unknown-json" {
550 parsed.forward_unknown_json = true;
551 } else if token == "--save-session" {
552 parsed.save_session = true;
553 } else if token == "--cleanup-session" {
554 parsed.cleanup_session = true;
555 } else if token == "--timeout-secs" {
556 if index + 1 >= argv.len() {
557 parsed.timeout_secs = Some(0);
558 } else {
559 let raw = &argv[index + 1];
560 parsed.timeout_secs = match raw.parse::<u64>() {
561 Ok(value) if value > 0 => Some(value),
562 _ => Some(0),
563 };
564 index += 1;
565 }
566 } else if token == "--runner-arg" {
567 if index + 1 >= argv.len() {
568 parsed.runner_args.push(String::new());
569 } else {
570 parsed.runner_args.push(argv[index + 1].clone());
571 index += 1;
572 }
573 } else if let Some(mode) = parse_output_mode_sugar(token) {
574 parsed.output_mode = Some(mode);
575 } else if token == "--fast" || token == "--no-fast" {
576 parsed.fast = Some(token == "--fast");
577 } else if token == "--yolo" || token == "-y" {
578 parsed.yolo = true;
579 parsed.permission_mode = Some("yolo".to_string());
580 } else if token == "--permission-mode" {
581 if index + 1 >= argv.len() {
582 parsed.permission_mode = Some(String::new());
583 } else {
584 let mode = argv[index + 1].to_lowercase();
585 parsed.yolo = mode == "yolo";
586 parsed.permission_mode = Some(mode);
587 index += 1;
588 }
589 } else if let Some((provider, model)) = parse_provider_model(token) {
590 parsed.provider = Some(provider.to_string());
591 parsed.model = Some(model.to_string());
592 } else if let Some(model) = parse_model_only(token) {
593 parsed.model = Some(model.to_string());
594 } else if let Some(alias_name) = parse_alias(token) {
595 parsed.alias = Some(alias_name.to_string());
596 } else {
597 positional.push(token.clone());
598 }
599 index += 1;
600 }
601
602 parsed.prompt = positional.join(" ");
603 parsed.prompt_supplied = !positional.is_empty();
604 parsed
605}
606
607pub fn resolve_command(
608 parsed: &ParsedArgs,
609 config: Option<&CccConfig>,
610) -> Result<(Vec<String>, BTreeMap<String, String>, Vec<String>), String> {
611 let config = config.cloned().unwrap_or_default();
612 let registry = RUNNER_REGISTRY.read().unwrap();
613 let mut warnings = Vec::new();
614
615 let (effective_runner_name, effective_runner, alias_def) =
616 resolve_effective_runner(parsed, &config, ®istry)
617 .ok_or_else(|| "no runner found".to_string())?;
618 if parsed.save_session && parsed.cleanup_session {
619 return Err("--save-session and --cleanup-session are mutually exclusive".to_string());
620 }
621 if parsed.timeout_secs == Some(0) {
622 return Err("--timeout-secs must be a positive integer".to_string());
623 }
624 if parsed.runner_args.iter().any(|arg| arg.is_empty()) {
625 return Err("--runner-arg requires a non-empty value".to_string());
626 }
627
628 let mut argv: Vec<String> = vec![effective_runner.binary.clone()];
629 argv.extend(effective_runner.extra_args.iter().cloned());
630 warnings.extend(session_persistence_warnings(
631 parsed,
632 &effective_runner_name,
633 effective_runner,
634 ));
635 let output_plan = resolve_output_plan(parsed, Some(&config)).map_err(str::to_string)?;
636 warnings.extend(output_plan.warnings.clone());
637 argv.extend(output_plan.argv_flags.iter().cloned());
638
639 let effective_thinking = parsed
640 .thinking
641 .or_else(|| alias_def.and_then(|a| a.thinking))
642 .or(effective_runner.default_thinking)
643 .or(config.default_thinking);
644
645 let mut thinking_flags_applied = false;
646 if let Some(level) = effective_thinking {
647 if !effective_runner.thinking_flags.is_empty() {
648 let mut lookup = level;
649 if !effective_runner.thinking_flags.contains_key(&lookup) {
650 if let Some(&top) = effective_runner.thinking_flags.keys().max() {
651 if lookup > top {
652 lookup = top;
653 }
654 }
655 }
656 if let Some(flags) = effective_runner.thinking_flags.get(&lookup) {
657 argv.extend(flags.iter().cloned());
658 thinking_flags_applied = true;
659 }
660 }
661 }
662
663 let effective_show_thinking = resolve_show_thinking(parsed, Some(&config));
664
665 if !thinking_flags_applied && effective_show_thinking {
666 if let Some(flags) = effective_runner.show_thinking_flags.get(&true) {
667 argv.extend(flags.iter().cloned());
668 }
669 }
670
671 let effective_provider: Option<String> = parsed
672 .provider
673 .clone()
674 .or_else(|| alias_def.and_then(|a| a.provider.clone()))
675 .or_else(|| {
676 if config.default_provider.is_empty() {
677 None
678 } else {
679 Some(config.default_provider.clone())
680 }
681 });
682
683 let effective_model: Option<String> = parsed
684 .model
685 .clone()
686 .or_else(|| alias_def.and_then(|a| a.model.clone()))
687 .or_else(|| {
688 if config.default_model.is_empty() {
689 None
690 } else {
691 Some(config.default_model.clone())
692 }
693 });
694
695 let mut env_overrides = BTreeMap::new();
696 if let Some(ref provider) = effective_provider {
697 env_overrides.insert("CCC_PROVIDER".to_string(), provider.clone());
698 }
699 if matches!(effective_runner_name.as_str(), "oc" | "opencode") {
700 env_overrides.insert(
701 "OPENCODE_DISABLE_TERMINAL_TITLE".to_string(),
702 "true".to_string(),
703 );
704 }
705
706 if let Some(ref provider) = effective_provider {
707 if !effective_runner.provider_flag.is_empty() {
708 argv.push(effective_runner.provider_flag.clone());
709 argv.push(provider.clone());
710 }
711 }
712
713 if let Some(ref model) = effective_model {
714 if !effective_runner.model_flag.is_empty() {
715 argv.push(effective_runner.model_flag.clone());
716 if matches!(effective_runner_name.as_str(), "oc" | "opencode") {
717 if let Some(ref provider) = effective_provider {
718 argv.push(format!("{}/{}", provider, model));
719 } else {
720 argv.push(model.clone());
721 }
722 } else {
723 argv.push(model.clone());
724 }
725 }
726 }
727
728 let effective_agent = if let Some(alias_def) = alias_def {
729 alias_def.agent.clone()
730 } else if unresolved_alias_runner_name(parsed, &config, ®istry).is_some() {
731 None
732 } else {
733 parsed.alias.clone()
734 };
735
736 if let Some(agent) = effective_agent {
737 if effective_runner.agent_flag.is_empty() {
738 warnings.push(format!(
739 "warning: runner \"{}\" does not support agents; ignoring @{}",
740 effective_runner_name, agent
741 ));
742 } else {
743 argv.push(effective_runner.agent_flag.clone());
744 argv.push(agent);
745 }
746 }
747
748 let effective_permission_mode = parsed.permission_mode.clone().or_else(|| {
749 if parsed.yolo {
750 Some("yolo".to_string())
751 } else {
752 None
753 }
754 });
755 if let Some(ref mode) = effective_permission_mode {
756 if mode.is_empty() {
757 return Err("permission mode requires one of: safe, auto, yolo, plan".to_string());
758 }
759 if !PERMISSION_MODES.iter().any(|known| known == mode) {
760 return Err("permission mode must be one of: safe, auto, yolo, plan".to_string());
761 }
762 }
763
764 if matches!(effective_permission_mode.as_deref(), Some("safe")) {
765 if matches!(effective_runner_name.as_str(), "cc" | "claude") {
766 argv.push("--permission-mode".to_string());
767 argv.push("default".to_string());
768 } else if matches!(effective_runner_name.as_str(), "oc" | "opencode") {
769 env_overrides.insert(
770 "OPENCODE_CONFIG_CONTENT".to_string(),
771 "{\"permission\":\"ask\"}".to_string(),
772 );
773 } else if matches!(effective_runner_name.as_str(), "cu" | "cursor") {
774 argv.push("--sandbox".to_string());
775 argv.push("enabled".to_string());
776 } else if matches!(effective_runner_name.as_str(), "g" | "gemini") {
777 argv.push("--approval-mode".to_string());
778 argv.push("default".to_string());
779 argv.push("--sandbox".to_string());
780 } else if matches!(effective_runner_name.as_str(), "gb" | "grok") {
781 argv.push("--permission-mode".to_string());
782 argv.push("default".to_string());
783 } else if matches!(effective_runner_name.as_str(), "rc" | "roocode") {
784 warnings.push(
785 "warning: runner \"roocode\" safe mode is unverified; leaving default permissions unchanged"
786 .to_string(),
787 );
788 }
789 } else if matches!(effective_permission_mode.as_deref(), Some("auto")) {
790 if matches!(effective_runner_name.as_str(), "cc" | "claude") {
791 argv.push("--permission-mode".to_string());
792 argv.push("auto".to_string());
793 } else if matches!(effective_runner_name.as_str(), "c" | "cx" | "codex") {
794 argv.push("--full-auto".to_string());
795 } else if matches!(effective_runner_name.as_str(), "g" | "gemini") {
796 argv.push("--approval-mode".to_string());
797 argv.push("auto_edit".to_string());
798 } else if matches!(effective_runner_name.as_str(), "gb" | "grok") {
799 argv.push("--permission-mode".to_string());
800 argv.push("auto".to_string());
801 } else {
802 warnings.push(format!(
803 "warning: runner \"{}\" does not support permission mode \"auto\"; ignoring it",
804 effective_runner_name
805 ));
806 }
807 } else if matches!(effective_permission_mode.as_deref(), Some("yolo")) {
808 if matches!(effective_runner_name.as_str(), "g" | "gemini") {
809 argv.push("--approval-mode".to_string());
810 argv.push("yolo".to_string());
811 } else if !effective_runner.yolo_flags.is_empty() {
812 argv.extend(effective_runner.yolo_flags.iter().cloned());
813 } else if matches!(effective_runner_name.as_str(), "oc" | "opencode") {
814 env_overrides.insert(
815 "OPENCODE_CONFIG_CONTENT".to_string(),
816 "{\"permission\":\"allow\"}".to_string(),
817 );
818 } else if matches!(effective_runner_name.as_str(), "cr" | "crush") {
819 warnings.push(
820 "warning: runner \"crush\" does not support yolo mode in non-interactive run mode; ignoring --yolo".to_string(),
821 );
822 } else if matches!(effective_runner_name.as_str(), "rc" | "roocode") {
823 warnings.push(
824 "warning: runner \"roocode\" yolo mode is unverified; ignoring --yolo".to_string(),
825 );
826 }
827 } else if matches!(effective_permission_mode.as_deref(), Some("plan")) {
828 if matches!(effective_runner_name.as_str(), "cc" | "claude") {
829 argv.push("--permission-mode".to_string());
830 argv.push("plan".to_string());
831 } else if matches!(effective_runner_name.as_str(), "k" | "kimi") {
832 argv.push("--plan".to_string());
833 } else if matches!(effective_runner_name.as_str(), "cu" | "cursor") {
834 argv.push("--mode".to_string());
835 argv.push("plan".to_string());
836 } else if matches!(effective_runner_name.as_str(), "g" | "gemini") {
837 argv.push("--approval-mode".to_string());
838 argv.push("plan".to_string());
839 } else if matches!(effective_runner_name.as_str(), "gb" | "grok") {
840 argv.push("--permission-mode".to_string());
841 argv.push("plan".to_string());
842 } else {
843 warnings.push(format!(
844 "warning: runner \"{}\" does not support permission mode \"plan\"; ignoring it",
845 effective_runner_name
846 ));
847 }
848 }
849
850 if let Some(fast) = parsed.fast {
851 if let Some(flags) = effective_runner.fast_flags.get(&fast) {
852 argv.extend(flags.iter().cloned());
853 } else {
854 let display = canonical_runner_name(&effective_runner_name, effective_runner);
855 let flag = if fast { "--fast" } else { "--no-fast" };
856 warnings.push(format!(
857 "warning: runner \"{display}\" does not support fast mode; ignoring {flag}"
858 ));
859 }
860 }
861
862 if !parsed.save_session {
863 argv.extend(effective_runner.no_persist_flags.iter().cloned());
864 }
865 argv.extend(parsed.runner_args.iter().cloned());
866
867 let prompt = resolve_prompt(parsed, alias_def)?;
868 if effective_runner.prompt_flag.is_empty() {
869 argv.push(prompt);
870 } else {
871 argv.push(effective_runner.prompt_flag.clone());
872 argv.push(prompt);
873 }
874
875 Ok((argv, env_overrides, warnings))
876}
877
878pub(crate) fn resolve_command_warnings(
879 parsed: &ParsedArgs,
880 config: Option<&CccConfig>,
881) -> Result<Vec<String>, String> {
882 resolve_command(parsed, config).map(|(_, _, warnings)| warnings)
883}
884
885fn canonical_runner_name(effective_runner_name: &str, info: &RunnerInfo) -> String {
886 match effective_runner_name {
887 "oc" | "opencode" => "opencode".to_string(),
888 "cc" | "claude" => "claude".to_string(),
889 "c" | "cx" | "codex" => "codex".to_string(),
890 "k" | "kimi" => "kimi".to_string(),
891 "cr" | "crush" => "crush".to_string(),
892 "rc" | "roocode" => "roocode".to_string(),
893 "cu" | "cursor" => "cursor".to_string(),
894 "g" | "gemini" => "gemini".to_string(),
895 "p" | "pi" => "pi".to_string(),
896 "gb" | "grok" => "grok".to_string(),
897 _ => info.binary.clone(),
898 }
899}
900
901fn session_persistence_warnings(
902 parsed: &ParsedArgs,
903 effective_runner_name: &str,
904 info: &RunnerInfo,
905) -> Vec<String> {
906 if parsed.save_session || !info.no_persist_flags.is_empty() {
907 return Vec::new();
908 }
909 let display = canonical_runner_name(effective_runner_name, info);
910 if parsed.cleanup_session {
911 if display == "opencode" || display == "kimi" || display == "grok" {
912 return Vec::new();
913 }
914 return vec![format!(
915 "warning: runner \"{display}\" does not support automatic session cleanup; pass --save-session to allow saved sessions explicitly"
916 )];
917 }
918 Vec::new()
919}
920
921fn resolve_prompt(parsed: &ParsedArgs, alias_def: Option<&AliasDef>) -> Result<String, String> {
922 let user_prompt = parsed.prompt.trim();
923 let alias_prompt = alias_def
924 .and_then(|alias| alias.prompt.as_deref())
925 .map(str::trim)
926 .unwrap_or("");
927 let prompt_mode = alias_def
928 .and_then(|alias| alias.prompt_mode.as_deref())
929 .map(str::trim)
930 .filter(|mode| !mode.is_empty())
931 .unwrap_or("default");
932
933 if !PROMPT_MODES.iter().any(|known| *known == prompt_mode) {
934 return Err("prompt_mode must be one of: default, prepend, append".to_string());
935 }
936
937 if prompt_mode == "default" {
938 let prompt = if user_prompt.is_empty() {
939 alias_prompt
940 } else {
941 user_prompt
942 };
943 if prompt.is_empty() {
944 return Err("prompt must not be empty".to_string());
945 }
946 return Ok(prompt.to_string());
947 }
948
949 if !parsed.prompt_supplied {
950 return Err(format!(
951 "prompt_mode {prompt_mode} requires an explicit prompt argument"
952 ));
953 }
954 if alias_prompt.is_empty() {
955 let alias_name = parsed.alias.as_deref().unwrap_or("<alias>");
956 return Err(format!(
957 "prompt_mode {prompt_mode} requires aliases.{alias_name}.prompt"
958 ));
959 }
960
961 let mut parts = vec![alias_prompt, user_prompt];
962 if prompt_mode == "append" {
963 parts.reverse();
964 }
965 Ok(parts
966 .into_iter()
967 .filter(|part| !part.is_empty())
968 .collect::<Vec<_>>()
969 .join("\n"))
970}
971
972fn resolve_alias_def<'a>(parsed: &'a ParsedArgs, config: &'a CccConfig) -> Option<&'a AliasDef> {
973 parsed
974 .alias
975 .as_ref()
976 .and_then(|alias| config.aliases.get(alias))
977}
978
979fn unresolved_alias_runner_name(
980 parsed: &ParsedArgs,
981 config: &CccConfig,
982 registry: &BTreeMap<String, RunnerInfo>,
983) -> Option<String> {
984 if parsed.runner.is_some() {
985 return None;
986 }
987 let alias = parsed.alias.as_ref()?;
988 if config.aliases.contains_key(alias) {
989 return None;
990 }
991 let exact = resolve_runner_name(Some(alias), config);
992 if registry.contains_key(&exact) {
993 return Some(exact);
994 }
995 let lower_alias = alias.to_ascii_lowercase();
996 let lower = resolve_runner_name(Some(&lower_alias), config);
997 registry.contains_key(&lower).then_some(lower)
998}
999
1000fn resolve_runner_name(parsed_runner: Option<&str>, config: &CccConfig) -> String {
1001 let runner_name = parsed_runner.unwrap_or(&config.default_runner);
1002 config
1003 .abbreviations
1004 .get(runner_name)
1005 .cloned()
1006 .unwrap_or_else(|| runner_name.to_string())
1007}
1008
1009fn resolve_effective_runner<'a>(
1010 parsed: &'a ParsedArgs,
1011 config: &'a CccConfig,
1012 registry: &'a BTreeMap<String, RunnerInfo>,
1013) -> Option<(String, &'a RunnerInfo, Option<&'a AliasDef>)> {
1014 let alias_def = resolve_alias_def(parsed, config);
1015 let mut runner_name = resolve_runner_name(parsed.runner.as_deref(), config);
1016 if parsed.runner.is_none() {
1017 if let Some(alias_runner) = alias_def.and_then(|alias| alias.runner.as_deref()) {
1018 runner_name = resolve_runner_name(Some(alias_runner), config);
1019 } else if let Some(alias_runner) = unresolved_alias_runner_name(parsed, config, registry) {
1020 runner_name = alias_runner;
1021 }
1022 }
1023 let info = registry
1024 .get(&runner_name)
1025 .or_else(|| registry.get(&config.default_runner))
1026 .or_else(|| registry.get("opencode"))?;
1027 Some((runner_name, info, alias_def))
1028}
1029
1030fn supported_output_modes(runner_name: &str) -> &'static [&'static str] {
1031 match runner_name {
1032 "cc" | "claude" | "oc" | "opencode" | "c" | "cx" | "codex" | "cu" | "cursor" => {
1033 OUTPUT_MODES
1034 }
1035 "k" | "kimi" => KIMI_OUTPUT_MODES,
1036 "g" | "gemini" | "p" | "pi" | "gb" | "grok" => &[
1037 "text",
1038 "stream-text",
1039 "json",
1040 "stream-json",
1041 "formatted",
1042 "stream-formatted",
1043 "pass-text",
1044 "stream-pass-text",
1045 "pass-json",
1046 "stream-pass-json",
1047 ],
1048 _ => TEXT_OUTPUT_MODES,
1049 }
1050}
1051
1052fn fallback_output_mode(supported: &[&str]) -> &'static str {
1053 if supported.iter().any(|mode| *mode == "text") {
1054 "text"
1055 } else {
1056 "stream-text"
1057 }
1058}
1059
1060pub fn resolve_output_mode(
1061 parsed: &ParsedArgs,
1062 config: Option<&CccConfig>,
1063) -> Result<String, &'static str> {
1064 resolve_output_mode_with_source(parsed, config).map(|(mode, _)| mode)
1065}
1066
1067fn resolve_output_mode_with_source(
1068 parsed: &ParsedArgs,
1069 config: Option<&CccConfig>,
1070) -> Result<(String, &'static str), &'static str> {
1071 let config = config.cloned().unwrap_or_default();
1072 let alias_def = resolve_alias_def(parsed, &config);
1073 let mut mode = parsed.output_mode.clone();
1074 let mut source = "argument";
1075 if mode.is_none() {
1076 mode = alias_def.and_then(|alias| alias.output_mode.clone());
1077 if mode.is_some() {
1078 source = "alias";
1079 }
1080 }
1081 let mode = mode.unwrap_or_else(|| {
1082 source = "configured";
1083 config.default_output_mode.clone()
1084 });
1085 if mode.is_empty() {
1086 return Err(
1087 "output mode requires one of: text, stream-text, json, stream-json, formatted, stream-formatted, pass-text, pt, stream-pass-text, stream-pt, pass-json, pj, stream-pass-json, stream-pj",
1088 );
1089 }
1090 match parse_output_mode(&mode) {
1091 Some(mode) => Ok((mode, source)),
1092 None => Err(
1093 "output mode must be one of: text, stream-text, json, stream-json, formatted, stream-formatted, pass-text, pt, stream-pass-text, stream-pt, pass-json, pj, stream-pass-json, stream-pj",
1094 ),
1095 }
1096}
1097
1098pub fn resolve_show_thinking(parsed: &ParsedArgs, config: Option<&CccConfig>) -> bool {
1099 let config = config.cloned().unwrap_or_default();
1100 parsed
1101 .show_thinking
1102 .or_else(|| resolve_alias_def(parsed, &config).and_then(|alias| alias.show_thinking))
1103 .unwrap_or(config.default_show_thinking)
1104}
1105
1106pub fn resolve_sanitize_osc(parsed: &ParsedArgs, config: Option<&CccConfig>) -> bool {
1107 let config = config.cloned().unwrap_or_default();
1108 parsed
1109 .sanitize_osc
1110 .or_else(|| resolve_alias_def(parsed, &config).and_then(|alias| alias.sanitize_osc))
1111 .or(config.default_sanitize_osc)
1112 .unwrap_or_else(|| {
1113 resolve_output_plan(parsed, Some(&config))
1114 .map(|plan| plan.mode.contains("formatted"))
1115 .unwrap_or(false)
1116 })
1117}
1118
1119pub fn resolve_output_plan(
1120 parsed: &ParsedArgs,
1121 config: Option<&CccConfig>,
1122) -> Result<OutputPlan, &'static str> {
1123 let config = config.cloned().unwrap_or_default();
1124 let registry = RUNNER_REGISTRY.read().unwrap();
1125 let (runner_name, info, _) =
1126 resolve_effective_runner(parsed, &config, ®istry).ok_or("no runner found")?;
1127 let (mut mode, mode_source) = resolve_output_mode_with_source(parsed, Some(&config))?;
1128 let supported = supported_output_modes(&runner_name);
1129 let mut warnings = Vec::new();
1130 if !supported.iter().any(|candidate| *candidate == mode) {
1131 if mode_source == "argument" {
1132 return Err("gemini runner does not support requested output mode");
1133 }
1134 let fallback = fallback_output_mode(supported);
1135 warnings.push(format!(
1136 "warning: runner \"{}\" does not support {} output mode \"{}\"; falling back to \"{}\"",
1137 canonical_runner_name(&runner_name, info),
1138 mode_source,
1139 mode,
1140 fallback,
1141 ));
1142 mode = fallback.to_string();
1143 }
1144
1145 if matches!(mode.as_str(), "text" | "stream-text") {
1146 return Ok(OutputPlan {
1147 runner_name,
1148 mode: mode.clone(),
1149 stream: mode.starts_with("stream-"),
1150 formatted: false,
1151 schema: None,
1152 argv_flags: Vec::new(),
1153 warnings,
1154 });
1155 }
1156
1157 if matches!(
1158 mode.as_str(),
1159 "pass-text" | "stream-pass-text" | "pass-json" | "stream-pass-json"
1160 ) {
1161 let argv_flags = if mode == "pass-json" {
1162 if matches!(runner_name.as_str(), "cc" | "claude") {
1163 vec!["--output-format".into(), "json".into()]
1164 } else if matches!(runner_name.as_str(), "cu" | "cursor") {
1165 vec!["--output-format".into(), "json".into()]
1166 } else if matches!(runner_name.as_str(), "g" | "gemini") {
1167 vec!["--output-format".into(), "json".into()]
1168 } else if matches!(runner_name.as_str(), "gb" | "grok") {
1169 vec!["--output-format".into(), "json".into()]
1170 } else if matches!(runner_name.as_str(), "c" | "cx" | "codex") {
1171 vec!["--json".into()]
1172 } else if matches!(runner_name.as_str(), "k" | "kimi") {
1173 vec![
1174 "--print".into(),
1175 "--output-format".into(),
1176 "stream-json".into(),
1177 ]
1178 } else if matches!(runner_name.as_str(), "oc" | "opencode") {
1179 vec!["--format".into(), "json".into()]
1180 } else if matches!(runner_name.as_str(), "p" | "pi") {
1181 vec!["--mode".into(), "json".into()]
1182 } else {
1183 vec![]
1184 }
1185 } else if mode == "stream-pass-json" {
1186 if matches!(runner_name.as_str(), "cc" | "claude") {
1187 vec![
1188 "--verbose".into(),
1189 "--output-format".into(),
1190 "stream-json".into(),
1191 ]
1192 } else if matches!(runner_name.as_str(), "cu" | "cursor") {
1193 vec!["--output-format".into(), "stream-json".into()]
1194 } else if matches!(runner_name.as_str(), "g" | "gemini") {
1195 vec!["--output-format".into(), "stream-json".into()]
1196 } else if matches!(runner_name.as_str(), "gb" | "grok") {
1197 vec!["--output-format".into(), "streaming-json".into()]
1198 } else if matches!(runner_name.as_str(), "c" | "cx" | "codex") {
1199 vec!["--json".into()]
1200 } else if matches!(runner_name.as_str(), "k" | "kimi") {
1201 vec![
1202 "--print".into(),
1203 "--output-format".into(),
1204 "stream-json".into(),
1205 ]
1206 } else if matches!(runner_name.as_str(), "oc" | "opencode") {
1207 vec!["--format".into(), "json".into()]
1208 } else if matches!(runner_name.as_str(), "p" | "pi") {
1209 vec!["--mode".into(), "json".into()]
1210 } else {
1211 vec![]
1212 }
1213 } else {
1214 vec![]
1215 };
1216 return Ok(OutputPlan {
1217 runner_name,
1218 mode: mode.clone(),
1219 stream: mode.starts_with("stream-"),
1220 formatted: false,
1221 schema: None,
1222 argv_flags,
1223 warnings,
1224 });
1225 }
1226
1227 if matches!(runner_name.as_str(), "cc" | "claude") {
1228 let mut argv_flags = if mode == "json" {
1229 vec!["--output-format".into(), "json".into()]
1230 } else if mode == "formatted" {
1231 vec![
1232 "--verbose".into(),
1233 "--output-format".into(),
1234 "stream-json".into(),
1235 ]
1236 } else if mode == "stream-formatted" {
1237 vec![
1238 "--verbose".into(),
1239 "--output-format".into(),
1240 "stream-json".into(),
1241 ]
1242 } else if mode == "stream-json" {
1243 vec![
1244 "--verbose".into(),
1245 "--output-format".into(),
1246 "stream-json".into(),
1247 ]
1248 } else {
1249 vec![]
1250 };
1251 if mode == "stream-formatted" {
1252 argv_flags.push("--include-partial-messages".into());
1253 }
1254 return Ok(OutputPlan {
1255 runner_name,
1256 mode: mode.clone(),
1257 stream: mode.starts_with("stream-"),
1258 formatted: mode.contains("formatted"),
1259 schema: Some("claude-code".into()),
1260 argv_flags,
1261 warnings,
1262 });
1263 }
1264
1265 if matches!(runner_name.as_str(), "k" | "kimi") {
1266 return Ok(OutputPlan {
1267 runner_name,
1268 mode: mode.clone(),
1269 stream: mode.starts_with("stream-"),
1270 formatted: mode.contains("formatted"),
1271 schema: Some("kimi".into()),
1272 argv_flags: vec![
1273 "--print".into(),
1274 "--output-format".into(),
1275 "stream-json".into(),
1276 ],
1277 warnings,
1278 });
1279 }
1280
1281 if matches!(runner_name.as_str(), "oc" | "opencode") {
1282 return Ok(OutputPlan {
1283 runner_name,
1284 mode: mode.clone(),
1285 stream: mode.starts_with("stream-"),
1286 formatted: mode.contains("formatted"),
1287 schema: Some("opencode".into()),
1288 argv_flags: vec!["--format".into(), "json".into()],
1289 warnings,
1290 });
1291 }
1292
1293 if matches!(runner_name.as_str(), "c" | "cx" | "codex") {
1294 return Ok(OutputPlan {
1295 runner_name,
1296 mode: mode.clone(),
1297 stream: mode.starts_with("stream-"),
1298 formatted: mode.contains("formatted"),
1299 schema: Some("codex".into()),
1300 argv_flags: vec!["--json".into()],
1301 warnings,
1302 });
1303 }
1304
1305 if matches!(runner_name.as_str(), "cu" | "cursor") {
1306 let argv_flags = if mode == "json" {
1307 vec!["--output-format".into(), "json".into()]
1308 } else {
1309 vec!["--output-format".into(), "stream-json".into()]
1310 };
1311 return Ok(OutputPlan {
1312 runner_name,
1313 mode: mode.clone(),
1314 stream: mode.starts_with("stream-"),
1315 formatted: mode.contains("formatted"),
1316 schema: Some("cursor-agent".into()),
1317 argv_flags,
1318 warnings,
1319 });
1320 }
1321
1322 if matches!(runner_name.as_str(), "g" | "gemini") {
1323 let argv_flags = if mode == "json" {
1324 vec!["--output-format".into(), "json".into()]
1325 } else {
1326 vec!["--output-format".into(), "stream-json".into()]
1327 };
1328 return Ok(OutputPlan {
1329 runner_name,
1330 mode: mode.clone(),
1331 stream: mode.starts_with("stream-"),
1332 formatted: mode.contains("formatted"),
1333 schema: Some("gemini".into()),
1334 argv_flags,
1335 warnings,
1336 });
1337 }
1338
1339 if matches!(runner_name.as_str(), "p" | "pi") {
1340 return Ok(OutputPlan {
1341 runner_name,
1342 mode: mode.clone(),
1343 stream: mode.starts_with("stream-"),
1344 formatted: mode.contains("formatted"),
1345 schema: Some("pi".into()),
1346 argv_flags: vec!["--mode".into(), "json".into()],
1347 warnings,
1348 });
1349 }
1350
1351 if matches!(runner_name.as_str(), "gb" | "grok") {
1352 let argv_flags = if mode == "json" {
1353 vec!["--output-format".into(), "json".into()]
1354 } else {
1355 vec!["--output-format".into(), "streaming-json".into()]
1356 };
1357 return Ok(OutputPlan {
1358 runner_name,
1359 mode: mode.clone(),
1360 stream: mode.starts_with("stream-"),
1361 formatted: mode.contains("formatted"),
1362 schema: Some("grok".into()),
1363 argv_flags,
1364 warnings,
1365 });
1366 }
1367
1368 Ok(OutputPlan {
1369 runner_name,
1370 mode: mode.clone(),
1371 stream: mode.starts_with("stream-"),
1372 formatted: false,
1373 schema: None,
1374 argv_flags: Vec::new(),
1375 warnings,
1376 })
1377}