Feature/consolidate setup (#3092)

* start consolidating

* add start-cli flash-os

* combine install and setup and refactor all

* use http

* undo mock

* fix translation

* translations

* use dialogservice wrapper

* better ST messaging on setup

* only warn on update if breakages (#3097)

* finish setup wizard and ui language-keyboard feature

* fix typo

* wip: localization

* remove start-tunnel readme

* switch to posix strings for language internal

* revert mock

* translate backend strings

* fix missing about text

* help text for args

* feat: add "Add new gateway" option (#3098)

* feat: add "Add new gateway" option

* Update web/projects/ui/src/app/routes/portal/components/form/controls/select.component.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* add translation

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Matt Hill <mattnine@protonmail.com>

* fix dns selection

* keyboard keymap also

* ability to shutdown after install

* revert mock

* working setup flow + manifest localization

* (mostly) redundant localization on frontend

* version bump

* omit live medium from disk list and better space management

* ignore missing package archive on 035 migration

* fix device migration

* add i18n helper to sdk

* fix install over 0.3.5.1

* fix grub config

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
Co-authored-by: Alex Inkin <alexander@inkin.ru>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Aiden McClelland
2026-01-27 14:44:41 -08:00
committed by GitHub
parent 99871805bd
commit c65db31fd9
251 changed files with 12163 additions and 3966 deletions

View File

@@ -56,7 +56,7 @@ pub async fn add_package(
let Some(([url], rest)) = urls.split_at_checked(1) else {
return Err(Error::new(
eyre!("must specify at least 1 url"),
eyre!("{}", t!("registry.package.add.must-specify-url")),
ErrorKind::InvalidRequest,
));
};
@@ -112,7 +112,7 @@ pub async fn add_package(
Ok(())
} else {
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
Err(Error::new(eyre!("{}", t!("registry.package.add.unauthorized")), ErrorKind::Authorization))
}
})
.await
@@ -123,10 +123,11 @@ pub async fn add_package(
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
pub struct CliAddPackageParams {
#[arg(help = "help.arg.s9pk-file-path")]
pub file: PathBuf,
#[arg(long)]
#[arg(long, help = "help.arg.package-url")]
pub url: Vec<Url>,
#[arg(long)]
#[arg(long, help = "help.arg.no-verify")]
pub no_verify: bool,
}
@@ -205,9 +206,11 @@ pub async fn cli_add_package(
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RemovePackageParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
#[arg(help = "help.arg.package-version")]
pub version: VersionString,
#[arg(long)]
#[arg(long, help = "help.arg.signature-hash")]
pub sighash: Option<Base64<[u8; 32]>>,
#[ts(skip)]
#[arg(skip)]
@@ -226,7 +229,7 @@ pub async fn remove_package(
) -> Result<bool, Error> {
let peek = ctx.db.peek().await;
let signer =
signer.ok_or_else(|| Error::new(eyre!("missing signer"), ErrorKind::InvalidRequest))?;
signer.ok_or_else(|| Error::new(eyre!("{}", t!("registry.package.missing-signer")), ErrorKind::InvalidRequest))?;
let signer_guid = peek.as_index().as_signers().get_signer(&signer)?;
let rev = ctx
@@ -267,7 +270,7 @@ pub async fn remove_package(
}
Ok(())
} else {
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
Err(Error::new(eyre!("{}", t!("registry.package.unauthorized")), ErrorKind::Authorization))
}
})
.await;
@@ -342,7 +345,7 @@ pub async fn add_mirror(
Ok(())
} else {
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
Err(Error::new(eyre!("{}", t!("registry.package.add-mirror.unauthorized")), ErrorKind::Authorization))
}
})
.await
@@ -353,8 +356,11 @@ pub async fn add_mirror(
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
pub struct CliAddMirrorParams {
#[arg(help = "help.arg.s9pk-file-path")]
pub file: PathBuf,
#[arg(help = "help.arg.mirror-url")]
pub url: Url,
#[arg(long, help = "help.arg.no-verify")]
pub no_verify: bool,
}
@@ -432,9 +438,11 @@ pub async fn cli_add_mirror(
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RemoveMirrorParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
#[arg(help = "help.arg.package-version")]
pub version: VersionString,
#[arg(long)]
#[arg(long, help = "help.arg.mirror-url")]
#[ts(type = "string")]
pub url: Url,
#[ts(skip)]
@@ -454,7 +462,7 @@ pub async fn remove_mirror(
) -> Result<(), Error> {
let peek = ctx.db.peek().await;
let signer =
signer.ok_or_else(|| Error::new(eyre!("missing signer"), ErrorKind::InvalidRequest))?;
signer.ok_or_else(|| Error::new(eyre!("{}", t!("registry.package.missing-signer")), ErrorKind::InvalidRequest))?;
let signer_guid = peek.as_index().as_signers().get_signer(&signer)?;
ctx.db
@@ -483,7 +491,7 @@ pub async fn remove_mirror(
.for_each(|(_, asset)| asset.urls.retain(|u| u != &url));
if s.iter().any(|(_, asset)| asset.urls.is_empty()) {
Err(Error::new(
eyre!("cannot remove last mirror from an s9pk"),
eyre!("{}", t!("registry.package.cannot-remove-last-mirror")),
ErrorKind::InvalidRequest,
))
} else {
@@ -493,7 +501,7 @@ pub async fn remove_mirror(
}
Ok(())
} else {
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
Err(Error::new(eyre!("{}", t!("registry.package.remove-mirror.unauthorized")), ErrorKind::Authorization))
}
})
.await

View File

@@ -11,6 +11,7 @@ use crate::context::CliContext;
use crate::prelude::*;
use crate::registry::context::RegistryContext;
use crate::registry::package::index::Category;
use crate::s9pk::manifest::LocaleString;
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
pub fn category_api<C: Context>() -> ParentHandler<C> {
@@ -20,7 +21,7 @@ pub fn category_api<C: Context>() -> ParentHandler<C> {
from_fn_async(add_category)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Add a category to the registry")
.with_about("about.add-category-registry")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -28,7 +29,7 @@ pub fn category_api<C: Context>() -> ParentHandler<C> {
from_fn_async(remove_category)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Remove a category from the registry")
.with_about("about.remove-category-registry")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -36,7 +37,7 @@ pub fn category_api<C: Context>() -> ParentHandler<C> {
from_fn_async(add_package)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Add a package to a category")
.with_about("about.add-package-category")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -44,7 +45,7 @@ pub fn category_api<C: Context>() -> ParentHandler<C> {
from_fn_async(remove_package)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Remove a package from a category")
.with_about("about.remove-package-category")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -66,7 +67,7 @@ pub fn category_api<C: Context>() -> ParentHandler<C> {
pub struct AddCategoryParams {
#[ts(type = "string")]
pub id: InternedString,
pub name: String,
pub name: LocaleString,
}
pub async fn add_category(
@@ -196,7 +197,7 @@ pub fn display_categories<T>(
"NAME",
]);
for (id, info) in categories {
table.add_row(row![&*id, &info.name]);
table.add_row(row![&*id, &info.name.localized()]);
}
table.print_tty(false)?;
Ok(())

View File

@@ -51,17 +51,18 @@ pub struct PackageInfoShort {
#[ts(export)]
#[model = "Model<Self>"]
pub struct GetPackageParams {
#[arg(help = "help.arg.package-id")]
pub id: Option<PackageId>,
#[ts(type = "string | null")]
#[arg(long, short = 'v')]
#[arg(long, short = 'v', help = "help.arg.target-version-range")]
pub target_version: Option<VersionRange>,
#[arg(long)]
#[arg(long, help = "help.arg.source-version")]
pub source_version: Option<VersionString>,
#[ts(skip)]
#[arg(skip)]
#[serde(rename = "__DeviceInfo_device_info")]
pub device_info: Option<DeviceInfo>,
#[arg(default_value = "none")]
#[arg(default_value = "none", help = "help.arg.other-versions-detail")]
pub other_versions: Option<PackageDetailLevel>,
}
@@ -78,20 +79,20 @@ pub struct GetPackageResponse {
pub other_versions: Option<BTreeMap<VersionString, PackageInfoShort>>,
}
impl GetPackageResponse {
pub fn tables(&self) -> Vec<prettytable::Table> {
pub fn tables(self) -> Vec<prettytable::Table> {
use prettytable::*;
let mut res = Vec::with_capacity(self.best.len());
for (version, info) in &self.best {
let mut table = info.table(version);
for (version, info) in self.best {
let mut table = info.table(&version);
let lesser_versions: BTreeMap<_, _> = self
.other_versions
.as_ref()
.into_iter()
.flatten()
.filter(|(v, _)| ***v < **version)
.filter(|(v, _)| ***v < *version)
.collect();
if !lesser_versions.is_empty() {
@@ -120,13 +121,17 @@ pub struct GetPackageResponseFull {
pub other_versions: BTreeMap<VersionString, PackageVersionInfo>,
}
impl GetPackageResponseFull {
pub fn tables(&self) -> Vec<prettytable::Table> {
pub fn tables(self) -> Vec<prettytable::Table> {
let mut res = Vec::with_capacity(self.best.len());
let all: BTreeMap<_, _> = self.best.iter().chain(self.other_versions.iter()).collect();
let all: BTreeMap<_, _> = self
.best
.into_iter()
.chain(self.other_versions.into_iter())
.collect();
for (version, info) in all {
res.push(info.table(version));
res.push(info.table(&version));
}
res
@@ -401,11 +406,12 @@ pub fn display_package_info(
#[derive(Debug, Deserialize, Serialize, TS, Parser)]
#[serde(rename_all = "camelCase")]
pub struct CliDownloadParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
#[arg(long, short = 'v')]
#[arg(long, short = 'v', help = "help.arg.target-version-range")]
#[ts(type = "string | null")]
pub target_version: Option<VersionRange>,
#[arg(short, long)]
#[arg(short, long, help = "help.arg.destination-path")]
pub dest: Option<PathBuf>,
}
@@ -441,8 +447,12 @@ pub async fn cli_download(
0 => {
return Err(Error::new(
eyre!(
"Could not find a version of {id} that satisfies {}",
target_version.unwrap_or(VersionRange::Any)
"{}",
t!(
"registry.package.get.version-not-found",
id = id,
version = target_version.unwrap_or(VersionRange::Any)
)
),
ErrorKind::NotFound,
));
@@ -462,8 +472,12 @@ pub async fn cli_download(
0 => {
return Err(Error::new(
eyre!(
"Could not find a version of {id} that satisfies {}",
target_version.unwrap_or(VersionRange::Any)
"{}",
t!(
"registry.package.get.version-not-found",
id = id,
version = target_version.unwrap_or(VersionRange::Any)
)
),
ErrorKind::NotFound,
));
@@ -551,7 +565,7 @@ pub async fn cli_download(
progress_tracker.complete();
progress.await.unwrap();
println!("Download Complete");
println!("{}", t!("registry.package.get.download-complete"));
Ok(())
}

View File

@@ -17,7 +17,7 @@ use crate::registry::device_info::DeviceInfo;
use crate::rpc_continuations::Guid;
use crate::s9pk::S9pk;
use crate::s9pk::git_hash::GitHash;
use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements};
use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements, LocaleString};
use crate::s9pk::merkle_archive::source::FileSource;
use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
use crate::sign::{AnySignature, AnyVerifyingKey};
@@ -49,22 +49,27 @@ pub struct PackageInfo {
#[model = "Model<Self>"]
#[ts(export)]
pub struct Category {
pub name: String,
pub name: LocaleString,
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS, PartialEq, Eq)]
#[derive(Debug, Deserialize, Serialize, HasModel, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct DependencyMetadata {
#[ts(type = "string | null")]
pub title: Option<InternedString>,
pub title: Option<LocaleString>,
pub icon: Option<DataUrl<'static>>,
pub description: Option<String>,
pub description: Option<LocaleString>,
pub optional: bool,
}
impl DependencyMetadata {
pub fn localize_for(&mut self, locale: &str) {
self.title.as_mut().map(|t| t.localize_for(locale));
self.description.as_mut().map(|d| d.localize_for(locale));
}
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS, PartialEq, Eq)]
#[derive(Debug, Deserialize, Serialize, HasModel, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
pub struct PackageMetadata {
@@ -72,7 +77,7 @@ pub struct PackageMetadata {
pub title: InternedString,
pub icon: DataUrl<'static>,
pub description: Description,
pub release_notes: String,
pub release_notes: LocaleString,
pub git_hash: Option<GitHash>,
#[ts(type = "string")]
pub license: InternedString,
@@ -199,20 +204,20 @@ impl PackageVersionInfo {
self.s9pks.sort_by_key(|(h, _)| h.specificity_desc());
Ok(())
}
pub fn table(&self, version: &VersionString) -> prettytable::Table {
pub fn table(self, version: &VersionString) -> prettytable::Table {
use prettytable::*;
let mut table = Table::new();
table.add_row(row![bc => &self.metadata.title]);
table.add_row(row![br -> "VERSION", AsRef::<str>::as_ref(version)]);
table.add_row(row![br -> "RELEASE NOTES", &self.metadata.release_notes]);
table.add_row(row![br -> "RELEASE NOTES", &self.metadata.release_notes.localized()]);
table.add_row(
row![br -> "ABOUT", &textwrap::wrap(&self.metadata.description.short, 80).join("\n")],
row![br -> "ABOUT", &textwrap::wrap(&self.metadata.description.short.localized(), 80).join("\n")],
);
table.add_row(row![
br -> "DESCRIPTION",
&textwrap::wrap(&self.metadata.description.long, 80).join("\n")
&textwrap::wrap(&self.metadata.description.long.localized(), 80).join("\n")
]);
table.add_row(row![br -> "GIT HASH", self.metadata.git_hash.as_deref().unwrap_or("N/A")]);
table.add_row(row![br -> "LICENSE", &self.metadata.license]);
@@ -280,6 +285,24 @@ impl Model<PackageVersionInfo> {
{
return Ok(false);
}
if let Some(locale) = device_info.os.language.as_deref() {
let metadata = self.as_metadata_mut();
metadata
.as_alerts_mut()
.mutate(|a| Ok(a.localize_for(locale)))?;
metadata
.as_dependency_metadata_mut()
.as_entries_mut()?
.into_iter()
.try_for_each(|(_, d)| d.mutate(|d| Ok(d.localize_for(locale))))?;
metadata
.as_description_mut()
.mutate(|d| Ok(d.localize_for(locale)))?;
metadata
.as_release_notes_mut()
.mutate(|r| Ok(r.localize_for(locale)))?;
}
}
Ok(true)

View File

@@ -17,7 +17,7 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
from_fn_async(index::get_package_index)
.with_metadata("authenticated", Value::Bool(false))
.with_display_serializable()
.with_about("List packages and categories")
.with_about("about.list-packages-categories")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -30,7 +30,7 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
"add",
from_fn_async(add::cli_add_package)
.no_display()
.with_about("Add package to registry index"),
.with_about("about.add-package-registry"),
)
.subcommand(
"add-mirror",
@@ -42,7 +42,7 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
"add-mirror",
from_fn_async(add::cli_add_mirror)
.no_display()
.with_about("Add a mirror for an s9pk"),
.with_about("about.add-mirror-s9pk"),
)
.subcommand(
"remove",
@@ -51,17 +51,17 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
.with_custom_display_fn(|args, changed| {
if !changed {
tracing::warn!(
"{}@{}{} does not exist, so not removed",
args.params.id,
args.params.version,
args.params
.sighash
.map_or(String::new(), |h| format!("#{h}"))
"{}",
t!("registry.package.remove-not-exist",
id = args.params.id,
version = args.params.version,
sighash = args.params.sighash.map_or(String::new(), |h| format!("#{h}"))
)
);
}
Ok(())
})
.with_about("Remove package from registry index")
.with_about("about.remove-package-registry")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -69,12 +69,12 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
from_fn_async(add::remove_mirror)
.with_metadata("get_signer", Value::Bool(true))
.no_display()
.with_about("Remove a mirror from a package")
.with_about("about.remove-mirror-package")
.with_call_remote::<CliContext>(),
)
.subcommand(
"signer",
signer::signer_api::<C>().with_about("Add, remove, and list package signers"),
signer::signer_api::<C>().with_about("about.add-remove-list-package-signers"),
)
.subcommand(
"get",
@@ -85,18 +85,18 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
.with_custom_display_fn(|handle, result| {
get::display_package_info(handle.params, result)
})
.with_about("List installation candidate package(s)")
.with_about("about.list-installation-candidates")
.with_call_remote::<CliContext>(),
)
.subcommand(
"download",
from_fn_async_local(get::cli_download)
.no_display()
.with_about("Download an s9pk"),
.with_about("about.download-s9pk"),
)
.subcommand(
"category",
category::category_api::<C>()
.with_about("Update the categories for packages on the registry"),
.with_about("about.update-categories-registry"),
)
}

View File

@@ -22,7 +22,7 @@ pub fn signer_api<C: Context>() -> ParentHandler<C> {
from_fn_async(add_package_signer)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Add package signer")
.with_about("about.add-package-signer")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -30,7 +30,7 @@ pub fn signer_api<C: Context>() -> ParentHandler<C> {
from_fn_async(remove_package_signer)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Remove package signer")
.with_about("about.remove-package-signer")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -41,7 +41,7 @@ pub fn signer_api<C: Context>() -> ParentHandler<C> {
.with_custom_display_fn(|handle, result| {
display_package_signers(handle.params, result)
})
.with_about("List package signers and related signer info")
.with_about("about.list-package-signers")
.with_call_remote::<CliContext>(),
)
}
@@ -51,9 +51,11 @@ pub fn signer_api<C: Context>() -> ParentHandler<C> {
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct AddPackageSignerParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
#[arg(help = "help.arg.signer-id")]
pub signer: Guid,
#[arg(long)]
#[arg(long, help = "help.arg.version-range")]
#[ts(type = "string | null")]
pub versions: Option<VersionRange>,
}
@@ -93,7 +95,9 @@ pub async fn add_package_signer(
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RemovePackageSignerParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
#[arg(help = "help.arg.signer-id")]
pub signer: Guid,
}
@@ -114,7 +118,7 @@ pub async fn remove_package_signer(
.is_some()
{
return Err(Error::new(
eyre!("signer {signer} is not authorized to sign for {id}"),
eyre!("{}", t!("registry.package.signer.not-authorized", signer = signer, id = id)),
ErrorKind::NotFound,
));
}
@@ -130,6 +134,7 @@ pub async fn remove_package_signer(
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct ListPackageSignersParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
}