1use anyhow::{Result, anyhow};
2use clap::{Parser, Subcommand};
3use colored::*;
4use std::io::Read;
5use zoi_core::utils::is_zoios;
6use zoi_system::config::load_system_lua;
7
8#[cfg(unix)]
9use zoi_system::client::send_request;
10#[cfg(unix)]
11use zoi_system::protocol::{Request, Response};
12
13#[derive(Parser, Debug)]
14pub struct SystemCommand {
15 #[command(subcommand)]
16 pub command: SystemSubcommands,
17}
18
19#[derive(Subcommand, Debug)]
20pub enum SystemSubcommands {
21 Apply {
23 #[arg(default_value = "/etc/zoi/system.lua")]
25 file: String,
26 },
27 List,
29 Status,
31 Rollback {
33 id: u32,
35 },
36 Secret {
38 #[command(subcommand)]
39 command: SecretSubcommands,
40 },
41 Distro {
43 #[command(subcommand)]
44 command: DistroSubcommands,
45 },
46}
47
48#[derive(Subcommand, Debug)]
49pub enum DistroSubcommands {
50 Build {
52 #[arg(short, long)]
54 target: String,
55 #[arg(short, long)]
57 config: String,
58 #[arg(long)]
60 dry_run: bool,
61 },
62 Chroot {
64 target: String,
66 #[arg(short, long)]
68 run: Option<String>,
69 #[arg(long, short)]
71 verbose: bool,
72 },
73}
74
75#[derive(Subcommand, Debug)]
76pub enum SecretSubcommands {
77 Hash {
79 password: String,
81 },
82 Encrypt {
84 value: String,
86 },
87 Decrypt {
89 secret: String,
91 },
92}
93
94pub fn run(args: SystemCommand, yes: bool) -> Result<()> {
95 let is_secret = matches!(args.command, SystemSubcommands::Secret { .. });
96 let is_distro = matches!(args.command, SystemSubcommands::Distro { .. });
97
98 if !is_secret && !is_distro && !is_zoios() {
99 return Err(anyhow!(
100 "OS management features are only available on ZoiOS systems."
101 ));
102 }
103
104 match args.command {
105 SystemSubcommands::Secret { command } => match command {
106 SecretSubcommands::Hash { password } => {
107 let hash = zoi_system::secret::hash_password(&password)?;
108 println!("Password hash generated successfully. Use this in your system.lua:");
109 println!("\n {}", hash.green());
110 }
111 SecretSubcommands::Encrypt { value } => {
112 let encrypted = zoi_system::secret::encrypt_secret(&value)?;
113 println!("Value encrypted successfully. Use this in your system.lua or home.lua:");
114 println!("\n {}", encrypted.yellow());
115 println!(
116 "\n{}",
117 "Note: This can only be decrypted by Zoi on this specific machine.".dimmed()
118 );
119 }
120 SecretSubcommands::Decrypt { secret } => {
121 let decrypted = zoi_system::secret::decrypt_secret(&secret)?;
122 if decrypted == secret {
123 return Err(anyhow!("Input is not a valid Zoi secret string."));
124 }
125 println!("Secret decrypted successfully:");
126 println!("\n {}", decrypted.green());
127 }
128 },
129 SystemSubcommands::Distro { command } => match command {
130 DistroSubcommands::Build {
131 target,
132 config,
133 dry_run,
134 } => {
135 let target_path = std::path::Path::new(&target);
136 let config = load_system_lua(&config)?;
137
138 println!(
140 "{} Validating {} packages...",
141 "::".bold().blue(),
142 config.packages.len().to_string().cyan()
143 );
144 for pkg_id in &config.packages {
145 if let Err(e) = zoi_resolver::resolve::resolve_source(pkg_id, None, true, true)
146 {
147 return Err(anyhow!("Package validation failed for '{}': {}", pkg_id, e));
148 }
149 }
150
151 print_build_summary(&target, &config, dry_run);
152
153 if !dry_run
154 && !zoi_core::utils::ask_for_confirmation(
155 "Are you sure you want to proceed with the build? This will install ZoiOS to the target device.",
156 yes,
157 )
158 {
159 return Err(anyhow!("Build aborted by user."));
160 }
161
162 println!(
163 "{} Orchestrating ZoiOS build on {}...",
164 "::".bold().blue(),
165 target.cyan()
166 );
167
168 zoi_system::distro::initialize_zoios_marker(
170 target_path,
171 config.system.hostname.as_deref(),
172 dry_run,
173 )?;
174
175 if dry_run {
177 println!(
178 " {} Would install base packages: {}",
179 "[DRY-RUN]".dimmed(),
180 config.packages.join(", ")
181 );
182 } else {
183 println!(
184 "{} Installing base packages to {}...",
185 "::".bold().blue(),
186 target.cyan()
187 );
188
189 crate::cmd::install::run(
191 &config.packages,
192 None,
193 false,
194 false,
195 true,
196 Some(crate::cli::InstallScope::System),
197 false,
198 false,
199 false,
200 None,
201 false,
202 None,
203 false,
204 false,
205 false,
206 false,
207 3,
208 false,
209 false,
210 )?;
211 }
212
213 zoi_system::distro::finalize_first_generation(
215 target_path,
216 config.packages.clone(),
217 dry_run,
218 )?;
219
220 let success_msg = if dry_run {
221 "Dry-run complete."
222 } else {
223 "ZoiOS build complete."
224 };
225 println!(
226 "{} {} on {}.",
227 "Success:".green(),
228 success_msg,
229 target.cyan()
230 );
231 }
232 DistroSubcommands::Chroot {
233 target,
234 run,
235 verbose,
236 } => {
237 let target_path = std::path::Path::new(&target);
238 if !target_path.exists() {
239 return Err(anyhow!("Target path '{}' does not exist.", target));
240 }
241
242 let os_release = target_path.join("etc/os-release");
243 if !os_release.exists() {
244 return Err(anyhow!(
245 "Target path '{}' is not a valid ZoiOS root (missing /etc/os-release).",
246 target
247 ));
248 }
249
250 let fs_type = std::process::Command::new("stat")
254 .arg("-f")
255 .arg("-c")
256 .arg("%T")
257 .arg(&target)
258 .output();
259 if let Ok(out) = fs_type {
260 let t = String::from_utf8_lossy(&out.stdout).trim().to_string();
261 if t == "msdos" || t == "vfat" {
262 eprintln!(
263 "\n{} CRITICAL: Target filesystem is '{}'. ZoiOS requires a Linux filesystem (ext4, btrfs, xfs) to support hard links and permissions. Your bootstrap will NOT work on FAT32.",
264 "Error:".red().bold(),
265 t.yellow()
266 );
267 } else if verbose {
268 println!("{} Filesystem type: {}", "::".bold().blue(), t.green());
269 }
270 }
271
272 let mut broken_layout = false;
274 for sym in &["bin", "sbin", "lib", "lib64"] {
275 let p = target_path.join(sym);
276 let meta = std::fs::symlink_metadata(&p);
277 if let Ok(m) = meta {
278 if !m.file_type().is_symlink() {
279 eprintln!(
280 "{} WARNING: '/{}' is a real directory, but ZoiOS expects a merged-usr symlink to 'usr/{}'.",
281 "::".bold().yellow(),
282 sym,
283 sym
284 );
285 broken_layout = true;
286 } else if let Ok(target) = std::fs::read_link(&p) {
287 if target.is_absolute() {
288 eprintln!(
289 "{} WARNING: '/{}' is an absolute symlink to '{}'. This WILL break inside the chroot. It should be relative (e.g. 'usr/{}').",
290 "::".bold().yellow(),
291 sym,
292 target.display(),
293 sym
294 );
295 broken_layout = true;
296 } else {
297 let abs_target = target_path.join(target);
298 if !abs_target.exists() {
299 eprintln!(
300 "{} WARNING: Symlink '/{}' points to non-existent path '{}'.",
301 "::".bold().yellow(),
302 sym,
303 abs_target.display()
304 );
305 broken_layout = true;
306 }
307 }
308 }
309 } else if *sym != "sbin" {
310 eprintln!(
311 "{} WARNING: '/{}' is missing! Your binaries will likely fail to find their loader or shell.",
312 "::".bold().yellow(),
313 sym
314 );
315 broken_layout = true;
316 }
317 }
318
319 let mut loader_found = false;
321 let loaders = [
322 "usr/lib/ld-linux-x86-64.so.2",
323 "lib64/ld-linux-x86-64.so.2",
324 "lib/ld-linux-x86-64.so.2",
325 ];
326 for l in &loaders {
327 let lp = target_path.join(l);
328 if lp.exists() {
329 loader_found = true;
330 if let Ok(mut file) = std::fs::File::open(&lp) {
331 let mut magic = [0u8; 4];
332 if file.read_exact(&mut magic).is_ok() {
333 if magic != [0x7f, b'E', b'L', b'F'] {
334 eprintln!(
335 "{} CRITICAL: Dynamic loader '{}' is NOT an ELF file! Your glibc installation is corrupted.",
336 "Error:".red().bold(),
337 l
338 );
339 }
340 } else {
341 eprintln!(
342 "{} CRITICAL: Dynamic loader '{}' is 0 bytes or unreadable.",
343 "Error:".red().bold(),
344 l
345 );
346 }
347 }
348 break;
349 }
350 }
351 if !loader_found {
352 eprintln!(
353 "{} Dynamic loader not found. Binaries WILL Segfault (139).",
354 "::".bold().yellow()
355 );
356 }
357
358 if broken_layout || !loader_found {
359 println!(
360 "{} Hint: Your ZoiOS bootstrap appears incomplete or corrupted. Please verify your base system packages.",
361 "::".bold().blue()
362 );
363 }
364
365 if verbose {
366 println!(
367 "{} Entering sysroot at {}...",
368 "::".bold().blue(),
369 target.cyan()
370 );
371 }
372
373 let mut envs = std::collections::HashMap::new();
374 envs.insert(
375 "PATH".to_string(),
376 "/usr/bin:/bin:/usr/sbin:/sbin".to_string(),
377 );
378 envs.insert("SHELL".to_string(), "/usr/bin/bash".to_string());
379 envs.insert(
380 "TERM".to_string(),
381 std::env::var("TERM").unwrap_or_else(|_| "xterm-256color".to_string()),
382 );
383
384 let mut shell_bin = std::path::PathBuf::from("/usr/bin/bash");
386 if !target_path.join("usr/bin/bash").exists()
387 && target_path.join("bin/bash").exists()
388 {
389 shell_bin = std::path::PathBuf::from("/bin/bash");
390 }
391
392 if verbose {
393 let cmd_display = if let Some(r) = &run {
394 format!("{} -c '{}'", shell_bin.display(), r)
395 } else {
396 shell_bin.display().to_string()
397 };
398 println!(
399 "{} Running inside chroot: {}",
400 "::".bold().blue(),
401 cmd_display.green()
402 );
403 }
404
405 #[cfg(target_os = "linux")]
406 {
407 let mut cmd = if let Some(run_cmd) = run {
408 let args = vec!["-c".to_string(), run_cmd];
409 crate::sandbox::wrap_command_in_root(
410 target_path,
411 &shell_bin,
412 &args,
413 &envs,
414 &[],
415 false,
416 )?
417 } else {
418 crate::sandbox::wrap_command_in_root(
419 target_path,
420 &shell_bin,
421 &[],
422 &envs,
423 &[],
424 false,
425 )?
426 };
427
428 if verbose {
429 println!("{} Full command: {:?}", "::".bold().blue(), cmd);
430 }
431
432 let status = cmd.status()?;
433 if !status.success() {
434 let code = status.code().unwrap_or(1);
435 eprintln!(
436 "\n{} Chroot execution failed with exit code: {}",
437 "Error:".red().bold(),
438 code.to_string().yellow()
439 );
440 if code == 139 {
441 println!(
442 "{} Hint: Segfaults (139) often indicate an instruction set mismatch (e.g. x86-64-v3 binaries on older CPUs).",
443 "::".bold().blue()
444 );
445 }
446 std::process::exit(code);
447 }
448 }
449
450 #[cfg(not(target_os = "linux"))]
451 return Err(anyhow!(
452 "Distro chroot is only supported on Linux via Bubblewrap."
453 ));
454 }
455 },
456 SystemSubcommands::Apply { file } => {
457 #[cfg(unix)]
458 {
459 println!("Reading system configuration from {}...", file.cyan());
460 let config = load_system_lua(&file)?;
461 let response = send_request(Request::ApplySystemConfig(Box::new(config)))?;
462 handle_response(response)?;
463 }
464 #[cfg(not(unix))]
465 return Err(anyhow!(
466 "OS management daemon commands are only supported on Unix."
467 ));
468 }
469 SystemSubcommands::List => {
470 #[cfg(unix)]
471 {
472 let response = send_request(Request::ListGenerations)?;
473 match response {
474 Response::Generations(gens) => {
475 println!("{:<5} {:<25} {:<50}", "ID", "Created At", "Packages");
476 println!("{:-<80}", "");
477 for generation in gens {
478 println!(
479 "{:<5} {:<25} {:<50}",
480 generation.id,
481 generation.created_at.to_rfc3339(),
482 generation.packages.join(", ")
483 );
484 }
485 }
486 _ => handle_response(response)?,
487 }
488 }
489 #[cfg(not(unix))]
490 return Err(anyhow!(
491 "OS management daemon commands are only supported on Unix."
492 ));
493 }
494 SystemSubcommands::Status => {
495 #[cfg(unix)]
496 {
497 let response = send_request(Request::GetStatus)?;
498 handle_response(response)?;
499 }
500 #[cfg(not(unix))]
501 return Err(anyhow!(
502 "OS management daemon commands are only supported on Unix."
503 ));
504 }
505 SystemSubcommands::Rollback { id } => {
506 #[cfg(unix)]
507 {
508 println!("Rolling back to generation {}...", id.to_string().yellow());
509 let response = send_request(Request::RollbackGeneration(id))?;
510 handle_response(response)?;
511 }
512 #[cfg(not(unix))]
513 let _ = id;
514 #[cfg(not(unix))]
515 return Err(anyhow!(
516 "OS management daemon commands are only supported on Unix."
517 ));
518 }
519 }
520
521 Ok(())
522}
523
524fn print_build_summary(target: &str, config: &zoi_system::config::SystemConfig, dry_run: bool) {
525 use comfy_table::modifiers::UTF8_ROUND_CORNERS;
526 use comfy_table::presets::UTF8_FULL_CONDENSED;
527 use comfy_table::{Cell, Color, Table};
528
529 println!("\n{}", " ZoiOS Build Plan ".bold().on_blue().white());
530 if dry_run {
531 println!(
532 "{}",
533 " [DRY-RUN MODE - NO CHANGES WILL BE MADE] "
534 .on_yellow()
535 .black()
536 .bold()
537 );
538 }
539 println!("{} {}\n", "Target Root:".bold(), target.cyan());
540
541 let mut fs_table = Table::new();
543 fs_table
544 .load_preset(UTF8_FULL_CONDENSED)
545 .apply_modifier(UTF8_ROUND_CORNERS)
546 .set_header(vec![
547 Cell::new("Action").fg(Color::Yellow),
548 Cell::new("Device").fg(Color::Yellow),
549 Cell::new("FS Type").fg(Color::Yellow),
550 Cell::new("Mount Point").fg(Color::Yellow),
551 Cell::new("Options").fg(Color::Yellow),
552 ]);
553
554 for fs in &config.filesystems {
555 fs_table.add_row(vec![
556 Cell::new("Configure (fstab)").fg(Color::Blue),
557 Cell::new(&fs.device),
558 Cell::new(&fs.fs_type),
559 Cell::new(&fs.mount).fg(Color::Cyan),
560 Cell::new(fs.options.as_deref().unwrap_or("defaults")),
561 ]);
562 }
563 println!("{}", " 1. Filesystem & Partitioning ".bold().underline());
564 println!("{}\n", fs_table);
565
566 let mut sys_table = Table::new();
568 sys_table
569 .load_preset(UTF8_FULL_CONDENSED)
570 .apply_modifier(UTF8_ROUND_CORNERS)
571 .set_header(vec![
572 Cell::new("Property").fg(Color::Yellow),
573 Cell::new("Value").fg(Color::Yellow),
574 ]);
575
576 sys_table.add_row(vec![
577 Cell::new("Hostname"),
578 Cell::new(config.system.hostname.as_deref().unwrap_or("zoios")).fg(Color::Cyan),
579 ]);
580 sys_table.add_row(vec![
581 Cell::new("Timezone"),
582 Cell::new(config.system.timezone.as_deref().unwrap_or("UTC")),
583 ]);
584 sys_table.add_row(vec![
585 Cell::new("Locale"),
586 Cell::new(config.system.locale.as_deref().unwrap_or("en_US.UTF-8")),
587 ]);
588
589 println!("{}", " 2. System Configuration ".bold().underline());
590 println!("{}\n", sys_table);
591
592 println!("{}", " 3. Packages ".bold().underline());
594 println!(
595 "{} base packages will be installed from the registry.\n",
596 config.packages.len().to_string().green().bold()
597 );
598
599 let mut pkg_list = String::new();
600 for (i, pkg) in config.packages.iter().enumerate() {
601 pkg_list.push_str(&format!("{}", pkg.cyan()));
602 if i < config.packages.len() - 1 {
603 pkg_list.push_str(", ");
604 }
605 if (i + 1) % 5 == 0 {
606 pkg_list.push('\n');
607 }
608 }
609 println!("{}\n", pkg_list);
610}
611
612#[cfg(unix)]
613fn handle_response(response: Response) -> Result<()> {
614 match response {
615 Response::Ok => println!("{}", "Operation successful.".green()),
616 Response::Success(msg) => println!("{} {}", "Success:".green(), msg),
617 Response::Status(msg) => println!("Daemon status: {}", msg.cyan()),
618 Response::Error(err) => return Err(anyhow!("Daemon error: {}", err)),
619 _ => return Err(anyhow!("Unexpected response from daemon")),
620 }
621 Ok(())
622}