-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Expand file tree
/
Copy pathcli.rs
More file actions
1839 lines (1628 loc) · 54.5 KB
/
Copy pathcli.rs
File metadata and controls
1839 lines (1628 loc) · 54.5 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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//! NOTE: Always use singular names for commands and subcommands.
//! For example: `forge provider login` instead of `forge providers login`.
//!
//! NOTE: With every change to this CLI structure, verify that the ZSH plugin
//! remains compatible. The plugin at `shell-plugin/forge.plugin.zsh` implements
//! shell completion and command shortcuts that depend on the CLI structure.
use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
use forge_domain::{AgentId, ConversationId, Effort, ModelId, ProviderId};
#[derive(Parser)]
#[command(version = env!("CARGO_PKG_VERSION"))]
pub struct Cli {
/// Direct prompt to process without entering interactive mode.
///
/// When provided, executes a single command and exits instead of starting
/// an interactive session. Content can also be piped: `cat prompt.txt |
/// forge`.
#[arg(long, short = 'p', allow_hyphen_values = true)]
pub prompt: Option<String>,
/// Piped input from stdin (populated internally)
///
/// This field is automatically populated when content is piped to forge
/// via stdin. It's kept separate from the prompt to allow proper handling
/// as a droppable message.
#[arg(skip)]
pub piped_input: Option<String>,
/// Path to a JSON file containing the conversation to execute.
#[arg(long)]
pub conversation: Option<PathBuf>,
/// Conversation ID to use for this session.
///
/// When provided, resumes or continues an existing conversation instead of
/// generating a new conversation ID.
#[arg(long, alias = "cid")]
pub conversation_id: Option<ConversationId>,
/// Working directory to use before starting the session.
///
/// When provided, changes to this directory before starting forge.
#[arg(long, short = 'C')]
pub directory: Option<PathBuf>,
/// Name for an isolated git worktree to create for experimentation.
#[arg(long)]
pub sandbox: Option<String>,
/// Enable verbose logging output.
#[arg(long, default_value_t = false)]
pub verbose: bool,
/// Agent ID to use for this session.
#[arg(long, alias = "aid")]
pub agent: Option<AgentId>,
/// Top-level subcommands.
#[command(subcommand)]
pub subcommands: Option<TopLevelCommand>,
/// Event to dispatch to the workflow in JSON format.
#[arg(long, short = 'e')]
pub event: Option<String>,
}
impl Cli {
/// Determines whether the CLI should start in interactive mode.
///
/// Returns true when no prompt, piped input, or subcommand is provided,
/// indicating the user wants to enter interactive mode.
pub fn is_interactive(&self) -> bool {
self.prompt.is_none() && self.piped_input.is_none() && self.subcommands.is_none()
}
}
#[derive(Subcommand, Debug, Clone)]
pub enum TopLevelCommand {
/// Manage agents.
Agent(AgentCommandGroup),
/// Generate shell extension scripts.
#[command(subcommand, alias = "extension")]
Zsh(ZshCommandGroup),
/// List agents, models, providers, tools, or MCP servers.
List(ListCommandGroup),
/// Display the banner with version information.
Banner,
/// Show configuration, active model, and environment status.
Info {
/// Conversation ID for session-specific information.
#[arg(long, alias = "cid")]
conversation_id: Option<ConversationId>,
/// Output in machine-readable format.
#[arg(long)]
porcelain: bool,
},
/// Get, set, or list configuration values.
Config(ConfigCommandGroup),
/// Manage conversation history and state.
#[command(alias = "session")]
Conversation(ConversationCommandGroup),
/// Generate and optionally commit changes with AI-generated message
Commit(CommitCommandGroup),
/// Manage Model Context Protocol servers.
Mcp(McpCommandGroup),
/// Suggest shell commands from natural language.
Suggest {
/// Natural language description of the desired command.
prompt: String,
},
/// Manage API provider authentication.
Provider(ProviderCommandGroup),
/// Run or list custom commands.
#[command(aliases = ["command", "commands"])]
Cmd(CmdCommandGroup),
/// Manage workspaces for semantic search.
Workspace(WorkspaceCommandGroup),
/// Process JSONL data through LLM with schema-constrained tools.
Data(DataCommandGroup),
/// VS Code integration commands.
#[command(subcommand)]
Vscode(VscodeCommand),
/// Paste image from clipboard and return the @[path] string.
PasteImage {
/// Optional path to save the image to.
#[arg(long, short = 'o')]
output: Option<PathBuf>,
},
/// Update forge to the latest version.
Update(UpdateArgs),
/// Setup zsh integration by updating .zshrc with plugin and theme (alias
/// for `zsh setup`).
Setup,
/// Run diagnostics on shell environment (alias for `zsh doctor`).
Doctor,
}
/// Command group for custom command management.
#[derive(Parser, Debug, Clone)]
pub struct CmdCommandGroup {
#[command(subcommand)]
pub command: CmdCommand,
/// Conversation ID to execute the command within.
#[arg(long, alias = "cid", global = true)]
pub conversation_id: Option<ConversationId>,
/// Output in machine-readable format.
#[arg(long, global = true)]
pub porcelain: bool,
}
#[derive(Subcommand, Debug, Clone)]
pub enum CmdCommand {
/// List all available custom commands.
List {
/// Shows only custom commands
#[arg(long)]
custom: bool,
},
/// Execute a custom command.
Execute {
/// Name of the custom command to execute, followed by any arguments.
commands: Vec<String>,
},
}
/// Command group for agent management.
#[derive(Parser, Debug, Clone)]
pub struct AgentCommandGroup {
#[command(subcommand)]
pub command: AgentCommand,
/// Output in machine-readable format.
#[arg(long, global = true)]
pub porcelain: bool,
}
/// Agent management commands.
#[derive(Subcommand, Debug, Clone)]
pub enum AgentCommand {
/// List available agents.
#[command(alias = "ls")]
List,
}
/// Command group for workspace management.
#[derive(Parser, Debug, Clone)]
pub struct WorkspaceCommandGroup {
#[command(subcommand)]
pub command: WorkspaceCommand,
}
#[derive(Subcommand, Debug, Clone)]
pub enum WorkspaceCommand {
/// Synchronize a directory for semantic search.
Sync {
/// Path to the directory to sync
#[arg(default_value = ".")]
path: PathBuf,
/// Automatically initialize the workspace before syncing if it has not
/// been initialized yet.
#[arg(long)]
init: bool,
},
/// List all workspaces.
List {
/// Output in machine-readable format
#[arg(short, long)]
porcelain: bool,
},
/// Query the workspace.
Query {
/// Search query.
query: String,
/// Path to the directory to index (used when no subcommand is
/// provided).
#[arg(default_value = ".")]
path: PathBuf,
/// Maximum number of results to return.
#[arg(short, long, default_value = "10")]
limit: usize,
/// Number of highest probability tokens to consider (1-1000).
#[arg(long)]
top_k: Option<u32>,
/// Describe your intent or goal to filter results for relevance.
#[arg(long, short = 'r')]
use_case: String,
/// Filter results to files starting with this prefix.
#[arg(long)]
starts_with: Option<String>,
/// Filter results to files ending with this suffix.
#[arg(long)]
ends_with: Option<String>,
},
/// Show workspace information for an indexed directory.
Info {
/// Path to the directory to get information for
#[arg(default_value = ".")]
path: PathBuf,
},
/// Delete one or more workspaces.
Delete {
/// Workspace IDs to delete
workspace_ids: Vec<String>,
},
/// Show sync status of all files in the workspace.
Status {
/// Path to the directory to check status for
#[arg(default_value = ".")]
path: PathBuf,
/// Output in machine-readable format
#[arg(short, long)]
porcelain: bool,
},
/// Initialize an empty workspace for the provided directory
Init {
/// Path to the directory to initialize as a workspace
#[arg(default_value = ".")]
path: PathBuf,
},
}
/// Command group for listing resources.
#[derive(Parser, Debug, Clone)]
pub struct ListCommandGroup {
#[command(subcommand)]
pub command: ListCommand,
/// Output in machine-readable format.
#[arg(long, global = true)]
pub porcelain: bool,
}
#[derive(Subcommand, Debug, Clone)]
pub enum ListCommand {
/// List available agents.
#[command(alias = "agents")]
Agent {
/// Shows only custom agents
#[arg(long)]
custom: bool,
},
/// List available API providers.
#[command(alias = "providers")]
Provider {
/// Filter providers by type (e.g., llm, context_engine). Can be
/// specified multiple times.
#[arg(long = "type", short = 't')]
types: Vec<forge_domain::ProviderType>,
},
/// List available models.
#[command(alias = "models")]
Model,
/// List available commands.
#[command(hide = true, alias = "commands")]
Command {
/// Shows only custom commands
#[arg(long)]
custom: bool,
},
/// List configuration values.
#[command(alias = "configs")]
Config,
/// List tools for a specific agent.
#[command(alias = "tools")]
Tool {
/// Agent ID to list tools for.
agent: AgentId,
},
/// List MCP servers.
#[command(alias = "mcps")]
Mcp,
/// List conversation history.
#[command(alias = "session")]
Conversation,
/// List custom commands.
#[command(alias = "cmds")]
Cmd,
/// List available skills.
#[command(alias = "skills")]
Skill {
/// Shows only custom skills
#[arg(long)]
custom: bool,
},
}
/// Shell extension commands.
#[derive(Subcommand, Debug, Clone)]
pub enum ZshCommandGroup {
/// Generate shell plugin script
Plugin,
/// Generate shell theme
Theme,
/// Run diagnostics on shell environment
Doctor,
/// Get rprompt information (model and conversation stats) for shell
/// integration.
Rprompt,
/// Setup zsh integration by updating .zshrc with plugin and theme
Setup,
/// Show keyboard shortcuts for ZSH line editor
Keyboard,
}
/// Command group for MCP server management.
#[derive(Parser, Debug, Clone)]
pub struct McpCommandGroup {
#[command(subcommand)]
pub command: McpCommand,
/// Output in machine-readable format.
#[arg(long, global = true)]
pub porcelain: bool,
}
#[derive(Subcommand, Debug, Clone)]
pub enum McpCommand {
/// Import server configuration from JSON.
Import(McpImportArgs),
/// List configured servers.
List,
/// Remove a configured server.
Remove(McpRemoveArgs),
/// Show server configuration details.
Show(McpShowArgs),
/// Reload servers and rebuild caches.
Reload,
/// Authenticate with an OAuth-enabled MCP server.
Login(McpAuthArgs),
/// Remove stored OAuth credentials for an MCP server.
Logout(McpLogoutArgs),
}
#[derive(Parser, Debug, Clone)]
pub struct McpImportArgs {
/// JSON configuration to import.
#[arg()]
pub json: String,
/// Configuration scope.
#[arg(short = 's', long = "scope", default_value = "local")]
pub scope: Scope,
}
#[derive(Parser, Debug, Clone)]
pub struct McpRemoveArgs {
/// Configuration scope.
#[arg(short = 's', long = "scope", default_value = "local")]
pub scope: Scope,
/// Name of the server to remove.
pub name: String,
}
#[derive(Parser, Debug, Clone)]
pub struct McpShowArgs {
/// Name of the server to show details for.
pub name: String,
}
#[derive(Parser, Debug, Clone)]
pub struct McpAuthArgs {
/// Name of the MCP server to authenticate with.
pub name: String,
}
#[derive(Parser, Debug, Clone)]
pub struct McpLogoutArgs {
/// Name of the MCP server to remove credentials for, or "all" to
/// remove all MCP OAuth credentials.
pub name: String,
}
/// Configuration scope for settings.
#[derive(Copy, Clone, Debug, ValueEnum, Default)]
pub enum Scope {
/// Local configuration (project-specific).
#[default]
Local,
/// User configuration (global to the user).
User,
}
impl From<Scope> for forge_domain::Scope {
fn from(value: Scope) -> Self {
match value {
Scope::Local => forge_domain::Scope::Local,
Scope::User => forge_domain::Scope::User,
}
}
}
/// Transport protocol for communication.
#[derive(Copy, Clone, Debug, ValueEnum)]
#[clap(rename_all = "lower")]
pub enum Transport {
/// Standard input/output communication.
Stdio,
/// Server-sent events communication.
Sse,
}
/// Command group for configuration management.
#[derive(Parser, Debug, Clone)]
pub struct ConfigCommandGroup {
#[command(subcommand)]
pub command: ConfigCommand,
/// Output in machine-readable format.
#[arg(long, global = true)]
pub porcelain: bool,
}
#[derive(Subcommand, Debug, Clone)]
pub enum ConfigCommand {
/// Set a configuration value.
Set(ConfigSetArgs),
/// Get a configuration value.
Get(ConfigGetArgs),
/// List configuration values.
List,
}
/// Arguments for `forge config set`.
#[derive(Parser, Debug, Clone)]
pub struct ConfigSetArgs {
#[command(subcommand)]
pub field: ConfigSetField,
}
/// Arguments for `forge config get`.
#[derive(Parser, Debug, Clone)]
pub struct ConfigGetArgs {
#[command(subcommand)]
pub field: ConfigGetField,
}
/// Type-safe subcommands for `forge config set`.
#[derive(Subcommand, Debug, Clone)]
pub enum ConfigSetField {
/// Set the active model and provider atomically.
Model {
/// Provider ID to set as default.
provider: ProviderId,
/// Model ID to set as default.
model: ModelId,
},
/// Set the provider and model for commit message generation.
Commit {
/// Provider ID to use for commit message generation.
provider: ProviderId,
/// Model ID to use for commit message generation.
model: ModelId,
},
/// Set the provider and model for command suggestion generation.
Suggest {
/// Provider ID to use for command suggestion generation.
provider: ProviderId,
/// Model ID to use for command suggestion generation.
model: ModelId,
},
/// Set the reasoning effort level applied to all agents.
ReasoningEffort {
/// Effort level: none, minimal, low, medium, high, xhigh, max.
effort: Effort,
},
}
/// Type-safe subcommands for `forge config get`.
#[derive(Subcommand, Debug, Clone)]
pub enum ConfigGetField {
/// Get the active model.
Model,
/// Get the active provider.
Provider,
/// Get the commit message generation config.
Commit,
/// Get the command suggestion generation config.
Suggest,
/// Get the reasoning effort level.
ReasoningEffort,
}
/// Command group for conversation management.
#[derive(Parser, Debug, Clone)]
pub struct ConversationCommandGroup {
#[command(subcommand)]
pub command: ConversationCommand,
}
#[derive(Subcommand, Debug, Clone)]
pub enum ConversationCommand {
/// List conversation history.
List {
/// Output in machine-readable format.
#[arg(long)]
porcelain: bool,
},
/// Create a new conversation.
New,
/// Export conversation as JSON or HTML.
Dump {
/// Conversation ID to export.
id: ConversationId,
/// Export as HTML instead of JSON.
#[arg(long)]
html: bool,
},
/// Compact conversation to reduce token usage.
Compact {
/// Conversation ID to compact.
id: ConversationId,
},
/// Retry last command without modifying context.
Retry {
/// Conversation ID to retry.
id: ConversationId,
},
/// Resume conversation in interactive mode.
Resume {
/// Conversation ID to resume.
id: ConversationId,
},
/// Show last assistant message.
Show {
/// Conversation ID.
id: ConversationId,
/// Print raw markdown without rendering.
#[arg(long)]
md: bool,
},
/// Show conversation details.
Info {
/// Conversation ID.
id: ConversationId,
},
/// Show conversation statistics.
Stats {
/// Conversation ID.
id: ConversationId,
/// Output in machine-readable format.
#[arg(long)]
porcelain: bool,
},
/// Clone conversation with a new ID.
Clone {
/// Conversation ID to clone.
id: ConversationId,
/// Output in machine-readable format.
#[arg(long)]
porcelain: bool,
},
/// Delete a conversation permanently.
Delete {
/// Conversation ID to delete.
id: String,
},
/// Rename a conversation.
Rename {
/// Conversation ID to rename.
id: ConversationId,
/// New name for the conversation.
name: String,
},
}
/// Command group for provider authentication management.
#[derive(Parser, Debug, Clone)]
pub struct ProviderCommandGroup {
#[command(subcommand)]
pub command: ProviderCommand,
/// Output in machine-readable format.
#[arg(long, global = true)]
pub porcelain: bool,
}
#[derive(Subcommand, Debug, Clone)]
pub enum ProviderCommand {
/// Authenticate with an API provider.
///
/// Shows an interactive menu when no provider name is specified.
Login {
/// Provider name to authenticate with.
provider: Option<ProviderId>,
},
/// Remove provider credentials.
///
/// Shows an interactive menu when no provider name is specified.
Logout {
/// Provider name to log out from.
provider: Option<ProviderId>,
},
/// List available providers.
List {
/// Filter providers by type (e.g., llm, context_engine). Can be
/// specified multiple times.
#[arg(long = "type", short = 't')]
types: Vec<forge_domain::ProviderType>,
},
}
/// Group of Commit-related commands
#[derive(Parser, Debug, Clone)]
pub struct CommitCommandGroup {
/// Preview the commit message without committing
#[arg(long)]
pub preview: bool,
/// Maximum git diff size in bytes (default: 100k)
///
/// Limits the size of the git diff sent to the AI model. Large diffs are
/// truncated to save tokens and reduce API costs. Minimum value is 5000
/// bytes.
#[arg(long = "max-diff", default_value = "100000", value_parser = clap::builder::RangedI64ValueParser::<usize>::new().range(5000..))]
pub max_diff_size: Option<usize>,
/// Git diff content (used internally for piped input)
///
/// This field is populated when diff content is piped to the commit
/// command. Users typically don't set this directly; instead, they pipe
/// diff content: `git diff | forge commit --preview`
#[arg(skip)]
pub diff: Option<String>,
/// Additional text to customize the commit message
///
/// Provide additional context or instructions for the AI to use when
/// generating the commit message. Multiple words can be provided without
/// quotes: `forge commit fix typo in readme`
pub text: Vec<String>,
}
/// Group of Data-related commands
#[derive(Parser, Debug, Clone)]
pub struct DataCommandGroup {
/// Path to JSONL file to process
#[arg(long)]
pub input: String,
/// Path to JSON schema file for LLM tool definition
#[arg(long)]
pub schema: String,
/// Path to Handlebars template file for system prompt
#[arg(long)]
pub system_prompt: Option<String>,
/// Path to Handlebars template file for user prompt
#[arg(long)]
pub user_prompt: Option<String>,
/// Maximum number of concurrent LLM requests
#[arg(long, default_value = "10")]
pub concurrency: usize,
}
impl From<DataCommandGroup> for forge_domain::DataGenerationParameters {
fn from(value: DataCommandGroup) -> Self {
Self {
input: value.input.into(),
schema: value.schema.into(),
system_prompt: value.system_prompt.map(Into::into),
user_prompt: value.user_prompt.map(Into::into),
concurrency: value.concurrency,
}
}
}
/// VS Code integration commands.
#[derive(Subcommand, Debug, Clone)]
pub enum VscodeCommand {
/// Install the Forge VS Code extension.
InstallExtension,
}
/// Update command arguments.
#[derive(Parser, Debug, Clone)]
pub struct UpdateArgs {
/// Skip the confirmation prompt when applying updates.
#[arg(long, default_value_t = false)]
pub no_confirm: bool,
}
#[cfg(test)]
mod tests {
use clap::Parser;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_data_command_group_conversion() {
use std::path::PathBuf;
let fixture = DataCommandGroup {
input: "path/to/input.jsonl".to_string(),
schema: "path/to/schema.json".to_string(),
system_prompt: Some("system prompt".to_string()),
user_prompt: None,
concurrency: 5,
};
let actual: forge_domain::DataGenerationParameters = fixture.into();
let expected = forge_domain::DataGenerationParameters {
input: PathBuf::from("path/to/input.jsonl"),
schema: PathBuf::from("path/to/schema.json"),
system_prompt: Some(PathBuf::from("system prompt")),
user_prompt: None,
concurrency: 5,
};
assert_eq!(actual, expected);
}
#[test]
fn test_commit_default_max_diff_size() {
let fixture = Cli::parse_from(["forge", "commit", "--preview"]);
let actual = match fixture.subcommands {
Some(TopLevelCommand::Commit(commit)) => commit.max_diff_size,
_ => panic!("Expected Commit command"),
};
let expected = Some(100000);
assert_eq!(actual, expected);
}
#[test]
fn test_commit_custom_max_diff_size() {
let fixture = Cli::parse_from(["forge", "commit", "--preview", "--max-diff", "50000"]);
let actual = match fixture.subcommands {
Some(TopLevelCommand::Commit(commit)) => commit.max_diff_size,
_ => panic!("Expected Commit command"),
};
let expected = Some(50000);
assert_eq!(actual, expected);
}
#[test]
fn test_config_set_with_provider_and_model() {
let fixture = Cli::parse_from([
"forge",
"config",
"set",
"model",
"anthropic",
"claude-sonnet-4-20250514",
]);
let actual = match fixture.subcommands {
Some(TopLevelCommand::Config(config)) => match config.command {
ConfigCommand::Set(args) => match args.field {
ConfigSetField::Model { provider, model } => {
Some((provider.to_string(), model.as_str().to_string()))
}
_ => None,
},
_ => None,
},
_ => None,
};
let expected = Some((
"Anthropic".to_string(),
"claude-sonnet-4-20250514".to_string(),
));
assert_eq!(actual, expected);
}
#[test]
fn test_config_list() {
let fixture = Cli::parse_from(["forge", "config", "list"]);
let actual = match fixture.subcommands {
Some(TopLevelCommand::Config(config)) => matches!(config.command, ConfigCommand::List),
_ => false,
};
let expected = true;
assert_eq!(actual, expected);
}
#[test]
fn test_config_get_specific_field() {
let fixture = Cli::parse_from(["forge", "config", "get", "model"]);
let actual = match fixture.subcommands {
Some(TopLevelCommand::Config(config)) => match config.command {
ConfigCommand::Get(args) => matches!(args.field, ConfigGetField::Model),
_ => panic!("Expected ConfigCommand::Get"),
},
_ => panic!("Expected TopLevelCommand::Config"),
};
assert!(actual);
}
#[test]
fn test_config_set_commit_with_provider_and_model() {
let fixture = Cli::parse_from([
"forge",
"config",
"set",
"commit",
"anthropic",
"claude-haiku-4-20250514",
]);
let actual = match fixture.subcommands {
Some(TopLevelCommand::Config(config)) => match config.command {
ConfigCommand::Set(args) => match args.field {
ConfigSetField::Commit { provider, model } => {
Some((provider.to_string(), model.as_str().to_string()))
}
_ => None,
},
_ => None,
},
_ => None,
};
let expected = Some((
"Anthropic".to_string(),
"claude-haiku-4-20250514".to_string(),
));
assert_eq!(actual, expected);
}
#[test]
fn test_conversation_list() {
let fixture = Cli::parse_from(["forge", "conversation", "list"]);
let is_list = match fixture.subcommands {
Some(TopLevelCommand::Conversation(conversation)) => {
matches!(conversation.command, ConversationCommand::List { .. })
}
_ => false,
};
assert_eq!(is_list, true);
}
#[test]
fn test_session_alias_list() {
let fixture = Cli::parse_from(["forge", "session", "list"]);
let is_list = match fixture.subcommands {
Some(TopLevelCommand::Conversation(conversation)) => {
matches!(conversation.command, ConversationCommand::List { .. })
}
_ => false,
};
assert_eq!(is_list, true);
}
#[test]
fn test_agent_id_long_flag() {
let fixture = Cli::parse_from(["forge", "--agent", "sage"]);
assert_eq!(fixture.agent, Some(AgentId::new("sage")));
}
#[test]
fn test_agent_id_short_alias() {
let fixture = Cli::parse_from(["forge", "--aid", "muse"]);
assert_eq!(fixture.agent, Some(AgentId::new("muse")));
}
#[test]
fn test_agent_id_with_prompt() {
let fixture = Cli::parse_from(["forge", "--agent", "forge", "-p", "test prompt"]);
assert_eq!(fixture.agent, Some(AgentId::new("forge")));
assert_eq!(fixture.prompt, Some("test prompt".to_string()));
}
#[test]
fn test_agent_id_not_provided() {
let fixture = Cli::parse_from(["forge"]);
assert_eq!(fixture.agent, None);
}
#[test]
fn test_conversation_dump_json_with_id() {
let fixture = Cli::parse_from([
"forge",
"conversation",
"dump",
"550e8400-e29b-41d4-a716-446655440000",
]);
let (id, html) = match fixture.subcommands {
Some(TopLevelCommand::Conversation(conversation)) => match conversation.command {
ConversationCommand::Dump { id, html } => (id, html),
_ => (ConversationId::default(), true),
},
_ => (ConversationId::default(), true),
};
assert_eq!(
id,
ConversationId::parse("550e8400-e29b-41d4-a716-446655440000").unwrap()