forgot to commit, a lot of stuff happened

This commit is contained in:
2026-07-13 23:25:35 -05:00
parent 1da8ea923b
commit 8487a612eb
8 changed files with 623 additions and 70 deletions
Generated
+32
View File
@@ -451,7 +451,9 @@ dependencies = [
"dotenvy", "dotenvy",
"hex", "hex",
"keyring", "keyring",
"serde",
"tokio", "tokio",
"toml",
"uuid", "uuid",
] ]
@@ -1467,6 +1469,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "serde_spanned"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.9" version = "0.10.9"
@@ -1642,6 +1653,21 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "toml"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "1.1.1+spec-1.1.0" version = "1.1.1+spec-1.1.0"
@@ -1672,6 +1698,12 @@ dependencies = [
"winnow", "winnow",
] ]
[[package]]
name = "toml_writer"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.44" version = "0.1.44"
+2
View File
@@ -22,6 +22,8 @@ chrono = "0.4.45"
dotenvy = "0.15" dotenvy = "0.15"
uuid = { version = "1.23.5", features = ["v4"] } uuid = { version = "1.23.5", features = ["v4"] }
hex = "0.4" hex = "0.4"
serde = { version = "1.0.228", features = ["derive"] }
toml = "1.1.2"
[[bin]] [[bin]]
name = "harbor" name = "harbor"
@@ -10,7 +10,8 @@ CREATE TABLE secrets (
id TEXT PRIMARY KEY NOT NULL, id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
project_id TEXT NOT NULL, project_id TEXT NOT NULL,
config TEXT NOT NULL, config TEXT NOT NULL
CHECK (config IN ('dev', 'prod', 'staging')),
secret BLOB NOT NULL, secret BLOB NOT NULL,
nonce BLOB NOT NULL, nonce BLOB NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+81
View File
@@ -0,0 +1,81 @@
use crate::Environment;
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
const MAIN_CONFIG_FILE: &str = ".harbor.toml";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Config {
pub name: String,
pub version: String,
pub default_env: Environment,
}
#[derive(Debug)]
pub enum ConfigError {
Io(std::io::Error),
Toml(toml::de::Error),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigError::Io(err) => write!(f, "IO error: {}", err),
ConfigError::Toml(err) => write!(f, "TOML error: {}", err),
}
}
}
impl std::error::Error for ConfigError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ConfigError::Io(err) => Some(err),
ConfigError::Toml(err) => Some(err),
_ => None,
}
}
}
impl From<std::io::Error> for ConfigError {
fn from(err: std::io::Error) -> Self {
ConfigError::Io(err)
}
}
impl From<toml::de::Error> for ConfigError {
fn from(err: toml::de::Error) -> Self {
ConfigError::Toml(err)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct HarborToml {
#[serde(alias = "project")]
name: String,
version: String,
config: Environment,
}
impl Config {
pub fn from_repo_root(root: impl AsRef<Path>) -> Result<Self, ConfigError> {
let root = root.as_ref();
let main = Self::read_main_file(root.join(MAIN_CONFIG_FILE))?;
Ok(Config {
name: main.name,
version: main.version,
default_env: main.config,
})
}
pub fn read_main_file(path: impl AsRef<Path>) -> Result<HarborToml, ConfigError> {
let contents = fs::read_to_string(path)?;
let parsed: HarborToml = toml::from_str(&contents)?;
Ok(parsed)
}
pub fn merged_env_vars(&self, store_vars: &HashMap<String, String>) -> HashMap<String, String> {
store_vars.clone()
}
}
+2 -8
View File
@@ -1,3 +1,4 @@
use crate::Environment;
use crate::db::schema::{projects, secrets}; use crate::db::schema::{projects, secrets};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::prelude::*; use diesel::prelude::*;
@@ -11,13 +12,6 @@ pub struct Project {
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
} }
#[derive(Insertable)]
#[diesel(table_name = projects)]
pub struct NewProject<'a> {
pub id: &'a str,
pub name: &'a str,
}
#[derive(Debug, Queryable, Selectable)] #[derive(Debug, Queryable, Selectable)]
#[diesel(table_name = secrets)] #[diesel(table_name = secrets)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
@@ -25,7 +19,7 @@ pub struct Secret {
pub id: String, pub id: String,
pub name: String, pub name: String,
pub project_id: String, pub project_id: String,
pub config: String, pub config: Environment,
pub secret: Vec<u8>, pub secret: Vec<u8>,
pub nonce: Vec<u8>, pub nonce: Vec<u8>,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
+255 -36
View File
@@ -1,13 +1,87 @@
use base64::{Engine, engine::general_purpose::STANDARD}; use base64::{Engine, engine::general_purpose::STANDARD};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use colored::Colorize;
use crypto::helper::gen_key; use crypto::helper::gen_key;
use diesel::backend::Backend;
use diesel::deserialize::{self, FromSql};
use diesel::serialize::{self, IsNull, Output, ToSql};
use diesel::sql_types::Text;
use diesel::sqlite::Sqlite;
use keyring::Entry; use keyring::Entry;
use serde::Deserialize;
use std::error::Error; use std::error::Error;
use std::io::{self, Write}; use std::io::{self, Write};
pub mod config;
mod db; mod db;
mod store; mod store;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Deserialize, diesel::AsExpression, diesel::FromSqlRow,
)]
#[diesel(sql_type = Text)]
#[serde(rename_all = "kebab-case")]
pub enum Environment {
Dev,
Prod,
Staging,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnvironmentParseError(String);
impl Environment {
pub const fn as_str(self) -> &'static str {
match self {
Environment::Dev => "dev",
Environment::Prod => "prod",
Environment::Staging => "staging",
}
}
}
impl std::fmt::Display for Environment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for Environment {
type Err = EnvironmentParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"dev" => Ok(Environment::Dev),
"prod" => Ok(Environment::Prod),
"staging" => Ok(Environment::Staging),
_ => Err(EnvironmentParseError(
"Environment must be one of: dev, prod, staging".to_string(),
)),
}
}
}
impl std::fmt::Display for EnvironmentParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for EnvironmentParseError {}
impl ToSql<Text, Sqlite> for Environment {
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
out.set_value(self.as_str());
Ok(IsNull::No)
}
}
impl FromSql<Text, Sqlite> for Environment {
fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
let raw = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
raw.parse::<Environment>()
.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync>)
}
}
#[derive(Parser)] #[derive(Parser)]
#[clap( #[clap(
name = "Harbor", name = "Harbor",
@@ -26,24 +100,82 @@ pub struct Cli {
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum Commands { pub enum Commands {
Inject { Inject {
#[arg(short, long)]
verbose: bool,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)] #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
after: Vec<String>, after: Vec<String>,
}, },
Shell {}, Shell {},
Add {}, #[command(alias = "add")]
Set {
#[arg(short = 'e', long = "environment")]
environment: Option<String>,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
vars: Vec<String>,
},
Delete {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
keys: Vec<String>,
},
Setup {}, Setup {},
List {}, List {},
Project { Project {
#[command(subcommand)] #[command(subcommand)]
command: ProjectCommands, command: ProjectCommands,
}, },
Config { }
#[command(subcommand)]
command: ConfigCommands, pub fn parse_secret_pairs(raw: &[String]) -> Result<Vec<(String, String)>, Box<dyn Error>> {
}, let mut pairs = Vec::new();
for item in raw {
let trimmed = item.trim();
if trimmed.is_empty() {
continue;
}
let (key, value) = trimmed.split_once('=').ok_or_else(|| {
Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Invalid secret format: {}", item),
)) as Box<dyn Error>
})?;
if key.trim().is_empty() {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Secret key cannot be empty",
)));
}
pairs.push((key.to_string(), value.to_string()));
}
Ok(pairs)
}
pub fn format_doppler_set_command(secrets: &[(String, String)]) -> String {
let mut command = String::from("doppler secrets set");
if secrets.is_empty() {
return command;
}
command.push_str(" \\");
for (index, (key, value)) in secrets.iter().enumerate() {
command.push_str("\n ");
command.push_str(key);
command.push_str("=\"");
command.push_str(value);
command.push('"');
if index + 1 != secrets.len() {
command.push_str(" \\");
}
}
command
} }
#[derive(Subcommand)] #[derive(Subcommand)]
@@ -53,36 +185,31 @@ pub enum ProjectCommands {
Delete { name: String }, Delete { name: String },
} }
#[derive(Subcommand)]
pub enum ConfigCommands {
List {},
Create { project: String },
Delete { project: String, name: String },
}
pub mod interactions { pub mod interactions {
use super::db::models::{NewProject, Project}; use super::db::establish_connection;
use super::db::{ use super::db::models::Project;
establish_connection, use crate::Environment;
schema::projects::dsl::{name as project_name, projects}, use diesel::dsl::{insert_into, update};
};
use diesel::result::{DatabaseErrorKind, Error as DieselError}; use diesel::result::{DatabaseErrorKind, Error as DieselError};
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper}; use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper};
use std::error::Error; use std::error::Error;
use uuid::Uuid; use uuid::Uuid;
fn construct_error(message: &str) -> Result<(), Box<dyn Error>> { type Result<T> = std::result::Result<T, Box<dyn Error>>;
fn construct_error(message: &str) -> Result<()> {
Err(Box::new(std::io::Error::new( Err(Box::new(std::io::Error::new(
std::io::ErrorKind::AlreadyExists, std::io::ErrorKind::AlreadyExists,
message.to_string(), message.to_string(),
))) )))
} }
pub fn project_exists(proj_name: &str) -> Result<bool, Box<dyn Error>> { pub fn project_exists(proj_name: &str) -> Result<bool> {
use super::db::schema::projects::dsl::{name, projects};
let mut conn = establish_connection(); let mut conn = establish_connection();
let existing_project = projects let existing_project = projects
.filter(project_name.eq(proj_name)) .filter(name.eq(proj_name))
.select(Project::as_select()) .select(Project::as_select())
.first::<Project>(&mut conn) .first::<Project>(&mut conn)
.optional()?; .optional()?;
@@ -90,21 +217,22 @@ pub mod interactions {
Ok(existing_project.is_some()) Ok(existing_project.is_some())
} }
pub fn create_project(proj_name: &str) -> Result<(), Box<dyn Error>> { pub fn create_project(proj_name: &str) -> Result<()> {
use super::db::schema::projects::dsl::{created_at, id, name, projects};
let mut conn = establish_connection(); let mut conn = establish_connection();
if project_exists(proj_name)? { if project_exists(proj_name)? {
return construct_error("Project already exists"); return construct_error("Project already exists");
} }
let project_id = Uuid::new_v4().to_string(); let proj_id = Uuid::new_v4().to_string();
let new_project = NewProject {
id: &project_id,
name: proj_name,
};
match diesel::insert_into(projects) match insert_into(projects)
.values(&new_project) .values((
id.eq(proj_id),
name.eq(proj_name),
created_at.eq(chrono::Utc::now().naive_utc()),
))
.execute(&mut conn) .execute(&mut conn)
{ {
Ok(_) => Ok(()), Ok(_) => Ok(()),
@@ -115,19 +243,106 @@ pub mod interactions {
} }
} }
pub fn delete_project(proj_name: &str) -> Result<(), Box<dyn Error>> { pub fn delete_project(proj_name: &str) -> Result<()> {
use super::db::schema::projects::dsl::{name, projects};
let mut conn = establish_connection(); let mut conn = establish_connection();
if !project_exists(proj_name)? { if !project_exists(proj_name)? {
return construct_error("Project does not exist"); return construct_error("Project does not exist");
} }
diesel::delete(projects.filter(project_name.eq(proj_name))).execute(&mut conn)?; diesel::delete(projects.filter(name.eq(proj_name))).execute(&mut conn)?;
Ok(())
}
pub fn get_projects() -> Result<Vec<Project>> {
use super::db::schema::projects::dsl::projects;
let mut conn = establish_connection();
let results = projects
.select(Project::as_select())
.load::<Project>(&mut conn)?;
Ok(results)
}
pub fn secret_exists() -> Result<bool> {
use super::db::schema::secrets::dsl::{id, secrets};
let mut conn = establish_connection();
let existing_secret = secrets.select(id).first::<String>(&mut conn).optional()?;
Ok(existing_secret.is_some())
}
pub fn get_project_id(proj_name: &str) -> Result<String> {
use super::db::schema::projects::dsl::{id, name, projects};
let mut conn = establish_connection();
let project_id = projects
.filter(name.eq(proj_name))
.select(id)
.first::<String>(&mut conn)
.optional()?;
match project_id {
Some(pid) => Ok(pid),
None => Err(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Project not found",
))),
}
}
pub fn set_secret(
proj_id: &str,
secret_name: &str,
secret_value: Vec<u8>,
conf: Environment,
non: crypto::Nonce,
) -> Result<()> {
use super::db::schema::secrets::dsl::{
config, created_at, name, nonce, project_id, secret, secrets,
};
let mut conn = establish_connection();
if secret_exists()? {
update(secrets.filter(name.eq(secret_name)))
.set(secret.eq(secret_value))
.execute(&mut conn)?;
} else {
insert_into(secrets)
.values((
name.eq(secret_name),
secret.eq(secret_value),
project_id.eq(proj_id),
config.eq(conf),
nonce.eq(non.to_vec()),
created_at.eq(chrono::Utc::now().naive_utc()),
))
.execute(&mut conn)?;
}
Ok(())
}
pub fn delete_secret(secret_name: &str) -> Result<()> {
use super::db::schema::secrets::dsl::{name, secrets};
let mut conn = establish_connection();
if !secret_exists()? {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Secret does not exist",
)));
}
diesel::delete(secrets.filter(name.eq(secret_name))).execute(&mut conn)?;
Ok(()) Ok(())
} }
} }
pub fn gen_or_get_key() -> Result<crypto::Key, Box<dyn Error>> { pub fn gen_or_get_key() -> Result<crypto::Key, Box<dyn Error>> {
let entry = Entry::new("harbor", "encryption-key")?; let entry = Entry::new("harbor", "encryption-key")?;
@@ -153,7 +368,11 @@ pub fn gen_or_get_key() -> Result<crypto::Key, Box<dyn Error>> {
} }
} }
pub fn get_input(message: impl std::fmt::Display, prompt: char, new_line: bool) -> Result<String, Box<dyn Error>> { pub fn get_input(
message: impl std::fmt::Display,
prompt: char,
new_line: bool,
) -> Result<String, Box<dyn Error>> {
let mut input = String::new(); let mut input = String::new();
if new_line { if new_line {
+247 -23
View File
@@ -3,17 +3,27 @@ use std::process;
use clap::Parser; use clap::Parser;
use clap::crate_version; use clap::crate_version;
use cli::Cli; use cli::Cli;
use cli::Environment;
use cli::config::{Config, ConfigError};
use cli::gen_or_get_key;
use cli::get_input; use cli::get_input;
use cli::interactions::delete_secret;
use cli::interactions::get_project_id;
use cli::interactions::get_projects;
use cli::interactions::set_secret;
use colored::*; use colored::*;
use crypto::encrypt;
const GIT_VERSION: &str = env!("GIT_VERSION"); const GIT_VERSION: &str = env!("GIT_VERSION");
fn main() { fn main() {
use cli::{Commands, ConfigCommands, ProjectCommands}; use cli::{Commands, ProjectCommands};
let cli = Cli::parse(); let cli = Cli::parse();
if cli.version { if cli.version {
let top_msg = format!("Harbor version {}", crate_version!().green()).bright_cyan(); let top_msg = format!("Harbor version {}", crate_version!().green())
.bright_cyan()
.bold();
let bottom_msg = let bottom_msg =
format!("An open source secrets management and distribution platform.").blue(); format!("An open source secrets management and distribution platform.").blue();
let commit_msg = format!("Git commit {}", GIT_VERSION).dimmed(); let commit_msg = format!("Git commit {}", GIT_VERSION).dimmed();
@@ -22,39 +32,146 @@ fn main() {
process::exit(0); process::exit(0);
} }
let root = match std::env::current_dir() {
Ok(path) => path,
Err(err) => {
eprintln!("Error resolving current directory: {}", err);
process::exit(1);
}
};
let config_path = root.join(".harbor.toml");
let has_config = config_path.exists();
if !has_config {
eprintln!(
"{}",
"Warning: .harbor.toml not found. Some commands are disabled.".yellow()
);
}
match cli.command { match cli.command {
Some(c) => match c { Some(c) => match c {
Commands::Add {} => { _ if requires_config(&c) && !has_config => {
println!("Add command executed"); eprintln!(
} "{}",
Commands::Config { command } => match command { "Missing .harbor.toml. Run `harbor config create` first.".yellow()
ConfigCommands::List {} => {
println!("Config list command executed");
}
ConfigCommands::Create { project } => {
println!("Config create command executed for project: {}", project);
}
ConfigCommands::Delete { project, name } => {
println!(
"Config delete command executed for project: {}, name: {}",
project, name
); );
process::exit(1);
}
Commands::Set { environment, vars } => {
let config = require_config(&root);
let environment: Environment = match environment {
Some(env) => match env.parse::<Environment>() {
Ok(parsed) => parsed,
Err(_) => {
eprintln!("Invalid environment '{}'.", env);
process::exit(1);
} }
}, },
Commands::Inject { verbose, after } => { None => config.default_env.into(),
println!( };
"Inject command executed with verbose: {}, after: {:?}",
verbose, after let pairs = match cli::parse_secret_pairs(&vars) {
); Ok(pairs) => pairs,
Err(e) => {
eprintln!("Error parsing secrets: {}", e);
process::exit(1);
}
};
if pairs.is_empty() {
eprintln!("No secrets provided. Use KEY=VALUE pairs.");
process::exit(1);
}
let project = match get_project_id(&config.name) {
Ok(p) => p,
Err(e) => {
eprintln!("Unable to get project ID for '{}': {}", config.name, e);
process::exit(1);
}
};
for pair in pairs {
let key = match gen_or_get_key() {
Ok(k) => k,
Err(_) => {
eprintln!("Error generating or getting key");
process::exit(1);
}
};
let (nonce, encrypted) = match encrypt(&key, pair.1.as_bytes().to_vec()) {
Ok(result) => result,
Err(e) => {
eprintln!("Error encrypting secret for key '{}': {}", pair.0, e);
process::exit(1);
}
};
match set_secret(&project, &pair.0, encrypted, environment, nonce) {
Ok(()) => {}
Err(e) => {
eprintln!("Error setting secret for key '{}': {}", pair.0, e);
process::exit(1);
}
}
}
}
Commands::Delete { keys } => {
let mut cleaned = Vec::new();
for key in keys {
let trimmed = key.trim();
if !trimmed.is_empty() {
cleaned.push(trimmed.to_string());
}
}
// Redefining as immutable
let cleaned = cleaned;
if cleaned.is_empty() {
eprintln!("No secret keys provided. Use one or more keys.");
process::exit(1);
}
for key in cleaned {
match delete_secret(&key) {
Ok(()) => {
println!("Deleted secret for key '{}'", key);
}
Err(e) => {
eprintln!("Error deleting secret for key '{}': {}", key, e);
process::exit(1);
}
};
}
}
Commands::Inject { after } => {
let _config = require_config(&root);
// for harbor inject -- bun dev // for harbor inject -- bun dev
// after = ["bun", "dev"] // after = ["bun", "dev"]
} }
Commands::List {} => { Commands::List {} => {
let _config = require_config(&root);
println!("List command executed"); println!("List command executed");
} }
Commands::Project { command } => match command { Commands::Project { command } => match command {
ProjectCommands::List {} => { ProjectCommands::List {} => {
println!("Project list command executed"); let projects = match get_projects() {
Ok(projects) => projects,
Err(e) => {
eprintln!("Error getting projects: {}", e);
process::exit(1);
}
};
for project in projects {
println!(" - {}", project.name.blue().bold());
}
} }
ProjectCommands::Create {} => { ProjectCommands::Create {} => {
let project_name = match get_input("Project name", ':', false) { let project_name = match get_input("Project name", ':', false) {
@@ -124,7 +241,86 @@ fn main() {
println!("Shell command executed"); println!("Shell command executed");
} }
Commands::Setup {} => { Commands::Setup {} => {
println!("Setup command executed"); use std::process;
// Setup will get all projects and prompt the user to select one,
// then create a .harbor.toml file in the current directory with the selected project name.
let projects = match get_projects() {
Ok(projects) => projects,
Err(e) => {
eprintln!("Error getting projects: {}", e);
process::exit(1);
}
};
if projects.is_empty() {
eprintln!("No projects found. Please create a project first.");
process::exit(1);
}
// We'll just use fzf, and pipe the project names to it, and get the selected project name back.
let project_names: Vec<String> = projects.iter().map(|p| p.name.clone()).collect();
let mut fzf = match process::Command::new("fzf")
.stdin(process::Stdio::piped())
.stdout(process::Stdio::piped())
.spawn()
{
Ok(c) => c,
Err(e) => {
eprintln!("Error spawning fzf: {}", e);
process::exit(1);
}
};
{
let mut stdin = fzf.stdin.take().expect("Failed to open stdin");
for name in &project_names {
use std::io::Write;
writeln!(stdin, "{}", name).expect("Failed to write to stdin");
}
drop(stdin);
}
let output = match fzf.wait_with_output() {
Ok(o) => o,
Err(e) => {
eprintln!("Error waiting for fzf: {}", e);
process::exit(1);
}
};
if !output.status.success() {
eprintln!("fzf exited with non-zero status");
process::exit(1);
}
let project = String::from_utf8_lossy(&output.stdout).trim().to_string();
if project.is_empty() {
eprintln!("No project selected.");
process::exit(1);
}
let new_config = format!(
r#"version = "1"
name = "{}"
config = "dev""#,
project
);
let config_path = root.join(".harbor.toml");
match std::fs::write(&config_path, new_config) {
Ok(_) => {
println!("Created .harbor.toml for project '{}'.", project);
}
Err(e) => {
eprintln!(
"Error creating .harbor.toml for project '{}': {}",
project, e
);
process::exit(1);
}
}
} }
}, },
None => { None => {
@@ -137,3 +333,31 @@ fn main() {
} }
} }
} }
fn require_config(root: &std::path::Path) -> Config {
match Config::from_repo_root(root) {
Ok(config) => config,
Err(ConfigError::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
eprintln!(
"{}",
"Missing .harbor.toml. Run `harbor config create` first.".yellow()
);
process::exit(1);
}
Err(err) => {
eprintln!("Error reading config: {}", err);
process::exit(1);
}
}
}
fn requires_config(command: &cli::Commands) -> bool {
matches!(
command,
cli::Commands::Inject { .. }
| cli::Commands::Set { .. }
| cli::Commands::Delete { .. }
| cli::Commands::Shell { .. }
| cli::Commands::List { .. }
)
}
View File