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
use std::{
env, fs,
io::{self, IsTerminal, Write},
panic::AssertUnwindSafe,
path::Path,
process,
};
use clap::CommandFactory;
use color_eyre::{Result, eyre::Context};
use intelli_shell::{
app::App,
cli::{Cli, CliProcess, ConfigProcess, LogsProcess, Shell},
config::Config,
errors::{self, AppError},
format_error, logging,
process::{OutputInfo, ProcessOutput},
service::IntelliShellService,
storage::SqliteStorage,
utils::execute_shell_command_inherit,
};
use tokio_util::sync::CancellationToken;
// --- Shell Integration Constants ---
const STATUS_DIRTY: &str = "DIRTY\n";
const STATUS_CLEAN: &str = "CLEAN\n";
const ACTION_EXECUTE: &str = "EXECUTE\n";
const ACTION_EXECUTED: &str = "EXECUTED\n";
const ACTION_REPLACE: &str = "REPLACE\n";
// --- Init Script Constants ---
const BASH_INIT: &str = include_str!("./_shell/intelli-shell.bash");
const ZSH_INIT: &str = include_str!("./_shell/intelli-shell.zsh");
const FISH_INIT: &str = include_str!("./_shell/intelli-shell.fish");
const NUSHELL_INIT: &str = include_str!("./_shell/intelli-shell.nu");
const POWERSHELL_INIT: &str = include_str!("./_shell/intelli-shell.ps1");
#[tokio::main]
async fn main() -> Result<()> {
// Read and initialize config
let (config, stats) = Config::init(env::var("INTELLI_CONFIG").ok().map(Into::into))?;
// Prepare logging
let (logs_path, logs_filter) = logging::resolve_path_and_filter(&config);
// Initialize error handling
errors::init(
logs_filter.is_some().then(|| logs_path.clone()),
AssertUnwindSafe(async move {
// Parse cli arguments
let args = Cli::parse_extended();
// Create a cancellation token
let cancellation_token = CancellationToken::new();
let ctrl_c_token = cancellation_token.clone();
// Link the cancellation token with the ctrl+c signal
tokio::spawn(async move {
tokio::signal::ctrl_c().await.ok();
ctrl_c_token.cancel();
});
// Check for static processes before initialization, to avoid unnecessary overhead
match args.process {
CliProcess::Init(init) => {
let mut output = String::new();
let include_integration = init.integration || !init.completions;
let include_completions = init.completions || !init.integration;
if include_integration {
let script = match init.shell {
Shell::Bash => BASH_INIT,
Shell::Zsh => ZSH_INIT,
Shell::Fish => FISH_INIT,
Shell::Nushell => NUSHELL_INIT,
Shell::Powershell => POWERSHELL_INIT,
};
output.push_str(script);
}
if include_completions {
let mut cmd = Cli::command();
let cmd_bin_name = cmd.get_name().to_string();
let mut completions = Vec::new();
match init.shell {
Shell::Bash => clap_complete::generate(
clap_complete::Shell::Bash,
&mut cmd,
cmd_bin_name,
&mut completions,
),
Shell::Zsh => clap_complete::generate(
clap_complete::Shell::Zsh,
&mut cmd,
cmd_bin_name,
&mut completions,
),
Shell::Fish => clap_complete::generate(
clap_complete::Shell::Fish,
&mut cmd,
cmd_bin_name,
&mut completions,
),
Shell::Powershell => clap_complete::generate(
clap_complete::Shell::PowerShell,
&mut cmd,
cmd_bin_name,
&mut completions,
),
Shell::Nushell => clap_complete::generate(
clap_complete_nushell::Nushell,
&mut cmd,
cmd_bin_name,
&mut completions,
),
}
if include_integration {
output.push('\n');
}
output.push_str(&String::from_utf8_lossy(&completions));
}
let output_info = OutputInfo {
stdout: Some(output),
..Default::default()
};
return handle_output(
ProcessOutput::Output(output_info),
args.file_output,
args.skip_execution,
cancellation_token,
)
.await;
}
CliProcess::Config(ConfigProcess { path }) => {
if path {
println!("{}", stats.config_path.display());
} else {
if let Some(parent) = stats.config_path.parent() {
fs::create_dir_all(parent)
.wrap_err_with(|| format!("Failed to create config directory: {}", parent.display()))?;
}
edit::edit_file(&stats.config_path)
.wrap_err_with(|| format!("Failed to open config file: {}", stats.config_path.display()))?;
}
return Ok(());
}
CliProcess::Logs(LogsProcess { path }) => {
if path {
println!("{}", logs_path.display());
} else {
match fs::read_to_string(&logs_path) {
Ok(logs_content) if !logs_content.is_empty() => {
println!("{logs_content}");
}
_ => {
eprintln!(
"{}",
format_error!(
config.theme,
"No logs found on: {}\n\nMake sure logging is enabled in the config file: {}",
logs_path.display(),
stats.config_path.display()
)
)
}
}
}
return Ok(());
}
_ => (),
}
// Initialize logging
logging::init(logs_path, logs_filter)?;
// Initial logs
tracing::info!("intelli-shell v{}", env!("CARGO_PKG_VERSION"));
match (stats.config_loaded, stats.default_config_path) {
(true, true) => tracing::info!("Loaded config from default path: {}", stats.config_path.display()),
(true, false) => tracing::info!("Loaded config from custom path: {}", stats.config_path.display()),
(false, true) => tracing::info!("No config found at default path: {}", stats.config_path.display()),
(false, false) => tracing::warn!("No config found at custom path: {}", stats.config_path.display()),
}
if stats.default_data_dir {
tracing::info!("Using default data dir: {}", config.data_dir.display());
} else {
tracing::info!("Using custom data dir: {}", config.data_dir.display());
}
// Initialize the storage and the service
let storage = SqliteStorage::new(&config.data_dir)
.await
.map_err(AppError::into_report)?;
let service = IntelliShellService::new(
storage,
config.tuning,
config.ai.clone(),
&config.data_dir,
config.check_updates,
);
// Run the app
let app_cancellation_token = cancellation_token.clone();
let output = App::new(app_cancellation_token)?
.run(config, service, args.process, args.extra_line)
.await?;
// Process the output
handle_output(output, args.file_output, args.skip_execution, cancellation_token).await
}),
)
.await
}
/// Handles the process output according to the specified options
async fn handle_output(
output: ProcessOutput,
file_output_path: Option<String>,
skip_execution: bool,
cancellation_token: CancellationToken,
) -> Result<()> {
// --- Shell Integration ---
if let Some(path_str) = &file_output_path {
let mut file_content = String::new();
match &output {
ProcessOutput::Execute { cmd } => {
// When executing a command, the terminal is clean
file_content.push_str(STATUS_CLEAN);
if skip_execution {
// Shell can execute; tell it to run this command
file_content.push_str(ACTION_EXECUTE);
file_content.push_str(cmd);
} else {
// Shell cannot execute; intelli-shell ran it
file_content.push_str(ACTION_EXECUTED);
// No command content is needed
}
}
ProcessOutput::Output(info) => {
// Determine status based on stderr
if info.stderr.is_some() {
file_content.push_str(STATUS_DIRTY);
} else {
file_content.push_str(STATUS_CLEAN);
}
// If there's content for the buffer, add the REPLACE action
if let Some(cmd) = &info.fileout {
file_content.push_str(ACTION_REPLACE);
file_content.push_str(cmd);
}
}
}
// Remove trailing newline to keep the file clean
let content = file_content.trim_end_matches('\n');
tracing::info!("[fileout]\n{content}");
let path_output = Path::new(&path_str);
if let Some(parent) = path_output.parent() {
fs::create_dir_all(parent)
.wrap_err_with(|| format!("Failed to create parent directories for: {}", parent.display()))?;
}
fs::write(path_output, content).wrap_err_with(|| format!("Failed to write to fileout path: {path_str}"))?;
}
// Handle the output based on its variant
match output {
// The process wants to execute a command
ProcessOutput::Execute { cmd } => {
// If shell integration is NOT active OR the shell is not capable of executing the command itself
if !skip_execution {
// Execute it here
let status =
execute_shell_command_inherit(&cmd, file_output_path.is_none(), cancellation_token).await?;
// And check if the command failed
if !status.success() {
let code = status.code().unwrap_or(1);
tracing::info!("[exit code] {code}");
process::exit(code);
}
}
}
// The process has output to show
ProcessOutput::Output(info) => {
// Determine color usage for stdout and stderr based on env vars and TTY
let use_color_stderr = should_use_color(io::stderr().is_terminal());
let use_color_stdout = should_use_color(io::stdout().is_terminal());
// Print stderr if it exists
if let Some(stderr) = info.stderr {
let stderr_nocolor = strip_ansi_escapes::strip_str(&stderr);
tracing::info!("[stderr] {stderr_nocolor}");
let write_result = if use_color_stderr {
writeln!(io::stderr(), "{stderr}")
} else {
writeln!(io::stderr(), "{stderr_nocolor}")
};
// Handle broken pipe
if let Err(err) = write_result {
if err.kind() != io::ErrorKind::BrokenPipe {
return Err(err).wrap_err("Failed writing to stderr");
}
tracing::error!("Failed writing to stderr: Broken pipe");
}
}
// Only print to stdout if NOT using file output
if file_output_path.is_none()
&& let Some(stdout) = info.stdout
{
let stdout_nocolor = strip_ansi_escapes::strip_str(&stdout);
tracing::info!("[stdout] {stdout_nocolor}");
let write_result = if use_color_stdout {
writeln!(io::stdout(), "{stdout}")
} else {
writeln!(io::stdout(), "{stdout_nocolor}")
};
// Handle broken pipe
if let Err(err) = write_result {
if err.kind() != io::ErrorKind::BrokenPipe {
return Err(err).wrap_err("Failed writing to stdout");
}
tracing::error!("Failed writing to stdout: Broken pipe");
}
}
// Exit with a non-zero status code when the process failed
if info.failed {
tracing::info!("[exit code] 1");
process::exit(1);
}
}
}
Ok(())
}
/// Determines whether to use color for a given output stream.
///
/// Precedence:
/// 1. `NO_COLOR` environment variable (if set to any value, disables color)
/// 2. `CLICOLOR_FORCE` environment variable (if set and not "0", forces color)
/// 3. `CLICOLOR` environment variable (if set to "0", disables color)
/// 4. `stream_is_tty` (default if not overridden by env vars)
fn should_use_color(stream_is_tty: bool) -> bool {
// 1. NO_COLOR environment variable (takes highest precedence)
if env::var("NO_COLOR").is_ok() {
return false;
}
// 2. CLICOLOR_FORCE environment variable
if let Ok(force_val) = env::var("CLICOLOR_FORCE")
&& !force_val.is_empty()
&& force_val != "0"
{
return true;
}
// 3. CLICOLOR environment variable
if let Ok(clicolor_val) = env::var("CLICOLOR")
&& clicolor_val == "0"
{
return false;
}
// 4. TTY status (default if no strong opinions from env vars)
stream_is_tty
}