mirror of
https://github.com/marcoallegretti/karapace.git
synced 2026-03-26 21:43:09 +00:00
Add new and tui CLI commands
Add 'karapace new' to generate a manifest from templates or prompts. Add 'karapace tui' to launch the terminal UI. Improve env-id resolution errors in non-JSON output with suggestions. Add dialoguer and toml as CLI dependencies.
This commit is contained in:
parent
e6e0f3dd6d
commit
8e90f45efc
19 changed files with 376 additions and 34 deletions
57
Cargo.lock
generated
57
Cargo.lock
generated
|
|
@ -682,6 +682,19 @@ dependencies = [
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dialoguer"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de"
|
||||||
|
dependencies = [
|
||||||
|
"console",
|
||||||
|
"shell-words",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dispatch2"
|
name = "dispatch2"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
@ -1078,6 +1091,7 @@ dependencies = [
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
"clap_mangen",
|
"clap_mangen",
|
||||||
"console",
|
"console",
|
||||||
|
"dialoguer",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"karapace-core",
|
"karapace-core",
|
||||||
"karapace-remote",
|
"karapace-remote",
|
||||||
|
|
@ -1089,6 +1103,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
@ -1110,7 +1125,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1125,7 +1140,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
|
@ -1142,7 +1157,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"tracing",
|
"tracing",
|
||||||
"ureq",
|
"ureq",
|
||||||
]
|
]
|
||||||
|
|
@ -1158,7 +1173,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1170,7 +1185,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1203,7 +1218,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tar",
|
"tar",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1902,6 +1917,12 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shell-words"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
|
@ -2026,7 +2047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
|
checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"windows",
|
"windows",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
]
|
]
|
||||||
|
|
@ -2044,13 +2065,33 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.18"
|
version = "2.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,11 @@ tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
toml.workspace = true
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
indicatif.workspace = true
|
indicatif.workspace = true
|
||||||
console.workspace = true
|
console.workspace = true
|
||||||
|
dialoguer = "0.11"
|
||||||
libc.workspace = true
|
libc.workspace = true
|
||||||
karapace-schema = { path = "../karapace-schema" }
|
karapace-schema = { path = "../karapace-schema" }
|
||||||
karapace-core = { path = "../karapace-core" }
|
karapace-core = { path = "../karapace-core" }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{resolve_env_id, EXIT_SUCCESS};
|
use super::{resolve_env_id_pretty, EXIT_SUCCESS};
|
||||||
use karapace_core::{Engine, StoreLock};
|
use karapace_core::{Engine, StoreLock};
|
||||||
use karapace_store::StoreLayout;
|
use karapace_store::StoreLayout;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -7,7 +7,7 @@ pub fn run(engine: &Engine, store_path: &Path, env_id: &str) -> Result<u8, Strin
|
||||||
let layout = StoreLayout::new(store_path);
|
let layout = StoreLayout::new(store_path);
|
||||||
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
||||||
|
|
||||||
let resolved = resolve_env_id(engine, env_id)?;
|
let resolved = resolve_env_id_pretty(engine, env_id)?;
|
||||||
engine.archive(&resolved).map_err(|e| e.to_string())?;
|
engine.archive(&resolved).map_err(|e| e.to_string())?;
|
||||||
println!("archived environment {env_id}");
|
println!("archived environment {env_id}");
|
||||||
Ok(EXIT_SUCCESS)
|
Ok(EXIT_SUCCESS)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{json_pretty, resolve_env_id, EXIT_SUCCESS};
|
use super::{json_pretty, resolve_env_id, resolve_env_id_pretty, EXIT_SUCCESS};
|
||||||
use karapace_core::{Engine, StoreLock};
|
use karapace_core::{Engine, StoreLock};
|
||||||
use karapace_store::StoreLayout;
|
use karapace_store::StoreLayout;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -7,7 +7,11 @@ pub fn run(engine: &Engine, store_path: &Path, env_id: &str, json: bool) -> Resu
|
||||||
let layout = StoreLayout::new(store_path);
|
let layout = StoreLayout::new(store_path);
|
||||||
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
||||||
|
|
||||||
let resolved = resolve_env_id(engine, env_id)?;
|
let resolved = if json {
|
||||||
|
resolve_env_id(engine, env_id)?
|
||||||
|
} else {
|
||||||
|
resolve_env_id_pretty(engine, env_id)?
|
||||||
|
};
|
||||||
let tar_hash = engine.commit(&resolved).map_err(|e| e.to_string())?;
|
let tar_hash = engine.commit(&resolved).map_err(|e| e.to_string())?;
|
||||||
if json {
|
if json {
|
||||||
let payload = serde_json::json!({
|
let payload = serde_json::json!({
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{resolve_env_id, EXIT_SUCCESS};
|
use super::{resolve_env_id_pretty, EXIT_SUCCESS};
|
||||||
use karapace_core::{Engine, StoreLock};
|
use karapace_core::{Engine, StoreLock};
|
||||||
use karapace_store::StoreLayout;
|
use karapace_store::StoreLayout;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -7,7 +7,7 @@ pub fn run(engine: &Engine, store_path: &Path, env_id: &str) -> Result<u8, Strin
|
||||||
let layout = StoreLayout::new(store_path);
|
let layout = StoreLayout::new(store_path);
|
||||||
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
||||||
|
|
||||||
let resolved = resolve_env_id(engine, env_id)?;
|
let resolved = resolve_env_id_pretty(engine, env_id)?;
|
||||||
engine.destroy(&resolved).map_err(|e| e.to_string())?;
|
engine.destroy(&resolved).map_err(|e| e.to_string())?;
|
||||||
println!("destroyed environment {env_id}");
|
println!("destroyed environment {env_id}");
|
||||||
Ok(EXIT_SUCCESS)
|
Ok(EXIT_SUCCESS)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
use super::{json_pretty, resolve_env_id, EXIT_SUCCESS};
|
use super::{json_pretty, resolve_env_id, resolve_env_id_pretty, EXIT_SUCCESS};
|
||||||
use karapace_core::Engine;
|
use karapace_core::Engine;
|
||||||
|
|
||||||
pub fn run(engine: &Engine, env_id: &str, json: bool) -> Result<u8, String> {
|
pub fn run(engine: &Engine, env_id: &str, json: bool) -> Result<u8, String> {
|
||||||
let resolved = resolve_env_id(engine, env_id)?;
|
let resolved = if json {
|
||||||
|
resolve_env_id(engine, env_id)?
|
||||||
|
} else {
|
||||||
|
resolve_env_id_pretty(engine, env_id)?
|
||||||
|
};
|
||||||
let report =
|
let report =
|
||||||
karapace_core::diff_overlay(engine.store_layout(), &resolved).map_err(|e| e.to_string())?;
|
karapace_core::diff_overlay(engine.store_layout(), &resolved).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{resolve_env_id, EXIT_SUCCESS};
|
use super::{resolve_env_id_pretty, EXIT_SUCCESS};
|
||||||
use karapace_core::{Engine, StoreLock};
|
use karapace_core::{Engine, StoreLock};
|
||||||
use karapace_store::StoreLayout;
|
use karapace_store::StoreLayout;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -12,7 +12,7 @@ pub fn run(
|
||||||
let layout = StoreLayout::new(store_path);
|
let layout = StoreLayout::new(store_path);
|
||||||
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
||||||
|
|
||||||
let resolved = resolve_env_id(engine, env_id)?;
|
let resolved = resolve_env_id_pretty(engine, env_id)?;
|
||||||
if command.is_empty() {
|
if command.is_empty() {
|
||||||
engine.enter(&resolved).map_err(|e| e.to_string())?;
|
engine.enter(&resolved).map_err(|e| e.to_string())?;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{resolve_env_id, EXIT_SUCCESS};
|
use super::{resolve_env_id_pretty, EXIT_SUCCESS};
|
||||||
use karapace_core::{Engine, StoreLock};
|
use karapace_core::{Engine, StoreLock};
|
||||||
use karapace_store::StoreLayout;
|
use karapace_store::StoreLayout;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -13,7 +13,7 @@ pub fn run(
|
||||||
let layout = StoreLayout::new(store_path);
|
let layout = StoreLayout::new(store_path);
|
||||||
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
||||||
|
|
||||||
let resolved = resolve_env_id(engine, env_id)?;
|
let resolved = resolve_env_id_pretty(engine, env_id)?;
|
||||||
engine.exec(&resolved, command).map_err(|e| e.to_string())?;
|
engine.exec(&resolved, command).map_err(|e| e.to_string())?;
|
||||||
Ok(EXIT_SUCCESS)
|
Ok(EXIT_SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{resolve_env_id, EXIT_SUCCESS};
|
use super::{resolve_env_id_pretty, EXIT_SUCCESS};
|
||||||
use karapace_core::{Engine, StoreLock};
|
use karapace_core::{Engine, StoreLock};
|
||||||
use karapace_store::StoreLayout;
|
use karapace_store::StoreLayout;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -7,7 +7,7 @@ pub fn run(engine: &Engine, store_path: &Path, env_id: &str) -> Result<u8, Strin
|
||||||
let layout = StoreLayout::new(store_path);
|
let layout = StoreLayout::new(store_path);
|
||||||
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
||||||
|
|
||||||
let resolved = resolve_env_id(engine, env_id)?;
|
let resolved = resolve_env_id_pretty(engine, env_id)?;
|
||||||
engine.freeze(&resolved).map_err(|e| e.to_string())?;
|
engine.freeze(&resolved).map_err(|e| e.to_string())?;
|
||||||
println!("frozen environment {env_id}");
|
println!("frozen environment {env_id}");
|
||||||
Ok(EXIT_SUCCESS)
|
Ok(EXIT_SUCCESS)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
use super::{colorize_state, json_pretty, resolve_env_id, EXIT_SUCCESS};
|
use super::{colorize_state, json_pretty, resolve_env_id, resolve_env_id_pretty, EXIT_SUCCESS};
|
||||||
use karapace_core::Engine;
|
use karapace_core::Engine;
|
||||||
|
|
||||||
pub fn run(engine: &Engine, env_id: &str, json: bool) -> Result<u8, String> {
|
pub fn run(engine: &Engine, env_id: &str, json: bool) -> Result<u8, String> {
|
||||||
let resolved = resolve_env_id(engine, env_id)?;
|
let resolved = if json {
|
||||||
|
resolve_env_id(engine, env_id)?
|
||||||
|
} else {
|
||||||
|
resolve_env_id_pretty(engine, env_id)?
|
||||||
|
};
|
||||||
let meta = engine.inspect(&resolved).map_err(|e| e.to_string())?;
|
let meta = engine.inspect(&resolved).map_err(|e| e.to_string())?;
|
||||||
if json {
|
if json {
|
||||||
println!("{}", json_pretty(&meta)?);
|
println!("{}", json_pretty(&meta)?);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ pub mod inspect;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
pub mod man_pages;
|
pub mod man_pages;
|
||||||
pub mod migrate;
|
pub mod migrate;
|
||||||
|
pub mod new;
|
||||||
pub mod pull;
|
pub mod pull;
|
||||||
pub mod push;
|
pub mod push;
|
||||||
pub mod rebuild;
|
pub mod rebuild;
|
||||||
|
|
@ -20,6 +21,7 @@ pub mod rename;
|
||||||
pub mod restore;
|
pub mod restore;
|
||||||
pub mod snapshots;
|
pub mod snapshots;
|
||||||
pub mod stop;
|
pub mod stop;
|
||||||
|
pub mod tui;
|
||||||
pub mod verify_store;
|
pub mod verify_store;
|
||||||
|
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
|
@ -96,6 +98,75 @@ pub fn resolve_env_id(engine: &Engine, input: &str) -> Result<String, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_env_suggestion(meta: &karapace_store::EnvMetadata) -> String {
|
||||||
|
let label = meta
|
||||||
|
.name
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_else(|| meta.short_id.as_str());
|
||||||
|
format!("{label} ({}) {}", meta.short_id, meta.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_env_id_pretty(engine: &Engine, input: &str) -> Result<String, String> {
|
||||||
|
if input.len() == 64 {
|
||||||
|
return Ok(input.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
let envs = engine.list().map_err(|e| e.to_string())?;
|
||||||
|
for e in &envs {
|
||||||
|
if *e.env_id == *input || *e.short_id == *input || e.name.as_deref() == Some(input) {
|
||||||
|
return Ok(e.env_id.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefix_matches: Vec<_> = envs
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.env_id.starts_with(input) || e.short_id.starts_with(input))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match prefix_matches.len() {
|
||||||
|
0 => {
|
||||||
|
let needle = input.to_lowercase();
|
||||||
|
let mut suggestions: Vec<_> = envs
|
||||||
|
.iter()
|
||||||
|
.filter(|e| {
|
||||||
|
e.name
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_lowercase()
|
||||||
|
.contains(&needle)
|
||||||
|
|| e.short_id.to_lowercase().contains(&needle)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
suggestions.truncate(5);
|
||||||
|
|
||||||
|
if suggestions.is_empty() {
|
||||||
|
Err(format!("no environment matching '{input}'"))
|
||||||
|
} else {
|
||||||
|
let rendered = suggestions
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| format!(" {}", format_env_suggestion(m)))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
Err(format!(
|
||||||
|
"no environment matching '{input}'\n\nDid you mean:\n{rendered}\n\nUse 'karapace list' to see all environments."
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1 => Ok(prefix_matches[0].env_id.to_string()),
|
||||||
|
n => {
|
||||||
|
let rendered = prefix_matches
|
||||||
|
.iter()
|
||||||
|
.take(10)
|
||||||
|
.map(|m| format!(" {}", format_env_suggestion(m)))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
Err(format!(
|
||||||
|
"ambiguous env_id prefix '{input}': matches {n} environments\n\nMatches:\n{rendered}\n\nUse a longer prefix, a full env_id, or a unique name."
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn make_remote_backend(
|
pub fn make_remote_backend(
|
||||||
remote_url: Option<&str>,
|
remote_url: Option<&str>,
|
||||||
) -> Result<karapace_remote::http::HttpBackend, String> {
|
) -> Result<karapace_remote::http::HttpBackend, String> {
|
||||||
|
|
|
||||||
177
crates/karapace-cli/src/commands/new.rs
Normal file
177
crates/karapace-cli/src/commands/new.rs
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
use super::{json_pretty, EXIT_SUCCESS};
|
||||||
|
use dialoguer::{Confirm, Input, Select};
|
||||||
|
use karapace_schema::manifest::{
|
||||||
|
parse_manifest_str, BaseSection, ManifestV1, MountsSection, RuntimeSection, SystemSection,
|
||||||
|
};
|
||||||
|
use std::io::{stdin, IsTerminal};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
const DEST_MANIFEST: &str = "karapace.toml";
|
||||||
|
|
||||||
|
fn template_source(name: &str) -> Option<&'static str> {
|
||||||
|
match name {
|
||||||
|
"minimal" => Some(include_str!("../../../../examples/minimal.toml")),
|
||||||
|
"dev" => Some(include_str!("../../../../examples/dev.toml")),
|
||||||
|
"gui-dev" => Some(include_str!("../../../../examples/gui-dev.toml")),
|
||||||
|
"rust-dev" => Some(include_str!("../../../../examples/rust-dev.toml")),
|
||||||
|
"ubuntu-dev" => Some(include_str!("../../../../examples/ubuntu-dev.toml")),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_template(name: &str) -> Result<ManifestV1, String> {
|
||||||
|
let src = template_source(name).ok_or_else(|| {
|
||||||
|
format!("unknown template '{name}' (expected: minimal, dev, gui-dev, rust-dev, ubuntu-dev)")
|
||||||
|
})?;
|
||||||
|
parse_manifest_str(src).map_err(|e| format!("template parse error: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_atomic(dest: &Path, content: &str) -> Result<(), String> {
|
||||||
|
let dir = dest
|
||||||
|
.parent()
|
||||||
|
.map(Path::to_path_buf)
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."));
|
||||||
|
let mut tmp = NamedTempFile::new_in(&dir).map_err(|e| format!("write temp file: {e}"))?;
|
||||||
|
use std::io::Write;
|
||||||
|
tmp.write_all(content.as_bytes())
|
||||||
|
.map_err(|e| format!("write temp file: {e}"))?;
|
||||||
|
tmp.as_file()
|
||||||
|
.sync_all()
|
||||||
|
.map_err(|e| format!("fsync temp file: {e}"))?;
|
||||||
|
tmp.persist(dest)
|
||||||
|
.map_err(|e| format!("persist manifest: {}", e.error))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(name: &str, template: Option<&str>, force: bool, json: bool) -> Result<u8, String> {
|
||||||
|
let dest = Path::new(DEST_MANIFEST);
|
||||||
|
let is_tty = stdin().is_terminal();
|
||||||
|
|
||||||
|
if dest.exists() && !force {
|
||||||
|
if is_tty {
|
||||||
|
let overwrite = Confirm::new()
|
||||||
|
.with_prompt(format!("overwrite ./{DEST_MANIFEST}?"))
|
||||||
|
.default(false)
|
||||||
|
.interact()
|
||||||
|
.map_err(|e| format!("prompt failed: {e}"))?;
|
||||||
|
if !overwrite {
|
||||||
|
return Err(format!(
|
||||||
|
"refusing to overwrite existing ./{DEST_MANIFEST} (pass --force)"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(format!(
|
||||||
|
"refusing to overwrite existing ./{DEST_MANIFEST} (pass --force)"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut manifest = match template {
|
||||||
|
Some(tpl) => load_template(tpl)?,
|
||||||
|
None => {
|
||||||
|
if !is_tty {
|
||||||
|
return Err("no --template provided and stdin is not a TTY".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
let image: String = Input::new()
|
||||||
|
.with_prompt("base image")
|
||||||
|
.default("rolling".to_owned())
|
||||||
|
.interact_text()
|
||||||
|
.map_err(|e| format!("prompt failed: {e}"))?;
|
||||||
|
|
||||||
|
ManifestV1 {
|
||||||
|
manifest_version: 1,
|
||||||
|
base: BaseSection { image },
|
||||||
|
system: SystemSection::default(),
|
||||||
|
gui: Default::default(),
|
||||||
|
hardware: Default::default(),
|
||||||
|
mounts: MountsSection::default(),
|
||||||
|
runtime: RuntimeSection::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_tty {
|
||||||
|
let packages: String = Input::new()
|
||||||
|
.with_prompt("packages (space-separated, empty to skip)")
|
||||||
|
.allow_empty(true)
|
||||||
|
.interact_text()
|
||||||
|
.map_err(|e| format!("prompt failed: {e}"))?;
|
||||||
|
if !packages.trim().is_empty() {
|
||||||
|
manifest
|
||||||
|
.system
|
||||||
|
.packages
|
||||||
|
.extend(packages.split_whitespace().map(str::to_owned));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mount: String = Input::new()
|
||||||
|
.with_prompt("mount (format '<host>:<container>', empty to skip)")
|
||||||
|
.allow_empty(true)
|
||||||
|
.interact_text()
|
||||||
|
.map_err(|e| format!("prompt failed: {e}"))?;
|
||||||
|
if !mount.trim().is_empty() {
|
||||||
|
manifest
|
||||||
|
.mounts
|
||||||
|
.entries
|
||||||
|
.insert("workspace".to_owned(), mount);
|
||||||
|
}
|
||||||
|
|
||||||
|
let backends = ["namespace", "oci", "mock"];
|
||||||
|
let default_idx = backends
|
||||||
|
.iter()
|
||||||
|
.position(|b| *b == manifest.runtime.backend.as_str())
|
||||||
|
.unwrap_or(0);
|
||||||
|
let idx = Select::new()
|
||||||
|
.with_prompt("runtime backend")
|
||||||
|
.items(&backends)
|
||||||
|
.default(default_idx)
|
||||||
|
.interact()
|
||||||
|
.map_err(|e| format!("prompt failed: {e}"))?;
|
||||||
|
manifest.runtime.backend = backends[idx].to_owned();
|
||||||
|
|
||||||
|
let isolated = Confirm::new()
|
||||||
|
.with_prompt("enable network isolation?")
|
||||||
|
.default(manifest.runtime.network_isolation)
|
||||||
|
.interact()
|
||||||
|
.map_err(|e| format!("prompt failed: {e}"))?;
|
||||||
|
manifest.runtime.network_isolation = isolated;
|
||||||
|
} else if template.is_none() {
|
||||||
|
return Err("interactive prompts require a TTY".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
let toml =
|
||||||
|
toml::to_string_pretty(&manifest).map_err(|e| format!("TOML serialization failed: {e}"))?;
|
||||||
|
write_atomic(dest, &toml)?;
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"status": "written",
|
||||||
|
"path": format!("./{DEST_MANIFEST}"),
|
||||||
|
"name": name,
|
||||||
|
"template": template,
|
||||||
|
});
|
||||||
|
println!("{}", json_pretty(&payload)?);
|
||||||
|
} else {
|
||||||
|
println!("wrote ./{DEST_MANIFEST} for '{name}'");
|
||||||
|
if let Some(tpl) = template {
|
||||||
|
println!("template: {tpl}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(EXIT_SUCCESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn templates_parse() {
|
||||||
|
for tpl in ["minimal", "dev", "gui-dev", "rust-dev", "ubuntu-dev"] {
|
||||||
|
let m = load_template(tpl).unwrap();
|
||||||
|
assert_eq!(m.manifest_version, 1);
|
||||||
|
assert!(!m.base.image.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use super::{
|
use super::{
|
||||||
json_pretty, make_remote_backend, resolve_env_id, spin_fail, spin_ok, spinner, EXIT_SUCCESS,
|
json_pretty, make_remote_backend, resolve_env_id, resolve_env_id_pretty, spin_fail, spin_ok,
|
||||||
|
spinner, EXIT_SUCCESS,
|
||||||
};
|
};
|
||||||
use karapace_core::Engine;
|
use karapace_core::Engine;
|
||||||
|
|
||||||
|
|
@ -10,7 +11,11 @@ pub fn run(
|
||||||
remote_url: Option<&str>,
|
remote_url: Option<&str>,
|
||||||
json: bool,
|
json: bool,
|
||||||
) -> Result<u8, String> {
|
) -> Result<u8, String> {
|
||||||
let resolved = resolve_env_id(engine, env_id)?;
|
let resolved = if json {
|
||||||
|
resolve_env_id(engine, env_id)?
|
||||||
|
} else {
|
||||||
|
resolve_env_id_pretty(engine, env_id)?
|
||||||
|
};
|
||||||
let backend = make_remote_backend(remote_url)?;
|
let backend = make_remote_backend(remote_url)?;
|
||||||
|
|
||||||
let pb = spinner("pushing environment…");
|
let pb = spinner("pushing environment…");
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{resolve_env_id, EXIT_SUCCESS};
|
use super::{resolve_env_id_pretty, EXIT_SUCCESS};
|
||||||
use karapace_core::{Engine, StoreLock};
|
use karapace_core::{Engine, StoreLock};
|
||||||
use karapace_store::StoreLayout;
|
use karapace_store::StoreLayout;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -7,7 +7,7 @@ pub fn run(engine: &Engine, store_path: &Path, env_id: &str, new_name: &str) ->
|
||||||
let layout = StoreLayout::new(store_path);
|
let layout = StoreLayout::new(store_path);
|
||||||
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
||||||
|
|
||||||
let resolved = resolve_env_id(engine, env_id)?;
|
let resolved = resolve_env_id_pretty(engine, env_id)?;
|
||||||
engine
|
engine
|
||||||
.rename(&resolved, new_name)
|
.rename(&resolved, new_name)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{json_pretty, resolve_env_id, EXIT_SUCCESS};
|
use super::{json_pretty, resolve_env_id, resolve_env_id_pretty, EXIT_SUCCESS};
|
||||||
use karapace_core::{Engine, StoreLock};
|
use karapace_core::{Engine, StoreLock};
|
||||||
use karapace_store::StoreLayout;
|
use karapace_store::StoreLayout;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -13,7 +13,11 @@ pub fn run(
|
||||||
let layout = StoreLayout::new(store_path);
|
let layout = StoreLayout::new(store_path);
|
||||||
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
||||||
|
|
||||||
let resolved = resolve_env_id(engine, env_id)?;
|
let resolved = if json {
|
||||||
|
resolve_env_id(engine, env_id)?
|
||||||
|
} else {
|
||||||
|
resolve_env_id_pretty(engine, env_id)?
|
||||||
|
};
|
||||||
engine
|
engine
|
||||||
.restore(&resolved, snapshot_hash)
|
.restore(&resolved, snapshot_hash)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{json_pretty, resolve_env_id, EXIT_SUCCESS};
|
use super::{json_pretty, resolve_env_id, resolve_env_id_pretty, EXIT_SUCCESS};
|
||||||
use karapace_core::Engine;
|
use karapace_core::Engine;
|
||||||
use karapace_store::StoreLayout;
|
use karapace_store::StoreLayout;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -6,7 +6,11 @@ use std::path::Path;
|
||||||
pub fn run(engine: &Engine, store_path: &Path, env_id: &str, json: bool) -> Result<u8, String> {
|
pub fn run(engine: &Engine, store_path: &Path, env_id: &str, json: bool) -> Result<u8, String> {
|
||||||
let _layout = StoreLayout::new(store_path);
|
let _layout = StoreLayout::new(store_path);
|
||||||
|
|
||||||
let resolved = resolve_env_id(engine, env_id)?;
|
let resolved = if json {
|
||||||
|
resolve_env_id(engine, env_id)?
|
||||||
|
} else {
|
||||||
|
resolve_env_id_pretty(engine, env_id)?
|
||||||
|
};
|
||||||
let snapshots = engine
|
let snapshots = engine
|
||||||
.list_snapshots(&resolved)
|
.list_snapshots(&resolved)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{resolve_env_id, EXIT_SUCCESS};
|
use super::{resolve_env_id_pretty, EXIT_SUCCESS};
|
||||||
use karapace_core::{Engine, StoreLock};
|
use karapace_core::{Engine, StoreLock};
|
||||||
use karapace_store::StoreLayout;
|
use karapace_store::StoreLayout;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -7,7 +7,7 @@ pub fn run(engine: &Engine, store_path: &Path, env_id: &str) -> Result<u8, Strin
|
||||||
let layout = StoreLayout::new(store_path);
|
let layout = StoreLayout::new(store_path);
|
||||||
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
let _lock = StoreLock::acquire(&layout.lock_file()).map_err(|e| format!("store lock: {e}"))?;
|
||||||
|
|
||||||
let resolved = resolve_env_id(engine, env_id)?;
|
let resolved = resolve_env_id_pretty(engine, env_id)?;
|
||||||
engine.stop(&resolved).map_err(|e| e.to_string())?;
|
engine.stop(&resolved).map_err(|e| e.to_string())?;
|
||||||
println!("stopped environment {env_id}");
|
println!("stopped environment {env_id}");
|
||||||
Ok(EXIT_SUCCESS)
|
Ok(EXIT_SUCCESS)
|
||||||
|
|
|
||||||
10
crates/karapace-cli/src/commands/tui.rs
Normal file
10
crates/karapace-cli/src/commands/tui.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
use super::EXIT_SUCCESS;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn run(store_path: &Path, json: bool) -> Result<u8, String> {
|
||||||
|
if json {
|
||||||
|
return Err("JSON output is not supported for 'tui'".to_owned());
|
||||||
|
}
|
||||||
|
karapace_tui::run(store_path)?;
|
||||||
|
Ok(EXIT_SUCCESS)
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,13 @@ struct Cli {
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
|
New {
|
||||||
|
name: String,
|
||||||
|
#[arg(long)]
|
||||||
|
template: Option<String>,
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
force: bool,
|
||||||
|
},
|
||||||
/// Build an environment from a manifest.
|
/// Build an environment from a manifest.
|
||||||
Build {
|
Build {
|
||||||
/// Path to manifest TOML file.
|
/// Path to manifest TOML file.
|
||||||
|
|
@ -164,6 +171,8 @@ enum Commands {
|
||||||
#[arg(default_value = "man")]
|
#[arg(default_value = "man")]
|
||||||
dir: PathBuf,
|
dir: PathBuf,
|
||||||
},
|
},
|
||||||
|
/// Launch the terminal UI.
|
||||||
|
Tui,
|
||||||
/// Run diagnostic checks on the system and store.
|
/// Run diagnostic checks on the system and store.
|
||||||
Doctor,
|
Doctor,
|
||||||
/// Check store version and show migration guidance.
|
/// Check store version and show migration guidance.
|
||||||
|
|
@ -202,6 +211,7 @@ fn main() -> ExitCode {
|
||||||
| Commands::Enter { .. }
|
| Commands::Enter { .. }
|
||||||
| Commands::Exec { .. }
|
| Commands::Exec { .. }
|
||||||
| Commands::Rebuild { .. }
|
| Commands::Rebuild { .. }
|
||||||
|
| Commands::Tui
|
||||||
);
|
);
|
||||||
if needs_runtime && std::env::var("KARAPACE_SKIP_PREREQS").as_deref() != Ok("1") {
|
if needs_runtime && std::env::var("KARAPACE_SKIP_PREREQS").as_deref() != Ok("1") {
|
||||||
let missing = karapace_runtime::check_namespace_prereqs();
|
let missing = karapace_runtime::check_namespace_prereqs();
|
||||||
|
|
@ -212,6 +222,11 @@ fn main() -> ExitCode {
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = match cli.command {
|
let result = match cli.command {
|
||||||
|
Commands::New {
|
||||||
|
name,
|
||||||
|
template,
|
||||||
|
force,
|
||||||
|
} => commands::new::run(&name, template.as_deref(), force, json_output),
|
||||||
Commands::Build { manifest, name } => commands::build::run(
|
Commands::Build { manifest, name } => commands::build::run(
|
||||||
&engine,
|
&engine,
|
||||||
&store_path,
|
&store_path,
|
||||||
|
|
@ -269,6 +284,7 @@ fn main() -> ExitCode {
|
||||||
}
|
}
|
||||||
Commands::Completions { shell } => commands::completions::run::<Cli>(shell),
|
Commands::Completions { shell } => commands::completions::run::<Cli>(shell),
|
||||||
Commands::ManPages { dir } => commands::man_pages::run::<Cli>(&dir),
|
Commands::ManPages { dir } => commands::man_pages::run::<Cli>(&dir),
|
||||||
|
Commands::Tui => commands::tui::run(&store_path, json_output),
|
||||||
Commands::Doctor => commands::doctor::run(&store_path, json_output),
|
Commands::Doctor => commands::doctor::run(&store_path, json_output),
|
||||||
Commands::Migrate => commands::migrate::run(&store_path, json_output),
|
Commands::Migrate => commands::migrate::run(&store_path, json_output),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue