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 let project_config = zoi_project::config::ProjectConfig {
191 name: "system".to_string(),
192 registries: std::collections::HashMap::new(),
193 packages: Vec::new(),
194 pkgs: config.packages.clone(),
195 pkgs_v2: config.packages_v2.clone(),
196 config: zoi_project::config::ProjectLocalConfig::default(),
197 commands: Vec::new(),
198 environments: Vec::new(),
199 shell: Some(zoi_project::config::ShellSpec::default()),
200 };
201
202 crate::cmd::install::run(
203 &config.packages,
204 None,
205 false, false, yes,
208 Some(crate::cli::InstallScope::System),
209 false,
210 false,
211 false,
212 None,
213 false,
214 None,
215 false,
216 false,
217 false,
218 false,
219 3,
220 false,
221 false,
222 Some(project_config),
223 )?;
224 }
225
226 zoi_system::distro::finalize_first_generation(
228 target_path,
229 config.packages.clone(),
230 dry_run,
231 )?;
232
233 let success_msg = if dry_run {
234 "Dry-run complete."
235 } else {
236 "ZoiOS build complete."
237 };
238 println!(
239 "{} {} on {}.",
240 "Success:".green(),
241 success_msg,
242 target.cyan()
243 );
244 }
245 DistroSubcommands::Chroot {
246 target,
247 run,
248 verbose,
249 } => {
250 let target_path = std::path::Path::new(&target);
251 if !target_path.exists() {
252 return Err(anyhow!("Target path '{}' does not exist.", target));
253 }
254
255 let os_release = target_path.join("etc/os-release");
256 if !os_release.exists() {
257 return Err(anyhow!(
258 "Target path '{}' is not a valid ZoiOS root (missing /etc/os-release).",
259 target
260 ));
261 }
262
263 let fs_type = std::process::Command::new("stat")
267 .arg("-f")
268 .arg("-c")
269 .arg("%T")
270 .arg(&target)
271 .output();
272 if let Ok(out) = fs_type {
273 let t = String::from_utf8_lossy(&out.stdout).trim().to_string();
274 if t == "msdos" || t == "vfat" {
275 eprintln!(
276 "\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.",
277 "Error:".red().bold(),
278 t.yellow()
279 );
280 } else if verbose {
281 println!("{} Filesystem type: {}", "::".bold().blue(), t.green());
282 }
283 }
284
285 let mut broken_layout = false;
287 for sym in &["bin", "sbin", "lib", "lib64"] {
288 let p = target_path.join(sym);
289 let meta = std::fs::symlink_metadata(&p);
290 if let Ok(m) = meta {
291 if !m.file_type().is_symlink() {
292 eprintln!(
293 "{} WARNING: '/{}' is a real directory, but ZoiOS expects a merged-usr symlink to 'usr/{}'.",
294 "::".bold().yellow(),
295 sym,
296 sym
297 );
298 broken_layout = true;
299 } else if let Ok(target) = std::fs::read_link(&p) {
300 if target.is_absolute() {
301 eprintln!(
302 "{} WARNING: '/{}' is an absolute symlink to '{}'. This WILL break inside the chroot. It should be relative (e.g. 'usr/{}').",
303 "::".bold().yellow(),
304 sym,
305 target.display(),
306 sym
307 );
308 broken_layout = true;
309 } else {
310 let abs_target = target_path.join(target);
311 if !abs_target.exists() {
312 eprintln!(
313 "{} WARNING: Symlink '/{}' points to non-existent path '{}'.",
314 "::".bold().yellow(),
315 sym,
316 abs_target.display()
317 );
318 broken_layout = true;
319 }
320 }
321 }
322 } else if *sym != "sbin" {
323 eprintln!(
324 "{} WARNING: '/{}' is missing! Your binaries will likely fail to find their loader or shell.",
325 "::".bold().yellow(),
326 sym
327 );
328 broken_layout = true;
329 }
330 }
331
332 let mut loader_found = false;
334 let loaders = [
335 "usr/lib/ld-linux-x86-64.so.2",
336 "lib64/ld-linux-x86-64.so.2",
337 "lib/ld-linux-x86-64.so.2",
338 ];
339 for l in &loaders {
340 let lp = target_path.join(l);
341 if lp.exists() {
342 loader_found = true;
343 if let Ok(mut file) = std::fs::File::open(&lp) {
344 let mut magic = [0u8; 4];
345 if file.read_exact(&mut magic).is_ok() {
346 if magic != [0x7f, b'E', b'L', b'F'] {
347 eprintln!(
348 "{} CRITICAL: Dynamic loader '{}' is NOT an ELF file! Your glibc installation is corrupted.",
349 "Error:".red().bold(),
350 l
351 );
352 }
353 } else {
354 eprintln!(
355 "{} CRITICAL: Dynamic loader '{}' is 0 bytes or unreadable.",
356 "Error:".red().bold(),
357 l
358 );
359 }
360 }
361 break;
362 }
363 }
364 if !loader_found {
365 eprintln!(
366 "{} Dynamic loader not found. Binaries WILL Segfault (139).",
367 "::".bold().yellow()
368 );
369 }
370
371 if broken_layout || !loader_found {
372 println!(
373 "{} Hint: Your ZoiOS bootstrap appears incomplete or corrupted. Please verify your base system packages.",
374 "::".bold().blue()
375 );
376 }
377
378 if verbose {
379 println!(
380 "{} Entering sysroot at {}...",
381 "::".bold().blue(),
382 target.cyan()
383 );
384 }
385
386 let mut envs = std::collections::HashMap::new();
387 envs.insert(
388 "PATH".to_string(),
389 "/usr/bin:/bin:/usr/sbin:/sbin".to_string(),
390 );
391 envs.insert("SHELL".to_string(), "/usr/bin/bash".to_string());
392 envs.insert(
393 "TERM".to_string(),
394 std::env::var("TERM").unwrap_or_else(|_| "xterm-256color".to_string()),
395 );
396
397 let mut shell_bin = std::path::PathBuf::from("/usr/bin/bash");
399 if !target_path.join("usr/bin/bash").exists()
400 && target_path.join("bin/bash").exists()
401 {
402 shell_bin = std::path::PathBuf::from("/bin/bash");
403 }
404
405 if verbose {
406 let cmd_display = if let Some(r) = &run {
407 format!("{} -c '{}'", shell_bin.display(), r)
408 } else {
409 shell_bin.display().to_string()
410 };
411 println!(
412 "{} Running inside chroot: {}",
413 "::".bold().blue(),
414 cmd_display.green()
415 );
416 }
417
418 #[cfg(target_os = "linux")]
419 {
420 let mut cmd = if let Some(run_cmd) = run {
421 let args = vec!["-c".to_string(), run_cmd];
422 crate::sandbox::wrap_command_in_root(
423 target_path,
424 &shell_bin,
425 &args,
426 &envs,
427 &[],
428 false,
429 )?
430 } else {
431 crate::sandbox::wrap_command_in_root(
432 target_path,
433 &shell_bin,
434 &[],
435 &envs,
436 &[],
437 false,
438 )?
439 };
440
441 if verbose {
442 println!("{} Full command: {:?}", "::".bold().blue(), cmd);
443 }
444
445 let status = cmd.status()?;
446 if !status.success() {
447 let code = status.code().unwrap_or(1);
448 eprintln!(
449 "\n{} Chroot execution failed with exit code: {}",
450 "Error:".red().bold(),
451 code.to_string().yellow()
452 );
453 if code == 139 {
454 println!(
455 "{} Hint: Segfaults (139) often indicate an instruction set mismatch (e.g. x86-64-v3 binaries on older CPUs).",
456 "::".bold().blue()
457 );
458 }
459 std::process::exit(code);
460 }
461 }
462
463 #[cfg(not(target_os = "linux"))]
464 return Err(anyhow!(
465 "Distro chroot is only supported on Linux via Bubblewrap."
466 ));
467 }
468 },
469 SystemSubcommands::Apply { file } => {
470 #[cfg(unix)]
471 {
472 println!("Reading system configuration from {}...", file.cyan());
473 let config = load_system_lua(&file)?;
474 let response = send_request(Request::ApplySystemConfig(Box::new(config)))?;
475 handle_response(response)?;
476 }
477 #[cfg(not(unix))]
478 return Err(anyhow!(
479 "OS management daemon commands are only supported on Unix."
480 ));
481 }
482 SystemSubcommands::List => {
483 #[cfg(unix)]
484 {
485 let response = send_request(Request::ListGenerations)?;
486 match response {
487 Response::Generations(gens) => {
488 println!("{:<5} {:<25} {:<50}", "ID", "Created At", "Packages");
489 println!("{:-<80}", "");
490 for generation in gens {
491 println!(
492 "{:<5} {:<25} {:<50}",
493 generation.id,
494 generation.created_at.to_rfc3339(),
495 generation.packages.join(", ")
496 );
497 }
498 }
499 _ => handle_response(response)?,
500 }
501 }
502 #[cfg(not(unix))]
503 return Err(anyhow!(
504 "OS management daemon commands are only supported on Unix."
505 ));
506 }
507 SystemSubcommands::Status => {
508 #[cfg(unix)]
509 {
510 let response = send_request(Request::GetStatus)?;
511 handle_response(response)?;
512 }
513 #[cfg(not(unix))]
514 return Err(anyhow!(
515 "OS management daemon commands are only supported on Unix."
516 ));
517 }
518 SystemSubcommands::Rollback { id } => {
519 #[cfg(unix)]
520 {
521 println!("Rolling back to generation {}...", id.to_string().yellow());
522 let response = send_request(Request::RollbackGeneration(id))?;
523 handle_response(response)?;
524 }
525 #[cfg(not(unix))]
526 let _ = id;
527 #[cfg(not(unix))]
528 return Err(anyhow!(
529 "OS management daemon commands are only supported on Unix."
530 ));
531 }
532 }
533
534 Ok(())
535}
536
537fn print_build_summary(target: &str, config: &zoi_system::config::SystemConfig, dry_run: bool) {
538 use comfy_table::modifiers::UTF8_ROUND_CORNERS;
539 use comfy_table::presets::UTF8_FULL_CONDENSED;
540 use comfy_table::{Cell, Color, Table};
541
542 println!("\n{}", " ZoiOS Build Plan ".bold().on_blue().white());
543 if dry_run {
544 println!(
545 "{}",
546 " [DRY-RUN MODE - NO CHANGES WILL BE MADE] "
547 .on_yellow()
548 .black()
549 .bold()
550 );
551 }
552 println!("{} {}\n", "Target Root:".bold(), target.cyan());
553
554 let mut fs_table = Table::new();
556 fs_table
557 .load_preset(UTF8_FULL_CONDENSED)
558 .apply_modifier(UTF8_ROUND_CORNERS)
559 .set_header(vec![
560 Cell::new("Action").fg(Color::Yellow),
561 Cell::new("Device").fg(Color::Yellow),
562 Cell::new("FS Type").fg(Color::Yellow),
563 Cell::new("Mount Point").fg(Color::Yellow),
564 Cell::new("Options").fg(Color::Yellow),
565 ]);
566
567 for fs in &config.filesystems {
568 fs_table.add_row(vec![
569 Cell::new("Configure (fstab)").fg(Color::Blue),
570 Cell::new(&fs.device),
571 Cell::new(&fs.fs_type),
572 Cell::new(&fs.mount).fg(Color::Cyan),
573 Cell::new(fs.options.as_deref().unwrap_or("defaults")),
574 ]);
575 }
576 println!("{}", " 1. Filesystem & Partitioning ".bold().underline());
577 println!("{}\n", fs_table);
578
579 let mut sys_table = Table::new();
581 sys_table
582 .load_preset(UTF8_FULL_CONDENSED)
583 .apply_modifier(UTF8_ROUND_CORNERS)
584 .set_header(vec![
585 Cell::new("Property").fg(Color::Yellow),
586 Cell::new("Value").fg(Color::Yellow),
587 ]);
588
589 sys_table.add_row(vec![
590 Cell::new("Hostname"),
591 Cell::new(config.system.hostname.as_deref().unwrap_or("zoios")).fg(Color::Cyan),
592 ]);
593 sys_table.add_row(vec![
594 Cell::new("Timezone"),
595 Cell::new(config.system.timezone.as_deref().unwrap_or("UTC")),
596 ]);
597 sys_table.add_row(vec![
598 Cell::new("Locale"),
599 Cell::new(config.system.locale.as_deref().unwrap_or("en_US.UTF-8")),
600 ]);
601
602 println!("{}", " 2. System Configuration ".bold().underline());
603 println!("{}\n", sys_table);
604
605 println!("{}", " 3. Packages ".bold().underline());
607 println!(
608 "{} base packages will be installed from the registry.\n",
609 config.packages.len().to_string().green().bold()
610 );
611
612 let mut pkg_list = String::new();
613 for (i, pkg) in config.packages.iter().enumerate() {
614 pkg_list.push_str(&format!("{}", pkg.cyan()));
615 if i < config.packages.len() - 1 {
616 pkg_list.push_str(", ");
617 }
618 if (i + 1) % 5 == 0 {
619 pkg_list.push('\n');
620 }
621 }
622 println!("{}\n", pkg_list);
623}
624
625#[cfg(unix)]
626fn handle_response(response: Response) -> Result<()> {
627 match response {
628 Response::Ok => println!("{}", "Operation successful.".green()),
629 Response::Success(msg) => println!("{} {}", "Success:".green(), msg),
630 Response::Status(msg) => println!("Daemon status: {}", msg.cyan()),
631 Response::Error(err) => return Err(anyhow!("Daemon error: {}", err)),
632 _ => return Err(anyhow!("Unexpected response from daemon")),
633 }
634 Ok(())
635}