use anyhow::bail;
use channel_plugin::message::LogLevel;
use clap::{Args, Parser, Subcommand};
use greentic::{
apps::{App, cmd_init, download},
config::{ConfigManager, EnvConfigManager},
flow_commands::{deploy_flow_file, move_flow_file, validate_flow_file},
logger::{FileTelemetry, init_tracing},
schema::write_schema,
secret::{EnvSecretsManager, SecretsManager},
};
use std::{env, fs, path::PathBuf, process};
use tracing::{error, info};
#[derive(Parser, Debug)]
#[command(
name = "greentic",
about = "The Greener Digital Workers Platform",
version = "0.2.0-rc4"
)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand, Debug)]
enum Commands {
Run(RunArgs),
Schema(SchemaArgs),
Init,
Flow(FlowArgs),
Secrets(SecretArgs),
Config(ConfigArgs),
}
#[derive(Args, Debug)]
struct SecretArgs {
#[command(subcommand)]
command: SecretCommands,
}
#[derive(Args, Debug)]
struct ConfigArgs {
#[command(subcommand)]
command: ConfigCommands,
}
#[derive(Args, Debug)]
struct FlowArgs {
#[command(subcommand)]
command: FlowCommands,
}
#[derive(Args, Debug)]
struct RunArgs {
#[arg(long, default_value = "1800")]
session_timeout: u64,
#[arg(long, default_value = "info")]
log_level: String,
#[arg(long)]
otel_logs_endpoint: Option<String>,
#[arg(long)]
otel_events_endpoint: Option<String>,
}
#[derive(Args, Debug)]
struct SchemaArgs {
#[arg(long, default_value = "info")]
log_level: String,
#[arg(long, default_value_t = true)]
logs_enabled: bool,
}
#[derive(Subcommand, Debug)]
enum FlowCommands {
Validate { file: PathBuf },
Deploy { file: PathBuf },
Pull { name: String },
Start { name: String },
Stop { name: String },
}
#[derive(Subcommand, Debug)]
enum SecretCommands {
Add { key: String, secret: String },
Update { key: String, secret: String },
Delete { key: String },
}
#[derive(Subcommand, Debug)]
enum ConfigCommands {
Add { key: String, value: String },
Update { key: String, value: String },
Delete { key: String },
}
pub fn resolve_root_dir() -> PathBuf {
if let Ok(path) = env::var("GREENTIC_ROOT") {
PathBuf::from(path)
} else {
PathBuf::from("./")
}
}
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() -> anyhow::Result<()> {
let root = resolve_root_dir().join("greentic");
let config_dir = root.join("config");
let secrets_dir = root.join("secrets");
let env_file = config_dir.join(".env");
let config_manager = ConfigManager(EnvConfigManager::new(env_file.clone()));
let secrets_manager = SecretsManager(EnvSecretsManager::new(Some(secrets_dir.clone())));
let cli = Cli::parse();
match cli.command.unwrap_or(Commands::Run(RunArgs {
session_timeout: 1800,
log_level: "info".to_string(),
otel_logs_endpoint: None,
otel_events_endpoint: None,
})) {
Commands::Run(args) => {
run(
root,
args.session_timeout,
args.log_level,
args.otel_logs_endpoint,
args.otel_events_endpoint,
secrets_manager,
config_manager,
)
.await?;
Ok(())
}
Commands::Schema(args) => {
let out_dir = root.join("schemas");
let log_level = args.log_level;
let log_file = root.join("logs/greentic-schema.log");
let event_file = root.join("logs/greentic-schema.json");
let tools_dir = root.join("plugins").join("tools");
let channels_dir = root.join("plugins").join("channels").join("running");
let _processes_dir = root.join("plugins").join("processes");
fs::create_dir_all(&out_dir)?;
write_schema(
out_dir.clone(),
tools_dir.clone(),
channels_dir.clone(),
vec![],
log_level,
log_file.clone(),
event_file.clone(),
)
.await?;
println!("Schemas written to {}", out_dir.display());
process::exit(0);
}
Commands::Init => {
let log_file = root.join("logs/greentic-init.log");
let event_file = root.join("logs/greentic-init.json");
let _ = FileTelemetry::init_files("info", log_file, event_file);
cmd_init(root.clone()).await?;
println!("Greentic has been initialised. You can start it with 'greentic run'");
process::exit(0);
}
Commands::Secrets(secret_args) => match secret_args.command {
SecretCommands::Add { key, secret } => {
match secrets_manager.add_secret(&key, &secret).await {
Ok(_) => println!("✅ Secret added."),
Err(_) => eprintln!("❌ Secret could not be added."),
}
Ok(())
}
SecretCommands::Update { key, secret } => {
match secrets_manager.update_secret(&key, &secret).await {
Ok(_) => println!("✅ Secret updated."),
Err(_) => eprintln!("❌ Secret could not be updated."),
}
Ok(())
}
SecretCommands::Delete { key } => {
match secrets_manager.delete_secret(&key).await {
Ok(_) => println!("✅ Secret deleted."),
Err(_) => eprintln!("❌ Secret could not be deleted."),
}
Ok(())
}
},
Commands::Config(config_args) => match config_args.command {
ConfigCommands::Add { key, value } => {
match config_manager.0.set(&key, &value).await {
Ok(_) => println!("✅ Config added."),
Err(_) => eprintln!("❌ Config could not be added."),
}
Ok(())
}
ConfigCommands::Update { key, value } => {
match config_manager.0.set(&key, &value).await {
Ok(_) => println!("✅ Config updated."),
Err(_) => eprintln!("❌ Config could not be updated."),
}
Ok(())
}
ConfigCommands::Delete { key } => {
config_manager.0.del(&key).await;
Ok(())
}
},
Commands::Flow(flow_args) => match flow_args.command {
FlowCommands::Validate { file } => {
let tools_dir = root.join("plugins").join("tools");
validate_flow_file(file, root, tools_dir, secrets_manager, config_manager).await?;
println!("✅ Flow file is valid.");
Ok(())
}
FlowCommands::Pull { name } => {
let token_handle = secrets_manager.0.get("GREENTIC_TOKEN");
if token_handle.is_none() {
println!(
"Please run 'greentic init' one time before calling 'greentic pull ...'"
);
return Ok(());
}
let token = secrets_manager
.0
.reveal(token_handle.unwrap())
.await
.unwrap()
.unwrap();
let out_dir = root.join("flows/running");
let result = download(&token, "flows", &name, out_dir.clone()).await;
if result.is_err() {
println!(
"Sorry we could not download {} because {:?}",
name,
result.err().unwrap().to_string()
);
}
let tools_dir = root.join("plugins").join("tools");
validate_flow_file(
out_dir.join(name),
root,
tools_dir,
secrets_manager,
config_manager,
)
.await?;
Ok(())
}
FlowCommands::Deploy { file } => {
let tools_dir = root.join("plugins").join("tools");
deploy_flow_file(file, root, tools_dir, secrets_manager, config_manager).await?;
Ok(())
}
FlowCommands::Start { name } => {
let from = root.join("flows/stopped");
let to = root.join("flows/running");
move_flow_file(&name, &from, &to)?;
println!(
"✅ Flow `{}` started. Please make sure greentic is running, i.e. greentic run",
name
);
Ok(())
}
FlowCommands::Stop { name } => {
let from = root.join("flows/running");
let to = root.join("flows/stopped");
move_flow_file(&name, &from, &to)?;
println!("✅ Flow `{}` stopped.", name);
Ok(())
}
},
}
}
async fn run(
root: PathBuf,
session_timeout: u64,
log_level: String,
otel_logs_endpoint: Option<String>,
otel_events_endpoint: Option<String>,
secrets_manager: SecretsManager,
config_manager: ConfigManager,
) -> anyhow::Result<()> {
let flows_dir = root.join("flows/running");
let log_file = "logs/greentic_logs.log".to_string();
let log_dir = Some(root.join("logs"));
let event_file = "logs/greentic_events.log".to_string();
let tools_dir = root.join("plugins").join("tools");
let processes_dir = root.join("plugins").join("processes");
let channels_dir = root.join("plugins").join("channels/running");
let logger = init_tracing(
root.clone(),
log_file.clone(),
event_file.clone(),
log_level.clone(),
otel_logs_endpoint.clone(),
otel_events_endpoint.clone(),
)
.expect("could not create logger");
info!("Greentic runtime starting up…");
println!("Greentic runtime starting up…");
if !root.exists() {
let err = format!(
"Root directory `{}` does not exist. \
Please run `greentic init -r <your-dir>` first.",
root.display()
);
error!("{}", err);
bail!(err);
}
let mut app = App::new();
let result = app
.bootstrap(
session_timeout.clone(),
flows_dir.clone(),
channels_dir.clone(),
tools_dir.clone(),
processes_dir.clone(),
config_manager,
logger,
convert_level(log_level),
log_dir,
otel_logs_endpoint,
secrets_manager,
)
.await;
if result.is_err() {
error!(
"Failed to bootstrap greentic runtime: {:#}",
result.err().unwrap()
);
process::exit(1);
};
println!(
r#"
@%@@ @@@@ @@@@@@@
@%*#%##%@@%++%@ @@%##*+++*%@@
@#========+++#@ @@#***++++*%@
@%%%%@@@@#*+===+=====#@ @%#*#*++++++*%
@***===++++**+=++*=====#@@#*@%######**%@
@#=======++%============*@@@#%%#+++++*#%@@
%*================+#+===+@ @###+##*+*@@
@#===============*%=====+%%%*%*+++**%@
@@@@@@#+=====+#@#======#*#*#*+*#%@@@@
@%+#@#***#%%#===+===*+===+*%%@ @@@
@#==++*++===+%+=============#@ @@%*%@
@@%%###%%#+================+%@@%*=#@
@@++++================+=====*@@*+%@@
@@+=================+*======+#==#@
@+==============++=%=======+##@@
@*==#*===*======+#*%=======+%@
@*===*#@@#======*#*%*======#@
@@+===+#@@#======%@@@%*====+%@
@@+*#*+*%@@##+#*=+@@@%**#*+=#@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
G R E E N T I C A I 🦕
Fast | Secure | Extendable
"#
);
info!("Greentic runtime running; press Ctrl‐C to exit");
println!("Greentic runtime running; press Ctrl‐C to exit");
tokio::signal::ctrl_c().await?;
println!("\nShutting down…");
info!("Greentic runtime shutting down");
app.shutdown().await;
println!("Goodbye!");
process::exit(0);
}
fn convert_level(level: String) -> LogLevel {
match level.to_lowercase().as_str() {
"trace" => LogLevel::Trace,
"debug" => LogLevel::Debug,
"info" => LogLevel::Info,
"warn" => LogLevel::Warn,
"error" => LogLevel::Error,
"critical" => LogLevel::Critical,
_ => LogLevel::Info,
}
}