-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcli.rs
More file actions
338 lines (292 loc) · 11.5 KB
/
Copy pathcli.rs
File metadata and controls
338 lines (292 loc) · 11.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
//! CLI interface for nixmac - allows running evolve from the command line.
//!
//! Usage:
//! nixmac evolve "your prompt here"
//! nixmac evolve "your prompt" --config /path/to/config
//! nixmac evolve "your prompt" --max-token-budget 50000 --host aarch64-darwin
//!
//! NOTE NOTE NOTE: If you pass any CLI arguments corresponding to settings that
//! come from the app store, those CLI arguments will override the store values
//! for that run of the evolution.
//! However, the CLI arguments will also update the store with those values,
//! so subsequent runs (even from the UI) will use the CLI-provided values as defaults.
//! This is because we don't currently have a good way to pipe these settings
//! through a single run.
//! This is something we should consider refactoring for in the future.
use clap::{Parser, Subcommand};
use serde_json::json;
use std::path::PathBuf;
use tauri::AppHandle;
#[derive(Clone)]
pub struct EvolveConfig {
pub prompt: String,
pub config: Option<PathBuf>,
pub max_iterations: Option<usize>,
pub max_output_tokens: Option<usize>,
pub max_token_budget: Option<u32>,
pub evolve_provider: Option<String>,
pub evolve_model: Option<String>,
pub summary_provider: Option<String>,
pub summary_model: Option<String>,
pub openai_key: Option<String>,
pub openrouter_key: Option<String>,
pub ollama_url: Option<String>,
pub host: Option<String>,
pub out: Option<PathBuf>,
}
#[derive(Parser)]
#[command(name = "nixmac")]
#[command(about = "macOS nix-darwin configuration manager", long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Subcommand)]
pub enum Commands {
/// Run an evolution with the given prompt
Evolve {
/// The prompt to use for evolution
prompt: String,
/// Path to the config directory
#[arg(short, long)]
config: Option<PathBuf>,
/// Legacy fallback for providers that do not report token usage
#[arg(short, long, hide = true)]
max_iterations: Option<usize>,
/// Maximum output tokens requested per evolution model call
#[arg(long)]
max_output_tokens: Option<usize>,
/// Maximum provider-reported tokens for the evolution
#[arg(long)]
max_token_budget: Option<u32>,
/// Provider for evolution (e.g., openai, openrouter, ollama)
#[arg(long)]
evolve_provider: Option<String>,
/// Model name for evolution
#[arg(long)]
evolve_model: Option<String>,
/// Provider for summarization
#[arg(long)]
summary_provider: Option<String>,
/// Model name for summarization
#[arg(long)]
summary_model: Option<String>,
/// OpenAI API key
#[arg(long)]
openai_key: Option<String>,
/// OpenRouter API key
#[arg(long)]
openrouter_key: Option<String>,
/// Ollama API base URL
#[arg(long)]
ollama_url: Option<String>,
/// Host attribute (e.g., aarch64-darwin)
#[arg(long)]
host: Option<String>,
/// Optional output file to write JSON result
#[arg(long)]
out: Option<PathBuf>,
},
}
/// Runs evolution with provided or default settings
pub async fn handle_evolve_command(app: &AppHandle, cfg: EvolveConfig) -> Result<(), String> {
let EvolveConfig {
prompt,
config,
max_iterations,
max_output_tokens,
max_token_budget,
evolve_provider,
evolve_model,
summary_provider,
summary_model,
openai_key,
openrouter_key,
ollama_url,
host,
out,
} = cfg;
// Config
if let Some(config_path) = config {
if !config_path.exists() || !config_path.is_dir() {
return Err(format!(
"Config path must be an existing directory: {}",
config_path.display()
));
}
crate::storage::store::set_config_dir(app, &config_path.to_string_lossy())
.map_err(|e| format!("Failed to set config dir: {}", e))?;
}
// API keys and URLs
if let Some(ref key) = openai_key {
crate::storage::store::set_openai_api_key(app, key)
.map_err(|e| format!("Failed to set OpenAI key: {}", e))?;
}
if let Some(ref key) = openrouter_key {
crate::storage::store::set_openrouter_api_key(app, key)
.map_err(|e| format!("Failed to set OpenRouter key: {}", e))?;
}
if let Some(ref url) = ollama_url {
crate::storage::store::set_ollama_api_base_url(app, url)
.map_err(|e| format!("Failed to set Ollama URL: {}", e))?;
}
// Model prefs
if let Some(ref provider) = evolve_provider {
crate::storage::store::set_evolve_provider(app, provider)
.map_err(|e| format!("Failed to set evolve provider: {}", e))?;
}
if let Some(ref model) = evolve_model {
crate::storage::store::set_evolve_model(app, model)
.map_err(|e| format!("Failed to set evolve model: {}", e))?;
}
if let Some(ref provider) = summary_provider {
crate::storage::store::set_summary_provider(app, provider)
.map_err(|e| format!("Failed to set summary provider: {}", e))?;
}
if let Some(ref model) = summary_model {
crate::storage::store::set_summary_model(app, model)
.map_err(|e| format!("Failed to set summary model: {}", e))?;
}
// Resolve effective values: prefer CLI-provided, otherwise read from store if available
let effective_evolve_provider: Option<String> = match &evolve_provider {
Some(p) => Some(p.clone()),
None => crate::storage::store::get_evolve_provider(app)
.ok()
.flatten(),
};
let effective_evolve_model: Option<String> = match &evolve_model {
Some(m) => Some(m.clone()),
None => crate::storage::store::get_evolve_model(app).ok().flatten(),
};
let effective_summary_provider: Option<String> = match &summary_provider {
Some(p) => Some(p.clone()),
None => crate::storage::store::get_summary_provider(app)
.ok()
.flatten(),
};
let effective_summary_model: Option<String> = match &summary_model {
Some(m) => Some(m.clone()),
None => crate::storage::store::get_summary_model(app).ok().flatten(),
};
// Effective legacy iteration fallback: prefer CLI value, otherwise read from store (has default)
let effective_max_iterations: usize = match max_iterations {
Some(v) => v,
None => crate::storage::store::get_max_iterations(app)
.unwrap_or(crate::storage::store::DEFAULT_MAX_ITERATIONS),
};
let effective_max_output_tokens: usize = match max_output_tokens {
Some(v) => v,
None => crate::storage::store::get_max_output_tokens(app)
.unwrap_or(crate::storage::store::DEFAULT_MAX_OUTPUT_TOKENS),
};
// Effective max token budget: prefer CLI value, otherwise read from store (has default)
let effective_max_token_budget: u32 = match max_token_budget {
Some(v) => v,
None => crate::storage::store::get_max_token_budget(app)
.unwrap_or(crate::storage::store::DEFAULT_MAX_TOKEN_BUDGET),
};
// Legacy max iterations
if let Some(iterations) = max_iterations {
crate::storage::store::set_max_iterations(app, iterations)
.map_err(|e| format!("Failed to set max iterations: {}", e))?;
}
if let Some(output_tokens) = max_output_tokens {
crate::storage::store::set_max_output_tokens(app, output_tokens)
.map_err(|e| format!("Failed to set max output tokens: {}", e))?;
}
// Max token budget
if let Some(token_budget) = max_token_budget {
crate::storage::store::set_max_token_budget(app, token_budget)
.map_err(|e| format!("Failed to set max token budget: {}", e))?;
}
// Host
if let Some(ref host_attr) = host {
crate::storage::store::set_host_attr(app, host_attr)
.map_err(|e| format!("Failed to set host: {}", e))?;
}
// DO IT!
println!("Starting evolution with prompt: {}", prompt);
// Disable tools that require user input in the middle of evolution, since we won't be able to respond to questions in CLI mode.
let banned_tools = ["ask_user"];
let outcome = crate::evolve::lifecycle::backup_evolve_and_record_changeset(
app,
&prompt,
Some(&banned_tools),
)
.await;
let (ok, output_value, failure_message) = match outcome {
Ok(output) => {
match output.telemetry.state {
crate::shared_types::EvolutionState::Conversational => {
println!("(conversational response — no changes made)");
}
crate::shared_types::EvolutionState::LimitReached => {
println!(
"Evolution stopped after reaching a safety limit (iterations, build attempts, token budget, or stale progress). Review any partial changes and re-run with adjusted limits to continue."
);
}
_ => {
println!("Evolution completed successfully");
}
}
let output_value = match serde_json::to_value(&output) {
Ok(v) => v,
Err(_) => serde_json::json!({ "raw": format!("{:#?}", output) }),
};
(true, output_value, None)
}
Err(failure) => {
println!("Evolution failed: {}", failure.error);
let output_value = match serde_json::to_value(&failure) {
Ok(v) => v,
Err(_) => serde_json::json!({ "error": failure.error.clone() }),
};
(false, output_value, Some(failure.error))
}
};
if let Some(out_path) = out {
// Hoist `state` to the envelope so test suites can branch on it without
// digging into result.telemetry.state.
let state_str = output_value
.get("state")
.and_then(|v| v.as_str())
.unwrap_or(if ok { "generated" } else { "failed" });
let combined = json!({
"ok": ok,
"state": state_str,
"prompt": prompt,
"maxIterations": effective_max_iterations,
"maxOutputTokens": effective_max_output_tokens,
"maxTokenBudget": effective_max_token_budget,
"evolveProvider": effective_evolve_provider,
"evolveModel": effective_evolve_model,
"summaryProvider": effective_summary_provider,
"summaryModel": effective_summary_model,
"result": output_value,
});
let serialized = serde_json::to_string_pretty(&combined)
.map_err(|e| format!("Failed to serialize combined output: {}", e))?;
std::fs::write(&out_path, serialized)
.map_err(|e| format!("Failed to write output file {}: {}", out_path.display(), e))?;
println!("Wrote output to {}", out_path.display());
}
if let Some(message) = failure_message {
return Err(format!("Evolution failed: {}", message));
}
Ok(())
}
/// Check if CLI mode should be activated based on arguments
pub fn should_run_cli() -> bool {
let args: Vec<String> = std::env::args().collect();
// Skip the binary name (args[0])
if args.len() > 1 {
let subcommand = &args[1];
subcommand == "evolve"
} else {
false
}
}
/// Parse args
pub fn parse_cli() -> Result<Cli, String> {
Cli::try_parse().map_err(|error| error.to_string())
}