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",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.0"
|
||||
|
|
@ -1078,6 +1091,7 @@ dependencies = [
|
|||
"clap_complete",
|
||||
"clap_mangen",
|
||||
"console",
|
||||
"dialoguer",
|
||||
"indicatif",
|
||||
"karapace-core",
|
||||
"karapace-remote",
|
||||
|
|
@ -1089,6 +1103,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
|
@ -1110,7 +1125,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
|
@ -1125,7 +1140,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
|
|
@ -1142,7 +1157,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"ureq",
|
||||
]
|
||||
|
|
@ -1158,7 +1173,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
|
@ -1170,7 +1185,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"toml",
|
||||
]
|
||||
|
||||
|
|
@ -1203,7 +1218,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
|
@ -1902,6 +1917,12 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
|
|
@ -2026,7 +2047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"windows",
|
||||
"windows-version",
|
||||
]
|
||||
|
|
@ -2044,13 +2065,33 @@ dependencies = [
|
|||
"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]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
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]]
|
||||
|
|
|
|||
|
|
@ -21,9 +21,11 @@ tracing.workspace = true
|
|||
tracing-subscriber.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
toml.workspace = true
|
||||
tempfile.workspace = true
|
||||
indicatif.workspace = true
|
||||
console.workspace = true
|
||||
dialoguer = "0.11"
|
||||
libc.workspace = true
|
||||
karapace-schema = { path = "../karapace-schema" }
|
||||
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_store::StoreLayout;
|
||||
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 _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())?;
|
||||
println!("archived environment {env_id}");
|
||||
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_store::StoreLayout;
|
||||
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 _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())?;
|
||||
if 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_store::StoreLayout;
|
||||
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 _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())?;
|
||||
println!("destroyed environment {env_id}");
|
||||
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;
|
||||
|
||||
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 =
|
||||
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_store::StoreLayout;
|
||||
use std::path::Path;
|
||||
|
|
@ -12,7 +12,7 @@ pub fn run(
|
|||
let layout = StoreLayout::new(store_path);
|
||||
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() {
|
||||
engine.enter(&resolved).map_err(|e| e.to_string())?;
|
||||
} 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_store::StoreLayout;
|
||||
use std::path::Path;
|
||||
|
|
@ -13,7 +13,7 @@ pub fn run(
|
|||
let layout = StoreLayout::new(store_path);
|
||||
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())?;
|
||||
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_store::StoreLayout;
|
||||
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 _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())?;
|
||||
println!("frozen environment {env_id}");
|
||||
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;
|
||||
|
||||
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())?;
|
||||
if json {
|
||||
println!("{}", json_pretty(&meta)?);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ pub mod inspect;
|
|||
pub mod list;
|
||||
pub mod man_pages;
|
||||
pub mod migrate;
|
||||
pub mod new;
|
||||
pub mod pull;
|
||||
pub mod push;
|
||||
pub mod rebuild;
|
||||
|
|
@ -20,6 +21,7 @@ pub mod rename;
|
|||
pub mod restore;
|
||||
pub mod snapshots;
|
||||
pub mod stop;
|
||||
pub mod tui;
|
||||
pub mod verify_store;
|
||||
|
||||
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(
|
||||
remote_url: Option<&str>,
|
||||
) -> 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::{
|
||||
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;
|
||||
|
||||
|
|
@ -10,7 +11,11 @@ pub fn run(
|
|||
remote_url: Option<&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 backend = make_remote_backend(remote_url)?;
|
||||
|
||||
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_store::StoreLayout;
|
||||
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 _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
|
||||
.rename(&resolved, new_name)
|
||||
.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_store::StoreLayout;
|
||||
use std::path::Path;
|
||||
|
|
@ -13,7 +13,11 @@ pub fn run(
|
|||
let layout = StoreLayout::new(store_path);
|
||||
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
|
||||
.restore(&resolved, snapshot_hash)
|
||||
.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_store::StoreLayout;
|
||||
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> {
|
||||
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
|
||||
.list_snapshots(&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_store::StoreLayout;
|
||||
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 _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())?;
|
||||
println!("stopped environment {env_id}");
|
||||
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)]
|
||||
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 {
|
||||
/// Path to manifest TOML file.
|
||||
|
|
@ -164,6 +171,8 @@ enum Commands {
|
|||
#[arg(default_value = "man")]
|
||||
dir: PathBuf,
|
||||
},
|
||||
/// Launch the terminal UI.
|
||||
Tui,
|
||||
/// Run diagnostic checks on the system and store.
|
||||
Doctor,
|
||||
/// Check store version and show migration guidance.
|
||||
|
|
@ -202,6 +211,7 @@ fn main() -> ExitCode {
|
|||
| Commands::Enter { .. }
|
||||
| Commands::Exec { .. }
|
||||
| Commands::Rebuild { .. }
|
||||
| Commands::Tui
|
||||
);
|
||||
if needs_runtime && std::env::var("KARAPACE_SKIP_PREREQS").as_deref() != Ok("1") {
|
||||
let missing = karapace_runtime::check_namespace_prereqs();
|
||||
|
|
@ -212,6 +222,11 @@ fn main() -> ExitCode {
|
|||
}
|
||||
|
||||
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(
|
||||
&engine,
|
||||
&store_path,
|
||||
|
|
@ -269,6 +284,7 @@ fn main() -> ExitCode {
|
|||
}
|
||||
Commands::Completions { shell } => commands::completions::run::<Cli>(shell),
|
||||
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::Migrate => commands::migrate::run(&store_path, json_output),
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue