From cacfb14428c3cc91533e704e5d245fab4a555b46 Mon Sep 17 00:00:00 2001 From: gram Date: Sun, 27 Sep 2026 08:45:15 +0200 Subject: [PATCH] Install all apps of the given author --- src/args.rs | 11 ++++------- src/commands/catalog.rs | 11 ++++++----- src/commands/import.rs | 44 ++++++++++++++++++++++++++++++----------- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/args.rs b/src/args.rs index cd2ac8b..f0047f3 100644 --- a/src/args.rs +++ b/src/args.rs @@ -217,15 +217,16 @@ pub struct ImportArgs { /// /// 3. App ID in the catalog (for example, `sys.launcher`). /// - /// 4. The word "launcher" to install the latest version of the default launcher. - #[arg()] + /// 4. Author ID starting with "@". pub path: String, + + /// Any additional apps to install. + pub paths: Option>, } #[derive(Debug, Parser)] pub struct NewArgs { /// The directory name to create, the new project root and name. - #[arg()] pub name: String, /// The programming language to use for the project. @@ -320,13 +321,11 @@ pub struct CheatArgs { /// The command to pass into the app. /// /// Either an integer or a command listed in firefly.toml. - #[arg()] pub command: String, /// The value to pass into the app. /// /// Either an integer, boolean, or a character. - #[arg()] pub value: String, /// Path to the project root. @@ -398,7 +397,6 @@ pub enum RuntimeCommands { #[derive(Debug, Parser)] pub struct LaunchArgs { /// The app ID to launch. For example, "lux.snek". - #[arg()] pub id: String, } @@ -417,6 +415,5 @@ pub struct CatalogListArgs { #[derive(Debug, Parser)] pub struct CatalogShowArgs { /// The app/author ID to get info for. For example, "lux.snek". - #[arg()] pub id: String, } diff --git a/src/commands/catalog.rs b/src/commands/catalog.rs index aea35f3..79f4c9e 100644 --- a/src/commands/catalog.rs +++ b/src/commands/catalog.rs @@ -8,8 +8,8 @@ const BASE_URL: &str = "https://catalog.fireflyzero.com/"; const LIST_URL: &str = "https://catalog.fireflyzero.com/apps.json"; #[derive(Deserialize)] -struct ShortApp { - id: String, +pub struct ShortApp { + pub id: String, name: String, author: String, short: String, @@ -17,24 +17,25 @@ struct ShortApp { } #[derive(Deserialize)] -struct App { +pub struct App { name: String, author: Author, short: String, added: String, - download: String, + pub download: String, desc: String, links: Option>, categories: Vec, } #[derive(Deserialize)] -struct Author { +pub struct Author { name: String, pronouns: Option, links: HashMap, short: String, about: Option, + pub apps: Option>, } pub fn cmd_catalog_list(_args: &CatalogListArgs) -> Result<()> { diff --git a/src/commands/import.rs b/src/commands/import.rs index 4786203..71e6a6c 100644 --- a/src/commands/import.rs +++ b/src/commands/import.rs @@ -1,33 +1,54 @@ use crate::args::ImportArgs; +use crate::commands::catalog::{App, Author}; use crate::crypto::hash_dir; use crate::file_names::{HASH, META, STATS}; use crate::vfs::init_vfs; use anyhow::{Context, Result, bail}; use chrono::Datelike; use firefly_types::{Encode, Meta, validate_id}; -use serde::Deserialize; use std::env::temp_dir; use std::fs::{self, File, create_dir_all}; use std::io::Read; use std::path::{Path, PathBuf}; use zip::ZipArchive; -/// API response from the firefly catalog. -/// -/// Example: -#[derive(Deserialize)] -struct CatalogApp { - download: String, +// `ff import`: Install apps from catalog, file, or URL. +pub fn cmd_import(vfs: &Path, args: &ImportArgs) -> Result<()> { + // If an author ID is given, download all system apps. + if args.path.starts_with('@') { + let url = format!("https://catalog.fireflyzero.com/{}.json", args.path); + let resp = ureq::get(&url).call().context("send HTTP request")?; + let mut body = resp.into_body().into_reader(); + let author: Author = serde_json::from_reader(&mut body).context("parse JSON")?; + println!("⏳️ installing all apps of the author..."); + if let Some(apps) = author.apps { + for app in apps { + import_app(vfs, &app.id).context("download a system app")?; + } + } + println!("✅ all apps of the author have been installed"); + return Ok(()); + } + + // Download explicitly listed apps. + import_app(vfs, &args.path)?; + if let Some(paths) = &args.paths { + for path in paths { + import_app(vfs, path)?; + } + } + Ok(()) } -pub fn cmd_import(vfs: &Path, args: &ImportArgs) -> Result<()> { - let path = fetch_archive(&args.path).context("download ROM archive")?; +/// Download the given app. The path can be local file, URL, or app ID. +fn import_app(vfs: &Path, raw_path: &str) -> Result<()> { + let path = fetch_archive(raw_path).context("download ROM archive")?; let file = File::open(path).context("open archive file")?; let mut archive = ZipArchive::new(file).context("open archive")?; let meta_raw = read_meta_raw(&mut archive)?; let meta = Meta::decode(&meta_raw).context("parse meta")?; - if !id_matches(&args.path, &meta) { + if !id_matches(raw_path, &meta) { bail!( "app ID ({}.{}) doesn't match the expected ID", meta.author_id, @@ -97,7 +118,7 @@ fn fetch_archive(path: &str) -> Result { let url = format!("https://catalog.fireflyzero.com/{path}.json"); let resp = ureq::get(&url).call().context("send HTTP request")?; let mut body = resp.into_body().into_reader(); - let app: CatalogApp = serde_json::from_reader(&mut body).context("parse JSON")?; + let app: App = serde_json::from_reader(&mut body).context("parse JSON")?; // TODO(@orsinium): the download link might be a download page, // not the actual ROM file. path = app.download; @@ -319,6 +340,7 @@ mod tests { let path_str = archive_path.to_str().unwrap(); let args = ImportArgs { path: path_str.to_string(), + paths: None, }; cmd_import(&vfs2, &args).unwrap();