update marketplace url to reflect build version (#2914)

* update marketplace url to reflect build version

* adjust marketplace config

* use helper function to compare urls

* rework some registry stuff

* #2900, #2899, and other registry changes

* alpha.1

* trailing /

* add startosRegistry

* fix migration

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
This commit is contained in:
Lucy
2025-04-29 16:12:21 -04:00
committed by GitHub
parent 2adf34fbaf
commit 5c473eb9cc
63 changed files with 733 additions and 470 deletions

View File

@@ -17,7 +17,7 @@ COMPAT_SRC := $(shell git ls-files system-images/compat/)
UTILS_SRC := $(shell git ls-files system-images/utils/)
BINFMT_SRC := $(shell git ls-files system-images/binfmt/)
CORE_SRC := $(shell git ls-files core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE)
WEB_SHARED_SRC := $(shell git ls-files web/projects/shared) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules/.package-lock.json web/config.json patch-db/client/dist/index.js sdk/baseDist/package.json web/patchdb-ui-seed.json sdk/dist/package.json
WEB_SHARED_SRC := $(shell git ls-files web/projects/shared) $(shell git ls-files web/projects/marketplace) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules/.package-lock.json web/config.json patch-db/client/dist/index.js sdk/baseDist/package.json web/patchdb-ui-seed.json sdk/dist/package.json
WEB_UI_SRC := $(shell git ls-files web/projects/ui)
WEB_SETUP_WIZARD_SRC := $(shell git ls-files web/projects/setup-wizard)
WEB_INSTALL_WIZARD_SRC := $(shell git ls-files web/projects/install-wizard)
@@ -302,17 +302,7 @@ $(COMPRESSED_WEB_UIS): $(WEB_UIS) $(ENVIRONMENT_FILE)
./compress-uis.sh
web/config.json: $(GIT_HASH_FILE) web/config-sample.json
@ver=$$(cat VERSION.txt); \
case $$ver in \
*alpha*) osUrl="https://alpha-registry-x.start9.com/" ;; \
*beta*) osUrl="https://beta-registry.start9.com/" ;; \
*) osUrl="https://registry.start9.com/" ;; \
esac; \
gitHash=$$(cat GIT_HASH.txt); \
jq '.useMocks = false' web/config-sample.json \
| jq --arg gitHash "$$gitHash" '.gitHash = $$gitHash' \
| jq --arg osUrl "$$osUrl" '.ui.startosRegistry = $$osUrl' \
> web/config.json;
jq '.useMocks = false' web/config-sample.json | jq '.gitHash = "$(shell cat GIT_HASH.txt)"' > web/config.json
patch-db/client/node_modules/.package-lock.json: patch-db/client/package.json
npm --prefix patch-db/client ci

2
core/Cargo.lock generated
View File

@@ -6001,7 +6001,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "start-os"
version = "0.4.0-alpha.0"
version = "0.4.0-alpha.1"
dependencies = [
"aes 0.7.5",
"async-acme",

View File

@@ -14,7 +14,7 @@ keywords = [
name = "start-os"
readme = "README.md"
repository = "https://github.com/Start9Labs/start-os"
version = "0.4.0-alpha.0" # VERSION_BUMP
version = "0.4.0-alpha.1" # VERSION_BUMP
license = "MIT"
[lib]

View File

@@ -22,16 +22,30 @@ pub fn admin_api<C: Context>() -> ParentHandler<C> {
"signer",
signers_api::<C>().with_about("Commands to add or list signers"),
)
.subcommand("add", from_fn_async(add_admin).no_cli())
.subcommand(
"add",
from_fn_async(add_admin)
.with_metadata("admin", Value::Bool(true))
.no_cli(),
)
.subcommand(
"add",
from_fn_async(cli_add_admin)
.no_display()
.with_about("Add admin signer"),
)
.subcommand(
"remove",
from_fn_async(remove_admin)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Remove an admin signer")
.with_call_remote::<CliContext>(),
)
.subcommand(
"list",
from_fn_async(list_admins)
.with_metadata("admin", Value::Bool(true))
.with_display_serializable()
.with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result)))
.with_about("List admin signers")
@@ -285,6 +299,27 @@ pub async fn add_admin(
.result
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RemoveAdminParams {
pub signer: Guid,
}
// TODO: don't allow removing self?
pub async fn remove_admin(
ctx: RegistryContext,
RemoveAdminParams { signer }: RemoveAdminParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_admins_mut().mutate(|a| Ok(a.remove(&signer)))?;
Ok(())
})
.await
.result
}
#[derive(Debug, Deserialize, Serialize, Parser)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]

View File

@@ -51,6 +51,28 @@ pub fn add_api<C: Context>() -> ParentHandler<C> {
)
}
pub fn remove_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"iso",
from_fn_async(remove_iso)
.with_metadata("get_signer", Value::Bool(true))
.no_cli(),
)
.subcommand(
"img",
from_fn_async(remove_img)
.with_metadata("get_signer", Value::Bool(true))
.no_cli(),
)
.subcommand(
"squashfs",
from_fn_async(remove_squashfs)
.with_metadata("get_signer", Value::Bool(true))
.no_cli(),
)
}
#[derive(Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
@@ -99,6 +121,7 @@ async fn add_asset(
.as_authorized()
.de()?
.contains(&signer_guid)
|| db.as_admins().de()?.contains(&signer_guid)
{
accessor(
db.as_index_mut()
@@ -256,3 +279,74 @@ pub async fn cli_add_asset(
Ok(())
}
#[derive(Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RemoveAssetParams {
#[ts(type = "string")]
pub version: Version,
#[ts(type = "string")]
pub platform: InternedString,
#[serde(rename = "__auth_signer")]
#[ts(skip)]
pub signer: AnyVerifyingKey,
}
async fn remove_asset(
ctx: RegistryContext,
RemoveAssetParams {
version,
platform,
signer,
}: RemoveAssetParams,
accessor: impl FnOnce(
&mut Model<OsVersionInfo>,
) -> &mut Model<BTreeMap<InternedString, RegistryAsset<Blake3Commitment>>>
+ UnwindSafe
+ Send,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
let signer_guid = db.as_index().as_signers().get_signer(&signer)?;
if db
.as_index()
.as_os()
.as_versions()
.as_idx(&version)
.or_not_found(&version)?
.as_authorized()
.de()?
.contains(&signer_guid)
|| db.as_admins().de()?.contains(&signer_guid)
{
accessor(
db.as_index_mut()
.as_os_mut()
.as_versions_mut()
.as_idx_mut(&version)
.or_not_found(&version)?,
)
.remove(&platform)?;
Ok(())
} else {
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
}
})
.await
.result?;
Ok(())
}
pub async fn remove_iso(ctx: RegistryContext, params: RemoveAssetParams) -> Result<(), Error> {
remove_asset(ctx, params, |m| m.as_iso_mut()).await
}
pub async fn remove_img(ctx: RegistryContext, params: RemoveAssetParams) -> Result<(), Error> {
remove_asset(ctx, params, |m| m.as_img_mut()).await
}
pub async fn remove_squashfs(ctx: RegistryContext, params: RemoveAssetParams) -> Result<(), Error> {
remove_asset(ctx, params, |m| m.as_squashfs_mut()).await
}

View File

@@ -13,6 +13,7 @@ pub fn asset_api<C: Context>() -> ParentHandler<C> {
.no_display()
.with_about("Add asset to registry"),
)
.subcommand("remove", add::remove_api::<C>())
.subcommand("sign", sign::sign_api::<C>())
.subcommand(
"sign",
@@ -20,6 +21,7 @@ pub fn asset_api<C: Context>() -> ParentHandler<C> {
.no_display()
.with_about("Sign file and add to registry index"),
)
// TODO: remove signature api
.subcommand(
"get",
get::get_api::<C>().with_about("Commands to download image, iso, or squashfs files"),

View File

@@ -3,6 +3,7 @@ use std::collections::BTreeMap;
use chrono::Utc;
use clap::Parser;
use exver::{Version, VersionRange};
use imbl_value::InternedString;
use itertools::Itertools;
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
@@ -12,6 +13,7 @@ use ts_rs::TS;
use crate::context::CliContext;
use crate::prelude::*;
use crate::registry::context::RegistryContext;
use crate::registry::device_info::DeviceInfo;
use crate::registry::os::index::OsVersionInfo;
use crate::registry::signer::sign::AnyVerifyingKey;
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
@@ -44,6 +46,7 @@ pub fn version_api<C: Context>() -> ParentHandler<C> {
.subcommand(
"get",
from_fn_async(get_version)
.with_metadata("get_device_info", Value::Bool(true))
.with_display_serializable()
.with_custom_display_fn(|handle, result| {
Ok(display_version_info(handle.params, result))
@@ -133,35 +136,47 @@ pub async fn remove_version(
pub struct GetOsVersionParams {
#[ts(type = "string | null")]
#[arg(long = "src")]
pub source: Option<Version>,
#[ts(type = "string | null")]
#[arg(long = "target")]
pub target: Option<VersionRange>,
pub source_version: Option<Version>,
#[ts(type = "string | null")]
#[arg(long)]
pub target_version: Option<VersionRange>,
#[arg(long)]
pub include_prerelease: Option<bool>,
#[arg(long = "id")]
server_id: Option<String>,
#[ts(type = "string | null")]
#[arg(long = "arch")]
arch: Option<String>,
#[arg(long)]
platform: Option<InternedString>,
#[ts(skip)]
#[arg(skip)]
#[serde(rename = "__device_info")]
pub device_info: Option<DeviceInfo>,
}
pub async fn get_version(
ctx: RegistryContext,
GetOsVersionParams {
source,
target,
source_version: source,
target_version: target,
include_prerelease,
server_id,
arch,
platform,
device_info,
}: GetOsVersionParams,
) -> Result<BTreeMap<Version, OsVersionInfo>, Error> {
if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, arch) {
let source = source.or_else(|| device_info.as_ref().map(|d| d.os.version.clone()));
let platform = platform.or_else(|| device_info.as_ref().map(|d| d.os.platform.clone()));
let include_prerelease = include_prerelease
.or_else(|| source.as_ref().map(|s| !s.prerelease().is_empty()))
.unwrap_or(cfg!(feature = "dev"));
if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, &platform) {
let created_at = Utc::now();
query!(
"INSERT INTO user_activity (created_at, server_id, arch) VALUES ($1, $2, $3)",
created_at,
server_id,
arch
&**arch
)
.execute(pool)
.await?;
@@ -177,7 +192,11 @@ pub async fn get_version(
.into_iter()
.map(|(v, i)| i.de().map(|i| (v, i)))
.filter_ok(|(version, info)| {
version.satisfies(&target)
(version.prerelease().is_empty() || include_prerelease)
&& platform
.as_ref()
.map_or(true, |p| info.squashfs.contains_key(p))
&& version.satisfies(&target)
&& source
.as_ref()
.map_or(true, |s| s.satisfies(&info.source_version))

View File

@@ -4,6 +4,7 @@ use std::sync::Arc;
use clap::Parser;
use imbl_value::InternedString;
use itertools::Itertools;
use models::{PackageId, VersionString};
use rpc_toolkit::HandlerArgs;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
@@ -158,3 +159,55 @@ pub async fn cli_add_package(
Ok(())
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RemovePackageParams {
pub id: PackageId,
pub version: VersionString,
#[ts(skip)]
#[serde(rename = "__auth_signer")]
pub signer: AnyVerifyingKey,
}
pub async fn remove_package(
ctx: RegistryContext,
RemovePackageParams {
id,
version,
signer,
}: RemovePackageParams,
) -> Result<(), Error> {
let peek = ctx.db.peek().await;
let signer_guid = peek.as_index().as_signers().get_signer(&signer)?;
ctx.db
.mutate(|db| {
if db.as_admins().de()?.contains(&signer_guid)
|| db
.as_index()
.as_package()
.as_packages()
.as_idx(&id)
.or_not_found(&id)?
.as_authorized()
.de()?
.contains(&signer_guid)
{
if let Some(package) = db
.as_index_mut()
.as_package_mut()
.as_packages_mut()
.as_idx_mut(&id)
{
package.as_versions_mut().remove(&version)?;
}
Ok(())
} else {
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
}
})
.await
.result
}

View File

@@ -2,6 +2,7 @@ use std::collections::BTreeMap;
use clap::Parser;
use imbl_value::InternedString;
use models::PackageId;
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
@@ -10,7 +11,6 @@ use crate::context::CliContext;
use crate::prelude::*;
use crate::registry::context::RegistryContext;
use crate::registry::package::index::Category;
use crate::s9pk::manifest::Description;
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
pub fn category_api<C: Context>() -> ParentHandler<C> {
@@ -31,6 +31,22 @@ pub fn category_api<C: Context>() -> ParentHandler<C> {
.with_about("Remove a category from the registry")
.with_call_remote::<CliContext>(),
)
.subcommand(
"add-package",
from_fn_async(add_package)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Add a package to a category")
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove-package",
from_fn_async(remove_package)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Remove a package from a category")
.with_call_remote::<CliContext>(),
)
.subcommand(
"list",
from_fn_async(list_categories)
@@ -50,33 +66,18 @@ pub struct AddCategoryParams {
#[ts(type = "string")]
pub id: InternedString,
pub name: String,
#[arg(short, long, help = "Short description for the category")]
pub short: String,
#[arg(short, long, help = "Long description for the category")]
pub long: String,
}
pub async fn add_category(
ctx: RegistryContext,
AddCategoryParams {
id,
name,
short,
long,
}: AddCategoryParams,
AddCategoryParams { id, name }: AddCategoryParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_index_mut()
.as_package_mut()
.as_categories_mut()
.insert(
&id,
&Category {
name,
description: Description { short, long },
},
)
.insert(&id, &Category { name })
})
.await
.result?;
@@ -108,6 +109,64 @@ pub async fn remove_category(
Ok(())
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct AddPackageToCategoryParams {
#[ts(type = "string")]
pub id: InternedString,
pub package: PackageId,
}
pub async fn add_package(
ctx: RegistryContext,
AddPackageToCategoryParams { id, package }: AddPackageToCategoryParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_index_mut()
.as_package_mut()
.as_packages_mut()
.as_idx_mut(&package)
.or_not_found(&package)?
.as_categories_mut()
.mutate(|c| Ok(c.insert(id)))
})
.await
.result?;
Ok(())
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RemovePackageFromCategoryParams {
#[ts(type = "string")]
pub id: InternedString,
pub package: PackageId,
}
pub async fn remove_package(
ctx: RegistryContext,
RemovePackageFromCategoryParams { id, package }: RemovePackageFromCategoryParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_index_mut()
.as_package_mut()
.as_packages_mut()
.as_idx_mut(&package)
.or_not_found(&package)?
.as_categories_mut()
.mutate(|c| Ok(c.remove(&id)))
})
.await
.result?;
Ok(())
}
pub async fn list_categories(
ctx: RegistryContext,
) -> Result<BTreeMap<InternedString, Category>, Error> {
@@ -134,16 +193,9 @@ pub fn display_categories<T>(
table.add_row(row![bc =>
"ID",
"NAME",
"SHORT DESCRIPTION",
"LONG DESCRIPTION",
]);
for (id, info) in categories {
table.add_row(row![
&*id,
&info.name,
&info.description.short,
&info.description.long,
]);
table.add_row(row![&*id, &info.name]);
}
table.print_tty(false).unwrap();
}

View File

@@ -45,7 +45,9 @@ pub struct PackageInfoShort {
pub struct GetPackageParams {
pub id: Option<PackageId>,
#[ts(type = "string | null")]
pub version: Option<VersionRange>,
#[arg(long, short = 'v')]
pub target_version: Option<VersionRange>,
#[arg(long)]
pub source_version: Option<VersionString>,
#[ts(skip)]
#[arg(skip)]
@@ -188,7 +190,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
let package_best = best.entry(id.clone()).or_default();
let package_other = other.entry(id.clone()).or_default();
if params
.version
.target_version
.as_ref()
.map_or(true, |v| version.satisfies(v))
&& package_best.keys().all(|k| !(**k > version))

View File

@@ -47,7 +47,6 @@ pub struct PackageInfo {
#[ts(export)]
pub struct Category {
pub name: String,
pub description: Description,
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]

View File

@@ -31,6 +31,13 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
.no_display()
.with_about("Add package to registry index"),
)
.subcommand(
"remove",
from_fn_async(add::remove_package)
.no_display()
.with_about("Add package to registry index")
.with_call_remote::<CliContext>(),
)
.subcommand(
"signer",
signer::signer_api::<C>().with_about("Add, remove, and list package signers"),

View File

@@ -40,8 +40,9 @@ mod v0_3_6_alpha_17;
mod v0_3_6_alpha_18;
mod v0_4_0_alpha_0;
mod v0_4_0_alpha_1;
pub type Current = v0_4_0_alpha_0::Version; // VERSION_BUMP
pub type Current = v0_4_0_alpha_1::Version; // VERSION_BUMP
impl Current {
#[instrument(skip(self, db))]
@@ -145,7 +146,8 @@ enum Version {
V0_3_6_alpha_16(Wrapper<v0_3_6_alpha_16::Version>),
V0_3_6_alpha_17(Wrapper<v0_3_6_alpha_17::Version>),
V0_3_6_alpha_18(Wrapper<v0_3_6_alpha_18::Version>),
V0_4_0_alpha_0(Wrapper<v0_4_0_alpha_0::Version>), // VERSION_BUMP
V0_4_0_alpha_0(Wrapper<v0_4_0_alpha_0::Version>),
V0_4_0_alpha_1(Wrapper<v0_4_0_alpha_1::Version>), // VERSION_BUMP
Other(exver::Version),
}
@@ -187,8 +189,9 @@ impl Version {
Self::V0_3_6_alpha_15(v) => DynVersion(Box::new(v.0)),
Self::V0_3_6_alpha_16(v) => DynVersion(Box::new(v.0)),
Self::V0_3_6_alpha_17(v) => DynVersion(Box::new(v.0)),
Self::V0_3_6_alpha_18(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
Self::V0_4_0_alpha_0(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
Self::V0_3_6_alpha_18(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_0(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_1(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
Self::Other(v) => {
return Err(Error::new(
eyre!("unknown version {v}"),
@@ -223,7 +226,8 @@ impl Version {
Version::V0_3_6_alpha_16(Wrapper(x)) => x.semver(),
Version::V0_3_6_alpha_17(Wrapper(x)) => x.semver(),
Version::V0_3_6_alpha_18(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_0(Wrapper(x)) => x.semver(), // VERSION_BUMP
Version::V0_4_0_alpha_0(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_1(Wrapper(x)) => x.semver(), // VERSION_BUMP
Version::Other(x) => x.clone(),
}
}

View File

@@ -0,0 +1,72 @@
use exver::{PreReleaseSegment, VersionRange};
use imbl_value::json;
use super::v0_3_5::V0_3_0_COMPAT;
use super::{v0_4_0_alpha_0, VersionT};
use crate::prelude::*;
lazy_static::lazy_static! {
static ref V0_4_0_alpha_1: exver::Version = exver::Version::new(
[0, 4, 0],
[PreReleaseSegment::String("alpha".into()), 1.into()]
);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Version;
impl VersionT for Version {
type Previous = v0_4_0_alpha_0::Version;
type PreUpRes = ();
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
Ok(())
}
fn semver(self) -> exver::Version {
V0_4_0_alpha_1.clone()
}
fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT
}
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> {
let Some(ui) = db["public"]["ui"].as_object_mut() else {
return Err(Error::new(
eyre!("db.public.ui is not an object"),
ErrorKind::Database,
));
};
ui.insert(
"registries".into(),
Value::Object(
ui.get("marketplace")
.and_then(|m| m.get("knownHosts"))
.and_then(|kh| kh.as_object())
.into_iter()
.flatten()
.map(|(k, v)| (k.clone(), v["name"].clone()))
.collect(),
),
);
if let Some(highscore) = ui
.get_mut("gaming")
.and_then(|g| g.get_mut("snake"))
.and_then(|s| s.get_mut("highScore"))
.map(|hs| hs.take())
.filter(|s| s.is_number())
{
ui.insert("snakeHighScore".into(), highscore);
}
ui.insert(
"startosRegistry".into(),
json!("https://registry.start9.com/"),
);
ui.remove("marketplace");
ui.remove("gaming");
ui.remove("theme");
Ok(())
}
fn down(self, _db: &mut Value) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -1,8 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AddCategoryParams = {
id: string
name: string
short: string
long: string
}
export type AddCategoryParams = { id: string; name: string }

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackageId } from "./PackageId"
export type AddPackageToCategoryParams = { id: string; package: PackageId }

View File

@@ -1,4 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Description } from "./Description"
export type Category = { name: string; description: Description }
export type Category = { name: string }

View File

@@ -1,8 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type GetOsVersionParams = {
source: string | null
target: string | null
sourceVersion: string | null
targetVersion: string | null
includePrerelease: boolean | null
serverId: string | null
arch: string | null
platform: string | null
}

View File

@@ -5,7 +5,7 @@ import type { Version } from "./Version"
export type GetPackageParams = {
id: PackageId | null
version: string | null
targetVersion: string | null
sourceVersion: Version | null
otherVersions: PackageDetailLevel
}

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Guid } from "./Guid"
export type RemoveAdminParams = { signer: Guid }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type RemoveAssetParams = { version: string; platform: string }

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackageId } from "./PackageId"
export type RemovePackageFromCategoryParams = { id: string; package: PackageId }

View File

@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackageId } from "./PackageId"
import type { Version } from "./Version"
export type RemovePackageParams = { id: PackageId; version: Version }

View File

@@ -4,13 +4,13 @@ export { AcmeSettings } from "./AcmeSettings"
export { ActionId } from "./ActionId"
export { ActionInput } from "./ActionInput"
export { ActionMetadata } from "./ActionMetadata"
export { ActionRequest } from "./ActionRequest"
export { ActionRequestCondition } from "./ActionRequestCondition"
export { ActionRequestEntry } from "./ActionRequestEntry"
export { ActionRequestInput } from "./ActionRequestInput"
export { ActionRequestTrigger } from "./ActionRequestTrigger"
export { ActionResult } from "./ActionResult"
export { ActionRequest } from "./ActionRequest"
export { ActionResultMember } from "./ActionResultMember"
export { ActionResult } from "./ActionResult"
export { ActionResultV0 } from "./ActionResultV0"
export { ActionResultV1 } from "./ActionResultV1"
export { ActionResultValue } from "./ActionResultValue"
@@ -20,13 +20,14 @@ export { AddAdminParams } from "./AddAdminParams"
export { AddAssetParams } from "./AddAssetParams"
export { AddCategoryParams } from "./AddCategoryParams"
export { AddPackageParams } from "./AddPackageParams"
export { AddPackageToCategoryParams } from "./AddPackageToCategoryParams"
export { AddressInfo } from "./AddressInfo"
export { AddSslOptions } from "./AddSslOptions"
export { AddVersionParams } from "./AddVersionParams"
export { AddressInfo } from "./AddressInfo"
export { Alerts } from "./Alerts"
export { Algorithm } from "./Algorithm"
export { AllPackageData } from "./AllPackageData"
export { AllowedStatuses } from "./AllowedStatuses"
export { AllPackageData } from "./AllPackageData"
export { AlpnInfo } from "./AlpnInfo"
export { AnySignature } from "./AnySignature"
export { AnySigningKey } from "./AnySigningKey"
@@ -38,9 +39,9 @@ export { BackupTargetFS } from "./BackupTargetFS"
export { Base64 } from "./Base64"
export { BindId } from "./BindId"
export { BindInfo } from "./BindInfo"
export { BindingSetPublicParams } from "./BindingSetPublicParams"
export { BindOptions } from "./BindOptions"
export { BindParams } from "./BindParams"
export { BindingSetPublicParams } from "./BindingSetPublicParams"
export { Blake3Commitment } from "./Blake3Commitment"
export { BlockDev } from "./BlockDev"
export { BuildArg } from "./BuildArg"
@@ -61,11 +62,11 @@ export { CreateSubcontainerFsParams } from "./CreateSubcontainerFsParams"
export { CurrentDependencies } from "./CurrentDependencies"
export { CurrentDependencyInfo } from "./CurrentDependencyInfo"
export { DataUrl } from "./DataUrl"
export { DepInfo } from "./DepInfo"
export { Dependencies } from "./Dependencies"
export { DependencyKind } from "./DependencyKind"
export { DependencyMetadata } from "./DependencyMetadata"
export { DependencyRequirement } from "./DependencyRequirement"
export { DepInfo } from "./DepInfo"
export { Description } from "./Description"
export { DestroySubcontainerFsParams } from "./DestroySubcontainerFsParams"
export { DeviceFilter } from "./DeviceFilter"
@@ -86,8 +87,8 @@ export { GetHostInfoParams } from "./GetHostInfoParams"
export { GetOsAssetParams } from "./GetOsAssetParams"
export { GetOsVersionParams } from "./GetOsVersionParams"
export { GetPackageParams } from "./GetPackageParams"
export { GetPackageResponse } from "./GetPackageResponse"
export { GetPackageResponseFull } from "./GetPackageResponseFull"
export { GetPackageResponse } from "./GetPackageResponse"
export { GetServiceInterfaceParams } from "./GetServiceInterfaceParams"
export { GetServicePortForwardParams } from "./GetServicePortForwardParams"
export { GetSslCertificateParams } from "./GetSslCertificateParams"
@@ -101,21 +102,21 @@ export { Governor } from "./Governor"
export { Guid } from "./Guid"
export { HardwareRequirements } from "./HardwareRequirements"
export { HealthCheckId } from "./HealthCheckId"
export { Host } from "./Host"
export { HostAddress } from "./HostAddress"
export { HostId } from "./HostId"
export { HostnameInfo } from "./HostnameInfo"
export { Hosts } from "./Hosts"
export { Host } from "./Host"
export { ImageConfig } from "./ImageConfig"
export { ImageId } from "./ImageId"
export { ImageMetadata } from "./ImageMetadata"
export { ImageSource } from "./ImageSource"
export { InitProgressRes } from "./InitProgressRes"
export { InstallParams } from "./InstallParams"
export { InstalledState } from "./InstalledState"
export { InstalledVersionParams } from "./InstalledVersionParams"
export { InstallingInfo } from "./InstallingInfo"
export { InstallingState } from "./InstallingState"
export { InstallParams } from "./InstallParams"
export { IpHostname } from "./IpHostname"
export { IpInfo } from "./IpInfo"
export { ListPackageSignersParams } from "./ListPackageSignersParams"
@@ -130,11 +131,11 @@ export { Manifest } from "./Manifest"
export { MaybeUtf8String } from "./MaybeUtf8String"
export { MebiBytes } from "./MebiBytes"
export { MerkleArchiveCommitment } from "./MerkleArchiveCommitment"
export { Metrics } from "./Metrics"
export { MetricsCpu } from "./MetricsCpu"
export { MetricsDisk } from "./MetricsDisk"
export { MetricsGeneral } from "./MetricsGeneral"
export { MetricsMemory } from "./MetricsMemory"
export { Metrics } from "./Metrics"
export { MountParams } from "./MountParams"
export { MountTarget } from "./MountTarget"
export { NamedHealthCheckResult } from "./NamedHealthCheckResult"
@@ -146,14 +147,14 @@ export { NetworkInterfaceSetInboundParams } from "./NetworkInterfaceSetInboundPa
export { NetworkInterfaceType } from "./NetworkInterfaceType"
export { OnionHostname } from "./OnionHostname"
export { OsIndex } from "./OsIndex"
export { OsVersionInfo } from "./OsVersionInfo"
export { OsVersionInfoMap } from "./OsVersionInfoMap"
export { OsVersionInfo } from "./OsVersionInfo"
export { PackageDataEntry } from "./PackageDataEntry"
export { PackageDetailLevel } from "./PackageDetailLevel"
export { PackageId } from "./PackageId"
export { PackageIndex } from "./PackageIndex"
export { PackageInfo } from "./PackageInfo"
export { PackageInfoShort } from "./PackageInfoShort"
export { PackageInfo } from "./PackageInfo"
export { PackageSignerParams } from "./PackageSignerParams"
export { PackageState } from "./PackageState"
export { PackageVersionInfo } from "./PackageVersionInfo"
@@ -166,7 +167,11 @@ export { Public } from "./Public"
export { RecoverySource } from "./RecoverySource"
export { RegistryAsset } from "./RegistryAsset"
export { RegistryInfo } from "./RegistryInfo"
export { RemoveAdminParams } from "./RemoveAdminParams"
export { RemoveAssetParams } from "./RemoveAssetParams"
export { RemoveCategoryParams } from "./RemoveCategoryParams"
export { RemovePackageFromCategoryParams } from "./RemovePackageFromCategoryParams"
export { RemovePackageParams } from "./RemovePackageParams"
export { RemoveVersionParams } from "./RemoveVersionParams"
export { ReplayId } from "./ReplayId"
export { RequestActionParams } from "./RequestActionParams"
@@ -176,18 +181,18 @@ export { Security } from "./Security"
export { ServerInfo } from "./ServerInfo"
export { ServerSpecs } from "./ServerSpecs"
export { ServerStatus } from "./ServerStatus"
export { ServiceInterface } from "./ServiceInterface"
export { ServiceInterfaceId } from "./ServiceInterfaceId"
export { ServiceInterface } from "./ServiceInterface"
export { ServiceInterfaceType } from "./ServiceInterfaceType"
export { Session } from "./Session"
export { SessionList } from "./SessionList"
export { Sessions } from "./Sessions"
export { Session } from "./Session"
export { SetDataVersionParams } from "./SetDataVersionParams"
export { SetDependenciesParams } from "./SetDependenciesParams"
export { SetHealth } from "./SetHealth"
export { SetIconParams } from "./SetIconParams"
export { SetMainStatus } from "./SetMainStatus"
export { SetMainStatusStatus } from "./SetMainStatusStatus"
export { SetMainStatus } from "./SetMainStatus"
export { SetNameParams } from "./SetNameParams"
export { SetStoreParams } from "./SetStoreParams"
export { SetupExecuteParams } from "./SetupExecuteParams"
@@ -202,7 +207,7 @@ export { TestSmtpParams } from "./TestSmtpParams"
export { UnsetInboundParams } from "./UnsetInboundParams"
export { UpdatingState } from "./UpdatingState"
export { VerifyCifsParams } from "./VerifyCifsParams"
export { Version } from "./Version"
export { VersionSignerParams } from "./VersionSignerParams"
export { Version } from "./Version"
export { VolumeId } from "./VolumeId"
export { WifiInfo } from "./WifiInfo"

View File

@@ -82,7 +82,7 @@ import * as actions from "../../base/lib/actions"
import { setupInit } from "./inits/setupInit"
import * as fs from "node:fs/promises"
export const OSVersion = testTypeVersion("0.4.0-alpha.0")
export const OSVersion = testTypeVersion("0.4.0-alpha.1")
// prettier-ignore
type AnyNeverCond<T extends any[], Then, Else> =

View File

@@ -5,11 +5,6 @@
"url": "rpc",
"version": "v1"
},
"marketplace": {
"start9": "https://registry.start9.com/",
"community": "https://community-registry.start9.com/"
},
"startosRegistry": "https://registry.start9.com/",
"mocks": {
"maskAs": "tor",
"maskAsHttps": true,

4
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "startos-ui",
"version": "0.4.0-alpha.0",
"version": "0.4.0-alpha.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "startos-ui",
"version": "0.4.0-alpha.0",
"version": "0.4.0-alpha.1",
"license": "MIT",
"dependencies": {
"@angular/animations": "^17.3.1",

View File

@@ -1,6 +1,6 @@
{
"name": "startos-ui",
"version": "0.4.0-alpha.0",
"version": "0.4.0-alpha.1",
"author": "Start9 Labs, Inc",
"homepage": "https://start9.com/",
"license": "MIT",

View File

@@ -1,21 +1,10 @@
{
"name": null,
"marketplace": {
"selectedUrl": "https://registry.start9.com/",
"knownHosts": {
"https://registry.start9.com/": {
"name": "Start9 Registry"
},
"https://community-registry.start9.com/": {
"name": "Community Registry"
}
}
"registries": {
"https://registry.start9.com/": "Start9 Registry",
"https://community-registry.start9.com/": "Community Registry"
},
"gaming": {
"snake": {
"highScore": 0
}
},
"ackInstructions": {},
"theme": "Dark"
"startosRegisrty": "https://registry.start9.com/",
"snakeHighScore": 0,
"ackInstructions": {}
}

View File

@@ -5,7 +5,6 @@
[style.border-radius.%]="!registry ? 100 : null"
size="60px"
[url]="registry?.url || ''"
[marketplace]="iconConfig"
/>
<h1 [tuiSkeleton]="!registry">
{{ registry?.info?.name || 'Unnamed Registry' }}
@@ -27,7 +26,6 @@
[style.height.px]="42"
[style.border-radius.%]="100"
[url]="registry?.url || ''"
[marketplace]="iconConfig"
[tuiSkeleton]="!registry"
/>
<tui-drawer

View File

@@ -60,7 +60,6 @@ header {
}
store-icon {
border-radius: 100%;
height: 64px;
}

View File

@@ -6,7 +6,6 @@ import {
OnDestroy,
signal,
} from '@angular/core'
import { MarketplaceConfig } from '@start9labs/shared'
import { Subject, takeUntil } from 'rxjs'
import { AbstractCategoryService } from '../../services/category.service'
import { StoreDataWithUrl } from '../../types'
@@ -18,9 +17,6 @@ import { StoreDataWithUrl } from '../../types'
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MenuComponent implements OnDestroy {
@Input({ required: true })
iconConfig!: MarketplaceConfig
@Input({ required: true })
registry!: StoreDataWithUrl | null

View File

@@ -1,13 +1,12 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
import { StoreIconComponentModule } from './store-icon/store-icon.component.module'
import { MarketplaceConfig } from '@start9labs/shared'
@Component({
standalone: true,
selector: '[registry]',
template: `
<store-icon [url]="registry.url" [marketplace]="marketplace" size="40px" />
<store-icon [url]="registry.url" size="40px" />
<div tuiTitle>
{{ registry.name }}
<div tuiSubtitle>{{ registry.url }}</div>
@@ -24,8 +23,5 @@ import { MarketplaceConfig } from '@start9labs/shared'
})
export class MarketplaceRegistryComponent {
@Input()
marketplace!: MarketplaceConfig
@Input()
registry!: { url: string; selected: boolean; name?: string }
registry!: { url: string; selected: boolean; name: string }
}

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplaceConfig, sameUrl } from '@start9labs/shared'
import { knownRegistries, sameUrl } from '@start9labs/shared'
@Component({
selector: 'store-icon',
@@ -9,13 +9,13 @@ import { MarketplaceConfig, sameUrl } from '@start9labs/shared'
[style.border-radius.%]="100"
[style.max-width]="size || '100%'"
[src]="icon"
alt="Marketplace Icon"
alt="Registry Icon"
/>
<ng-template #noIcon>
<img
[style.max-width]="size || '100%'"
src="assets/img/storefront-outline.png"
alt="Marketplace Icon"
alt="Registry Icon"
/>
</ng-template>
`,
@@ -27,17 +27,20 @@ export class StoreIconComponent {
url = ''
@Input()
size?: string
@Input({ required: true })
marketplace!: MarketplaceConfig
get icon() {
const { start9, community } = this.marketplace
const { start9Alpha, start9Beta, start9, community } = knownRegistries
if (sameUrl(this.url, start9)) {
if (sameUrl(this.url, start9Alpha)) {
return 'assets/img/icon_alpha.png'
} else if (sameUrl(this.url, start9Beta)) {
return 'assets/img/icon_beta.png'
} else if (sameUrl(this.url, start9)) {
return 'assets/img/icon_transparent.png'
} else if (sameUrl(this.url, community)) {
return 'assets/img/community-store.png'
return 'assets/img/community-icon.png'
} else {
return 'assets/img/storefront-outline.png'
}
return null
}
}

View File

@@ -39,11 +39,11 @@ export class CategoriesComponent {
readonly categoryChange = new EventEmitter<string>()
readonly fallback: Record<string, T.Category> = {
a: { name: '', description: { short: 'a', long: 'a' } },
b: { name: '', description: { short: 'a', long: 'a' } },
c: { name: '', description: { short: 'a', long: 'a' } },
d: { name: '', description: { short: 'a', long: 'a' } },
e: { name: '', description: { short: 'a', long: 'a' } },
a: { name: '' },
b: { name: '' },
c: { name: '' },
d: { name: '' },
e: { name: '' },
}
switchCategory(category: string): void {

View File

@@ -3,7 +3,7 @@ import { T } from '@start9labs/start-sdk'
export type GetPackageReq = {
id: string
version: string | null
targetVersion: string | null
otherVersions: 'short'
}
export type GetPackageRes = T.GetPackageResponse & {
@@ -12,7 +12,7 @@ export type GetPackageRes = T.GetPackageResponse & {
export type GetPackagesReq = {
id: null
version: null
targetVersion: null
otherVersions: 'short'
}
@@ -22,7 +22,7 @@ export type GetPackagesRes = {
export type StoreIdentity = {
url: string
name?: string
name: string
}
export type Marketplace = Record<string, StoreData | null>

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -7,11 +7,6 @@ export type WorkspaceConfig = {
url: string
version: string
}
marketplace: MarketplaceConfig
startosRegistry:
| 'https://alpha-registry-x.start9.com/'
| 'https://beta-registry.start9.com/'
| 'https://registry.start9.com/'
mocks: {
maskAs: 'tor' | 'local' | 'localhost' | 'ipv4' | 'ipv6' | 'clearnet'
maskAsHttps: boolean
@@ -20,7 +15,13 @@ export type WorkspaceConfig = {
}
}
export interface MarketplaceConfig {
start9: 'https://registry.start9.com/'
community: 'https://community-registry.start9.com/'
}
export const defaultRegistries = {
start9: 'https://registry.start9.com/',
community: 'https://community-registry.start9.com/',
} as const
export const knownRegistries = {
...defaultRegistries,
start9Alpha: 'https://alpha-registry-x.start9.com/',
start9Beta: 'https://beta-registry.start9.com/',
} as const

View File

@@ -127,8 +127,8 @@ export class MarketplaceControlsComponent {
async tryInstall() {
const currentUrl = this.file
? null
: await firstValueFrom(this.marketplaceService.getRegistryUrl$())
const originalUrl = this.localPkg?.registry || ''
: await firstValueFrom(this.marketplaceService.getCurrentRegistryUrl$())
const originalUrl = this.localPkg?.registry || null
if (!this.localPkg) {
if (await this.alerts.alertInstall(this.pkg)) {

View File

@@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { CommonModule } from '@angular/common'
import { MenuModule } from '@start9labs/marketplace'
import { TuiIcon, TuiButton, TuiAppearance } from '@taiga-ui/core'
import { ConfigService } from 'src/app/services/config.service'
import { MARKETPLACE_REGISTRY } from '../modals/registry.component'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { DialogService, i18nPipe } from '@start9labs/shared'
@@ -11,7 +10,7 @@ import { DialogService, i18nPipe } from '@start9labs/shared'
standalone: true,
selector: 'marketplace-menu',
template: `
<menu [iconConfig]="marketplace" [registry]="registry$ | async">
<menu [registry]="registry$ | async">
<button
slot="desktop"
tuiIconButton
@@ -54,9 +53,8 @@ import { DialogService, i18nPipe } from '@start9labs/shared'
})
export class MarketplaceMenuComponent {
private readonly dialog = inject(DialogService)
readonly marketplace = inject(ConfigService).marketplace
private readonly marketplaceService = inject(MarketplaceService)
readonly registry$ = this.marketplaceService.getRegistry$()
readonly registry$ = this.marketplaceService.getCurrentRegistry$()
changeRegistry() {
this.dialog

View File

@@ -1,7 +1,6 @@
import { Component, inject, Input } from '@angular/core'
import { i18nPipe } from '@start9labs/shared'
import { Component, Input } from '@angular/core'
import { i18nPipe, knownRegistries, sameUrl } from '@start9labs/shared'
import { TuiNotification } from '@taiga-ui/core'
import { ConfigService } from 'src/app/services/config.service'
@Component({
standalone: true,
@@ -57,24 +56,24 @@ import { ConfigService } from 'src/app/services/config.service'
imports: [TuiNotification, i18nPipe],
})
export class MarketplaceNotificationComponent {
private readonly marketplace = inject(ConfigService).marketplace
@Input() url = ''
get status() {
if (this.url === this.marketplace.start9) {
const { start9, community, start9Beta, start9Alpha } = knownRegistries
if (sameUrl(this.url, start9)) {
return 'success'
}
if (this.url === this.marketplace.community) {
if (sameUrl(this.url, community)) {
return 'info'
}
if (this.url.includes('beta')) {
if (sameUrl(this.url, start9Beta)) {
return 'warning'
}
if (this.url.includes('alpha')) {
if (sameUrl(this.url, start9Alpha)) {
return 'error'
}

View File

@@ -7,7 +7,7 @@ import {
FilterPackagesPipe,
FilterPackagesPipeModule,
} from '@start9labs/marketplace'
import { i18nPipe } from '@start9labs/shared'
import { defaultRegistries, i18nPipe } from '@start9labs/shared'
import { TuiScrollbar } from '@taiga-ui/core'
import { PatchDB } from 'patch-db-client'
import { tap, withLatestFrom } from 'rxjs'
@@ -17,6 +17,7 @@ import { TitleDirective } from 'src/app/services/title.service'
import { MarketplaceMenuComponent } from './components/menu.component'
import { MarketplaceNotificationComponent } from './components/notification.component'
import { MarketplaceTileComponent } from './components/tile.component'
import { StorageService } from 'src/app/services/storage.service'
@Component({
standalone: true,
@@ -161,16 +162,19 @@ export default class MarketplaceComponent {
private readonly categoryService = inject(AbstractCategoryService)
private readonly marketplaceService = inject(MarketplaceService)
private readonly router = inject(Router)
private readonly patch = inject(PatchDB<DataModel>)
private readonly storage = inject(StorageService)
private readonly route = inject(ActivatedRoute)
.queryParamMap.pipe(
takeUntilDestroyed(),
withLatestFrom(this.patch.watch$('ui', 'marketplace', 'selectedUrl')),
tap(([params, selectedUrl]) => {
tap(params => {
const registry = params.get('registry')
if (!registry) {
this.router.navigate([], {
queryParams: { registry: selectedUrl },
queryParams: {
registry:
this.storage.get('selectedRegistry') ||
defaultRegistries.start9,
},
queryParamsHandling: 'merge',
})
} else {
@@ -180,8 +184,8 @@ export default class MarketplaceComponent {
)
.subscribe()
readonly url$ = this.marketplaceService.getRegistryUrl$()
readonly url$ = this.marketplaceService.getCurrentRegistryUrl$()
readonly category$ = this.categoryService.getCategory$()
readonly query$ = this.categoryService.getQuery$()
readonly registry$ = this.marketplaceService.getRegistry$()
readonly registry$ = this.marketplaceService.getCurrentRegistry$()
}

View File

@@ -194,7 +194,7 @@ export class MarketplacePreviewComponent {
readonly flavors$ = this.flavor$.pipe(
switchMap(current =>
this.marketplaceService.getRegistry$().pipe(
this.marketplaceService.getCurrentRegistry$().pipe(
map(({ packages }) =>
packages.filter(
({ id, flavor }) => id === this.pkgId && flavor !== current,

View File

@@ -14,36 +14,28 @@ import {
sameUrl,
toUrl,
} from '@start9labs/shared'
import {
TuiButton,
TuiDialogContext,
TuiDialogOptions,
TuiIcon,
TuiTitle,
} from '@taiga-ui/core'
import { TuiButton, TuiDialogContext, TuiIcon, TuiTitle } from '@taiga-ui/core'
import { TuiCell } from '@taiga-ui/layout'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { PatchDB } from 'patch-db-client'
import { combineLatest, filter, firstValueFrom, map, Subscription } from 'rxjs'
import { FormComponent } from 'src/app/routes/portal/components/form.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ConfigService } from 'src/app/services/config.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { DataModel, UIStore } from 'src/app/services/patch-db/data-model'
import { TuiConfirmData } from '@taiga-ui/kit'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { IST, utils } from '@start9labs/start-sdk'
import { StorageService } from 'src/app/services/storage.service'
@Component({
standalone: true,
template: `
@if (stores$ | async; as stores) {
@if (registries$ | async; as registries) {
<h3 class="g-title">{{ 'Default Registries' | i18n }}</h3>
@for (registry of stores.standard; track $index) {
@for (registry of registries.standard; track $index) {
<button
tuiCell
[disabled]="registry.selected"
[marketplace]="marketplaceConfig"
[registry]="registry"
(click)="connect(registry.url)"
></button>
@@ -53,19 +45,18 @@ import { IST, utils } from '@start9labs/start-sdk'
<tui-icon icon="@tui.plus" [style.margin-inline.rem]="'0.5'" />
<div tuiTitle>{{ 'Add custom registry' | i18n }}</div>
</button>
@for (registry of stores.alt; track $index) {
@for (registry of registries.alt; track $index) {
<div class="connect-container">
<button
tuiCell
[registry]="registry"
[marketplace]="marketplaceConfig"
(click)="connect(registry.url)"
></button>
<button
tuiIconButton
appearance="icon"
iconStart="@tui.trash-2"
(click)="delete(registry.url, registry.name)"
(click)="delete(registry.url)"
>
{{ 'Delete' | i18n }}
</button>
@@ -103,26 +94,28 @@ export class MarketplaceRegistryModal {
private readonly marketplaceService = inject(MarketplaceService)
private readonly context = injectContext<TuiDialogContext>()
private readonly router = inject(Router)
private readonly hosts$ = inject<PatchDB<DataModel>>(PatchDB).watch$(
private readonly rawRegistries$ = inject<PatchDB<DataModel>>(PatchDB).watch$(
'ui',
'marketplace',
'knownHosts',
'registries',
)
private readonly i18n = inject(i18nPipe)
readonly marketplaceConfig = inject(ConfigService).marketplace
private readonly storage = inject(StorageService)
readonly stores$ = combineLatest([
this.marketplaceService.getKnownHosts$(),
this.marketplaceService.getRegistryUrl$(),
readonly registries$ = combineLatest([
this.marketplaceService.getRegistries$(),
this.marketplaceService.getCurrentRegistryUrl$(),
]).pipe(
map(([stores, selectedUrl]) =>
stores.map(s => ({
map(([registries, currentUrl]) =>
registries.map(s => ({
...s,
selected: sameUrl(s.url, selectedUrl),
selected: sameUrl(s.url, currentUrl),
})),
),
// 0 and 1 are prod and community, 2 and beyond are alts
map(stores => ({ standard: stores.slice(0, 2), alt: stores.slice(2) })),
map(registries => ({
standard: registries.slice(0, 2),
alt: registries.slice(2),
})),
)
add() {
@@ -147,7 +140,7 @@ export class MarketplaceRegistryModal {
})
}
delete(url: string, name: string = '') {
delete(url: string) {
this.dialog
.openConfirm({
label: 'Confirm',
@@ -161,19 +154,21 @@ export class MarketplaceRegistryModal {
.pipe(filter(Boolean))
.subscribe(async () => {
const loader = this.loader.open('Deleting').subscribe()
const hosts = await firstValueFrom(this.hosts$)
const filtered: { [url: string]: UIStore } = Object.keys(hosts)
const rawRegistries = await firstValueFrom(this.rawRegistries$)
const filtered: { [url: string]: string | null } = Object.keys(
rawRegistries,
)
.filter(key => !sameUrl(key, url))
.reduce(
(prev, curr) => ({
...prev,
[curr]: hosts[curr],
[curr]: rawRegistries[curr],
}),
{},
)
try {
await this.api.setDbValue(['marketplace', 'knownHosts'], filtered)
await this.api.setDbValue(['registries'], filtered)
} catch (e: any) {
this.errorService.handleError(e)
} finally {
@@ -195,7 +190,7 @@ export class MarketplaceRegistryModal {
queryParams: { registry: url },
queryParamsHandling: 'merge',
})
this.api.setDbValue<string>(['marketplace', 'selectedUrl'], url)
this.storage.set('selectedRegistry', url)
this.context.$implicit.complete()
} catch (e: any) {
this.errorService.handleError(e)
@@ -255,8 +250,8 @@ export class MarketplaceRegistryModal {
loader: Subscription,
): Promise<void> {
// Error on duplicates
const hosts = await firstValueFrom(this.hosts$)
const currentUrls = Object.keys(hosts).map(toUrl)
const rawRegistries = await firstValueFrom(this.rawRegistries$)
const currentUrls = Object.keys(rawRegistries).map(toUrl)
if (currentUrls.includes(url))
throw new Error(this.i18n.transform('Registry already added'))
@@ -274,7 +269,7 @@ export class MarketplaceRegistryModal {
loader.closed = false
loader.add(this.loader.open('Saving').subscribe())
await this.api.setDbValue(['marketplace', 'knownHosts', url], { name })
await this.api.setDbValue(['altRegistries', url], name)
}
}

View File

@@ -1,29 +1,30 @@
import { inject, Injectable } from '@angular/core'
import { MarketplacePkgBase } from '@start9labs/marketplace'
import { PatchDB } from 'patch-db-client'
import { defaultIfEmpty, firstValueFrom } from 'rxjs'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared'
import { MarketplaceService } from 'src/app/services/marketplace.service'
@Injectable({
providedIn: 'root',
})
export class MarketplaceAlertsService {
private readonly dialog = inject(DialogService)
private readonly marketplace$ = inject<PatchDB<DataModel>>(PatchDB).watch$(
'ui',
'marketplace',
)
private readonly marketplaceService = inject(MarketplaceService)
private readonly i18n = inject(i18nPipe)
async alertMarketplace(url: string, originalUrl: string): Promise<boolean> {
const marketplaces = await firstValueFrom(this.marketplace$)
const name = marketplaces.knownHosts[url]?.name || url
const source = marketplaces.knownHosts[originalUrl]?.name || originalUrl
const message = source
? `${this.i18n.transform('installed from')} ${source}`
async alertMarketplace(
url: string,
originalUrl: string | null,
): Promise<boolean> {
const registries = await firstValueFrom(
this.marketplaceService.getRegistries$(),
)
const message = originalUrl
? `${this.i18n.transform('installed from')} ${registries.find(h => h.url === originalUrl) || originalUrl}`
: this.i18n.transform('sideloaded')
const currentName = registries.find(h => h.url === url) || url
return new Promise(async resolve => {
this.dialog
.openConfirm<boolean>({
@@ -31,7 +32,7 @@ export class MarketplaceAlertsService {
size: 's',
data: {
content:
`${this.i18n.transform('This service was originally')} ${message}, ${this.i18n.transform('but you are currently connected to')} ${name}. ${this.i18n.transform('To install from')} ${name} ${this.i18n.transform('anyway, click "Continue".')}` as i18nKey,
`${this.i18n.transform('This service was originally')} ${message}, ${this.i18n.transform('but you are currently connected to')} ${currentName}. ${this.i18n.transform('To install from')} ${currentName} ${this.i18n.transform('anyway, click "Continue".')}` as i18nKey,
yes: 'Continue',
no: 'Cancel',
},

View File

@@ -205,7 +205,6 @@ export default class SystemAcmeComponent {
}
private async saveAcme(providerUrl: string, contact: string[]) {
console.log(providerUrl, contact)
const loader = this.loader.open('Saving').subscribe()
try {

View File

@@ -242,10 +242,9 @@ export default class SystemGeneralComponent {
readonly translation: TuiStringHandler<TuiContext<Languages>> = ({
$implicit,
}) => this.i18n.transform($implicit)!
readonly score = toSignal(
this.patch.watch$('ui', 'gaming', 'snake', 'highScore'),
{ initialValue: 0 },
)
readonly score = toSignal(this.patch.watch$('ui', 'snakeHighScore'), {
initialValue: 0,
})
get language(): Languages | undefined {
return this.languages.find(lang => lang === this.i18nService.language)

View File

@@ -37,10 +37,7 @@ export class SnekDirective {
const loader = this.loader.open('Saving high score').subscribe()
try {
await this.api.setDbValue<number>(
['gaming', 'snake', 'highScore'],
score,
)
await this.api.setDbValue<number>(['snakeHighScore'], score)
} catch (e: any) {
this.errorService.handleError(e)
} finally {

View File

@@ -16,6 +16,9 @@ import { TuiDialogContext, TuiScrollbar, TuiButton } from '@taiga-ui/core'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { OSService } from 'src/app/services/os.service'
import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { firstValueFrom } from 'rxjs'
@Component({
template: `
@@ -61,13 +64,19 @@ export class SystemUpdateModal {
private readonly errorService: ErrorService,
private readonly embassyApi: ApiService,
private readonly os: OSService,
private readonly patch: PatchDB<DataModel>,
) {}
async update() {
const loader = this.loader.open('Beginning update').subscribe()
const { startosRegistry } = await firstValueFrom(this.patch.watch$('ui'))
try {
await this.embassyApi.updateServer()
await this.embassyApi.updateServer({
targetVersion: `=${this.versions[0]!.version}`,
registry: startosRegistry,
})
this.context.$implicit.complete()
} catch (e: any) {
this.errorService.handleError(e)

View File

@@ -22,7 +22,6 @@ import { TuiCell } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client'
import { combineLatest, map, tap } from 'rxjs'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { ConfigService } from 'src/app/services/config.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import {
DataModel,
@@ -66,7 +65,7 @@ interface UpdatesData {
(click)="current.set(registry)"
>
<tui-avatar>
<store-icon [url]="registry.url" [marketplace]="mp" />
<store-icon [url]="registry.url" />
</tui-avatar>
<span tuiTitle>
<b tuiFade>{{ registry.name }}</b>
@@ -221,15 +220,17 @@ export default class UpdatesComponent {
private readonly isMobile = inject(TUI_IS_MOBILE)
private readonly marketplaceService = inject(MarketplaceService)
readonly mp = inject(ConfigService).marketplace
readonly current = signal<StoreIdentity | null>(null)
readonly data = toSignal<UpdatesData>(
combineLatest({
hosts: this.marketplaceService
.getKnownHosts$(true)
.getRegistries$(true)
.pipe(
tap(([store]) => !this.isMobile && store && this.current.set(store)),
tap(
([registry]) =>
!this.isMobile && registry && this.current.set(registry),
),
),
marketplace: this.marketplaceService.marketplace$,
localPkgs: inject<PatchDB<DataModel>>(PatchDB)

View File

@@ -30,7 +30,7 @@ export namespace Mock {
shuttingDown: false,
}
export const RegistryOSUpdate: RR.GetRegistryOsUpdateRes = {
export const RegistryOSUpdate: RR.CheckOsUpdateRes = {
'0.3.6-alpha.17': {
headline: 'v0.3.6-alpha.17',
releaseNotes: '',
@@ -101,16 +101,16 @@ export namespace Mock {
},
img: {},
},
'0.4.0-alpha.0': {
headline: 'v0.4.0-alpha.0',
'0.4.0-alpha.1': {
headline: 'v0.4.0-alpha.1',
releaseNotes: '',
sourceVersion: '>=0.3.5:0 <=0.4.0-alpha.0:0',
sourceVersion: '>=0.3.5:0 <=0.4.0-alpha.1:0',
authorized: ['G24CSA5HNYEPIXJNMK7ZM4KD5SX5N6X4'],
iso: {},
squashfs: {
aarch64: {
publishedAt: '2025-04-21T20:58:48.140749883Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.0/startos-0.4.0-alpha.0-33ae46f~dev_aarch64.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.1/startos-0.4.0-alpha.1-33ae46f~dev_aarch64.squashfs',
commitment: {
hash: '4elBFVkd/r8hNadKmKtLIs42CoPltMvKe2z3LRqkphk=',
size: 1343500288,
@@ -122,7 +122,7 @@ export namespace Mock {
},
'aarch64-nonfree': {
publishedAt: '2025-04-21T21:07:00.249285116Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.0/startos-0.4.0-alpha.0-33ae46f~dev_aarch64-nonfree.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.1/startos-0.4.0-alpha.1-33ae46f~dev_aarch64-nonfree.squashfs',
commitment: {
hash: 'MrCEi4jxbmPS7zAiGk/JSKlMsiuKqQy6RbYOxlGHOIQ=',
size: 1653075968,
@@ -134,7 +134,7 @@ export namespace Mock {
},
raspberrypi: {
publishedAt: '2025-04-21T21:16:12.933319237Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.0/startos-0.4.0-alpha.0-33ae46f~dev_raspberrypi.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.1/startos-0.4.0-alpha.1-33ae46f~dev_raspberrypi.squashfs',
commitment: {
hash: '/XTVQRCqY3RK544PgitlKu7UplXjkmzWoXUh2E4HCw0=',
size: 1490731008,
@@ -146,7 +146,7 @@ export namespace Mock {
},
x86_64: {
publishedAt: '2025-04-21T21:14:20.246908903Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.0/startos-0.4.0-alpha.0-33ae46f~dev_x86_64.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.1/startos-0.4.0-alpha.1-33ae46f~dev_x86_64.squashfs',
commitment: {
hash: '/6romKTVQGSaOU7FqSZdw0kFyd7P+NBSYNwM3q7Fe44=',
size: 1411657728,
@@ -158,7 +158,7 @@ export namespace Mock {
},
'x86_64-nonfree': {
publishedAt: '2025-04-21T21:15:17.955265284Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.0/startos-0.4.0-alpha.0-33ae46f~dev_x86_64-nonfree.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.1/startos-0.4.0-alpha.1-33ae46f~dev_x86_64-nonfree.squashfs',
commitment: {
hash: 'HCRq9sr/0t85pMdrEgNBeM4x11zVKHszGnD1GDyZbSE=',
size: 1731035136,
@@ -179,27 +179,21 @@ export namespace Mock {
categories: {
bitcoin: {
name: 'Bitcoin',
description: mockDescription,
},
featured: {
name: 'Featured',
description: mockDescription,
},
lightning: {
name: 'Lightning',
description: mockDescription,
},
communications: {
name: 'Communications',
description: mockDescription,
},
data: {
name: 'Data',
description: mockDescription,
},
ai: {
name: 'AI',
description: mockDescription,
},
},
}

View File

@@ -3,6 +3,12 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
import { StartOSDiskInfo, FetchLogsReq, FetchLogsRes } from '@start9labs/shared'
import { IST, T } from '@start9labs/start-sdk'
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
import {
GetPackageReq,
GetPackageRes,
GetPackagesReq,
GetPackagesRes,
} from '@start9labs/marketplace'
export namespace RR {
// websocket
@@ -86,7 +92,7 @@ export namespace RR {
metrics: ServerMetrics
}
export type UpdateServerReq = { registry: string } // server.update
export type UpdateServerReq = { registry: string; targetVersion: string } // server.update
export type UpdateServerRes = 'updating' | 'no-updates'
export type RestartServerReq = {} // server.restart
@@ -362,8 +368,17 @@ export namespace RR {
// registry
/** these are returned in ASCENDING order. the newest available version will be the LAST in the object */
export type CheckOSUpdateReq = { serverId: string }
export type GetRegistryOsUpdateRes = { [version: string]: T.OsVersionInfo }
export type CheckOsUpdateReq = { registry: string; serverId: string }
export type CheckOsUpdateRes = { [version: string]: T.OsVersionInfo }
export type GetRegistryInfoReq = { registry: string }
export type GetRegistryInfoRes = T.RegistryInfo
export type GetRegistryPackageReq = GetPackageReq & { registry: string }
export type GetRegistryPackageRes = GetPackageRes
export type GetRegistryPackagesReq = GetPackagesReq & { registry: string }
export type GetRegistryPackagesRes = GetPackagesRes
}
export type Breakages = {

View File

@@ -1,9 +1,4 @@
import {
GetPackageRes,
GetPackagesRes,
MarketplacePkg,
} from '@start9labs/marketplace'
import { RPCOptions } from '@start9labs/shared'
import { MarketplacePkg } from '@start9labs/marketplace'
import { T } from '@start9labs/start-sdk'
import { RR } from './api.types'
import { WebSocketSubject } from 'rxjs/webSocket'
@@ -113,7 +108,7 @@ export abstract class ApiService {
params: RR.FollowServerMetricsReq,
): Promise<RR.FollowServerMetricsRes>
abstract updateServer(url?: string): Promise<RR.UpdateServerRes>
abstract updateServer(params: RR.UpdateServerReq): Promise<RR.UpdateServerRes>
abstract restartServer(
params: RR.RestartServerReq,
@@ -145,24 +140,21 @@ export abstract class ApiService {
// marketplace URLs
abstract registryRequest<T>(
registryUrl: string,
options: RPCOptions,
): Promise<T>
abstract checkOSUpdate(
qp: RR.CheckOSUpdateReq,
): Promise<RR.GetRegistryOsUpdateRes>
params: RR.CheckOsUpdateReq,
): Promise<RR.CheckOsUpdateRes>
abstract getRegistryInfo(registryUrl: string): Promise<T.RegistryInfo>
abstract getRegistryInfo(
params: RR.GetRegistryInfoReq,
): Promise<RR.GetRegistryInfoRes>
abstract getRegistryPackage(
url: string,
id: string,
versionRange: string | null,
): Promise<GetPackageRes>
params: RR.GetRegistryPackageReq,
): Promise<RR.GetRegistryPackageRes>
abstract getRegistryPackages(registryUrl: string): Promise<GetPackagesRes>
abstract getRegistryPackages(
params: RR.GetRegistryPackagesReq,
): Promise<RR.GetRegistryPackagesRes>
// notification

View File

@@ -10,7 +10,6 @@ import {
import { PATCH_CACHE } from 'src/app/services/patch-db/patch-db-source'
import { ApiService } from './embassy-api.service'
import { RR } from './api.types'
import { ConfigService } from '../config.service'
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'
import { Observable, filter, firstValueFrom } from 'rxjs'
import { AuthService } from '../auth.service'
@@ -18,13 +17,7 @@ import { DOCUMENT } from '@angular/common'
import { DataModel } from '../patch-db/data-model'
import { Dump, pathFromArray } from 'patch-db-client'
import { T } from '@start9labs/start-sdk'
import {
GetPackageReq,
GetPackageRes,
GetPackagesReq,
GetPackagesRes,
MarketplacePkg,
} from '@start9labs/marketplace'
import { MarketplacePkg } from '@start9labs/marketplace'
import { blake3 } from '@noble/hashes/blake3'
@Injectable()
@@ -32,7 +25,6 @@ export class LiveApiService extends ApiService {
constructor(
@Inject(DOCUMENT) private readonly document: Document,
private readonly http: HttpService,
private readonly config: ConfigService,
private readonly auth: AuthService,
@Inject(PATCH_CACHE) private readonly cache$: Observable<Dump<DataModel>>,
) {
@@ -248,10 +240,7 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'server.metrics.follow', params })
}
async updateServer(url?: string): Promise<RR.UpdateServerRes> {
const params = {
registry: url || this.config.startosRegistry,
}
async updateServer(params: RR.UpdateServerReq): Promise<RR.UpdateServerRes> {
return this.rpcRequest({ method: 'server.update', params })
}
@@ -283,61 +272,38 @@ export class LiveApiService extends ApiService {
// marketplace URLs
async registryRequest<T>(
registryUrl: string,
options: RPCOptions,
): Promise<T> {
return this.rpcRequest({
...options,
method: `registry.${options.method}`,
params: { registry: registryUrl, ...options.params },
})
}
async checkOSUpdate(
qp: RR.CheckOSUpdateReq,
): Promise<RR.GetRegistryOsUpdateRes> {
const { serverId } = qp
return this.registryRequest(this.config.startosRegistry, {
method: 'os.version.get',
params: { serverId },
})
}
async getRegistryInfo(registryUrl: string): Promise<T.RegistryInfo> {
return this.registryRequest(registryUrl, {
method: 'info',
params: {},
})
}
async getRegistryPackage(
registryUrl: string,
id: string,
versionRange: string | null,
): Promise<GetPackageRes> {
const params: GetPackageReq = {
id,
version: versionRange,
otherVersions: 'short',
}
return this.registryRequest<GetPackageRes>(registryUrl, {
method: 'package.get',
params: RR.CheckOsUpdateReq,
): Promise<RR.CheckOsUpdateRes> {
return this.rpcRequest({
method: 'registry.os.version.get',
params,
})
}
async getRegistryPackages(registryUrl: string): Promise<GetPackagesRes> {
const params: GetPackagesReq = {
id: null,
version: null,
otherVersions: 'short',
}
async getRegistryInfo(
params: RR.GetRegistryInfoReq,
): Promise<RR.GetRegistryInfoRes> {
return this.rpcRequest({
method: 'registry.info',
params,
})
}
return this.registryRequest<GetPackagesRes>(registryUrl, {
method: 'package.get',
async getRegistryPackage(
params: RR.GetRegistryPackageReq,
): Promise<RR.GetRegistryPackageRes> {
return this.rpcRequest({
method: 'registry.package.get',
params,
})
}
async getRegistryPackages(
params: RR.GetRegistryPackagesReq,
): Promise<RR.GetRegistryPackagesRes> {
return this.rpcRequest({
method: 'registry.package.get',
params,
})
}

View File

@@ -169,6 +169,7 @@ export class MockApiService extends ApiService {
pathArr: Array<string | number>,
value: T,
): Promise<RR.SetDBValueRes> {
console.warn(pathArr, value)
const pointer = pathFromArray(pathArr)
const params: RR.SetDBValueReq<T> = { pointer, value }
await pauseFor(2000)
@@ -367,7 +368,7 @@ export class MockApiService extends ApiService {
}
}
async updateServer(url?: string): Promise<RR.UpdateServerRes> {
async updateServer(params?: RR.UpdateServerReq): Promise<RR.UpdateServerRes> {
await pauseFor(2000)
const initialProgress = {
size: null,
@@ -475,41 +476,37 @@ export class MockApiService extends ApiService {
// marketplace URLs
async registryRequest(
registryUrl: string,
options: RPCOptions,
): Promise<any> {
await pauseFor(2000)
return Error('do not call directly')
}
async checkOSUpdate(
qp: RR.CheckOSUpdateReq,
): Promise<RR.GetRegistryOsUpdateRes> {
params: RR.CheckOsUpdateReq,
): Promise<RR.CheckOsUpdateRes> {
await pauseFor(2000)
return Mock.RegistryOSUpdate
}
async getRegistryInfo(registryUrl: string): Promise<T.RegistryInfo> {
async getRegistryInfo(
params: RR.GetRegistryInfoReq,
): Promise<RR.GetRegistryInfoRes> {
await pauseFor(2000)
return Mock.RegistryInfo
}
async getRegistryPackage(
url: string,
id: string,
versionRange: string,
): Promise<GetPackageRes> {
params: RR.GetRegistryPackageReq,
): Promise<RR.GetRegistryPackageRes> {
await pauseFor(2000)
if (!versionRange || versionRange === '=*') {
const { targetVersion, id } = params
if (!targetVersion || targetVersion === '=*') {
return Mock.RegistryPackages[id]!
} else {
return Mock.OtherPackageVersions[id]![versionRange]!
return Mock.OtherPackageVersions[id]![targetVersion]!
}
}
async getRegistryPackages(registryUrl: string): Promise<GetPackagesRes> {
async getRegistryPackages(
params: RR.GetRegistryPackagesReq,
): Promise<RR.GetRegistryPackagesRes> {
await pauseFor(2000)
return Mock.RegistryPackages
}

View File

@@ -6,26 +6,14 @@ const version = require('../../../../../../package.json').version
export const mockPatchData: DataModel = {
ui: {
name: `Matt's Server`,
theme: 'Dark',
marketplace: {
selectedUrl: 'https://registry.start9.com/',
knownHosts: {
'https://registry.start9.com/': {
name: 'Start9 Registry',
},
'https://community-registry.start9.com/': {},
'https://beta-registry.start9.com/': {
name: 'Dark9',
},
},
registries: {
'https://registry.start9.com/': 'Start9 Registry',
'https://community-registry.start9.com/': 'Community Registry',
},
gaming: {
snake: {
highScore: 0,
},
},
language: 'english',
startosRegistry: 'https://registry.start9.com/',
snakeHighScore: 0,
ackInstructions: {},
language: 'english',
},
serverInfo: {
arch: 'x86_64',

View File

@@ -7,7 +7,7 @@ import { PackageDataEntry } from './patch-db/data-model'
const {
gitHash,
useMocks,
ui: { api, marketplace, mocks, startosRegistry },
ui: { api, mocks },
} = require('../../../../../config.json') as WorkspaceConfig
@Injectable({
@@ -26,8 +26,6 @@ export class ConfigService {
mocks = mocks
gitHash = gitHash
api = api
marketplace = marketplace
startosRegistry = startosRegistry
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
supportsWebSockets = !!window.WebSocket

View File

@@ -7,7 +7,7 @@ import {
StoreDataWithUrl,
StoreIdentity,
} from '@start9labs/marketplace'
import { Exver, sameUrl } from '@start9labs/shared'
import { Exver, defaultRegistries, sameUrl } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { PatchDB } from 'patch-db-client'
import {
@@ -31,32 +31,28 @@ import {
} from 'rxjs'
import { RR } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DataModel, UIStore } from 'src/app/services/patch-db/data-model'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { ClientStorageService } from './client-storage.service'
import { ConfigService } from './config.service'
const { start9, community } = defaultRegistries
@Injectable({
providedIn: 'root',
})
export class MarketplaceService {
private readonly registryUrlSubject$ = new ReplaySubject<string>(1)
private readonly registryUrl$ = this.registryUrlSubject$.pipe(
private readonly currentRegistryUrlSubject$ = new ReplaySubject<string>(1)
private readonly currentRegistryUrl$ = this.currentRegistryUrlSubject$.pipe(
distinctUntilChanged(),
)
private readonly registry$: Observable<StoreDataWithUrl> =
this.registryUrl$.pipe(
private readonly currentRegistry$: Observable<StoreDataWithUrl> =
this.currentRegistryUrl$.pipe(
switchMap(url => this.fetchRegistry$(url)),
filter(Boolean),
map(registry => {
// @TODO Aiden let's drop description. We do not use it. categories should just be Record<string, string>
registry.info.categories = {
all: {
name: 'All',
description: {
short: 'All registry packages',
long: 'An unfiltered list of all packages available on this registry.',
},
},
...registry.info.categories,
}
@@ -66,36 +62,27 @@ export class MarketplaceService {
shareReplay(1),
)
private readonly knownHosts$: Observable<StoreIdentity[]> = this.patch
.watch$('ui', 'marketplace', 'knownHosts')
private readonly registries$: Observable<StoreIdentity[]> = this.patch
.watch$('ui', 'registries')
.pipe(
map(hosts => {
const { start9, community } = this.config.marketplace
let arr = [
toStoreIdentity(start9, hosts[start9]),
toStoreIdentity(community, {
...hosts[community],
name: 'Community Registry',
}),
]
return arr.concat(
Object.entries(hosts)
.filter(([url, _]) => ![start9, community].includes(url as any))
.map(([url, store]) => toStoreIdentity(url, store)),
)
}),
map(registries => [
toStoreIdentity(start9, registries[start9]),
toStoreIdentity(community, registries[community]),
...Object.entries(registries)
.filter(([url, _]) => ![start9, community].includes(url as any))
.map(([url, name]) => toStoreIdentity(url, name)),
]),
)
private readonly filteredKnownHosts$: Observable<StoreIdentity[]> =
private readonly filteredRegistries$: Observable<StoreIdentity[]> =
combineLatest([
this.clientStorageService.showDevTools$,
this.knownHosts$,
this.registries$,
]).pipe(
map(([devMode, knownHosts]) =>
map(([devMode, registries]) =>
devMode
? knownHosts
: knownHosts.filter(
? registries
: registries.filter(
({ url }) => !url.includes('alpha') && !url.includes('beta'),
),
),
@@ -103,7 +90,7 @@ export class MarketplaceService {
private readonly requestErrors$ = new BehaviorSubject<string[]>([])
readonly marketplace$: Observable<Marketplace> = this.knownHosts$.pipe(
readonly marketplace$: Observable<Marketplace> = this.registries$.pipe(
startWith<StoreIdentity[]>([]),
pairwise(),
mergeMap(([prev, curr]) =>
@@ -112,7 +99,8 @@ export class MarketplaceService {
mergeMap(({ url, name }) =>
this.fetchRegistry$(url).pipe(
tap(data => {
if (data?.info.name) this.updateStoreName(url, name, data.info.name)
if (data?.info.name)
this.updateRegistryName(url, name, data.info.name)
}),
map<StoreData | null, [string, StoreData | null]>(data => [url, data]),
startWith<[string, StoreData | null]>([url, null]),
@@ -132,27 +120,25 @@ export class MarketplaceService {
constructor(
private readonly api: ApiService,
private readonly patch: PatchDB<DataModel>,
private readonly config: ConfigService,
private readonly clientStorageService: ClientStorageService,
private readonly exver: Exver,
) {}
getKnownHosts$(filtered = false): Observable<StoreIdentity[]> {
getRegistries$(filtered = false): Observable<StoreIdentity[]> {
// option to filter out hosts containing 'alpha' or 'beta' substrings in registryURL
return filtered ? this.filteredKnownHosts$ : this.knownHosts$
return filtered ? this.filteredRegistries$ : this.registries$
}
getRegistryUrl$() {
return this.registryUrl$
getCurrentRegistryUrl$() {
return this.currentRegistryUrl$
}
setRegistryUrl(url: string | null) {
const registryUrl = url || this.config.marketplace.start9
this.registryUrlSubject$.next(registryUrl)
setRegistryUrl(url: string) {
this.currentRegistryUrlSubject$.next(url)
}
getRegistry$(): Observable<StoreDataWithUrl> {
return this.registry$
getCurrentRegistry$(): Observable<StoreDataWithUrl> {
return this.currentRegistry$
}
getPackage$(
@@ -161,7 +147,7 @@ export class MarketplaceService {
flavor: string | null,
registryUrl?: string,
): Observable<MarketplacePkg> {
return this.registry$.pipe(
return this.currentRegistry$.pipe(
switchMap(registry => {
const url = registryUrl || registry.url
const pkg = registry.packages.find(
@@ -176,17 +162,12 @@ export class MarketplaceService {
}
fetchInfo$(url: string): Observable<T.RegistryInfo> {
return from(this.api.getRegistryInfo(url)).pipe(
return from(this.api.getRegistryInfo({ registry: url })).pipe(
map(info => ({
...info,
// @TODO Aiden let's drop description. We do not use it. categories should just be Record<string, string>
categories: {
all: {
name: 'All',
description: {
short: '',
long: '',
},
},
...info.categories,
},
@@ -202,7 +183,7 @@ export class MarketplaceService {
}
private fetchRegistry$(url: string): Observable<StoreDataWithUrl | null> {
console.log('FETCHING REGISTRY: ', url)
console.warn('FETCHING REGISTRY: ', url)
return combineLatest([this.fetchInfo$(url), this.fetchPackages$(url)]).pipe(
map(([info, packages]) => ({ info, packages, url })),
catchError(e => {
@@ -214,7 +195,14 @@ export class MarketplaceService {
}
private fetchPackages$(url: string): Observable<MarketplacePkg[]> {
return from(this.api.getRegistryPackages(url)).pipe(
return from(
this.api.getRegistryPackages({
registry: url,
id: null,
targetVersion: null,
otherVersions: 'short',
}),
).pipe(
map(packages => {
return Object.entries(packages).flatMap(([id, pkgInfo]) =>
Object.keys(pkgInfo.best).map(version =>
@@ -237,7 +225,12 @@ export class MarketplaceService {
flavor: string | null,
): Observable<MarketplacePkg> {
return from(
this.api.getRegistryPackage(url, id, version ? `=${version}` : null),
this.api.getRegistryPackage({
registry: url,
id,
targetVersion: version ? `=${version}` : null,
otherVersions: 'short',
}),
).pipe(
map(pkgInfo =>
this.convertRegistryPkgToMarketplacePkg(id, version, flavor, pkgInfo),
@@ -288,23 +281,21 @@ export class MarketplaceService {
await this.api.installPackage(params)
}
private async updateStoreName(
private async updateRegistryName(
url: string,
oldName: string | undefined,
oldName: string | null,
newName: string,
): Promise<void> {
console.warn(oldName, newName)
if (oldName !== newName) {
this.api.setDbValue<string>(
['marketplace', 'knownHosts', url, 'name'],
newName,
)
this.api.setDbValue<string>(['registries', url], newName)
}
}
}
function toStoreIdentity(url: string, uiStore: UIStore): StoreIdentity {
function toStoreIdentity(url: string, name?: string | null): StoreIdentity {
return {
url,
...uiStore,
name: name || url,
}
}

View File

@@ -1,6 +1,12 @@
import { Injectable } from '@angular/core'
import { PatchDB } from 'patch-db-client'
import { BehaviorSubject, distinctUntilChanged, map, combineLatest } from 'rxjs'
import {
BehaviorSubject,
distinctUntilChanged,
map,
combineLatest,
firstValueFrom,
} from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { getServerInfo } from 'src/app/utils/get-server-info'
import { DataModel } from './patch-db/data-model'
@@ -11,7 +17,7 @@ import { RR } from './api/api.types'
providedIn: 'root',
})
export class OSService {
osUpdate?: RR.GetRegistryOsUpdateRes
osUpdate?: RR.CheckOsUpdateRes
updateAvailable$ = new BehaviorSubject<boolean>(false)
readonly updating$ = this.patch.watch$('serverInfo', 'statusInfo').pipe(
@@ -47,7 +53,12 @@ export class OSService {
async loadOS(): Promise<void> {
const { version, id } = await getServerInfo(this.patch)
this.osUpdate = await this.api.checkOSUpdate({ serverId: id })
const { startosRegistry } = await firstValueFrom(this.patch.watch$('ui'))
this.osUpdate = await this.api.checkOSUpdate({
registry: startosRegistry,
serverId: id,
})
const [latestVersion, _] = Object.entries(this.osUpdate).at(-1)!
const updateAvailable =
Version.parse(latestVersion).compare(Version.parse(version)) === 'greater'

View File

@@ -5,30 +5,13 @@ export type DataModel = T.Public & { ui: UIData; packageData: AllPackageData }
export type UIData = {
name: string | null
marketplace: UIMarketplaceData
gaming: {
snake: {
highScore: number
}
}
registries: Record<string, string | null>
ackInstructions: Record<string, boolean>
theme: string
snakeHighScore: number
startosRegistry: string
language: Languages
}
export type UIMarketplaceData = {
selectedUrl: string
knownHosts: {
'https://registry.start9.com/': UIStore
'https://community-registry.start9.com/': UIStore
[url: string]: UIStore
}
}
export type UIStore = {
name?: string
}
export type NetworkInfo = T.NetworkInfo & {
// @TODO 041
// start9To: {