FDS/OS 1.0

This commit is contained in:
2026-09-21 22:29:23 +08:00
commit 99bc3d15c5
430 changed files with 34876 additions and 0 deletions
+409
View File
@@ -0,0 +1,409 @@
use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
use fds_burn::cli::{BurnCommand, FormatCommand};
use fds_common::Bay;
use std::path::PathBuf;
#[derive(Debug, Parser)]
#[command(multicall = true)]
pub struct Cli {
#[command(subcommand)]
pub applet: Applet,
}
#[derive(Debug, Subcommand)]
pub enum Applet {
#[command(version, about = "FDS/OS system and cartridge control")]
Fds(FdsArgs),
/// Inspect a bay, image, or cartridge manifest.
#[command(version)]
FdsInspect {
#[arg(long, global = true)]
json: bool,
#[command(flatten)]
args: InspectArgs,
},
/// Unmount a cartridge before removal.
#[command(version)]
FdsEject {
#[arg(long, global = true)]
json: bool,
#[command(flatten)]
args: BayArgs,
},
/// Prepare native shutdown or inspect its status.
#[command(version)]
FdsPower {
#[arg(long, global = true)]
json: bool,
#[command(flatten)]
args: PowerArgs,
},
}
impl Cli {
pub fn into_command(self) -> (Option<Action>, bool) {
match self.applet {
Applet::Fds(args) => (args.command, args.json),
Applet::FdsInspect { json, args } => (Some(Action::Inspect(args)), json),
Applet::FdsEject { json, args } => (Some(Action::Eject(args)), json),
Applet::FdsPower { json, args } => (Some(Action::Power(args)), json),
}
}
}
#[derive(Debug, Args)]
pub struct FdsArgs {
/// Emit machine-readable JSON where supported.
#[arg(long, global = true)]
pub json: bool,
#[command(subcommand)]
pub command: Option<Action>,
}
#[derive(Debug, Subcommand)]
pub enum Action {
/// Show the system and tool identity.
Info,
/// Show all twelve bays.
Bays,
/// Show one bay and its cartridge state.
#[command(visible_alias = "cartridge")]
Bay(BayArgs),
/// Unmount a cartridge before removal.
Eject(BayArgs),
/// Select writable DATA.
Data {
#[command(subcommand)]
command: DataCommand,
},
/// Start a managed DATA job or PROGRAM executable.
Run {
#[arg(value_parser = fds_burn::client::bay)]
bay: Bay,
/// Executable and its arguments; separate them from FDS options with --.
#[arg(last = true, required = true, num_args = 1.., value_name = "EXECUTABLE_AND_ARGS")]
arguments: Vec<String>,
},
/// Show desktop and network state.
Profiles,
/// Activate a desktop or return to the console.
Profile {
#[command(subcommand)]
command: ProfileCommand,
},
/// Enable or stop DHCP networking.
Network {
#[arg(value_enum)]
state: Switch,
},
/// Refresh cartridge inventory.
Rescan,
/// Show USB paths for bay calibration.
Topology,
/// Inspect a bay, image, or manifest file.
Inspect(InspectArgs),
/// Preview and confirm a cartridge write, or manage an existing operation.
Burn {
#[command(subcommand)]
command: BurnCommand,
},
/// Create a cartridge and preview a confirmed write.
Format {
#[command(subcommand)]
command: FormatCommand,
},
/// Manage persistent machine settings and saved diagnostics.
Machine {
#[command(subcommand)]
command: Option<MachineCommand>,
},
/// Check DATA or confirm conservative repair from the root recovery console.
Recovery {
#[command(subcommand)]
command: Option<RecoveryCommand>,
},
/// Verify DATA and request native shutdown.
Poweroff,
/// Verify DATA and request a reboot.
Reboot,
/// Show, resume, or request native shutdown preparation.
Power(PowerArgs),
/// Show measured boot events.
BootProfile,
/// Show the tool version.
Version,
}
#[derive(Debug, Args)]
pub struct BayArgs {
#[arg(value_parser = fds_burn::client::bay)]
pub bay: Bay,
}
#[derive(Debug, Subcommand)]
pub enum DataCommand {
/// Select a DATA cartridge for /data.
Use(BayArgs),
}
#[derive(Debug, Subcommand)]
pub enum ProfileCommand {
/// Activate a profile.
Activate {
#[arg(value_parser = ["windowmaker", "cli"])]
name: String,
},
/// Return to the console.
Deactivate,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum Switch {
On,
Off,
}
#[derive(Debug)]
pub struct InspectArgs {
pub target: Option<PathBuf>,
pub command: Option<InspectCommand>,
}
#[derive(Debug, Args)]
#[command(subcommand_negates_reqs = true)]
struct InspectParser {
/// A bay number (1–12 or BAY1–BAY12), or a manifest file path.
#[arg(required = true, value_name = "BAY_OR_MANIFEST")]
target: Option<PathBuf>,
#[command(subcommand)]
command: Option<InspectCommand>,
}
impl TryFrom<InspectParser> for InspectArgs {
type Error = clap::Error;
fn try_from(parsed: InspectParser) -> Result<Self, Self::Error> {
// Clap's args_conflicts_with_subcommands also conflicts with global
// --json. Check only these two typed alternatives during conversion.
if parsed.target.is_some() && parsed.command.is_some() {
return Err(clap::Error::raw(
clap::error::ErrorKind::ArgumentConflict,
"Choose a bay/manifest target or the image subcommand, not both",
));
}
Ok(Self {
target: parsed.target,
command: parsed.command,
})
}
}
impl clap::FromArgMatches for InspectArgs {
fn from_arg_matches(matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
InspectParser::from_arg_matches(matches)?.try_into()
}
fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> {
let mut parsed = InspectParser {
target: self.target.clone(),
command: self.command.clone(),
};
parsed.update_from_arg_matches(matches)?;
*self = parsed.try_into()?;
Ok(())
}
}
impl Args for InspectArgs {
fn augment_args(command: clap::Command) -> clap::Command {
InspectParser::augment_args(command)
}
fn augment_args_for_update(command: clap::Command) -> clap::Command {
InspectParser::augment_args_for_update(command)
}
}
#[derive(Clone, Debug, Subcommand)]
pub enum InspectCommand {
/// Validate a regular cartridge image and calculate its SHA-256 digest.
Image { image: PathBuf },
}
#[derive(Debug, Args)]
pub struct PowerArgs {
#[command(subcommand)]
pub command: Option<PowerCommand>,
}
#[derive(Debug, Subcommand)]
pub enum PowerCommand {
/// Show preparation status or its blocking error (the default).
Status,
/// Resume operations before native shutdown starts.
Resume,
/// Verify DATA and request native shutdown.
Poweroff,
/// Verify DATA and request a reboot.
Reboot,
#[command(long_flag = "shutdown-hook", hide = true)]
ShutdownHook,
#[command(long_flag = "hold-shutdown", hide = true)]
HoldShutdown,
#[command(long_flag = "record-final", hide = true)]
RecordFinal {
#[arg(value_parser = ["failed"])]
outcome: Option<String>,
},
}
#[derive(Debug, Subcommand)]
pub enum RecoveryCommand {
/// Unmount and check DATA without repairs.
Check(BayArgs),
/// Preview repair, or confirm the exact token returned by the preview.
Repair {
#[arg(value_parser = fds_burn::client::bay)]
bay: Bay,
#[arg(long, value_name = "TOKEN")]
confirm: Option<String>,
},
}
#[derive(Debug, Subcommand)]
pub enum MachineCommand {
/// Show the active machine identity and settings source.
Status,
/// Export this boot's active settings into a new directory.
Export { new_directory: PathBuf },
/// Validate machine.toml, bays.toml and hardware-catalog.toml.
Validate { directory: PathBuf },
/// Validate and pack settings into a new JSON file.
Pack {
directory: PathBuf,
new_json_file: PathBuf,
},
/// Install settings for the next boot (root recovery console only).
Install { directory: PathBuf },
/// Save an explicit diagnostic of up to 16 MiB (root only).
Store { name: String, file: PathBuf },
/// Retrieve a saved diagnostic into a new file (root only).
Fetch { name: String, new_file: PathBuf },
#[command(long_flag = "load", hide = true)]
Load,
}
pub fn help(subcommand: Option<&str>) -> std::io::Result<()> {
let mut root = Cli::command();
root.build();
let fds = root.find_subcommand_mut("fds").expect("fds applet");
let command = match subcommand {
Some(name) => fds.find_subcommand_mut(name).expect("known subcommand"),
None => fds,
};
command.print_help()?;
println!();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn command_tree_and_installed_aliases() {
Cli::command().debug_assert();
for arguments in [
vec!["fds", "--json", "inspect", "BAY12"],
vec!["/usr/bin/fds-inspect", "BAY12", "--json"],
vec!["fds-inspect", "image", "card.img"],
vec!["fds-eject", "--json", "12"],
vec!["fds-power", "status", "--json"],
vec!["fds", "cartridge", "12"],
vec![
"fds",
"recovery",
"repair",
"BAY1",
"--confirm",
"exact phrase",
],
vec!["fds", "machine", "pack", "settings", "new.json"],
vec!["fds", "machine", "--load"],
vec!["fds", "power", "--shutdown-hook"],
vec!["fds-power", "--hold-shutdown"],
vec!["fds-power", "--record-final", "failed"],
] {
assert!(Cli::try_parse_from(&arguments).is_ok(), "{arguments:?}");
}
for name in ["fds", "fds-inspect", "fds-eject", "fds-power"] {
let error = Cli::try_parse_from([name, "--help"]).unwrap_err();
assert_eq!(error.kind(), clap::error::ErrorKind::DisplayHelp);
assert!(error.to_string().contains(&format!("Usage: {name}")));
}
}
#[test]
fn run_preserves_every_child_argument_after_separator() {
let (command, json) = Cli::try_parse_from([
"fds", "run", "BAY2", "--", "/bin/app", "--json", "--help", "-x", "", "--", "a b",
])
.unwrap()
.into_command();
assert!(!json);
let Some(Action::Run { bay, arguments }) = command else {
panic!("run command")
};
assert_eq!(bay, "2".parse::<Bay>().unwrap());
assert_eq!(
arguments,
["/bin/app", "--json", "--help", "-x", "", "--", "a b"]
);
let (_, json) = Cli::try_parse_from(["fds", "run", "--json", "2", "--", "/bin/app"])
.unwrap()
.into_command();
assert!(json);
}
#[test]
fn inspect_image_accepts_global_options_in_every_position() {
for arguments in [
vec!["fds", "--json", "inspect", "image", "card.img"],
vec!["fds", "inspect", "--json", "image", "card.img"],
vec!["fds", "inspect", "image", "card.img", "--json"],
vec!["fds-inspect", "--json", "image", "card.img"],
vec!["fds-inspect", "image", "--json", "card.img"],
] {
let (command, json) = Cli::try_parse_from(&arguments).unwrap().into_command();
assert!(json);
assert!(matches!(
command,
Some(Action::Inspect(InspectArgs {
command: Some(InspectCommand::Image { .. }),
target: None,
}))
));
}
}
#[test]
fn rejects_incomplete_or_conflicting_requests_before_execution() {
for arguments in [
vec!["fds", "eject", "0"],
vec!["fds-eject", "13"],
vec!["fds", "run", "1", "/bin/app"],
vec!["fds", "run", "1", "--"],
vec!["fds", "inspect", "image"],
vec!["fds-inspect", "1", "image", "card.img"],
vec!["fds", "recovery", "check", "1", "--confirm", "token"],
vec!["fds", "recovery", "repair", "1", "--confirm"],
vec![
"fds",
"recovery",
"repair",
"1",
"--confirm",
"one",
"--confirm",
"two",
],
vec!["fds", "power", "--shutdown-hook", "--record-final"],
vec!["fds", "power", "--record-final", "success"],
vec!["fds", "machine", "--load", "settings"],
vec!["fds", "network", "maybe"],
vec!["fds", "profile", "activate", "unknown"],
vec!["fds", "unknown"],
] {
assert!(Cli::try_parse_from(&arguments).is_err(), "{arguments:?}");
}
}
}