From a535fc17c320bfa778315eabbe670ee4c0a8f72e Mon Sep 17 00:00:00 2001 From: Lucy <12953208+elvece@users.noreply.github.com> Date: Mon, 22 Jul 2024 20:48:12 -0400 Subject: [PATCH] Feature/fe new registry (#2647) * bugfixes * update fe types * implement new registry types in marketplace and ui * fix marketplace types to have default params * add alt implementation toggle * merge cleanup * more cleanup and notes * fix build * cleanup sync with next/minor * add exver JS parser * parse ValidExVer to string * update types to interface * add VersionRange and comparative functions * Parse ExtendedVersion from string * add conjunction, disjunction, and inversion logic * consider flavor in satisfiedBy fn * consider prerelease for ordering * add compare fn for sorting * rename fns for consistency * refactoring * update compare fn to return null if flavors don't match * begin simplifying dependencies * under construction * wip * add dependency metadata to CurrentDependencyInfo * ditch inheritance for recursive VersionRange constructor. Recursive 'satisfiedBy' fn wip * preprocess manifest * misc fixes * use sdk version as osVersion in manifest * chore: Change the type to just validate and not generate all solutions. * add publishedAt * fix pegjs exports * integrate exver into sdk * misc fixes * complete satisfiedBy fn * refactor - use greaterThanOrEqual and lessThanOrEqual fns * fix tests * update dependency details * update types * remove interim types * rename alt implementation to flavor * cleanup os update * format exver.ts * add s9pk parsing endpoints * fix build * update to exver * exver and bug fixes * update static endpoints + cleanup * cleanup * update static proxy verification * make mocks more robust; fix dep icon fallback; cleanup * refactor alert versions and update fixtures * registry bugfixes * misc fixes * cleanup unused * convert patchdb ui seed to camelCase * update otherVersions type * change otherVersions: null to 'none' * refactor and complete feature * improve static endpoints * fix install params * mask systemd-networkd-wait-online * fix static file fetching * include non-matching versions in otherVersions * convert release notes to modal and clean up displayExver * alert for no other versions * Fix ack-instructions casing * fix indeterminate loader on service install --------- Co-authored-by: Aiden McClelland Co-authored-by: Shadowy Super Coder Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: J H Co-authored-by: Matt Hill --- .../Systems/SystemForEmbassy/index.ts | 20 +- core/Cargo.lock | 51 +- core/startos/Cargo.toml | 2 + core/startos/src/backup/target/mod.rs | 3 +- core/startos/src/bins/start_init.rs | 85 +- core/startos/src/context/init.rs | 3 + core/startos/src/db/model/package.rs | 9 +- core/startos/src/dependencies.rs | 13 + core/startos/src/init.rs | 55 +- core/startos/src/install/mod.rs | 1 + core/startos/src/middleware/db.rs | 1 - core/startos/src/net/static_server.rs | 459 ++- core/startos/src/registry/admin.rs | 25 +- core/startos/src/registry/asset.rs | 3 + core/startos/src/registry/auth.rs | 12 +- core/startos/src/registry/context.rs | 30 +- core/startos/src/registry/device_info.rs | 8 +- core/startos/src/registry/mod.rs | 29 +- core/startos/src/registry/os/asset/add.rs | 10 +- core/startos/src/registry/os/asset/sign.rs | 6 +- core/startos/src/registry/os/version/mod.rs | 8 +- core/startos/src/registry/package/add.rs | 20 +- core/startos/src/registry/package/get.rs | 81 +- core/startos/src/registry/package/index.rs | 46 +- core/startos/src/registry/package/mod.rs | 11 +- .../signer/commitment/merkle_archive.rs | 29 + .../src/registry/signer/commitment/request.rs | 5 +- .../src/s9pk/merkle_archive/expected.rs | 48 +- core/startos/src/s9pk/merkle_archive/mod.rs | 4 + .../src/s9pk/merkle_archive/source/mod.rs | 52 +- core/startos/src/s9pk/rpc.rs | 36 + core/startos/src/s9pk/v2/compat.rs | 6 +- core/startos/src/s9pk/v2/manifest.rs | 15 +- core/startos/src/s9pk/v2/mod.rs | 108 +- core/startos/src/s9pk/v2/pack.rs | 224 +- .../src/service/service_effect_handler.rs | 156 +- core/startos/src/util/mod.rs | 51 + core/startos/src/util/net.rs | 51 +- core/startos/src/util/rpc.rs | 31 +- core/startos/src/util/serde.rs | 18 +- debian/postinst | 2 +- sdk/.prettierignore | 1 + sdk/Makefile | 5 +- sdk/jest.config.js | 2 +- sdk/lib/Dependency.ts | 6 +- sdk/lib/StartSdk.ts | 29 +- sdk/lib/actions/createAction.ts | 9 +- sdk/lib/actions/setupActions.ts | 4 +- sdk/lib/backup/Backups.ts | 10 +- sdk/lib/backup/setupBackups.ts | 16 +- sdk/lib/config/configDependencies.ts | 15 +- sdk/lib/config/setupConfig.ts | 16 +- sdk/lib/dependencies/DependencyConfig.ts | 17 +- sdk/lib/dependencies/dependencies.ts | 44 +- sdk/lib/dependencies/setupDependencyConfig.ts | 8 +- sdk/lib/emverLite/mod.ts | 323 --- sdk/lib/exver/exver.pegjs | 99 + sdk/lib/exver/exver.ts | 2507 +++++++++++++++++ sdk/lib/exver/index.ts | 443 +++ sdk/lib/health/HealthCheck.ts | 9 +- sdk/lib/index.browser.ts | 6 +- sdk/lib/index.ts | 2 +- sdk/lib/inits/migrations/Migration.ts | 28 +- sdk/lib/inits/migrations/setupMigrations.ts | 38 +- sdk/lib/inits/setupInit.ts | 12 +- sdk/lib/inits/setupInstall.ts | 15 +- sdk/lib/inits/setupUninstall.ts | 15 +- sdk/lib/interfaces/setupInterfaces.ts | 12 +- sdk/lib/mainFn/CommandController.ts | 12 +- sdk/lib/mainFn/Daemon.ts | 11 +- sdk/lib/mainFn/Daemons.ts | 25 +- sdk/lib/mainFn/Mounts.ts | 9 +- sdk/lib/mainFn/index.ts | 8 +- sdk/lib/manifest/ManifestTypes.ts | 74 +- sdk/lib/manifest/setupManifest.ts | 64 +- sdk/lib/osBindings/CheckDependenciesResult.ts | 4 +- sdk/lib/osBindings/CurrentDependencyInfo.ts | 7 +- sdk/lib/osBindings/DepInfo.ts | 7 +- sdk/lib/osBindings/DependencyMetadata.ts | 9 + sdk/lib/osBindings/DependencyRequirement.ts | 5 +- sdk/lib/osBindings/FullIndex.ts | 1 + ...VersionParams.ts => GetOsVersionParams.ts} | 2 +- sdk/lib/osBindings/GetPackageParams.ts | 2 +- sdk/lib/osBindings/HardwareRequirements.ts | 2 +- sdk/lib/osBindings/InstallParams.ts | 9 + sdk/lib/osBindings/Manifest.ts | 1 + sdk/lib/osBindings/PackageDetailLevel.ts | 2 +- sdk/lib/osBindings/PackageVersionInfo.ts | 6 + sdk/lib/osBindings/PathOrUrl.ts | 3 + sdk/lib/osBindings/RegistryAsset.ts | 1 + sdk/lib/osBindings/RegistryInfo.ts | 9 + sdk/lib/osBindings/index.ts | 6 +- sdk/lib/test/configBuilder.test.ts | 5 +- sdk/lib/test/emverList.test.ts | 253 -- sdk/lib/test/exverList.test.ts | 355 +++ sdk/lib/test/output.sdk.ts | 5 +- sdk/lib/test/setupDependencyConfig.test.ts | 2 +- sdk/lib/test/utils.splitCommand.test.ts | 42 - sdk/lib/types.ts | 24 +- sdk/lib/util/splitCommand.ts | 11 +- sdk/package-lock.json | 295 ++ sdk/package.json | 2 + web/angular.json | 3 +- web/package-lock.json | 4 + web/package.json | 1 + web/patchdb-ui-seed.json | 18 +- .../release-notes.component.html | 17 +- .../release-notes.component.scss | 1 + .../release-notes/release-notes.component.ts | 69 + .../release-notes/release-notes.module.ts | 7 +- .../list/categories/categories.component.html | 8 +- .../list/categories/categories.component.ts | 7 +- .../src/pages/list/item/item.component.html | 10 +- .../release-notes/release-notes.component.ts | 39 - .../src/pages/show/about/about.component.html | 12 +- .../src/pages/show/about/about.component.ts | 13 + .../src/pages/show/about/about.module.ts | 7 +- .../show/additional/additional.component.html | 20 +- .../show/additional/additional.component.ts | 81 +- .../dependencies/dependencies.component.html | 8 +- .../dependencies/dependencies.component.ts | 3 +- .../show/dependencies/dependencies.module.ts | 7 +- .../pages/show/flavors/flavors.component.html | 21 + .../pages/show/flavors/flavors.component.ts | 12 + .../src/pages/show/flavors/flavors.module.ts | 19 + .../pages/show/package/package.component.html | 8 +- .../src/pages/show/package/package.module.ts | 4 +- .../src/pipes/filter-packages.pipe.ts | 15 +- web/projects/marketplace/src/public-api.ts | 4 +- .../src/services/marketplace.service.ts | 15 +- web/projects/marketplace/src/types.ts | 72 +- .../assets/img/service-icons/fallback.png | Bin 0 -> 30439 bytes .../shared/src/pipes/emver/emver.module.ts | 12 - .../shared/src/pipes/emver/emver.pipe.ts | 52 - .../shared/src/pipes/exver/exver.module.ts | 8 + .../shared/src/pipes/exver/exver.pipe.ts | 35 + web/projects/shared/src/public-api.ts | 6 +- .../shared/src/services/emver.service.ts | 18 - .../shared/src/services/exver.service.ts | 43 + web/projects/shared/src/types/http.types.ts | 6 +- .../ui/src/app/app/menu/menu.component.ts | 8 +- .../backup-drives/backup.service.ts | 10 +- .../form/form-array/form-array.component.html | 2 +- .../form-datetime.component.html | 8 +- .../refresh-alert/refresh-alert.service.ts | 9 +- .../app-recover-select/to-options.pipe.ts | 10 +- .../app-actions/app-actions.page.ts | 2 +- .../app-list-pkg/app-list-pkg.component.html | 4 +- .../apps-routes/app-list/app-list.module.ts | 4 +- .../apps-routes/app-show/app-show.module.ts | 4 +- .../apps-routes/app-show/app-show.page.html | 7 +- .../apps-routes/app-show/app-show.page.ts | 51 +- .../app-show-additional.component.html | 32 +- .../app-show-additional.component.ts | 15 +- .../app-show-dependencies.component.html | 2 +- .../app-show-header.component.html | 2 +- .../app-show-progress.component.html | 10 +- .../app-show-progress.component.ts | 2 +- .../app-show/pipes/to-buttons.pipe.ts | 15 +- .../login/ca-wizard/ca-wizard.component.html | 5 +- .../marketplace-list.module.ts | 4 +- .../marketplace-list.page.html | 5 +- .../marketplace-list/marketplace-list.page.ts | 20 +- .../marketplace-routing.module.ts | 7 - .../marketplace-show-controls.component.html | 24 +- .../marketplace-show-controls.component.ts | 17 +- .../marketplace-show-dependent.component.html | 8 +- .../marketplace-show-dependent.component.ts | 4 +- .../marketplace-show.module.ts | 6 +- .../marketplace-show.page.html | 22 +- .../marketplace-show/marketplace-show.page.ts | 68 +- .../marketplace-status.component.html | 6 +- .../marketplace-status.component.ts | 15 +- .../marketplace-status.module.ts | 4 +- .../app/pages/server-routes/lan/lan.page.html | 2 +- .../server-specs/server-specs.module.ts | 4 +- .../server-specs/server-specs.page.html | 2 +- .../server-routes/sideload/sideload.module.ts | 4 +- .../server-routes/sideload/sideload.page.html | 2 +- .../src/app/pages/updates/updates.module.ts | 4 +- .../src/app/pages/updates/updates.page.html | 30 +- .../ui/src/app/pages/updates/updates.page.ts | 34 +- .../ui/src/app/services/api/api-icons.ts | 10 +- .../ui/src/app/services/api/api.fixures.ts | 660 ++++- .../ui/src/app/services/api/api.types.ts | 27 +- .../app/services/api/embassy-api.service.ts | 38 +- .../services/api/embassy-live-api.service.ts | 137 +- .../services/api/embassy-mock-api.service.ts | 96 +- .../ui/src/app/services/api/mock-patch.ts | 10 +- .../ui/src/app/services/dep-error.service.ts | 20 +- .../ui/src/app/services/eos.service.ts | 6 +- .../src/app/services/marketplace.service.ts | 171 +- .../src/app/services/patch-db/data-model.ts | 4 + web/projects/ui/src/app/util/dry-update.ts | 7 +- web/projects/ui/src/manifest.webmanifest | 4 +- web/projects/ui/src/polyfills.ts | 4 +- 196 files changed, 7002 insertions(+), 2162 deletions(-) create mode 100644 sdk/.prettierignore delete mode 100644 sdk/lib/emverLite/mod.ts create mode 100644 sdk/lib/exver/exver.pegjs create mode 100644 sdk/lib/exver/exver.ts create mode 100644 sdk/lib/exver/index.ts create mode 100644 sdk/lib/osBindings/DependencyMetadata.ts rename sdk/lib/osBindings/{GetVersionParams.ts => GetOsVersionParams.ts} (85%) create mode 100644 sdk/lib/osBindings/InstallParams.ts create mode 100644 sdk/lib/osBindings/PathOrUrl.ts create mode 100644 sdk/lib/osBindings/RegistryInfo.ts delete mode 100644 sdk/lib/test/emverList.test.ts create mode 100644 sdk/lib/test/exverList.test.ts delete mode 100644 sdk/lib/test/utils.splitCommand.test.ts rename web/projects/marketplace/src/{pages => modals}/release-notes/release-notes.component.html (63%) rename web/projects/marketplace/src/{pages => modals}/release-notes/release-notes.component.scss (91%) create mode 100644 web/projects/marketplace/src/modals/release-notes/release-notes.component.ts rename web/projects/marketplace/src/{pages => modals}/release-notes/release-notes.module.ts (86%) delete mode 100644 web/projects/marketplace/src/pages/release-notes/release-notes.component.ts create mode 100644 web/projects/marketplace/src/pages/show/flavors/flavors.component.html create mode 100644 web/projects/marketplace/src/pages/show/flavors/flavors.component.ts create mode 100644 web/projects/marketplace/src/pages/show/flavors/flavors.module.ts create mode 100644 web/projects/shared/assets/img/service-icons/fallback.png delete mode 100644 web/projects/shared/src/pipes/emver/emver.module.ts delete mode 100644 web/projects/shared/src/pipes/emver/emver.pipe.ts create mode 100644 web/projects/shared/src/pipes/exver/exver.module.ts create mode 100644 web/projects/shared/src/pipes/exver/exver.pipe.ts delete mode 100644 web/projects/shared/src/services/emver.service.ts create mode 100644 web/projects/shared/src/services/exver.service.ts diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index dd2ccbab3..be8a4d163 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -1,4 +1,4 @@ -import { types as T, utils, EmVer } from "@start9labs/start-sdk" +import { ExtendedVersion, types as T, utils } from "@start9labs/start-sdk" import * as fs from "fs/promises" import { polyfillEffects } from "./polyfillEffects" @@ -647,13 +647,13 @@ export class SystemForEmbassy implements System { dependencies: Object.entries(dependsOn).flatMap(([key, value]) => { const dependency = this.manifest.dependencies?.[key] if (!dependency) return [] - const versionSpec = dependency.version + const versionRange = dependency.version const registryUrl = DEFAULT_REGISTRY const kind = "running" return [ { id: key, - versionSpec, + versionRange, registryUrl, kind, healthChecks: [...value], @@ -668,18 +668,24 @@ export class SystemForEmbassy implements System { fromVersion: string, timeoutMs: number | null, ): Promise { - const fromEmver = EmVer.from(fromVersion) - const currentEmver = EmVer.from(this.manifest.version) + const fromEmver = ExtendedVersion.parseEmver(fromVersion) + const currentEmver = ExtendedVersion.parseEmver(this.manifest.version) if (!this.manifest.migrations) return { configured: true } const fromMigration = Object.entries(this.manifest.migrations.from) - .map(([version, procedure]) => [EmVer.from(version), procedure] as const) + .map( + ([version, procedure]) => + [ExtendedVersion.parseEmver(version), procedure] as const, + ) .find( ([versionEmver, procedure]) => versionEmver.greaterThan(fromEmver) && versionEmver.lessThanOrEqual(currentEmver), ) const toMigration = Object.entries(this.manifest.migrations.to) - .map(([version, procedure]) => [EmVer.from(version), procedure] as const) + .map( + ([version, procedure]) => + [ExtendedVersion.parseEmver(version), procedure] as const, + ) .find( ([versionEmver, procedure]) => versionEmver.greaterThan(fromEmver) && diff --git a/core/Cargo.lock b/core/Cargo.lock index 1072f4203..e3576a8d8 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -471,9 +471,9 @@ checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" [[package]] name = "base32" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce0365f4d5fb6646220bb52fe547afd51796d90f914d4063cb0b032ebee088" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" [[package]] name = "base64" @@ -4632,6 +4632,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "socket2" version = "0.5.7" @@ -4953,7 +4959,7 @@ dependencies = [ "axum-server", "backhand", "barrage", - "base32 0.5.0", + "base32 0.5.1", "base64 0.22.1", "base64ct", "basic-cookies", @@ -4975,6 +4981,7 @@ dependencies = [ "ed25519-dalek 2.1.1", "exver", "fd-lock-rs", + "form_urlencoded", "futures", "gpt", "helpers", @@ -5044,6 +5051,7 @@ dependencies = [ "sscanf", "ssh-key", "tar", + "textwrap", "thiserror", "tokio", "tokio-rustls", @@ -5052,7 +5060,7 @@ dependencies = [ "tokio-tar", "tokio-tungstenite 0.23.1", "tokio-util", - "toml 0.8.14", + "toml 0.8.15", "torut", "tower-service", "tracing", @@ -5221,6 +5229,17 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thingbuf" version = "0.1.6" @@ -5233,18 +5252,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -5480,14 +5499,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.15", + "toml_edit 0.22.16", ] [[package]] @@ -5525,9 +5544,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.15" +version = "0.22.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" +checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" dependencies = [ "indexmap 2.2.6", "serde", @@ -5888,6 +5907,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-normalization" version = "0.1.23" diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index 2695c7813..a531477b2 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -90,6 +90,7 @@ exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", "serde", ] } fd-lock-rs = "0.1.4" +form_urlencoded = "1.2.1" futures = "0.3.28" gpt = "3.1.0" helpers = { path = "../helpers" } @@ -171,6 +172,7 @@ sscanf = "0.4.1" ssh-key = { version = "0.6.2", features = ["ed25519"] } tar = "0.4.40" thiserror = "1.0.49" +textwrap = "0.16.1" tokio = { version = "1.38.1", features = ["full"] } tokio-rustls = "0.26.0" tokio-socks = "0.5.1" diff --git a/core/startos/src/backup/target/mod.rs b/core/startos/src/backup/target/mod.rs index f3b6b5f5c..032f70848 100644 --- a/core/startos/src/backup/target/mod.rs +++ b/core/startos/src/backup/target/mod.rs @@ -8,6 +8,7 @@ use color_eyre::eyre::eyre; use digest::generic_array::GenericArray; use digest::OutputSizeUser; use exver::Version; +use imbl_value::InternedString; use models::PackageId; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; @@ -213,7 +214,7 @@ pub struct BackupInfo { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct PackageBackupInfo { - pub title: String, + pub title: InternedString, pub version: VersionString, pub os_version: Version, pub timestamp: DateTime, diff --git a/core/startos/src/bins/start_init.rs b/core/startos/src/bins/start_init.rs index f4aa411b5..394d42c8d 100644 --- a/core/startos/src/bins/start_init.rs +++ b/core/startos/src/bins/start_init.rs @@ -141,6 +141,7 @@ async fn setup_or_init( } else { let init_ctx = InitContext::init(config).await?; let handle = init_ctx.progress.clone(); + let err_channel = init_ctx.error.clone(); let mut disk_phase = handle.add_phase("Opening data drive".into(), Some(10)); let init_phases = InitPhases::new(&handle); @@ -148,47 +149,55 @@ async fn setup_or_init( server.serve_init(init_ctx); - disk_phase.start(); - let guid_string = tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy + async { + disk_phase.start(); + let guid_string = tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy + .await?; + let disk_guid = Arc::new(String::from(guid_string.trim())); + let requires_reboot = crate::disk::main::import( + &**disk_guid, + config.datadir(), + if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { + RepairStrategy::Aggressive + } else { + RepairStrategy::Preen + }, + if disk_guid.ends_with("_UNENC") { + None + } else { + Some(DEFAULT_PASSWORD) + }, + ) .await?; - let disk_guid = Arc::new(String::from(guid_string.trim())); - let requires_reboot = crate::disk::main::import( - &**disk_guid, - config.datadir(), if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { - RepairStrategy::Aggressive - } else { - RepairStrategy::Preen - }, - if disk_guid.ends_with("_UNENC") { - None - } else { - Some(DEFAULT_PASSWORD) - }, - ) - .await?; - if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { - tokio::fs::remove_file(REPAIR_DISK_PATH) - .await - .with_ctx(|_| (crate::ErrorKind::Filesystem, REPAIR_DISK_PATH))?; + tokio::fs::remove_file(REPAIR_DISK_PATH) + .await + .with_ctx(|_| (crate::ErrorKind::Filesystem, REPAIR_DISK_PATH))?; + } + disk_phase.complete(); + tracing::info!("Loaded Disk"); + + if requires_reboot.0 { + let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1)); + reboot_phase.start(); + return Ok(Err(Shutdown { + export_args: Some((disk_guid, config.datadir().to_owned())), + restart: true, + })); + } + + let InitResult { net_ctrl } = crate::init::init(config, init_phases).await?; + + let rpc_ctx = + RpcContext::init(config, disk_guid, Some(net_ctrl), rpc_ctx_phases).await?; + + Ok::<_, Error>(Ok((rpc_ctx, handle))) } - disk_phase.complete(); - tracing::info!("Loaded Disk"); - - if requires_reboot.0 { - let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1)); - reboot_phase.start(); - return Ok(Err(Shutdown { - export_args: Some((disk_guid, config.datadir().to_owned())), - restart: true, - })); - } - - let InitResult { net_ctrl } = crate::init::init(config, init_phases).await?; - - let rpc_ctx = RpcContext::init(config, disk_guid, Some(net_ctrl), rpc_ctx_phases).await?; - - Ok(Ok((rpc_ctx, handle))) + .await + .map_err(|e| { + err_channel.send_replace(Some(e.clone_output())); + e + }) } } diff --git a/core/startos/src/context/init.rs b/core/startos/src/context/init.rs index f5f4a5430..566457a9c 100644 --- a/core/startos/src/context/init.rs +++ b/core/startos/src/context/init.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use rpc_toolkit::Context; use tokio::sync::broadcast::Sender; +use tokio::sync::watch; use tracing::instrument; use crate::context::config::ServerConfig; @@ -12,6 +13,7 @@ use crate::Error; pub struct InitContextSeed { pub config: ServerConfig, + pub error: watch::Sender>, pub progress: FullProgressTracker, pub shutdown: Sender<()>, pub rpc_continuations: RpcContinuations, @@ -25,6 +27,7 @@ impl InitContext { let (shutdown, _) = tokio::sync::broadcast::channel(1); Ok(Self(Arc::new(InitContextSeed { config: cfg.clone(), + error: watch::channel(None).0, progress: FullProgressTracker::new(), shutdown, rpc_continuations: RpcContinuations::new(), diff --git a/core/startos/src/db/model/package.rs b/core/startos/src/db/model/package.rs index 22d6440bb..e3b6e613b 100644 --- a/core/startos/src/db/model/package.rs +++ b/core/startos/src/db/model/package.rs @@ -372,14 +372,13 @@ impl Map for CurrentDependencies { #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct CurrentDependencyInfo { + #[ts(type = "string | null")] + pub title: Option, + pub icon: Option>, #[serde(flatten)] pub kind: CurrentDependencyKind, - pub title: String, - pub icon: DataUrl<'static>, #[ts(type = "string")] - pub registry_url: Url, - #[ts(type = "string")] - pub version_spec: VersionRange, + pub version_range: VersionRange, pub config_satisfied: bool, } diff --git a/core/startos/src/dependencies.rs b/core/startos/src/dependencies.rs index f6ccc53ad..54e38f299 100644 --- a/core/startos/src/dependencies.rs +++ b/core/startos/src/dependencies.rs @@ -2,18 +2,21 @@ use std::collections::BTreeMap; use std::time::Duration; use clap::Parser; +use imbl_value::InternedString; use models::PackageId; use patch_db::json_patch::merge; use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tracing::instrument; use ts_rs::TS; +use url::Url; use crate::config::{Config, ConfigSpec, ConfigureContext}; use crate::context::RpcContext; use crate::db::model::package::CurrentDependencies; use crate::prelude::*; use crate::rpc_continuations::Guid; +use crate::util::PathOrUrl; use crate::Error; pub fn dependency() -> ParentHandler { @@ -42,6 +45,16 @@ impl Map for Dependencies { pub struct DepInfo { pub description: Option, pub optional: bool, + pub s9pk: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct DependencyMetadata { + #[ts(type = "string")] + pub title: InternedString, } #[derive(Deserialize, Serialize, Parser, TS)] diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index 48cb24c9a..735ca85a5 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -554,33 +554,54 @@ pub struct InitProgressRes { pub async fn init_progress(ctx: InitContext) -> Result { let progress_tracker = ctx.progress.clone(); let progress = progress_tracker.snapshot(); + let mut error = ctx.error.subscribe(); let guid = Guid::new(); ctx.rpc_continuations .add( guid.clone(), RpcContinuation::ws( |mut ws| async move { - if let Err(e) = async { - let mut stream = progress_tracker.stream(Some(Duration::from_millis(100))); - while let Some(progress) = stream.next().await { - ws.send(ws::Message::Text( - serde_json::to_string(&progress) - .with_kind(ErrorKind::Serialization)?, - )) - .await - .with_kind(ErrorKind::Network)?; - if progress.overall.is_complete() { - break; + let res = tokio::try_join!( + async { + let mut stream = + progress_tracker.stream(Some(Duration::from_millis(100))); + while let Some(progress) = stream.next().await { + ws.send(ws::Message::Text( + serde_json::to_string(&progress) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + if progress.overall.is_complete() { + break; + } + } + + Ok::<_, Error>(()) + }, + async { + if let Some(e) = error + .wait_for(|e| e.is_some()) + .await + .ok() + .and_then(|e| e.as_ref().map(|e| e.clone_output())) + { + Err::<(), _>(e) + } else { + Ok(()) } } + ); - ws.normal_close("complete").await?; - - Ok::<_, Error>(()) - } - .await + if let Err(e) = ws + .close_result(res.map(|_| "complete").map_err(|e| { + tracing::error!("error in init progress websocket: {e}"); + tracing::debug!("{e:?}"); + e + })) + .await { - tracing::error!("error in init progress websocket: {e}"); + tracing::error!("error closing init progress websocket: {e}"); tracing::debug!("{e:?}"); } }, diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index 18591dc8d..394136024 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -111,6 +111,7 @@ impl std::fmt::Display for MinMax { #[derive(Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] +#[ts(export)] pub struct InstallParams { #[ts(type = "string")] registry: Url, diff --git a/core/startos/src/middleware/db.rs b/core/startos/src/middleware/db.rs index dca0418e5..e8b4f8887 100644 --- a/core/startos/src/middleware/db.rs +++ b/core/startos/src/middleware/db.rs @@ -7,7 +7,6 @@ use serde::Deserialize; use crate::context::RpcContext; #[derive(Deserialize)] -#[serde(rename_all = "camelCase")] pub struct Metadata { #[serde(default)] sync_db: bool, diff --git a/core/startos/src/net/static_server.rs b/core/startos/src/net/static_server.rs index 3e5f0997d..cd93a9d65 100644 --- a/core/startos/src/net/static_server.rs +++ b/core/startos/src/net/static_server.rs @@ -1,5 +1,8 @@ +use std::cmp::min; use std::future::Future; +use std::io::Cursor; use std::path::{Path, PathBuf}; +use std::sync::Arc; use std::time::UNIX_EPOCH; use async_compression::tokio::bufread::GzipEncoder; @@ -8,36 +11,51 @@ use axum::extract::{self as x, Request}; use axum::response::Response; use axum::routing::{any, get, post}; use axum::Router; +use base64::display::Base64Display; use digest::Digest; use futures::future::ready; -use http::header::ACCEPT_ENCODING; +use http::header::{ + ACCEPT_ENCODING, ACCEPT_RANGES, CACHE_CONTROL, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, + CONTENT_RANGE, CONTENT_TYPE, ETAG, RANGE, +}; use http::request::Parts as RequestParts; -use http::{Method, StatusCode}; +use http::{HeaderValue, Method, StatusCode}; use imbl_value::InternedString; use include_dir::Dir; use new_mime_guess::MimeGuess; use openssl::hash::MessageDigest; use openssl::x509::X509; use rpc_toolkit::{Context, HttpServer, Server}; -use tokio::io::BufReader; +use sqlx::query; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, BufReader}; use tokio_util::io::ReaderStream; +use url::Url; use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; use crate::hostname::Hostname; +use crate::install::PKG_ARCHIVE_DIR; use crate::middleware::auth::{Auth, HasValidSession}; use crate::middleware::cors::Cors; use crate::middleware::db::SyncDb; +use crate::prelude::*; +use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; use crate::rpc_continuations::{Guid, RpcContinuations}; +use crate::s9pk::merkle_archive::source::http::HttpSource; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::s9pk::merkle_archive::source::FileSource; +use crate::s9pk::S9pk; use crate::util::io::open_file; -use crate::{ - diagnostic_api, init_api, install_api, main_api, setup_api, Error, ErrorKind, ResultExt, -}; +use crate::util::net::SyncBody; +use crate::util::serde::BASE64; +use crate::{diagnostic_api, init_api, install_api, main_api, setup_api}; const NOT_FOUND: &[u8] = b"Not Found"; const METHOD_NOT_ALLOWED: &[u8] = b"Method Not Allowed"; const NOT_AUTHORIZED: &[u8] = b"Not Authorized"; const INTERNAL_SERVER_ERROR: &[u8] = b"Internal Server Error"; +const PROXY_STRIP_HEADERS: &[&str] = &["cookie", "host", "origin", "referer", "user-agent"]; + #[cfg(all(feature = "daemon", not(feature = "test")))] const EMBEDDED_UIS: Dir<'_> = include_dir::include_dir!("$CARGO_MANIFEST_DIR/../../web/dist/static"); @@ -97,7 +115,7 @@ pub fn rpc_router>( fn serve_ui(req: Request, ui_mode: UiMode) -> Result { let (request_parts, _body) = req.into_parts(); match &request_parts.method { - &Method::GET => { + &Method::GET | &Method::HEAD => { let uri_path = ui_mode.path( request_parts .uri @@ -111,7 +129,7 @@ fn serve_ui(req: Request, ui_mode: UiMode) -> Result { .or_else(|| EMBEDDED_UIS.get_file(&*ui_mode.path("index.html"))); if let Some(file) = file { - FileData::from_embedded(&request_parts, file).into_response(&request_parts) + FileData::from_embedded(&request_parts, file)?.into_response(&request_parts) } else { Ok(not_found()) } @@ -161,14 +179,35 @@ pub fn init_ui_router(ctx: InitContext) -> Router { } pub fn main_ui_router(ctx: RpcContext) -> Router { - rpc_router( - ctx.clone(), + rpc_router(ctx.clone(), { + let ctx = ctx.clone(); Server::new(move || ready(Ok(ctx.clone())), main_api::()) .middleware(Cors::new()) .middleware(Auth::new()) - .middleware(SyncDb::new()), + .middleware(SyncDb::new()) + }) + .route("/proxy/:url", { + let ctx = ctx.clone(); + any(move |x::Path(url): x::Path, request: Request| { + let ctx = ctx.clone(); + async move { + proxy_request(ctx, request, url) + .await + .unwrap_or_else(server_error) + } + }) + }) + .nest("/s9pk", s9pk_router(ctx.clone())) + .route( + "/static/local-root-ca.crt", + get(move || { + let ctx = ctx.clone(); + async move { + let account = ctx.account.read().await; + cert_send(&account.root_ca_cert, &account.hostname) + } + }), ) - // TODO: cert .fallback(any(|request: Request| async move { serve_ui(request, UiMode::Main).unwrap_or_else(server_error) })) @@ -179,29 +218,133 @@ pub fn refresher() -> Router { let res = include_bytes!("./refresher.html"); FileData { data: Body::from(&res[..]), + content_range: None, e_tag: None, encoding: None, len: Some(res.len() as u64), mime: Some("text/html".into()), + digest: None, } .into_response(&request.into_parts().0) .unwrap_or_else(server_error) })) } +async fn proxy_request(ctx: RpcContext, request: Request, url: String) -> Result { + if_authorized(&ctx, request, |mut request| async { + for header in PROXY_STRIP_HEADERS { + request.headers_mut().remove(*header); + } + *request.uri_mut() = url.parse()?; + let request = request.map(|b| reqwest::Body::wrap_stream(SyncBody::from(b))); + let response = ctx.client.execute(request.try_into()?).await?; + Ok(Response::from(response).map(|b| Body::new(b))) + }) + .await +} + +fn s9pk_router(ctx: RpcContext) -> Router { + Router::new() + .route("/installed/:s9pk", { + let ctx = ctx.clone(); + any( + |x::Path(s9pk): x::Path, request: Request| async move { + if_authorized(&ctx, request, |request| async { + let (parts, _) = request.into_parts(); + match FileData::from_path( + &parts, + &ctx.datadir + .join(PKG_ARCHIVE_DIR) + .join("installed") + .join(s9pk), + ) + .await? + { + Some(file) => file.into_response(&parts), + None => Ok(not_found()), + } + }) + .await + .unwrap_or_else(server_error) + }, + ) + }) + .route("/installed/:s9pk/*path", { + let ctx = ctx.clone(); + any( + |x::Path(s9pk): x::Path, + x::Path(path): x::Path, + x::Query(commitment): x::Query>, + request: Request| async move { + if_authorized(&ctx, request, |request| async { + let s9pk = S9pk::deserialize( + &MultiCursorFile::from( + open_file( + ctx.datadir + .join(PKG_ARCHIVE_DIR) + .join("installed") + .join(s9pk), + ) + .await?, + ), + commitment.as_ref(), + ) + .await?; + let (parts, _) = request.into_parts(); + match FileData::from_s9pk(&parts, &s9pk, &path).await? { + Some(file) => file.into_response(&parts), + None => Ok(not_found()), + } + }) + .await + .unwrap_or_else(server_error) + }, + ) + }) + .route( + "/proxy/:url/*path", + any( + |x::Path((url, path)): x::Path<(Url, PathBuf)>, + x::RawQuery(query): x::RawQuery, + request: Request| async move { + if_authorized(&ctx, request, |request| async { + let s9pk = S9pk::deserialize( + &Arc::new(HttpSource::new(ctx.client.clone(), url).await?), + query + .as_deref() + .map(MerkleArchiveCommitment::from_query) + .and_then(|a| a.transpose()) + .transpose()? + .as_ref(), + ) + .await?; + let (parts, _) = request.into_parts(); + match FileData::from_s9pk(&parts, &s9pk, &path).await? { + Some(file) => file.into_response(&parts), + None => Ok(not_found()), + } + }) + .await + .unwrap_or_else(server_error) + }, + ), + ) +} + async fn if_authorized< - F: FnOnce() -> Fut, - Fut: Future> + Send + Sync, + F: FnOnce(Request) -> Fut, + Fut: Future> + Send, >( ctx: &RpcContext, - parts: &RequestParts, + request: Request, f: F, ) -> Result { - if let Err(e) = HasValidSession::from_header(parts.headers.get(http::header::COOKIE), ctx).await + if let Err(e) = + HasValidSession::from_header(request.headers().get(http::header::COOKIE), ctx).await { - Ok(unauthorized(e, parts.uri.path())) + Ok(unauthorized(e, request.uri().path())) } else { - f().await + f(request).await } } @@ -268,44 +411,117 @@ fn cert_send(cert: &X509, hostname: &Hostname) -> Result { .with_kind(ErrorKind::Network) } +fn parse_range(header: &HeaderValue, len: u64) -> Result<(u64, u64, u64), Error> { + let r = header + .to_str() + .with_kind(ErrorKind::Network)? + .trim() + .strip_prefix("bytes=") + .ok_or_else(|| Error::new(eyre!("invalid range units"), ErrorKind::InvalidRequest))?; + + if r.contains(",") { + return Err(Error::new( + eyre!("multi-range requests are unsupported"), + ErrorKind::InvalidRequest, + )); + } + if let Some((start, end)) = r.split_once("-").map(|(s, e)| (s.trim(), e.trim())) { + Ok(( + if start.is_empty() { + 0u64 + } else { + start.parse()? + }, + if end.is_empty() { + len - 1 + } else { + min(end.parse()?, len - 1) + }, + len, + )) + } else { + Ok((len - r.trim().parse::()?, len - 1, len)) + } +} + struct FileData { data: Body, len: Option, + content_range: Option<(u64, u64, u64)>, encoding: Option<&'static str>, e_tag: Option, mime: Option, + digest: Option<(&'static str, Vec)>, } impl FileData { - fn from_embedded(req: &RequestParts, file: &'static include_dir::File<'static>) -> Self { + fn from_embedded( + req: &RequestParts, + file: &'static include_dir::File<'static>, + ) -> Result { let path = file.path(); - let (encoding, data) = req - .headers - .get_all(ACCEPT_ENCODING) - .into_iter() - .filter_map(|h| h.to_str().ok()) - .flat_map(|s| s.split(",")) - .filter_map(|s| s.split(";").next()) - .map(|s| s.trim()) - .fold((None, file.contents()), |acc, e| { - if let Some(file) = (e == "br") - .then_some(()) - .and_then(|_| EMBEDDED_UIS.get_file(format!("{}.br", path.display()))) - { - (Some("br"), file.contents()) - } else if let Some(file) = (e == "gzip" && acc.0 != Some("br")) - .then_some(()) - .and_then(|_| EMBEDDED_UIS.get_file(format!("{}.gz", path.display()))) - { - (Some("gzip"), file.contents()) - } else { - acc - } - }); + let (encoding, data, len, content_range) = if let Some(range) = req.headers.get(RANGE) { + let data = file.contents(); + let (start, end, size) = parse_range(range, data.len() as u64)?; + let encoding = req + .headers + .get_all(ACCEPT_ENCODING) + .into_iter() + .filter_map(|h| h.to_str().ok()) + .flat_map(|s| s.split(",")) + .filter_map(|s| s.split(";").next()) + .map(|s| s.trim()) + .any(|e| e == "gzip") + .then_some("gzip"); + let data = if start > end { + &[] + } else { + &data[(start as usize)..=(end as usize)] + }; + let (len, data) = if encoding == Some("gzip") { + ( + None, + Body::from_stream(ReaderStream::new(GzipEncoder::new(Cursor::new(data)))), + ) + } else { + (Some(data.len() as u64), Body::from(data)) + }; + (encoding, data, len, Some((start, end, size))) + } else { + let (encoding, data) = req + .headers + .get_all(ACCEPT_ENCODING) + .into_iter() + .filter_map(|h| h.to_str().ok()) + .flat_map(|s| s.split(",")) + .filter_map(|s| s.split(";").next()) + .map(|s| s.trim()) + .fold((None, file.contents()), |acc, e| { + if let Some(file) = (e == "br") + .then_some(()) + .and_then(|_| EMBEDDED_UIS.get_file(format!("{}.br", path.display()))) + { + (Some("br"), file.contents()) + } else if let Some(file) = (e == "gzip" && acc.0 != Some("br")) + .then_some(()) + .and_then(|_| EMBEDDED_UIS.get_file(format!("{}.gz", path.display()))) + { + (Some("gzip"), file.contents()) + } else { + acc + } + }); + (encoding, Body::from(data), Some(data.len() as u64), None) + }; - Self { - len: Some(data.len() as u64), + Ok(Self { + len, encoding, - data: data.into(), + content_range, + data: if req.method == Method::HEAD { + Body::empty() + } else { + data + }, e_tag: file.metadata().map(|metadata| { e_tag( path, @@ -323,11 +539,28 @@ impl FileData { mime: MimeGuess::from_path(path) .first() .map(|m| m.essence_str().into()), + digest: None, + }) + } + + fn encode( + encoding: &mut Option<&str>, + data: R, + len: u64, + ) -> (Option, Body) { + if *encoding == Some("gzip") { + ( + None, + Body::from_stream(ReaderStream::new(GzipEncoder::new(BufReader::new(data)))), + ) + } else { + *encoding = None; + (Some(len), Body::from_stream(ReaderStream::new(data))) } } - async fn from_path(req: &RequestParts, path: &Path) -> Result { - let encoding = req + async fn from_path(req: &RequestParts, path: &Path) -> Result, Error> { + let mut encoding = req .headers .get_all(ACCEPT_ENCODING) .into_iter() @@ -338,12 +571,23 @@ impl FileData { .any(|e| e == "gzip") .then_some("gzip"); - let file = open_file(path).await?; + if tokio::fs::metadata(path).await.is_err() { + return Ok(None); + } + + let mut file = open_file(path).await?; + let metadata = file .metadata() .await .with_ctx(|_| (ErrorKind::Filesystem, path.display().to_string()))?; + let content_range = req + .headers + .get(RANGE) + .map(|r| parse_range(r, metadata.len())) + .transpose()?; + let e_tag = Some(e_tag( path, format!( @@ -357,51 +601,123 @@ impl FileData { .as_bytes(), )); - let (len, data) = if encoding == Some("gzip") { - ( - None, - Body::from_stream(ReaderStream::new(GzipEncoder::new(BufReader::new(file)))), - ) + let (len, data) = if let Some((start, end, _)) = content_range { + let len = end + 1 - start; + file.seek(std::io::SeekFrom::Start(start)).await?; + Self::encode(&mut encoding, file.take(len), len) } else { - ( - Some(metadata.len()), - Body::from_stream(ReaderStream::new(file)), - ) + Self::encode(&mut encoding, file, metadata.len()) }; - Ok(Self { - data, + Ok(Some(Self { + data: if req.method == Method::HEAD { + Body::empty() + } else { + data + }, len, + content_range, encoding, e_tag, mime: MimeGuess::from_path(path) .first() .map(|m| m.essence_str().into()), - }) + digest: None, + })) + } + + async fn from_s9pk( + req: &RequestParts, + s9pk: &S9pk, + path: &Path, + ) -> Result, Error> { + let mut encoding = req + .headers + .get_all(ACCEPT_ENCODING) + .into_iter() + .filter_map(|h| h.to_str().ok()) + .flat_map(|s| s.split(",")) + .filter_map(|s| s.split(";").next()) + .map(|s| s.trim()) + .any(|e| e == "gzip") + .then_some("gzip"); + + let Some(file) = s9pk.as_archive().contents().get_path(path) else { + return Ok(None); + }; + let Some(contents) = file.as_file() else { + return Ok(None); + }; + let (digest, len) = if let Some((hash, len)) = file.hash() { + (Some(("blake3", hash.as_bytes().to_vec())), len) + } else { + (None, contents.size().await?) + }; + + let content_range = req + .headers + .get(RANGE) + .map(|r| parse_range(r, len)) + .transpose()?; + + let (len, data) = if let Some((start, end, _)) = content_range { + let len = end + 1 - start; + Self::encode(&mut encoding, contents.slice(start, len).await?, len) + } else { + Self::encode(&mut encoding, contents.reader().await?.take(len), len) + }; + + Ok(Some(Self { + data: if req.method == Method::HEAD { + Body::empty() + } else { + data + }, + len, + content_range, + encoding, + e_tag: None, + mime: MimeGuess::from_path(path) + .first() + .map(|m| m.essence_str().into()), + digest, + })) } fn into_response(self, req: &RequestParts) -> Result { let mut builder = Response::builder(); if let Some(mime) = self.mime { - builder = builder.header(http::header::CONTENT_TYPE, &*mime); + builder = builder.header(CONTENT_TYPE, &*mime); } if let Some(e_tag) = &self.e_tag { - builder = builder.header(http::header::ETAG, &**e_tag); + builder = builder + .header(ETAG, &**e_tag) + .header(CACHE_CONTROL, "public, max-age=21000000, immutable"); + } + + builder = builder.header(ACCEPT_RANGES, "bytes"); + if let Some((start, end, size)) = self.content_range { + builder = builder + .header(CONTENT_RANGE, format!("bytes {start}-{end}/{size}")) + .status(StatusCode::PARTIAL_CONTENT); + } + + if let Some((algorithm, digest)) = self.digest { + builder = builder.header( + "Repr-Digest", + format!("{algorithm}=:{}:", Base64Display::new(&digest, &BASE64)), + ); } - builder = builder.header( - http::header::CACHE_CONTROL, - "public, max-age=21000000, immutable", - ); if req .headers - .get_all(http::header::CONNECTION) + .get_all(CONNECTION) .iter() .flat_map(|s| s.to_str().ok()) .flat_map(|s| s.split(",")) .any(|s| s.trim() == "keep-alive") { - builder = builder.header(http::header::CONNECTION, "keep-alive"); + builder = builder.header(CONNECTION, "keep-alive"); } if self.e_tag.is_some() @@ -411,14 +727,13 @@ impl FileData { .and_then(|h| h.to_str().ok()) == self.e_tag.as_deref() { - builder = builder.status(StatusCode::NOT_MODIFIED); - builder.body(Body::empty()) + builder.status(StatusCode::NOT_MODIFIED).body(Body::empty()) } else { if let Some(len) = self.len { - builder = builder.header(http::header::CONTENT_LENGTH, len); + builder = builder.header(CONTENT_LENGTH, len); } if let Some(encoding) = self.encoding { - builder = builder.header(http::header::CONTENT_ENCODING, encoding); + builder = builder.header(CONTENT_ENCODING, encoding); } builder.body(self.data) diff --git a/core/startos/src/registry/admin.rs b/core/startos/src/registry/admin.rs index cd795e5cd..8125580a4 100644 --- a/core/startos/src/registry/admin.rs +++ b/core/startos/src/registry/admin.rs @@ -46,7 +46,7 @@ fn signers_api() -> ParentHandler { .with_metadata("admin", Value::Bool(true)) .no_cli(), ) - .subcommand("add", from_fn_async(cli_add_signer).no_display()) + .subcommand("add", from_fn_async(cli_add_signer)) } impl Model> { @@ -71,7 +71,7 @@ impl Model> { .ok_or_else(|| Error::new(eyre!("unknown signer"), ErrorKind::Authorization)) } - pub fn add_signer(&mut self, signer: &SignerInfo) -> Result<(), Error> { + pub fn add_signer(&mut self, signer: &SignerInfo) -> Result { if let Some((guid, s)) = self .as_entries()? .into_iter() @@ -89,7 +89,9 @@ impl Model> { ErrorKind::InvalidRequest, )); } - self.insert(&Guid::new(), signer) + let id = Guid::new(); + self.insert(&id, signer)?; + Ok(id) } } @@ -122,7 +124,7 @@ pub fn display_signers(params: WithIoFormat, signers: BTreeMap Result<(), Error> { +pub async fn add_signer(ctx: RegistryContext, signer: SignerInfo) -> Result { ctx.db .mutate(|db| db.as_index_mut().as_signers_mut().add_signer(&signer)) .await @@ -155,7 +157,7 @@ pub async fn cli_add_signer( }, .. }: HandlerArgs, -) -> Result<(), Error> { +) -> Result { let signer = SignerInfo { name, contact, @@ -165,15 +167,16 @@ pub async fn cli_add_signer( TypedPatchDb::::load(PatchDb::open(database).await?) .await? .mutate(|db| db.as_index_mut().as_signers_mut().add_signer(&signer)) - .await?; + .await } else { - ctx.call_remote::( - &parent_method.into_iter().chain(method).join("."), - to_value(&signer)?, + from_value( + ctx.call_remote::( + &parent_method.into_iter().chain(method).join("."), + to_value(&signer)?, + ) + .await?, ) - .await?; } - Ok(()) } #[derive(Debug, Deserialize, Serialize, TS)] diff --git a/core/startos/src/registry/asset.rs b/core/startos/src/registry/asset.rs index ab251d2aa..7697a0c99 100644 --- a/core/startos/src/registry/asset.rs +++ b/core/startos/src/registry/asset.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::sync::Arc; +use chrono::{DateTime, Utc}; use reqwest::Client; use serde::{Deserialize, Serialize}; use tokio::io::AsyncWrite; @@ -20,6 +21,8 @@ use crate::s9pk::S9pk; #[serde(rename_all = "camelCase")] #[ts(export)] pub struct RegistryAsset { + #[ts(type = "string")] + pub published_at: DateTime, #[ts(type = "string")] pub url: Url, pub commitment: Commitment, diff --git a/core/startos/src/registry/auth.rs b/core/startos/src/registry/auth.rs index 27655c4a8..4707bf809 100644 --- a/core/startos/src/registry/auth.rs +++ b/core/startos/src/registry/auth.rs @@ -27,7 +27,6 @@ use crate::util::serde::Base64; pub const AUTH_SIG_HEADER: &str = "X-StartOS-Registry-Auth-Sig"; #[derive(Deserialize)] -#[serde(rename_all = "camelCase")] pub struct Metadata { #[serde(default)] admin: bool, @@ -75,9 +74,7 @@ pub struct RegistryAdminLogRecord { pub key: AnyVerifyingKey, } -#[derive(Serialize, Deserialize)] pub struct SignatureHeader { - #[serde(flatten)] pub commitment: RequestCommitment, pub signer: AnyVerifyingKey, pub signature: AnySignature, @@ -93,14 +90,9 @@ impl SignatureHeader { HeaderValue::from_str(url.query().unwrap_or_default()).unwrap() } pub fn from_header(header: &HeaderValue) -> Result { - let url: Url = format!( - "http://localhost/?{}", - header.to_str().with_kind(ErrorKind::Utf8)? - ) - .parse()?; - let query: BTreeMap<_, _> = url.query_pairs().collect(); + let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect(); Ok(Self { - commitment: RequestCommitment::from_query(&url)?, + commitment: RequestCommitment::from_query(&header)?, signer: query.get("signer").or_not_found("signer")?.parse()?, signature: query.get("signature").or_not_found("signature")?.parse()?, }) diff --git a/core/startos/src/registry/context.rs b/core/startos/src/registry/context.rs index 64a157073..d3eaf3691 100644 --- a/core/startos/src/registry/context.rs +++ b/core/startos/src/registry/context.rs @@ -200,6 +200,19 @@ impl CallRemote for CliContext { .send() .await?; + if !res.status().is_success() { + let status = res.status(); + let txt = res.text().await?; + let mut res = Err(Error::new( + eyre!("{}", status.canonical_reason().unwrap_or(status.as_str())), + ErrorKind::Network, + )); + if !txt.is_empty() { + res = res.with_ctx(|_| (ErrorKind::Network, txt)); + } + return res.map_err(From::from); + } + match res .headers() .get(CONTENT_TYPE) @@ -210,7 +223,7 @@ impl CallRemote for CliContext { .with_kind(ErrorKind::Deserialization)? .result } - _ => Err(Error::new(eyre!("missing content type"), ErrorKind::Network).into()), + _ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()), } } } @@ -247,6 +260,19 @@ impl CallRemote for RpcContext { .send() .await?; + if !res.status().is_success() { + let status = res.status(); + let txt = res.text().await?; + let mut res = Err(Error::new( + eyre!("{}", status.canonical_reason().unwrap_or(status.as_str())), + ErrorKind::Network, + )); + if !txt.is_empty() { + res = res.with_ctx(|_| (ErrorKind::Network, txt)); + } + return res.map_err(From::from); + } + match res .headers() .get(CONTENT_TYPE) @@ -257,7 +283,7 @@ impl CallRemote for RpcContext { .with_kind(ErrorKind::Deserialization)? .result } - _ => Err(Error::new(eyre!("missing content type"), ErrorKind::Network).into()), + _ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()), } } } diff --git a/core/startos/src/registry/device_info.rs b/core/startos/src/registry/device_info.rs index 9a357358a..172348a10 100644 --- a/core/startos/src/registry/device_info.rs +++ b/core/startos/src/registry/device_info.rs @@ -54,12 +54,7 @@ impl DeviceInfo { HeaderValue::from_str(url.query().unwrap_or_default()).unwrap() } pub fn from_header_value(header: &HeaderValue) -> Result { - let url: Url = format!( - "http://localhost/?{}", - header.to_str().with_kind(ErrorKind::ParseUrl)? - ) - .parse()?; - let query: BTreeMap<_, _> = url.query_pairs().collect(); + let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect(); Ok(Self { os: OsInfo { version: query @@ -151,7 +146,6 @@ impl From<&RpcContext> for HardwareInfo { } #[derive(Deserialize)] -#[serde(rename_all = "camelCase")] pub struct Metadata { #[serde(default)] get_device_info: bool, diff --git a/core/startos/src/registry/mod.rs b/core/startos/src/registry/mod.rs index 1039264df..d34ebb841 100644 --- a/core/startos/src/registry/mod.rs +++ b/core/startos/src/registry/mod.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; use axum::Router; use futures::future::ready; +use imbl_value::InternedString; use models::DataUrl; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler, Server}; use serde::{Deserialize, Serialize}; @@ -16,7 +17,7 @@ use crate::registry::auth::Auth; use crate::registry::context::RegistryContext; use crate::registry::device_info::DeviceInfoMiddleware; use crate::registry::os::index::OsIndex; -use crate::registry::package::index::PackageIndex; +use crate::registry::package::index::{Category, PackageIndex}; use crate::registry::signer::SignerInfo; use crate::rpc_continuations::Guid; use crate::util::serde::HandlerExtSerde; @@ -45,6 +46,7 @@ impl RegistryDatabase {} #[model = "Model"] #[ts(export)] pub struct FullIndex { + pub name: Option, pub icon: Option>, pub package: PackageIndex, pub os: OsIndex, @@ -55,6 +57,25 @@ pub async fn get_full_index(ctx: RegistryContext) -> Result { ctx.db.peek().await.into_index().de() } +#[derive(Debug, Default, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct RegistryInfo { + pub name: Option, + pub icon: Option>, + #[ts(as = "BTreeMap::")] + pub categories: BTreeMap, +} + +pub async fn get_info(ctx: RegistryContext) -> Result { + let peek = ctx.db.peek().await.into_index(); + Ok(RegistryInfo { + name: peek.as_name().de()?, + icon: peek.as_icon().de()?, + categories: peek.as_package().as_categories().de()?, + }) +} + pub fn registry_api() -> ParentHandler { ParentHandler::new() .subcommand( @@ -63,6 +84,12 @@ pub fn registry_api() -> ParentHandler { .with_display_serializable() .with_call_remote::(), ) + .subcommand( + "info", + from_fn_async(get_info) + .with_display_serializable() + .with_call_remote::(), + ) .subcommand("os", os::os_api::()) .subcommand("package", package::package_api::()) .subcommand("admin", admin::admin_api::()) diff --git a/core/startos/src/registry/os/asset/add.rs b/core/startos/src/registry/os/asset/add.rs index 6108dd5bc..c18d05b8f 100644 --- a/core/startos/src/registry/os/asset/add.rs +++ b/core/startos/src/registry/os/asset/add.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, HashMap}; use std::panic::UnwindSafe; use std::path::PathBuf; +use chrono::Utc; use clap::Parser; use imbl_value::InternedString; use itertools::Itertools; @@ -12,7 +13,7 @@ use url::Url; use crate::context::CliContext; use crate::prelude::*; -use crate::progress::{FullProgressTracker}; +use crate::progress::FullProgressTracker; use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::os::index::OsVersionInfo; @@ -33,19 +34,19 @@ pub fn add_api() -> ParentHandler { .subcommand( "iso", from_fn_async(add_iso) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) .subcommand( "img", from_fn_async(add_img) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) .subcommand( "squashfs", from_fn_async(add_squashfs) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) } @@ -107,6 +108,7 @@ async fn add_asset( ) .upsert(&platform, || { Ok(RegistryAsset { + published_at: Utc::now(), url, commitment: commitment.clone(), signatures: HashMap::new(), diff --git a/core/startos/src/registry/os/asset/sign.rs b/core/startos/src/registry/os/asset/sign.rs index 8bf1cfeb5..12903d8a1 100644 --- a/core/startos/src/registry/os/asset/sign.rs +++ b/core/startos/src/registry/os/asset/sign.rs @@ -30,19 +30,19 @@ pub fn sign_api() -> ParentHandler { .subcommand( "iso", from_fn_async(sign_iso) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) .subcommand( "img", from_fn_async(sign_img) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) .subcommand( "squashfs", from_fn_async(sign_squashfs) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) } diff --git a/core/startos/src/registry/os/version/mod.rs b/core/startos/src/registry/os/version/mod.rs index 9ebe8a696..242ae28a7 100644 --- a/core/startos/src/registry/os/version/mod.rs +++ b/core/startos/src/registry/os/version/mod.rs @@ -25,7 +25,7 @@ pub fn version_api() -> ParentHandler { "add", from_fn_async(add_version) .with_metadata("admin", Value::Bool(true)) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_display() .with_call_remote::(), ) @@ -121,7 +121,7 @@ pub async fn remove_version( #[command(rename_all = "kebab-case")] #[serde(rename_all = "camelCase")] #[ts(export)] -pub struct GetVersionParams { +pub struct GetOsVersionParams { #[ts(type = "string | null")] #[arg(long = "src")] pub source: Option, @@ -138,12 +138,12 @@ pub struct GetVersionParams { pub async fn get_version( ctx: RegistryContext, - GetVersionParams { + GetOsVersionParams { source, target, server_id, arch, - }: GetVersionParams, + }: GetOsVersionParams, ) -> Result, Error> { if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, arch) { let created_at = Utc::now(); diff --git a/core/startos/src/registry/package/add.rs b/core/startos/src/registry/package/add.rs index 9bc772f78..c52f06ac0 100644 --- a/core/startos/src/registry/package/add.rs +++ b/core/startos/src/registry/package/add.rs @@ -11,13 +11,14 @@ use url::Url; use crate::context::CliContext; use crate::prelude::*; -use crate::progress::FullProgressTracker; +use crate::progress::{FullProgressTracker, ProgressTrackerWriter}; use crate::registry::context::RegistryContext; use crate::registry::package::index::PackageVersionInfo; use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; use crate::registry::signer::sign::ed25519::Ed25519; use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; use crate::s9pk::merkle_archive::source::http::HttpSource; +use crate::s9pk::merkle_archive::source::ArchiveSource; use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::S9pk; use crate::util::io::TrackingIO; @@ -126,13 +127,16 @@ pub async fn cli_add_package( sign_phase.complete(); verify_phase.start(); - let mut src = S9pk::deserialize( - &Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?), - Some(&commitment), - ) - .await?; - src.serialize(&mut TrackingIO::new(0, tokio::io::sink()), true) + let source = HttpSource::new(ctx.client.clone(), url.clone()).await?; + let len = source.size().await; + let mut src = S9pk::deserialize(&Arc::new(source), Some(&commitment)).await?; + if let Some(len) = len { + verify_phase.set_total(len); + } + let mut verify_writer = ProgressTrackerWriter::new(tokio::io::sink(), verify_phase); + src.serialize(&mut TrackingIO::new(0, &mut verify_writer), true) .await?; + let (_, mut verify_phase) = verify_writer.into_inner(); verify_phase.complete(); index_phase.start(); @@ -140,7 +144,7 @@ pub async fn cli_add_package( &parent_method.into_iter().chain(method).join("."), imbl_value::json!({ "url": &url, - "signature": signature, + "signature": AnySignature::Ed25519(signature), "commitment": commitment, }), ) diff --git a/core/startos/src/registry/package/get.rs b/core/startos/src/registry/package/get.rs index fb63be1bc..cae1289a9 100644 --- a/core/startos/src/registry/package/get.rs +++ b/core/startos/src/registry/package/get.rs @@ -21,6 +21,7 @@ use crate::util::VersionString; #[serde(rename_all = "camelCase")] #[ts(export)] pub enum PackageDetailLevel { + None, Short, Full, } @@ -50,7 +51,9 @@ pub struct GetPackageParams { #[arg(skip)] #[serde(rename = "__device_info")] pub device_info: Option, - pub other_versions: Option, + #[serde(default)] + #[arg(default_value = "none")] + pub other_versions: PackageDetailLevel, } #[derive(Debug, Deserialize, Serialize, TS)] @@ -126,7 +129,6 @@ fn get_matching_models<'a>( db: &'a Model, GetPackageParams { id, - version, source_version, device_info, .. @@ -148,22 +150,18 @@ fn get_matching_models<'a>( .into_iter() .map(|(v, info)| { Ok::<_, Error>( - if version + if source_version.as_ref().map_or(Ok(true), |source_version| { + Ok::<_, Error>( + source_version.satisfies( + &info + .as_source_version() + .de()? + .unwrap_or(VersionRange::any()), + ), + ) + })? && device_info .as_ref() - .map_or(true, |version| v.satisfies(version)) - && source_version.as_ref().map_or(Ok(true), |source_version| { - Ok::<_, Error>( - source_version.satisfies( - &info - .as_source_version() - .de()? - .unwrap_or(VersionRange::any()), - ), - ) - })? - && device_info - .as_ref() - .map_or(Ok(true), |device_info| info.works_for_device(device_info))? + .map_or(Ok(true), |device_info| info.works_for_device(device_info))? { Some((k.clone(), ExtendedVersion::from(v), info)) } else { @@ -187,24 +185,27 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu let mut other: BTreeMap>> = Default::default(); for (id, version, info) in get_matching_models(&peek.as_index().as_package(), ¶ms)? { - let mut package_best = best.remove(&id).unwrap_or_default(); - let mut package_other = other.remove(&id).unwrap_or_default(); - for worse_version in package_best - .keys() - .filter(|k| ***k < version) - .cloned() - .collect_vec() + let package_best = best.entry(id.clone()).or_default(); + let package_other = other.entry(id.clone()).or_default(); + if params + .version + .as_ref() + .map_or(true, |v| version.satisfies(v)) + && package_best.keys().all(|k| !(**k > version)) { - if let Some(info) = package_best.remove(&worse_version) { - package_other.insert(worse_version, info); + for worse_version in package_best + .keys() + .filter(|k| ***k < version) + .cloned() + .collect_vec() + { + if let Some(info) = package_best.remove(&worse_version) { + package_other.insert(worse_version, info); + } } - } - if package_best.keys().all(|k| !(**k > version)) { package_best.insert(version.into(), info); - } - best.insert(id.clone(), package_best); - if params.other_versions.is_some() { - other.insert(id.clone(), package_other); + } else { + package_other.insert(version.into(), info); } } if let Some(id) = params.id { @@ -224,12 +225,12 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu .try_collect()?; let other = other.remove(&id).unwrap_or_default(); match params.other_versions { - None => to_value(&GetPackageResponse { + PackageDetailLevel::None => to_value(&GetPackageResponse { categories, best, other_versions: None, }), - Some(PackageDetailLevel::Short) => to_value(&GetPackageResponse { + PackageDetailLevel::Short => to_value(&GetPackageResponse { categories, best, other_versions: Some( @@ -239,7 +240,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu .try_collect()?, ), }), - Some(PackageDetailLevel::Full) => to_value(&GetPackageResponseFull { + PackageDetailLevel::Full => to_value(&GetPackageResponseFull { categories, best, other_versions: other @@ -250,7 +251,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu } } else { match params.other_versions { - None => to_value( + PackageDetailLevel::None => to_value( &best .into_iter() .map(|(id, best)| { @@ -276,7 +277,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu }) .try_collect::<_, GetPackagesResponse, _>()?, ), - Some(PackageDetailLevel::Short) => to_value( + PackageDetailLevel::Short => to_value( &best .into_iter() .map(|(id, best)| { @@ -310,7 +311,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu }) .try_collect::<_, GetPackagesResponse, _>()?, ), - Some(PackageDetailLevel::Full) => to_value( + PackageDetailLevel::Full => to_value( &best .into_iter() .map(|(id, best)| { @@ -354,7 +355,7 @@ pub fn display_package_info( } if let Some(_) = params.rest.id { - if params.rest.other_versions == Some(PackageDetailLevel::Full) { + if params.rest.other_versions == PackageDetailLevel::Full { for table in from_value::(info)?.tables() { table.print_tty(false)?; println!(); @@ -366,7 +367,7 @@ pub fn display_package_info( } } } else { - if params.rest.other_versions == Some(PackageDetailLevel::Full) { + if params.rest.other_versions == PackageDetailLevel::Full { for (_, package) in from_value::(info)? { for table in package.tables() { table.print_tty(false)?; diff --git a/core/startos/src/registry/package/index.rs b/core/startos/src/registry/package/index.rs index 80055f06d..12a17f634 100644 --- a/core/startos/src/registry/package/index.rs +++ b/core/startos/src/registry/package/index.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; +use chrono::Utc; use exver::{Version, VersionRange}; use imbl_value::InternedString; use models::{DataUrl, PackageId, VersionString}; @@ -15,7 +16,7 @@ use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey}; use crate::rpc_continuations::Guid; use crate::s9pk::git_hash::GitHash; -use crate::s9pk::manifest::{Description, HardwareRequirements}; +use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements}; use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::S9pk; @@ -49,12 +50,25 @@ pub struct Category { pub description: Description, } +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct DependencyMetadata { + #[ts(type = "string | null")] + pub title: Option, + pub icon: Option>, + pub description: Option, + pub optional: bool, +} + #[derive(Debug, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct PackageVersionInfo { - pub title: String, + #[ts(type = "string")] + pub title: InternedString, pub icon: DataUrl<'static>, pub description: Description, pub release_notes: String, @@ -70,6 +84,10 @@ pub struct PackageVersionInfo { pub support_site: Url, #[ts(type = "string")] pub marketing_site: Url, + #[ts(type = "string | null")] + pub donation_url: Option, + pub alerts: Alerts, + pub dependency_metadata: BTreeMap, #[ts(type = "string")] pub os_version: Version, pub hardware_requirements: HardwareRequirements, @@ -80,6 +98,19 @@ pub struct PackageVersionInfo { impl PackageVersionInfo { pub async fn from_s9pk(s9pk: &S9pk, url: Url) -> Result { let manifest = s9pk.as_manifest(); + let mut dependency_metadata = BTreeMap::new(); + for (id, info) in &manifest.dependencies.0 { + let metadata = s9pk.dependency_metadata(id).await?; + dependency_metadata.insert( + id.clone(), + DependencyMetadata { + title: metadata.map(|m| m.title), + icon: s9pk.dependency_icon_data_url(id).await?, + description: info.description.clone(), + optional: info.optional, + }, + ); + } Ok(Self { title: manifest.title.clone(), icon: s9pk.icon_data_url().await?, @@ -91,10 +122,14 @@ impl PackageVersionInfo { upstream_repo: manifest.upstream_repo.clone(), support_site: manifest.support_site.clone(), marketing_site: manifest.marketing_site.clone(), + donation_url: manifest.donation_url.clone(), + alerts: manifest.alerts.clone(), + dependency_metadata, os_version: manifest.os_version.clone(), hardware_requirements: manifest.hardware_requirements.clone(), source_version: None, // TODO s9pk: RegistryAsset { + published_at: Utc::now(), url, commitment: s9pk.as_archive().commitment().await?, signatures: [( @@ -114,8 +149,11 @@ impl PackageVersionInfo { table.add_row(row![bc => &self.title]); table.add_row(row![br -> "VERSION", AsRef::::as_ref(version)]); table.add_row(row![br -> "RELEASE NOTES", &self.release_notes]); - table.add_row(row![br -> "ABOUT", &self.description.short]); - table.add_row(row![br -> "DESCRIPTION", &self.description.long]); + table.add_row(row![br -> "ABOUT", &textwrap::wrap(&self.description.short, 80).join("\n")]); + table.add_row(row![ + br -> "DESCRIPTION", + &textwrap::wrap(&self.description.long, 80).join("\n") + ]); table.add_row(row![br -> "GIT HASH", AsRef::::as_ref(&self.git_hash)]); table.add_row(row![br -> "LICENSE", &self.license]); table.add_row(row![br -> "PACKAGE REPO", &self.wrapper_repo.to_string()]); diff --git a/core/startos/src/registry/package/mod.rs b/core/startos/src/registry/package/mod.rs index ac09afbb1..cb2d317f9 100644 --- a/core/startos/src/registry/package/mod.rs +++ b/core/startos/src/registry/package/mod.rs @@ -16,14 +16,21 @@ pub fn package_api() -> ParentHandler { .with_display_serializable() .with_call_remote::(), ) - .subcommand("add", from_fn_async(add::add_package).no_cli()) + .subcommand( + "add", + from_fn_async(add::add_package) + .with_metadata("get_signer", Value::Bool(true)) + .no_cli(), + ) .subcommand("add", from_fn_async(add::cli_add_package).no_display()) .subcommand( "get", from_fn_async(get::get_package) + .with_metadata("get_device_info", Value::Bool(true)) .with_display_serializable() .with_custom_display_fn(|handle, result| { get::display_package_info(handle.params, result) - }), + }) + .with_call_remote::(), ) } diff --git a/core/startos/src/registry/signer/commitment/merkle_archive.rs b/core/startos/src/registry/signer/commitment/merkle_archive.rs index 0b61734b4..1b9d7d1e0 100644 --- a/core/startos/src/registry/signer/commitment/merkle_archive.rs +++ b/core/startos/src/registry/signer/commitment/merkle_archive.rs @@ -20,6 +20,35 @@ pub struct MerkleArchiveCommitment { #[ts(type = "number")] pub root_maxsize: u64, } +impl MerkleArchiveCommitment { + pub fn from_query(query: &str) -> Result, Error> { + let mut root_sighash = None; + let mut root_maxsize = None; + for (k, v) in form_urlencoded::parse(dbg!(query).as_bytes()) { + match &*k { + "rootSighash" => { + root_sighash = Some(dbg!(v).parse()?); + } + "rootMaxsize" => { + root_maxsize = Some(v.parse()?); + } + _ => (), + } + } + if root_sighash.is_some() || root_maxsize.is_some() { + Ok(Some(Self { + root_sighash: root_sighash + .or_not_found("rootSighash required if rootMaxsize specified") + .with_kind(ErrorKind::InvalidRequest)?, + root_maxsize: root_maxsize + .or_not_found("rootMaxsize required if rootSighash specified") + .with_kind(ErrorKind::InvalidRequest)?, + })) + } else { + Ok(None) + } + } +} impl Digestable for MerkleArchiveCommitment { fn update(&self, digest: &mut D) { digest.update(&*self.root_sighash); diff --git a/core/startos/src/registry/signer/commitment/request.rs b/core/startos/src/registry/signer/commitment/request.rs index ce60b7f88..e5bb776bf 100644 --- a/core/startos/src/registry/signer/commitment/request.rs +++ b/core/startos/src/registry/signer/commitment/request.rs @@ -5,6 +5,7 @@ use axum::body::Body; use axum::extract::Request; use digest::Update; use futures::TryStreamExt; +use http::HeaderValue; use serde::{Deserialize, Serialize}; use tokio::io::AsyncWrite; use tokio_util::io::StreamReader; @@ -37,8 +38,8 @@ impl RequestCommitment { .append_pair("size", &self.size.to_string()) .append_pair("blake3", &self.blake3.to_string()); } - pub fn from_query(url: &Url) -> Result { - let query: BTreeMap<_, _> = url.query_pairs().collect(); + pub fn from_query(query: &HeaderValue) -> Result { + let query: BTreeMap<_, _> = form_urlencoded::parse(query.as_bytes()).collect(); Ok(Self { timestamp: query.get("timestamp").or_not_found("timestamp")?.parse()?, nonce: query.get("nonce").or_not_found("nonce")?.parse()?, diff --git a/core/startos/src/s9pk/merkle_archive/expected.rs b/core/startos/src/s9pk/merkle_archive/expected.rs index a0f095b7a..c9a2fd31b 100644 --- a/core/startos/src/s9pk/merkle_archive/expected.rs +++ b/core/startos/src/s9pk/merkle_archive/expected.rs @@ -1,4 +1,3 @@ - use std::ffi::OsStr; use std::path::Path; @@ -7,16 +6,16 @@ use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::merkle_archive::Entry; -/// An object for tracking the files expected to be in an s9pk +/// An object for tracking the files expected to be in an s9pk pub struct Expected<'a, T> { keep: DirectoryContents<()>, dir: &'a DirectoryContents, } impl<'a, T> Expected<'a, T> { - pub fn new(dir: &'a DirectoryContents,) -> Self { + pub fn new(dir: &'a DirectoryContents) -> Self { Self { keep: DirectoryContents::new(), - dir + dir, } } } @@ -42,22 +41,23 @@ impl<'a, T: Clone> Expected<'a, T> { path: impl AsRef, mut valid_extension: impl FnMut(Option<&OsStr>) -> bool, ) -> Result<(), Error> { - let (dir, stem) = if let Some(parent) = path.as_ref().parent().filter(|p| *p != Path::new("")) { - ( - self.dir - .get_path(parent) - .and_then(|e| e.as_directory()) - .ok_or_else(|| { - Error::new( - eyre!("directory {} missing from archive", parent.display()), - ErrorKind::ParseS9pk, - ) - })?, - path.as_ref().strip_prefix(parent).unwrap(), - ) - } else { - (self.dir, path.as_ref()) - }; + let (dir, stem) = + if let Some(parent) = path.as_ref().parent().filter(|p| *p != Path::new("")) { + ( + self.dir + .get_path(parent) + .and_then(|e| e.as_directory()) + .ok_or_else(|| { + Error::new( + eyre!("directory {} missing from archive", parent.display()), + ErrorKind::ParseS9pk, + ) + })?, + path.as_ref().strip_prefix(parent).unwrap(), + ) + } else { + (self.dir, path.as_ref()) + }; let name = dir .with_stem(&stem.as_os_str().to_string_lossy()) .filter(|(_, e)| e.as_file().is_some()) @@ -69,7 +69,7 @@ impl<'a, T: Clone> Expected<'a, T> { ), ErrorKind::ParseS9pk, )), - |acc, (name, _)| + |acc, (name, _)| if valid_extension(Path::new(&*name).extension()) { match acc { Ok(_) => Err(Error::new( @@ -96,8 +96,10 @@ impl<'a, T: Clone> Expected<'a, T> { pub struct Filter(DirectoryContents<()>); impl Filter { - pub fn keep_checked(&self, dir: &mut DirectoryContents) -> Result<(), Error> { + pub fn keep_checked( + &self, + dir: &mut DirectoryContents, + ) -> Result<(), Error> { dir.filter(|path| self.0.get_path(path).is_some()) } } - diff --git a/core/startos/src/s9pk/merkle_archive/mod.rs b/core/startos/src/s9pk/merkle_archive/mod.rs index 977e5ebb2..3f30a4ce1 100644 --- a/core/startos/src/s9pk/merkle_archive/mod.rs +++ b/core/startos/src/s9pk/merkle_archive/mod.rs @@ -233,6 +233,10 @@ impl Entry { _ => None, } } + pub fn expect_file(&self) -> Result<&FileContents, Error> { + self.as_file() + .ok_or_else(|| Error::new(eyre!("not a file"), ErrorKind::ParseS9pk)) + } pub fn as_directory(&self) -> Option<&DirectoryContents> { match self.as_contents() { EntryContents::Directory(d) => Some(d), diff --git a/core/startos/src/s9pk/merkle_archive/source/mod.rs b/core/startos/src/s9pk/merkle_archive/source/mod.rs index 6b7459787..cc9623ab6 100644 --- a/core/startos/src/s9pk/merkle_archive/source/mod.rs +++ b/core/startos/src/s9pk/merkle_archive/source/mod.rs @@ -1,3 +1,5 @@ +use std::cmp::min; +use std::io::SeekFrom; use std::ops::Deref; use std::path::PathBuf; use std::sync::Arc; @@ -6,7 +8,7 @@ use blake3::Hash; use futures::future::BoxFuture; use futures::{Future, FutureExt}; use tokio::fs::File; -use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, AsyncWrite, Take}; use crate::prelude::*; use crate::s9pk::merkle_archive::hash::VerifyingWriter; @@ -17,8 +19,14 @@ pub mod multi_cursor_file; pub trait FileSource: Send + Sync + Sized + 'static { type Reader: AsyncRead + Unpin + Send; + type SliceReader: AsyncRead + Unpin + Send; fn size(&self) -> impl Future> + Send; fn reader(&self) -> impl Future> + Send; + fn slice( + &self, + position: u64, + size: u64, + ) -> impl Future> + Send; fn copy( &self, w: &mut W, @@ -65,12 +73,16 @@ pub trait FileSource: Send + Sync + Sized + 'static { impl FileSource for Arc { type Reader = T::Reader; + type SliceReader = T::SliceReader; async fn size(&self) -> Result { self.deref().size().await } async fn reader(&self) -> Result { self.deref().reader().await } + async fn slice(&self, position: u64, size: u64) -> Result { + self.deref().slice(position, size).await + } async fn copy(&self, w: &mut W) -> Result<(), Error> { self.deref().copy(w).await } @@ -95,12 +107,16 @@ impl DynFileSource { } impl FileSource for DynFileSource { type Reader = Box; + type SliceReader = Box; async fn size(&self) -> Result { self.0.size().await } async fn reader(&self) -> Result { self.0.reader().await } + async fn slice(&self, position: u64, size: u64) -> Result { + self.0.slice(position, size).await + } async fn copy( &self, mut w: &mut W, @@ -123,6 +139,11 @@ impl FileSource for DynFileSource { trait DynableFileSource: Send + Sync + 'static { async fn size(&self) -> Result; async fn reader(&self) -> Result, Error>; + async fn slice( + &self, + position: u64, + size: u64, + ) -> Result, Error>; async fn copy(&self, w: &mut (dyn AsyncWrite + Unpin + Send)) -> Result<(), Error>; async fn copy_verify( &self, @@ -139,6 +160,13 @@ impl DynableFileSource for T { async fn reader(&self) -> Result, Error> { Ok(Box::new(FileSource::reader(self).await?)) } + async fn slice( + &self, + position: u64, + size: u64, + ) -> Result, Error> { + Ok(Box::new(FileSource::slice(self, position, size).await?)) + } async fn copy(&self, w: &mut (dyn AsyncWrite + Unpin + Send)) -> Result<(), Error> { FileSource::copy(self, w).await } @@ -156,22 +184,34 @@ impl DynableFileSource for T { impl FileSource for PathBuf { type Reader = File; + type SliceReader = Take; async fn size(&self) -> Result { Ok(tokio::fs::metadata(self).await?.len()) } async fn reader(&self) -> Result { Ok(open_file(self).await?) } + async fn slice(&self, position: u64, size: u64) -> Result { + let mut r = FileSource::reader(self).await?; + r.seek(SeekFrom::Start(position)).await?; + Ok(r.take(size)) + } } impl FileSource for Arc<[u8]> { type Reader = std::io::Cursor; + type SliceReader = Take; async fn size(&self) -> Result { Ok(self.len() as u64) } async fn reader(&self) -> Result { Ok(std::io::Cursor::new(self.clone())) } + async fn slice(&self, position: u64, size: u64) -> Result { + let mut r = FileSource::reader(self).await?; + r.seek(SeekFrom::Start(position)).await?; + Ok(r.take(size)) + } async fn copy(&self, w: &mut W) -> Result<(), Error> { use tokio::io::AsyncWriteExt; @@ -272,12 +312,18 @@ pub struct Section { } impl FileSource for Section { type Reader = S::FetchReader; + type SliceReader = S::FetchReader; async fn size(&self) -> Result { Ok(self.size) } async fn reader(&self) -> Result { self.source.fetch(self.position, self.size).await } + async fn slice(&self, position: u64, size: u64) -> Result { + self.source + .fetch(self.position + position, min(size, self.size)) + .await + } async fn copy(&self, w: &mut W) -> Result<(), Error> { self.source.copy_to(self.position, self.size, w).await } @@ -342,12 +388,16 @@ impl From> for DynFileSource { impl FileSource for TmpSource { type Reader = ::Reader; + type SliceReader = ::SliceReader; async fn size(&self) -> Result { self.source.size().await } async fn reader(&self) -> Result { self.source.reader().await } + async fn slice(&self, position: u64, size: u64) -> Result { + self.source.slice(position, size).await + } async fn copy( &self, mut w: &mut W, diff --git a/core/startos/src/s9pk/rpc.rs b/core/startos/src/s9pk/rpc.rs index 83b78dad7..92f952077 100644 --- a/core/startos/src/s9pk/rpc.rs +++ b/core/startos/src/s9pk/rpc.rs @@ -15,14 +15,36 @@ use crate::s9pk::v2::pack::ImageConfig; use crate::s9pk::v2::SIG_CONTEXT; use crate::util::io::{create_file, open_file, TmpDir}; use crate::util::serde::{apply_expr, HandlerExtSerde}; +use crate::util::Apply; pub const SKIP_ENV: &[&str] = &["TERM", "container", "HOME", "HOSTNAME"]; pub fn s9pk() -> ParentHandler { ParentHandler::new() .subcommand("pack", from_fn_async(super::v2::pack::pack).no_display()) + .subcommand( + "list-ingredients", + from_fn_async(super::v2::pack::list_ingredients).with_custom_display_fn( + |_, ingredients| { + ingredients + .into_iter() + .map(Some) + .apply(|i| itertools::intersperse(i, None)) + .for_each(|i| { + if let Some(p) = i { + print!("{}", p.display()) + } else { + print!(" ") + } + }); + println!(); + Ok(()) + }, + ), + ) .subcommand("edit", edit()) .subcommand("inspect", inspect()) + .subcommand("convert", from_fn_async(convert).no_display()) } #[derive(Deserialize, Serialize, Parser)] @@ -193,3 +215,17 @@ async fn inspect_manifest( .await?; Ok(s9pk.as_manifest().clone()) } + +async fn convert(ctx: CliContext, S9pkPath { s9pk: s9pk_path }: S9pkPath) -> Result<(), Error> { + let mut s9pk = super::load( + MultiCursorFile::from(open_file(&s9pk_path).await?), + || ctx.developer_key().cloned(), + None, + ) + .await?; + let tmp_path = s9pk_path.with_extension("s9pk.tmp"); + s9pk.serialize(&mut create_file(&tmp_path).await?, true) + .await?; + tokio::fs::rename(tmp_path, s9pk_path).await?; + Ok(()) +} diff --git a/core/startos/src/s9pk/v2/compat.rs b/core/startos/src/s9pk/v2/compat.rs index 914d2e5aa..22250419a 100644 --- a/core/startos/src/s9pk/v2/compat.rs +++ b/core/startos/src/s9pk/v2/compat.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; use std::sync::Arc; @@ -199,8 +199,9 @@ impl From for Manifest { let default_url = value.upstream_repo.clone(); Self { id: value.id, - title: value.title, + title: value.title.into(), version: ExtendedVersion::from(value.version).into(), + satisfies: BTreeSet::new(), release_notes: value.release_notes, license: value.license.into(), wrapper_repo: value.wrapper_repo, @@ -233,6 +234,7 @@ impl From for Manifest { DepInfo { description: value.description, optional: !value.requirement.required(), + s9pk: None, }, ) }) diff --git a/core/startos/src/s9pk/v2/manifest.rs b/core/startos/src/s9pk/v2/manifest.rs index 9607bb654..a10a65ddb 100644 --- a/core/startos/src/s9pk/v2/manifest.rs +++ b/core/startos/src/s9pk/v2/manifest.rs @@ -31,8 +31,10 @@ fn current_version() -> Version { #[ts(export)] pub struct Manifest { pub id: PackageId, - pub title: String, + #[ts(type = "string")] + pub title: InternedString, pub version: VersionString, + pub satisfies: BTreeSet, pub release_notes: String, #[ts(type = "string")] pub license: InternedString, // type of license @@ -81,6 +83,15 @@ impl Manifest { expected.check_file("LICENSE.md")?; expected.check_file("instructions.md")?; expected.check_file("javascript.squashfs")?; + for (dependency, _) in &self.dependencies.0 { + let dep_path = Path::new("dependencies").join(dependency); + let _ = expected.check_file(dep_path.join("metadata.json")); + let _ = expected.check_stem(dep_path.join("icon"), |ext| { + ext.and_then(|e| e.to_str()) + .and_then(mime) + .map_or(false, |mime| mime.starts_with("image/")) + }); + } for assets in &self.assets { expected.check_file(Path::new("assets").join(assets).with_extension("squashfs"))?; } @@ -148,7 +159,7 @@ impl Manifest { #[ts(export)] pub struct HardwareRequirements { #[serde(default)] - #[ts(type = "{ [key: string]: string }")] // TODO more specific key + #[ts(type = "{ device?: string, processor?: string }")] pub device: BTreeMap, #[ts(type = "number | null")] pub ram: Option, diff --git a/core/startos/src/s9pk/v2/mod.rs b/core/startos/src/s9pk/v2/mod.rs index 2477e63a0..e012480af 100644 --- a/core/startos/src/s9pk/v2/mod.rs +++ b/core/startos/src/s9pk/v2/mod.rs @@ -6,10 +6,10 @@ use imbl_value::InternedString; use models::{mime, DataUrl, PackageId}; use tokio::fs::File; +use crate::dependencies::DependencyMetadata; use crate::prelude::*; use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; use crate::s9pk::manifest::Manifest; -use crate::s9pk::merkle_archive::file_contents::FileContents; use crate::s9pk::merkle_archive::sink::Sink; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::{ @@ -18,6 +18,7 @@ use crate::s9pk::merkle_archive::source::{ use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; use crate::s9pk::v2::pack::{ImageSource, PackSource}; use crate::util::io::{open_file, TmpDir}; +use crate::util::serde::IoFormat; const MAGIC_AND_VERSION: &[u8] = &[0x3b, 0x3b, 0x02]; @@ -33,6 +34,10 @@ pub mod pack; ├── icon. ├── LICENSE.md ├── instructions.md + ├── dependencies + │ └── + │ ├── metadata.json + │ └── icon. ├── javascript.squashfs ├── assets │ └── .squashfs (xN) @@ -52,9 +57,10 @@ fn priority(s: &str) -> Option { a if Path::new(a).file_stem() == Some(OsStr::new("icon")) => Some(1), "LICENSE.md" => Some(2), "instructions.md" => Some(3), - "javascript.squashfs" => Some(4), - "assets" => Some(5), - "images" => Some(6), + "dependencies" => Some(4), + "javascript.squashfs" => Some(5), + "assets" => Some(6), + "images" => Some(7), _ => None, } } @@ -101,22 +107,16 @@ impl S9pk { filter.keep_checked(self.archive.contents_mut()) } - pub async fn icon(&self) -> Result<(InternedString, FileContents), Error> { + pub async fn icon(&self) -> Result<(InternedString, Entry), Error> { let mut best_icon = None; - for (path, icon) in self - .archive - .contents() - .with_stem("icon") - .filter(|(p, _)| { - Path::new(&*p) - .extension() - .and_then(|e| e.to_str()) - .and_then(mime) - .map_or(false, |e| e.starts_with("image/")) - }) - .filter_map(|(k, v)| v.into_file().map(|f| (k, f))) - { - let size = icon.size().await?; + for (path, icon) in self.archive.contents().with_stem("icon").filter(|(p, v)| { + Path::new(&*p) + .extension() + .and_then(|e| e.to_str()) + .and_then(mime) + .map_or(false, |e| e.starts_with("image/") && v.as_file().is_some()) + }) { + let size = icon.expect_file()?.size().await?; best_icon = match best_icon { Some((s, a)) if s >= size => Some((s, a)), _ => Some((size, (path, icon))), @@ -134,7 +134,75 @@ impl S9pk { .and_then(|e| e.to_str()) .and_then(mime) .unwrap_or("image/png"); - DataUrl::from_reader(mime, contents.reader().await?, Some(contents.size().await?)).await + Ok(DataUrl::from_vec( + mime, + contents.expect_file()?.to_vec(contents.hash()).await?, + )) + } + + pub async fn dependency_icon( + &self, + id: &PackageId, + ) -> Result)>, Error> { + let mut best_icon = None; + for (path, icon) in self + .archive + .contents() + .get_path(Path::new("dependencies").join(id)) + .and_then(|p| p.as_directory()) + .into_iter() + .flat_map(|d| { + d.with_stem("icon").filter(|(p, v)| { + Path::new(&*p) + .extension() + .and_then(|e| e.to_str()) + .and_then(mime) + .map_or(false, |e| e.starts_with("image/") && v.as_file().is_some()) + }) + }) + { + let size = icon.expect_file()?.size().await?; + best_icon = match best_icon { + Some((s, a)) if s >= size => Some((s, a)), + _ => Some((size, (path, icon))), + }; + } + Ok(best_icon.map(|(_, a)| a)) + } + + pub async fn dependency_icon_data_url( + &self, + id: &PackageId, + ) -> Result>, Error> { + let Some((name, contents)) = self.dependency_icon(id).await? else { + return Ok(None); + }; + let mime = Path::new(&*name) + .extension() + .and_then(|e| e.to_str()) + .and_then(mime) + .unwrap_or("image/png"); + Ok(Some(DataUrl::from_vec( + mime, + contents.expect_file()?.to_vec(contents.hash()).await?, + ))) + } + + pub async fn dependency_metadata( + &self, + id: &PackageId, + ) -> Result, Error> { + if let Some(entry) = self + .archive + .contents() + .get_path(Path::new("dependencies").join(id).join("metadata.json")) + { + Ok(Some(IoFormat::Json.from_slice( + &entry.expect_file()?.to_vec(entry.hash()).await?, + )?)) + } else { + Ok(None) + } } pub async fn serialize(&mut self, w: &mut W, verify: bool) -> Result<(), Error> { diff --git a/core/startos/src/s9pk/v2/pack.rs b/core/startos/src/s9pk/v2/pack.rs index ae6807c23..06a47b9d0 100644 --- a/core/startos/src/s9pk/v2/pack.rs +++ b/core/startos/src/s9pk/v2/pack.rs @@ -1,6 +1,4 @@ use std::collections::BTreeSet; -use std::ffi::OsStr; -use std::io::Cursor; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -10,25 +8,29 @@ use futures::{FutureExt, TryStreamExt}; use imbl_value::InternedString; use models::{ImageId, PackageId, VersionString}; use serde::{Deserialize, Serialize}; -use tokio::io::AsyncRead; use tokio::process::Command; use tokio::sync::OnceCell; use tokio_stream::wrappers::ReadDirStream; +use tracing::{debug, warn}; use ts_rs::TS; use crate::context::CliContext; +use crate::dependencies::DependencyMetadata; use crate::prelude::*; use crate::rpc_continuations::Guid; +use crate::s9pk::manifest::Manifest; use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; +use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::{ - into_dyn_read, ArchiveSource, DynFileSource, FileSource, TmpSource, + into_dyn_read, ArchiveSource, DynFileSource, DynRead, FileSource, TmpSource, }; use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::S9pk; use crate::util::io::{create_file, open_file, TmpDir}; -use crate::util::Invoke; +use crate::util::serde::IoFormat; +use crate::util::{new_guid, Invoke, PathOrUrl}; #[cfg(not(feature = "docker"))] pub const CONTAINER_TOOL: &str = "podman"; @@ -83,7 +85,8 @@ pub enum PackSource { Squashfs(Arc), } impl FileSource for PackSource { - type Reader = Box; + type Reader = DynRead; + type SliceReader = DynRead; async fn size(&self) -> Result { match self { Self::Buffered(a) => Ok(a.len() as u64), @@ -102,11 +105,23 @@ impl FileSource for PackSource { } async fn reader(&self) -> Result { match self { - Self::Buffered(a) => Ok(into_dyn_read(Cursor::new(a.clone()))), - Self::File(f) => Ok(into_dyn_read(open_file(f).await?)), + Self::Buffered(a) => Ok(into_dyn_read(FileSource::reader(a).await?)), + Self::File(f) => Ok(into_dyn_read(FileSource::reader(f).await?)), Self::Squashfs(dir) => dir.file().await?.fetch_all().await.map(into_dyn_read), } } + async fn slice(&self, position: u64, size: u64) -> Result { + match self { + Self::Buffered(a) => Ok(into_dyn_read(FileSource::slice(a, position, size).await?)), + Self::File(f) => Ok(into_dyn_read(FileSource::slice(f, position, size).await?)), + Self::Squashfs(dir) => dir + .file() + .await? + .fetch(position, size) + .await + .map(into_dyn_read), + } + } } impl From for DynFileSource { fn from(value: PackSource) -> Self { @@ -150,24 +165,71 @@ impl PackParams { if let Some(icon) = &self.icon { Ok(icon.clone()) } else { - ReadDirStream::new(tokio::fs::read_dir(self.path()).await?).try_filter(|x| ready(x.path().file_stem() == Some(OsStr::new("icon")))).map_err(Error::from).try_fold(Err(Error::new(eyre!("icon not found"), ErrorKind::NotFound)), |acc, x| async move { match acc { - Ok(_) => Err(Error::new(eyre!("multiple icons found in working directory, please specify which to use with `--icon`"), ErrorKind::InvalidRequest)), - Err(e) => Ok({ - let path = x.path(); - if path.file_stem().and_then(|s| s.to_str()) == Some("icon") { - Ok(path) - } else { - Err(e) - } + ReadDirStream::new(tokio::fs::read_dir(self.path()).await?) + .try_filter(|x| { + ready( + x.path() + .file_stem() + .map_or(false, |s| s.eq_ignore_ascii_case("icon")), + ) }) - }}).await? + .map_err(Error::from) + .try_fold( + Err(Error::new(eyre!("icon not found"), ErrorKind::NotFound)), + |acc, x| async move { + match acc { + Ok(_) => Err(Error::new(eyre!("multiple icons found in working directory, please specify which to use with `--icon`"), ErrorKind::InvalidRequest)), + Err(e) => Ok({ + let path = x.path(); + if path + .file_stem() + .map_or(false, |s| s.eq_ignore_ascii_case("icon")) + { + Ok(path) + } else { + Err(e) + } + }), + } + }, + ) + .await? } } - fn license(&self) -> PathBuf { - self.license - .as_ref() - .cloned() - .unwrap_or_else(|| self.path().join("LICENSE.md")) + async fn license(&self) -> Result { + if let Some(license) = &self.license { + Ok(license.clone()) + } else { + ReadDirStream::new(tokio::fs::read_dir(self.path()).await?) + .try_filter(|x| { + ready( + x.path() + .file_stem() + .map_or(false, |s| s.eq_ignore_ascii_case("license")), + ) + }) + .map_err(Error::from) + .try_fold( + Err(Error::new(eyre!("icon not found"), ErrorKind::NotFound)), + |acc, x| async move { + match acc { + Ok(_) => Err(Error::new(eyre!("multiple licenses found in working directory, please specify which to use with `--license`"), ErrorKind::InvalidRequest)), + Err(e) => Ok({ + let path = x.path(); + if path + .file_stem() + .map_or(false, |s| s.eq_ignore_ascii_case("license")) + { + Ok(path) + } else { + Err(e) + } + }), + } + }, + ) + .await? + } } fn instructions(&self) -> PathBuf { self.instructions @@ -282,6 +344,15 @@ pub enum ImageSource { DockerTag(String), } impl ImageSource { + pub fn ingredients(&self) -> Vec { + match self { + Self::Packed => Vec::new(), + Self::DockerBuild { dockerfile, .. } => { + vec![dockerfile.clone().unwrap_or_else(|| "Dockerfile".into())] + } + Self::DockerTag(_) => Vec::new(), + } + } #[instrument(skip_all)] pub fn load<'a, S: From> + FileSource + Clone>( &'a self, @@ -320,7 +391,7 @@ impl ImageSource { format!("--platform=linux/{arch}") }; // docker buildx build ${path} -o type=image,name=start9/${id} - let tag = format!("start9/{id}/{image_id}:{version}"); + let tag = format!("start9/{id}/{image_id}:{}", new_guid()); Command::new(CONTAINER_TOOL) .arg("build") .arg(workdir) @@ -501,7 +572,7 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { "LICENSE.md".into(), Entry::file(TmpSource::new( tmp_dir.clone(), - PackSource::File(params.license()), + PackSource::File(params.license().await?), )), ); files.insert( @@ -541,6 +612,54 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { s9pk.load_images(tmp_dir.clone()).await?; + let mut to_insert = Vec::new(); + for (id, dependency) in &mut s9pk.as_manifest_mut().dependencies.0 { + if let Some(s9pk) = dependency.s9pk.take() { + let s9pk = match s9pk { + PathOrUrl::Path(path) => { + S9pk::deserialize(&MultiCursorFile::from(open_file(path).await?), None) + .await? + .into_dyn() + } + PathOrUrl::Url(url) => { + if url.scheme() == "http" || url.scheme() == "https" { + S9pk::deserialize( + &Arc::new(HttpSource::new(ctx.client.clone(), url).await?), + None, + ) + .await? + .into_dyn() + } else { + return Err(Error::new( + eyre!("unknown scheme: {}", url.scheme()), + ErrorKind::InvalidRequest, + )); + } + } + }; + let dep_path = Path::new("dependencies").join(id); + to_insert.push(( + dep_path.join("metadata.json"), + Entry::file(PackSource::Buffered( + IoFormat::Json + .to_vec(&DependencyMetadata { + title: s9pk.as_manifest().title.clone(), + })? + .into(), + )), + )); + let icon = s9pk.icon().await?; + to_insert.push(( + dep_path.join(&*icon.0), + Entry::file(PackSource::Buffered( + icon.1.expect_file()?.to_vec(icon.1.hash()).await?.into(), + )), + )); + } else { + warn!("no s9pk specified for {id}, leaving metadata empty"); + } + } + s9pk.validate_and_filter(None)?; s9pk.serialize( @@ -555,3 +674,58 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { Ok(()) } + +#[instrument(skip_all)] +pub async fn list_ingredients(_: CliContext, params: PackParams) -> Result, Error> { + let js_path = params.javascript().join("index.js"); + let manifest: Manifest = match async { + serde_json::from_slice( + &Command::new("node") + .arg("-e") + .arg(format!( + "console.log(JSON.stringify(require('{}').manifest))", + js_path.display() + )) + .invoke(ErrorKind::Javascript) + .await?, + ) + .with_kind(ErrorKind::Deserialization) + } + .await + { + Ok(m) => m, + Err(e) => { + warn!("failed to load manifest: {e}"); + debug!("{e:?}"); + return Ok(vec![ + js_path, + params.icon().await?, + params.license().await?, + params.instructions(), + ]); + } + }; + let mut ingredients = vec![ + js_path, + params.icon().await?, + params.license().await?, + params.instructions(), + ]; + + for (_, dependency) in manifest.dependencies.0 { + if let Some(PathOrUrl::Path(p)) = dependency.s9pk { + ingredients.push(p); + } + } + + let assets_dir = params.assets(); + for assets in manifest.assets { + ingredients.push(assets_dir.join(assets)); + } + + for image in manifest.images.values() { + ingredients.extend(image.source.ingredients()); + } + + Ok(ingredients) +} diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index 7547584e0..79ca4dc42 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -1147,18 +1147,14 @@ enum DependencyRequirement { #[ts(type = "string[]")] health_checks: BTreeSet, #[ts(type = "string")] - version_spec: VersionRange, - #[ts(type = "string")] - registry_url: Url, + version_range: VersionRange, }, #[serde(rename_all = "camelCase")] Exists { #[ts(type = "string")] id: PackageId, #[ts(type = "string")] - version_spec: VersionRange, - #[ts(type = "string")] - registry_url: Url, + version_range: VersionRange, }, } // filebrowser:exists,bitcoind:running:foo+bar+baz @@ -1168,8 +1164,7 @@ impl FromStr for DependencyRequirement { match s.split_once(':') { Some((id, "e")) | Some((id, "exists")) => Ok(Self::Exists { id: id.parse()?, - registry_url: "".parse()?, // TODO - version_spec: "*".parse()?, // TODO + version_range: "*".parse()?, // TODO }), Some((id, rest)) => { let health_checks = match rest.split_once(':') { @@ -1192,15 +1187,13 @@ impl FromStr for DependencyRequirement { Ok(Self::Running { id: id.parse()?, health_checks, - registry_url: "".parse()?, // TODO - version_spec: "*".parse()?, // TODO + version_range: "*".parse()?, // TODO }) } None => Ok(Self::Running { id: s.parse()?, health_checks: BTreeSet::new(), - registry_url: "".parse()?, // TODO - version_spec: "*".parse()?, // TODO + version_range: "*".parse()?, // TODO }), } } @@ -1234,59 +1227,20 @@ async fn set_dependencies( let mut deps = BTreeMap::new(); for dependency in dependencies { - let (dep_id, kind, registry_url, version_spec) = match dependency { - DependencyRequirement::Exists { - id, - registry_url, - version_spec, - } => ( - id, - CurrentDependencyKind::Exists, - registry_url, - version_spec, - ), + let (dep_id, kind, version_range) = match dependency { + DependencyRequirement::Exists { id, version_range } => { + (id, CurrentDependencyKind::Exists, version_range) + } DependencyRequirement::Running { id, health_checks, - registry_url, - version_spec, + version_range, } => ( id, CurrentDependencyKind::Running { health_checks }, - registry_url, - version_spec, + version_range, ), }; - let (icon, title) = match async { - let remote_s9pk = S9pk::deserialize( - &Arc::new( - HttpSource::new( - context.seed.ctx.client.clone(), - registry_url - .join(&format!("package/v2/{}.s9pk?spec={}", dep_id, version_spec))?, - ) - .await?, - ), - None, // TODO - ) - .await?; - - let icon = remote_s9pk.icon_data_url().await?; - - Ok::<_, Error>((icon, remote_s9pk.as_manifest().title.clone())) - } - .await - { - Ok(a) => a, - Err(e) => { - tracing::error!("Error fetching remote s9pk: {e}"); - tracing::debug!("{e:?}"); - ( - DataUrl::from_slice("image/png", include_bytes!("../install/package-icon.png")), - dep_id.to_string(), - ) - } - }; let config_satisfied = if let Some(dep_service) = &*context.seed.ctx.services.get(&dep_id).await { context @@ -1300,17 +1254,25 @@ async fn set_dependencies( } else { true }; - deps.insert( - dep_id, - CurrentDependencyInfo { - kind, - registry_url, - version_spec, - icon, - title, - config_satisfied, - }, - ); + let info = CurrentDependencyInfo { + title: context + .seed + .persistent_container + .s9pk + .dependency_metadata(&dep_id) + .await? + .map(|m| m.title), + icon: context + .seed + .persistent_container + .s9pk + .dependency_icon_data_url(&dep_id) + .await?, + kind, + version_range, + config_satisfied, + }; + deps.insert(dep_id, info); } context .seed @@ -1343,23 +1305,19 @@ async fn get_dependencies(context: EffectContext) -> Result(match kind { - CurrentDependencyKind::Exists => DependencyRequirement::Exists { - id, - registry_url, - version_spec, - }, + CurrentDependencyKind::Exists => { + DependencyRequirement::Exists { id, version_range } + } CurrentDependencyKind::Running { health_checks } => { DependencyRequirement::Running { id, health_checks, - version_spec, - registry_url, + version_range, } } }) @@ -1381,7 +1339,8 @@ struct CheckDependenciesResult { package_id: PackageId, is_installed: bool, is_running: bool, - health_checks: Vec, + config_satisfied: bool, + health_checks: BTreeMap, #[ts(type = "string | null")] version: Option, } @@ -1415,24 +1374,27 @@ async fn check_dependencies( package_id, is_installed: false, is_running: false, - health_checks: vec![], + config_satisfied: false, + health_checks: Default::default(), version: None, }); continue; }; - let installed_version = package - .as_state_info() - .as_manifest(ManifestPreference::New) - .as_version() - .de()? - .into_version(); + let manifest = package.as_state_info().as_manifest(ManifestPreference::New); + let installed_version = manifest.as_version().de()?.into_version(); + let satisfies = manifest.as_satisfies().de()?; let version = Some(installed_version.clone()); - if !installed_version.satisfies(&dependency_info.version_spec) { + if ![installed_version] + .into_iter() + .chain(satisfies.into_iter().map(|v| v.into_version())) + .any(|v| v.satisfies(&dependency_info.version_range)) + { results.push(CheckDependenciesResult { package_id, is_installed: false, is_running: false, - health_checks: vec![], + config_satisfied: false, + health_checks: Default::default(), version, }); continue; @@ -1444,17 +1406,23 @@ async fn check_dependencies( } else { false }; - let health_checks = status - .health() - .cloned() - .unwrap_or_default() - .into_iter() - .map(|(_, val)| val) - .collect(); + let health_checks = + if let CurrentDependencyKind::Running { health_checks } = &dependency_info.kind { + status + .health() + .cloned() + .unwrap_or_default() + .into_iter() + .filter(|(id, _)| health_checks.contains(id)) + .collect() + } else { + Default::default() + }; results.push(CheckDependenciesResult { package_id, is_installed, is_running, + config_satisfied: dependency_info.config_satisfied, health_checks, version, }); diff --git a/core/startos/src/util/mod.rs b/core/startos/src/util/mod.rs index f591eaaf3..3b66d7507 100644 --- a/core/startos/src/util/mod.rs +++ b/core/startos/src/util/mod.rs @@ -1,13 +1,16 @@ use std::collections::{BTreeMap, VecDeque}; +use std::fmt; use std::future::Future; use std::marker::PhantomData; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::process::Stdio; +use std::str::FromStr; use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration; +use ::serde::{Deserialize, Serialize}; use async_trait::async_trait; use color_eyre::eyre::{self, eyre}; use fd_lock_rs::FdLock; @@ -24,9 +27,12 @@ use tokio::fs::File; use tokio::io::{AsyncRead, AsyncReadExt, BufReader}; use tokio::sync::{oneshot, Mutex, OwnedMutexGuard, RwLock}; use tracing::instrument; +use ts_rs::TS; +use url::Url; use crate::shutdown::Shutdown; use crate::util::io::create_file; +use crate::util::serde::{deserialize_from_str, serialize_display}; use crate::{Error, ErrorKind, ResultExt as _}; pub mod actor; pub mod clap; @@ -648,3 +654,48 @@ pub fn new_guid() -> InternedString { &buf, )) } + +#[derive(Debug, Clone, TS)] +#[ts(type = "string")] +pub enum PathOrUrl { + Path(PathBuf), + Url(Url), +} +impl FromStr for PathOrUrl { + type Err = ::Err; + fn from_str(s: &str) -> Result { + if let Ok(url) = s.parse::() { + if url.scheme() == "file" { + Ok(Self::Path(url.path().parse()?)) + } else { + Ok(Self::Url(url)) + } + } else { + Ok(Self::Path(s.parse()?)) + } + } +} +impl fmt::Display for PathOrUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Path(p) => write!(f, "file://{}", p.display()), + Self::Url(u) => write!(f, "{u}"), + } + } +} +impl<'de> Deserialize<'de> for PathOrUrl { + fn deserialize(deserializer: D) -> Result + where + D: ::serde::Deserializer<'de>, + { + deserialize_from_str(deserializer) + } +} +impl Serialize for PathOrUrl { + fn serialize(&self, serializer: S) -> Result + where + S: ::serde::Serializer, + { + serialize_display(self, serializer) + } +} diff --git a/core/startos/src/util/net.rs b/core/startos/src/util/net.rs index 93131f16e..9e1beeaba 100644 --- a/core/startos/src/util/net.rs +++ b/core/startos/src/util/net.rs @@ -1,19 +1,25 @@ +use core::fmt; use std::borrow::Cow; +use std::sync::Mutex; use axum::extract::ws::{self, CloseFrame}; -use futures::Future; +use futures::{Future, Stream, StreamExt}; use crate::prelude::*; pub trait WebSocketExt { fn normal_close( self, - msg: impl Into>, - ) -> impl Future>; + msg: impl Into> + Send, + ) -> impl Future> + Send; + fn close_result( + self, + result: Result> + Send, impl fmt::Display + Send>, + ) -> impl Future> + Send; } impl WebSocketExt for ws::WebSocket { - async fn normal_close(mut self, msg: impl Into>) -> Result<(), Error> { + async fn normal_close(mut self, msg: impl Into> + Send) -> Result<(), Error> { self.send(ws::Message::Close(Some(CloseFrame { code: 1000, reason: msg.into(), @@ -21,4 +27,41 @@ impl WebSocketExt for ws::WebSocket { .await .with_kind(ErrorKind::Network) } + async fn close_result( + mut self, + result: Result> + Send, impl fmt::Display + Send>, + ) -> Result<(), Error> { + match result { + Ok(msg) => self + .send(ws::Message::Close(Some(CloseFrame { + code: 1000, + reason: msg.into(), + }))) + .await + .with_kind(ErrorKind::Network), + Err(e) => self + .send(ws::Message::Close(Some(CloseFrame { + code: 1011, + reason: e.to_string().into(), + }))) + .await + .with_kind(ErrorKind::Network), + } + } +} + +pub struct SyncBody(Mutex); +impl From for SyncBody { + fn from(value: axum::body::Body) -> Self { + SyncBody(Mutex::new(value.into_data_stream())) + } +} +impl Stream for SyncBody { + type Item = ::Item; + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.0.lock().unwrap().poll_next_unpin(cx) + } } diff --git a/core/startos/src/util/rpc.rs b/core/startos/src/util/rpc.rs index 54664833b..80d6d9251 100644 --- a/core/startos/src/util/rpc.rs +++ b/core/startos/src/util/rpc.rs @@ -12,7 +12,7 @@ use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::ArchiveSource; use crate::util::io::{open_file, ParallelBlake3Writer}; use crate::util::serde::Base16; -use crate::util::Apply; +use crate::util::{Apply, PathOrUrl}; use crate::CAP_10_MiB; pub fn util() -> ParentHandler { @@ -45,21 +45,20 @@ pub async fn b3sum( } b3sum_source(file).await } - if let Ok(url) = file.parse::() { - if url.scheme() == "file" { - b3sum_file(url.path(), allow_mmap).await - } else if url.scheme() == "http" || url.scheme() == "https" { - HttpSource::new(ctx.client.clone(), url) - .await? - .apply(b3sum_source) - .await - } else { - return Err(Error::new( - eyre!("unknown scheme: {}", url.scheme()), - ErrorKind::InvalidRequest, - )); + match file.parse::()? { + PathOrUrl::Path(path) => b3sum_file(path, allow_mmap).await, + PathOrUrl::Url(url) => { + if url.scheme() == "http" || url.scheme() == "https" { + HttpSource::new(ctx.client.clone(), url) + .await? + .apply(b3sum_source) + .await + } else { + Err(Error::new( + eyre!("unknown scheme: {}", url.scheme()), + ErrorKind::InvalidRequest, + )) + } } - } else { - b3sum_file(file, allow_mmap).await } } diff --git a/core/startos/src/util/serde.rs b/core/startos/src/util/serde.rs index 44a69165e..bac2c524a 100644 --- a/core/startos/src/util/serde.rs +++ b/core/startos/src/util/serde.rs @@ -1,8 +1,10 @@ +use std::any::Any; use std::collections::VecDeque; use std::marker::PhantomData; use std::ops::Deref; use std::str::FromStr; +use base64::Engine; use clap::builder::ValueParserFactory; use clap::{ArgMatches, CommandFactory, FromArgMatches}; use color_eyre::eyre::eyre; @@ -37,7 +39,11 @@ pub fn deserialize_from_str< { type Value = T; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(formatter, "a parsable string") + write!( + formatter, + "a string that can be parsed as a {}", + std::any::type_name::() + ) } fn visit_str(self, v: &str) -> Result where @@ -988,18 +994,24 @@ impl> Serialize for Base32 { } } +pub const BASE64: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new( + &base64::alphabet::STANDARD, + base64::engine::GeneralPurposeConfig::new(), +); + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, TS)] #[ts(type = "string", concrete(T = Vec))] pub struct Base64(pub T); impl> std::fmt::Display for Base64 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&base64::encode(self.0.as_ref())) + f.write_str(&BASE64.encode(self.0.as_ref())) } } impl>> FromStr for Base64 { type Err = Error; fn from_str(s: &str) -> Result { - base64::decode(&s) + BASE64 + .decode(&s) .with_kind(ErrorKind::Deserialization)? .apply(TryFrom::try_from) .map(Self) diff --git a/debian/postinst b/debian/postinst index 96e392fc8..cafa691e0 100755 --- a/debian/postinst +++ b/debian/postinst @@ -49,9 +49,9 @@ managed=true EOF $SYSTEMCTL enable startd.service $SYSTEMCTL enable systemd-resolved.service -$SYSTEMCTL enable systemd-networkd-wait-online.service $SYSTEMCTL enable ssh.service $SYSTEMCTL disable wpa_supplicant.service +$SYSTEMCTL mask systemd-networkd-wait-online.service # currently use `NetworkManager-wait-online.service` $SYSTEMCTL disable docker.service $SYSTEMCTL disable postgresql.service diff --git a/sdk/.prettierignore b/sdk/.prettierignore new file mode 100644 index 000000000..19b24bbe8 --- /dev/null +++ b/sdk/.prettierignore @@ -0,0 +1 @@ +/lib/exver/exver.ts \ No newline at end of file diff --git a/sdk/Makefile b/sdk/Makefile index 4d01fa3d7..ef1c886f5 100644 --- a/sdk/Makefile +++ b/sdk/Makefile @@ -17,6 +17,9 @@ lib/test/output.ts: node_modules lib/test/makeOutput.ts scripts/oldSpecToBuilder bundle: dist | test fmt touch dist +lib/exver/exver.ts: node_modules lib/exver/exver.pegjs + npx peggy --allowed-start-rules '*' --plugin ./node_modules/ts-pegjs/dist/tspegjs -o lib/exver/exver.ts lib/exver/exver.pegjs + dist: $(TS_FILES) package.json node_modules README.md LICENSE npx tsc npx tsc --project tsconfig-cjs.json @@ -31,7 +34,7 @@ check: npm run check fmt: node_modules - npx prettier --write "**/*.ts" + npx prettier . "**/*.ts" --write node_modules: package.json npm ci diff --git a/sdk/jest.config.js b/sdk/jest.config.js index c6aed8f3d..c38fa5062 100644 --- a/sdk/jest.config.js +++ b/sdk/jest.config.js @@ -5,4 +5,4 @@ module.exports = { testEnvironment: "node", rootDir: "./lib/", modulePathIgnorePatterns: ["./dist/"], -}; +} diff --git a/sdk/lib/Dependency.ts b/sdk/lib/Dependency.ts index 1e70629da..067ed653e 100644 --- a/sdk/lib/Dependency.ts +++ b/sdk/lib/Dependency.ts @@ -1,17 +1,17 @@ -import { Checker } from "./emverLite/mod" +import { VersionRange } from "./exver" export class Dependency { constructor( readonly data: | { type: "running" - versionSpec: Checker + versionRange: VersionRange registryUrl: string healthChecks: string[] } | { type: "exists" - versionSpec: Checker + versionRange: VersionRange registryUrl: string }, ) {} diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index fac78a4af..f09d83750 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -1,4 +1,3 @@ -import { ManifestVersion, SDKManifest } from "./manifest/ManifestTypes" import { RequiredDefault, Value } from "./config/builder/value" import { Config, ExtractConfigType, LazyBuild } from "./config/builder/config" import { @@ -21,7 +20,6 @@ import { MaybePromise, ServiceInterfaceId, PackageId, - ValidIfNoStupidEscape, } from "./types" import * as patterns from "./util/patterns" import { DependencyConfig, Update } from "./dependencies/DependencyConfig" @@ -74,12 +72,14 @@ import { splitCommand } from "./util/splitCommand" import { Mounts } from "./mainFn/Mounts" import { Dependency } from "./Dependency" import * as T from "./types" -import { Checker, EmVer } from "./emverLite/mod" +import { testTypeVersion, ValidateExVer } from "./exver" import { ExposedStorePaths } from "./store/setupExposeStore" import { PathBuilder, extractJsonPath, pathBuilder } from "./store/PathBuilder" import { checkAllDependencies } from "./dependencies/dependencies" import { health } from "." +export const SDKVersion = testTypeVersion("0.3.6") + // prettier-ignore type AnyNeverCond = T extends [] ? Else : @@ -98,12 +98,12 @@ function removeConstType() { return (t: T) => t as T & (E extends MainEffects ? {} : { const: never }) } -export class StartSdk { +export class StartSdk { private constructor(readonly manifest: Manifest) {} static of() { return new StartSdk(null as never) } - withManifest(manifest: Manifest) { + withManifest(manifest: Manifest) { return new StartSdk(manifest) } withStore>() { @@ -191,7 +191,7 @@ export class StartSdk { id: keyof Manifest["images"] & T.ImageId sharedRun?: boolean }, - command: ValidIfNoStupidEscape | [string, ...string[]], + command: T.CommandType, options: CommandOptions & { mounts?: { path: string; options: MountOptions }[] }, @@ -335,7 +335,7 @@ export class StartSdk { ([ id, { - data: { versionSpec, ...x }, + data: { versionRange, ...x }, }, ]) => ({ id, @@ -348,7 +348,7 @@ export class StartSdk { : { kind: "exists", }), - versionSpec: versionSpec.range, + versionRange: versionRange.toString(), }), ), }) @@ -432,9 +432,6 @@ export class StartSdk { spec: Spec, ) => Config.of(spec), }, - Checker: { - parse: Checker.parse, - }, Daemons: { of(config: { effects: Effects @@ -474,10 +471,6 @@ export class StartSdk { >(dependencyConfig, update) }, }, - EmVer: { - from: EmVer.from, - parse: EmVer.parse, - }, List: { text: List.text, obj: >( @@ -524,8 +517,8 @@ export class StartSdk { ) => List.dynamicText(getA), }, Migration: { - of: (options: { - version: Version + of: (options: { + version: Version & ValidateExVer up: (opts: { effects: Effects }) => Promise down: (opts: { effects: Effects }) => Promise }) => Migration.of(options), @@ -720,7 +713,7 @@ export class StartSdk { } } -export async function runCommand( +export async function runCommand( effects: Effects, image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean }, command: string | [string, ...string[]], diff --git a/sdk/lib/actions/createAction.ts b/sdk/lib/actions/createAction.ts index 2fe4dfa74..4fa858d56 100644 --- a/sdk/lib/actions/createAction.ts +++ b/sdk/lib/actions/createAction.ts @@ -1,12 +1,13 @@ +import * as T from "../types" import { Config, ExtractConfigType } from "../config/builder/config" -import { SDKManifest } from "../manifest/ManifestTypes" + import { ActionMetadata, ActionResult, Effects, ExportedAction } from "../types" -export type MaybeFn = +export type MaybeFn = | Value | ((options: { effects: Effects }) => Promise | Value) export class CreatedAction< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, ConfigType extends | Record @@ -30,7 +31,7 @@ export class CreatedAction< ) {} static of< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, ConfigType extends | Record diff --git a/sdk/lib/actions/setupActions.ts b/sdk/lib/actions/setupActions.ts index 9dd9937b4..07b4e2606 100644 --- a/sdk/lib/actions/setupActions.ts +++ b/sdk/lib/actions/setupActions.ts @@ -1,8 +1,8 @@ -import { SDKManifest } from "../manifest/ManifestTypes" +import * as T from "../types" import { Effects, ExpectedExports } from "../types" import { CreatedAction } from "./createAction" -export function setupActions( +export function setupActions( ...createdActions: CreatedAction[] ) { const myActions = async (options: { effects: Effects }) => { diff --git a/sdk/lib/backup/Backups.ts b/sdk/lib/backup/Backups.ts index 6ffe74b70..6751b1910 100644 --- a/sdk/lib/backup/Backups.ts +++ b/sdk/lib/backup/Backups.ts @@ -1,5 +1,3 @@ -import { recursive } from "ts-matches" -import { SDKManifest } from "../manifest/ManifestTypes" import * as T from "../types" import * as child_process from "child_process" @@ -41,14 +39,14 @@ export type BackupSet = { * ).build()q * ``` */ -export class Backups { +export class Backups { static BACKUP: BACKUP = "BACKUP" private constructor( private options = DEFAULT_OPTIONS, private backupSet = [] as BackupSet[], ) {} - static volumes( + static volumes( ...volumeNames: Array ): Backups { return new Backups().addSets( @@ -60,12 +58,12 @@ export class Backups { })), ) } - static addSets( + static addSets( ...options: BackupSet[] ) { return new Backups().addSets(...options) } - static with_options( + static with_options( options?: Partial, ) { return new Backups({ ...DEFAULT_OPTIONS, ...options }) diff --git a/sdk/lib/backup/setupBackups.ts b/sdk/lib/backup/setupBackups.ts index 69d1bf7df..40be01829 100644 --- a/sdk/lib/backup/setupBackups.ts +++ b/sdk/lib/backup/setupBackups.ts @@ -1,13 +1,13 @@ import { Backups } from "./Backups" -import { SDKManifest } from "../manifest/ManifestTypes" -import { ExpectedExports, PathMaker } from "../types" + +import * as T from "../types" import { _ } from "../util" -export type SetupBackupsParams = Array< +export type SetupBackupsParams = Array< M["volumes"][number] | Backups > -export function setupBackups( +export function setupBackups( ...args: _> ) { const backups = Array>() @@ -21,22 +21,22 @@ export function setupBackups( } backups.push(Backups.volumes(...volumes)) const answer: { - createBackup: ExpectedExports.createBackup - restoreBackup: ExpectedExports.restoreBackup + createBackup: T.ExpectedExports.createBackup + restoreBackup: T.ExpectedExports.restoreBackup } = { get createBackup() { return (async (options) => { for (const backup of backups) { await backup.build(options.pathMaker).createBackup(options) } - }) as ExpectedExports.createBackup + }) as T.ExpectedExports.createBackup }, get restoreBackup() { return (async (options) => { for (const backup of backups) { await backup.build(options.pathMaker).restoreBackup(options) } - }) as ExpectedExports.restoreBackup + }) as T.ExpectedExports.restoreBackup }, } return answer diff --git a/sdk/lib/config/configDependencies.ts b/sdk/lib/config/configDependencies.ts index 2ab091e18..d9865f25c 100644 --- a/sdk/lib/config/configDependencies.ts +++ b/sdk/lib/config/configDependencies.ts @@ -1,22 +1,21 @@ -import { SDKManifest } from "../manifest/ManifestTypes" -import { Dependencies } from "../types" +import * as T from "../types" -export type ConfigDependencies = { - exists(id: keyof T["dependencies"]): Dependencies[number] +export type ConfigDependencies = { + exists(id: keyof T["dependencies"]): T.Dependencies[number] running( id: keyof T["dependencies"], healthChecks: string[], - ): Dependencies[number] + ): T.Dependencies[number] } export const configDependenciesSet = < - T extends SDKManifest, + T extends T.Manifest, >(): ConfigDependencies => ({ exists(id: keyof T["dependencies"]) { return { id, kind: "exists", - } as Dependencies[number] + } as T.Dependencies[number] }, running(id: keyof T["dependencies"], healthChecks: string[]) { @@ -24,6 +23,6 @@ export const configDependenciesSet = < id, kind: "running", healthChecks, - } as Dependencies[number] + } as T.Dependencies[number] }, }) diff --git a/sdk/lib/config/setupConfig.ts b/sdk/lib/config/setupConfig.ts index ba82dbad6..8a1550d57 100644 --- a/sdk/lib/config/setupConfig.ts +++ b/sdk/lib/config/setupConfig.ts @@ -1,5 +1,5 @@ -import { Effects, ExpectedExports } from "../types" -import { SDKManifest } from "../manifest/ManifestTypes" +import * as T from "../types" + import * as D from "./configDependencies" import { Config, ExtractConfigType } from "./builder/config" import nullIfEmpty from "../util/nullIfEmpty" @@ -16,7 +16,7 @@ export type Save< | Config, any> | Config, never>, > = (options: { - effects: Effects + effects: T.Effects input: ExtractConfigType & Record }) => Promise<{ dependenciesReceipt: DependenciesReceipt @@ -24,14 +24,14 @@ export type Save< restart: boolean }> export type Read< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, A extends | Record | Config, any> | Config, never>, > = (options: { - effects: Effects + effects: T.Effects }) => Promise & Record)> /** * We want to setup a config export with a get and set, this @@ -46,7 +46,7 @@ export function setupConfig< | Record | Config | Config, - Manifest extends SDKManifest, + Manifest extends T.Manifest, Type extends Record = ExtractConfigType, >( spec: Config | Config, @@ -69,7 +69,7 @@ export function setupConfig< if (restart) { await effects.restart() } - }) as ExpectedExports.setConfig, + }) as T.ExpectedExports.setConfig, getConfig: (async ({ effects }) => { const configValue = nullIfEmpty((await read({ effects })) || null) return { @@ -78,7 +78,7 @@ export function setupConfig< }), config: configValue, } - }) as ExpectedExports.getConfig, + }) as T.ExpectedExports.getConfig, } } diff --git a/sdk/lib/dependencies/DependencyConfig.ts b/sdk/lib/dependencies/DependencyConfig.ts index d7ce435ad..b48bf56d3 100644 --- a/sdk/lib/dependencies/DependencyConfig.ts +++ b/sdk/lib/dependencies/DependencyConfig.ts @@ -1,11 +1,6 @@ -import { - DependencyConfig as DependencyConfigType, - DeepPartial, - Effects, -} from "../types" +import * as T from "../types" import { deepEqual } from "../util/deepEqual" import { deepMerge } from "../util/deepMerge" -import { SDKManifest } from "../manifest/ManifestTypes" export type Update = (options: { remoteConfig: RemoteConfig @@ -13,7 +8,7 @@ export type Update = (options: { }) => Promise export class DependencyConfig< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, Input extends Record, RemoteConfig extends Record, @@ -26,16 +21,16 @@ export class DependencyConfig< } constructor( readonly dependencyConfig: (options: { - effects: Effects + effects: T.Effects localConfig: Input - }) => Promise>, + }) => Promise>, readonly update: Update< - void | DeepPartial, + void | T.DeepPartial, RemoteConfig > = DependencyConfig.defaultUpdate as any, ) {} - async query(options: { effects: Effects; localConfig: unknown }) { + async query(options: { effects: T.Effects; localConfig: unknown }) { return this.dependencyConfig({ localConfig: options.localConfig as Input, effects: options.effects, diff --git a/sdk/lib/dependencies/dependencies.ts b/sdk/lib/dependencies/dependencies.ts index c074d2ad7..28b04a07b 100644 --- a/sdk/lib/dependencies/dependencies.ts +++ b/sdk/lib/dependencies/dependencies.ts @@ -3,20 +3,23 @@ import { PackageId, DependencyRequirement, SetHealth, - CheckDependencyResult, + CheckDependenciesResult, } from "../types" export type CheckAllDependencies = { - notRunning: () => Promise - - notInstalled: () => Promise - + notInstalled: () => Promise + notRunning: () => Promise + configNotSatisfied: () => Promise healthErrors: () => Promise<{ [id: string]: SetHealth[] }> - throwIfNotRunning: () => Promise - throwIfNotValid: () => Promise - throwIfNotInstalled: () => Promise - throwIfError: () => Promise + isValid: () => Promise + + throwIfNotRunning: () => Promise + throwIfNotInstalled: () => Promise + throwIfConfigNotSatisfied: () => Promise + throwIfHealthError: () => Promise + + throwIfNotValid: () => Promise } export function checkAllDependencies(effects: Effects): CheckAllDependencies { const dependenciesPromise = effects.getDependencies() @@ -45,14 +48,16 @@ export function checkAllDependencies(effects: Effects): CheckAllDependencies { if (!dependency) continue if (dependency.kind !== "running") continue - const healthChecks = result.healthChecks - .filter((x) => dependency.healthChecks.includes(x.id)) + const healthChecks = Object.entries(result.healthChecks) + .map(([id, hc]) => ({ ...hc, id })) .filter((x) => !!x.message) if (healthChecks.length === 0) continue answer[result.packageId] = healthChecks } return answer } + const configNotSatisfied = () => + resultsPromise.then((x) => x.filter((x) => !x.configSatisfied)) const notInstalled = () => resultsPromise.then((x) => x.filter((x) => !x.isInstalled)) const notRunning = async () => { @@ -68,7 +73,7 @@ export function checkAllDependencies(effects: Effects): CheckAllDependencies { const entries = (x: { [k: string]: B }) => Object.entries(x) const first = (x: A[]): A | undefined => x[0] const sinkVoid = (x: A) => void 0 - const throwIfError = () => + const throwIfHealthError = () => healthErrors() .then(entries) .then(first) @@ -78,6 +83,14 @@ export function checkAllDependencies(effects: Effects): CheckAllDependencies { if (healthChecks.length > 0) throw `Package ${id} has the following errors: ${healthChecks.map((x) => x.message).join(", ")}` }) + + const throwIfConfigNotSatisfied = () => + configNotSatisfied().then((results) => { + throw new Error( + `Package ${results[0].packageId} does not have a valid configuration`, + ) + }) + const throwIfNotRunning = () => notRunning().then((results) => { if (results[0]) @@ -93,7 +106,8 @@ export function checkAllDependencies(effects: Effects): CheckAllDependencies { Promise.all([ throwIfNotRunning(), throwIfNotInstalled(), - throwIfError(), + throwIfConfigNotSatisfied(), + throwIfHealthError(), ]).then(sinkVoid) const isValid = () => @@ -105,11 +119,13 @@ export function checkAllDependencies(effects: Effects): CheckAllDependencies { return { notRunning, notInstalled, + configNotSatisfied, healthErrors, throwIfNotRunning, + throwIfConfigNotSatisfied, throwIfNotValid, throwIfNotInstalled, - throwIfError, + throwIfHealthError, isValid, } } diff --git a/sdk/lib/dependencies/setupDependencyConfig.ts b/sdk/lib/dependencies/setupDependencyConfig.ts index c67c46a44..2fde4bce5 100644 --- a/sdk/lib/dependencies/setupDependencyConfig.ts +++ b/sdk/lib/dependencies/setupDependencyConfig.ts @@ -1,12 +1,12 @@ import { Config } from "../config/builder/config" -import { SDKManifest } from "../manifest/ManifestTypes" -import { ExpectedExports } from "../types" + +import * as T from "../types" import { DependencyConfig } from "./DependencyConfig" export function setupDependencyConfig< Store, Input extends Record, - Manifest extends SDKManifest, + Manifest extends T.Manifest, >( _config: Config | Config, autoConfigs: { @@ -17,6 +17,6 @@ export function setupDependencyConfig< any > | null }, -): ExpectedExports.dependencyConfig { +): T.ExpectedExports.dependencyConfig { return autoConfigs } diff --git a/sdk/lib/emverLite/mod.ts b/sdk/lib/emverLite/mod.ts deleted file mode 100644 index 52fb4e347..000000000 --- a/sdk/lib/emverLite/mod.ts +++ /dev/null @@ -1,323 +0,0 @@ -import * as matches from "ts-matches" - -const starSub = /((\d+\.)*\d+)\.\*/ -// prettier-ignore -export type ValidEmVer = string; -// prettier-ignore -export type ValidEmVerRange = string; - -function incrementLastNumber(list: number[]) { - const newList = [...list] - newList[newList.length - 1]++ - return newList -} -/** - * Will take in a range, like `>1.2` or `<1.2.3.4` or `=1.2` or `1.*` - * and return a checker, that has the check function for checking that a version is in the valid - * @param range - * @returns - */ -export function rangeOf(range: string | Checker): Checker { - return Checker.parse(range) -} - -/** - * Used to create a checker that will `and` all the ranges passed in - * @param ranges - * @returns - */ -export function rangeAnd(...ranges: (string | Checker)[]): Checker { - if (ranges.length === 0) { - throw new Error("No ranges given") - } - const [firstCheck, ...rest] = ranges - return Checker.parse(firstCheck).and(...rest) -} - -/** - * Used to create a checker that will `or` all the ranges passed in - * @param ranges - * @returns - */ -export function rangeOr(...ranges: (string | Checker)[]): Checker { - if (ranges.length === 0) { - throw new Error("No ranges given") - } - const [firstCheck, ...rest] = ranges - return Checker.parse(firstCheck).or(...rest) -} - -/** - * This will negate the checker, so given a checker that checks for >= 1.0.0, it will check for < 1.0.0 - * @param range - * @returns - */ -export function notRange(range: string | Checker): Checker { - return rangeOf(range).not() -} - -/** - * EmVer is a set of versioning of any pattern like 1 or 1.2 or 1.2.3 or 1.2.3.4 or .. - */ -export class EmVer { - /** - * Convert the range, should be 1.2.* or * into a emver - * Or an already made emver - * IsUnsafe - */ - static from(range: string | EmVer): EmVer { - if (range instanceof EmVer) { - return range - } - return EmVer.parse(range) - } - /** - * Convert the range, should be 1.2.* or * into a emver - * IsUnsafe - */ - static parse(rangeExtra: string): EmVer { - const [range, extra] = rangeExtra.split("-") - const values = range.split(".").map((x) => parseInt(x)) - for (const value of values) { - if (isNaN(value)) { - throw new Error(`Couldn't parse range: ${range}`) - } - } - return new EmVer(values, extra) - } - private constructor( - public readonly values: number[], - readonly extra: string | null, - ) {} - - /** - * Used when we need a new emver that has the last number incremented, used in the 1.* like things - */ - public withLastIncremented() { - return new EmVer(incrementLastNumber(this.values), null) - } - - public greaterThan(other: EmVer): boolean { - for (const i in this.values) { - if (other.values[i] == null) { - return true - } - if (this.values[i] > other.values[i]) { - return true - } - - if (this.values[i] < other.values[i]) { - return false - } - } - return false - } - - public equals(other: EmVer): boolean { - if (other.values.length !== this.values.length) { - return false - } - for (const i in this.values) { - if (this.values[i] !== other.values[i]) { - return false - } - } - return true - } - public greaterThanOrEqual(other: EmVer): boolean { - return this.greaterThan(other) || this.equals(other) - } - public lessThanOrEqual(other: EmVer): boolean { - return !this.greaterThan(other) - } - public lessThan(other: EmVer): boolean { - return !this.greaterThanOrEqual(other) - } - /** - * Return a enum string that describes (used for switching/iffs) - * to know comparison - * @param other - * @returns - */ - public compare(other: EmVer) { - if (this.equals(other)) { - return "equal" as const - } else if (this.greaterThan(other)) { - return "greater" as const - } else { - return "less" as const - } - } - /** - * Used when sorting emver's in a list using the sort method - * @param other - * @returns - */ - public compareForSort(other: EmVer) { - return matches - .matches(this.compare(other)) - .when("equal", () => 0 as const) - .when("greater", () => 1 as const) - .when("less", () => -1 as const) - .unwrap() - } - - toString() { - return `${this.values.join(".")}${this.extra ? `-${this.extra}` : ""}` as ValidEmVer - } -} - -/** - * A checker is a function that takes a version and returns true if the version matches the checker. - * Used when we are doing range checking, like saying ">=1.0.0".check("1.2.3") will be true - */ -export class Checker { - /** - * Will take in a range, like `>1.2` or `<1.2.3.4` or `=1.2` or `1.*` - * and return a checker, that has the check function for checking that a version is in the valid - * @param range - * @returns - */ - static parse(range: string | Checker): Checker { - if (range instanceof Checker) { - return range - } - range = range.trim() - if (range.indexOf("||") !== -1) { - return rangeOr(...range.split("||").map((x) => Checker.parse(x))) - } - if (range.indexOf("&&") !== -1) { - return rangeAnd(...range.split("&&").map((x) => Checker.parse(x))) - } - if (range === "*") { - return new Checker((version) => { - EmVer.from(version) - return true - }, range) - } - if (range.startsWith("!!")) return Checker.parse(range.substring(2)) - if (range.startsWith("!")) { - const tempValue = Checker.parse(range.substring(1)) - return new Checker((x) => !tempValue.check(x), range) - } - const starSubMatches = starSub.exec(range) - if (starSubMatches != null) { - const emVarLower = EmVer.parse(starSubMatches[1]) - const emVarUpper = emVarLower.withLastIncremented() - - return new Checker((version) => { - const v = EmVer.from(version) - return ( - (v.greaterThan(emVarLower) || v.equals(emVarLower)) && - !v.greaterThan(emVarUpper) && - !v.equals(emVarUpper) - ) - }, range) - } - - switch (range.substring(0, 2)) { - case ">=": { - const emVar = EmVer.parse(range.substring(2)) - return new Checker((version) => { - const v = EmVer.from(version) - return v.greaterThanOrEqual(emVar) - }, range) - } - case "<=": { - const emVar = EmVer.parse(range.substring(2)) - return new Checker((version) => { - const v = EmVer.from(version) - return v.lessThanOrEqual(emVar) - }, range) - } - } - - switch (range.substring(0, 1)) { - case ">": { - const emVar = EmVer.parse(range.substring(1)) - return new Checker((version) => { - const v = EmVer.from(version) - return v.greaterThan(emVar) - }, range) - } - case "<": { - const emVar = EmVer.parse(range.substring(1)) - return new Checker((version) => { - const v = EmVer.from(version) - return v.lessThan(emVar) - }, range) - } - case "=": { - const emVar = EmVer.parse(range.substring(1)) - return new Checker((version) => { - const v = EmVer.from(version) - return v.equals(emVar) - }, `=${emVar.toString()}`) - } - } - throw new Error("Couldn't parse range: " + range) - } - constructor( - /** - * Check is the function that will be given a emver or unparsed emver and should give if it follows - * a pattern - */ - public readonly check: (value: ValidEmVer | EmVer) => boolean, - private readonly _range: string, - ) {} - - get range() { - return this._range as ValidEmVerRange - } - - /** - * Used when we want the `and` condition with another checker - */ - public and(...others: (Checker | string)[]): Checker { - const othersCheck = others.map(Checker.parse) - return new Checker( - (value) => { - if (!this.check(value)) { - return false - } - for (const other of othersCheck) { - if (!other.check(value)) { - return false - } - } - return true - }, - othersCheck.map((x) => x._range).join(" && "), - ) - } - - /** - * Used when we want the `or` condition with another checker - */ - public or(...others: (Checker | string)[]): Checker { - const othersCheck = others.map(Checker.parse) - return new Checker( - (value) => { - if (this.check(value)) { - return true - } - for (const other of othersCheck) { - if (other.check(value)) { - return true - } - } - return false - }, - othersCheck.map((x) => x._range).join(" || "), - ) - } - - /** - * A useful example is making sure we don't match an exact version, like !=1.2.3 - * @returns - */ - public not(): Checker { - let newRange = `!${this._range}` - return Checker.parse(newRange) - } -} diff --git a/sdk/lib/exver/exver.pegjs b/sdk/lib/exver/exver.pegjs new file mode 100644 index 000000000..3045b9224 --- /dev/null +++ b/sdk/lib/exver/exver.pegjs @@ -0,0 +1,99 @@ +// #flavor:0.1.2-beta.1:0 +// !( >=1:1 && <= 2:2) + +VersionRange + = first:VersionRangeAtom rest:(_ ((Or / And) _)? VersionRangeAtom)* + +Or = "||" + +And = "&&" + +VersionRangeAtom + = Parens + / Anchor + / Not + / Any + / None + +Parens + = "(" _ expr:VersionRange _ ")" { return { type: "Parens", expr } } + +Anchor + = operator:CmpOp? _ version:VersionSpec { return { type: "Anchor", operator, version } } + +VersionSpec + = flavor:Flavor? upstream:Version downstream:( ":" Version )? { return { flavor: flavor || null, upstream, downstream: downstream ? downstream[1] : { number: [0], prerelease: [] } } } + +Not = "!" _ value:VersionRangeAtom { return { type: "Not", value: value }} + +Any = "*" { return { type: "Any" } } + +None = "!" { return { type: "None" } } + +CmpOp + = ">=" { return ">="; } + / "<=" { return "<="; } + / ">" { return ">"; } + / "<" { return "<"; } + / "=" { return "="; } + / "!=" { return "!="; } + / "^" { return "^"; } + / "~" { return "~"; } + +ExtendedVersion + = flavor:Flavor? upstream:Version ":" downstream:Version { + return { flavor: flavor || null, upstream, downstream } + } + +EmVer + = major:Digit "." minor:Digit "." patch:Digit ("." revision:Digit)? { + return { + flavor: null, + upstream: { + number: [major, minor, patch], + prerelease: [], + }, + downstream: { + number: [revision || 0], + prerelease: [], + }, + } + } + +Flavor + = "#" flavor:Lowercase ":" { return flavor } + +Lowercase + = [a-z]+ { return text() } + +String + = [a-zA-Z]+ { return text(); } + +Version + = number:VersionNumber prerelease: PreRelease? { + return { + number, + prerelease: prerelease || [] + }; + } + +PreRelease + = "-" first:PreReleaseSegment rest:("." PreReleaseSegment)* { + return [first].concat(rest.map(r => r[1])); + } + +PreReleaseSegment + = "."? segment:(Digit / String) { + return segment; + } + +VersionNumber + = first:Digit rest:("." Digit)* { + return [first].concat(rest.map(r => r[1])); + } + +Digit + = [0-9]+ { return parseInt(text(), 10); } + +_ "whitespace" + = [ \t\n\r]* \ No newline at end of file diff --git a/sdk/lib/exver/exver.ts b/sdk/lib/exver/exver.ts new file mode 100644 index 000000000..be9ea3e0e --- /dev/null +++ b/sdk/lib/exver/exver.ts @@ -0,0 +1,2507 @@ +/* eslint-disable */ + + + +const peggyParser: {parse: any, SyntaxError: any, DefaultTracer?: any} = // Generated by Peggy 3.0.2. +// +// https://peggyjs.org/ +// @ts-ignore +(function() { +// @ts-ignore + "use strict"; + +// @ts-ignore +function peg$subclass(child, parent) { +// @ts-ignore + function C() { this.constructor = child; } +// @ts-ignore + C.prototype = parent.prototype; +// @ts-ignore + child.prototype = new C(); +} + +// @ts-ignore +function peg$SyntaxError(message, expected, found, location) { +// @ts-ignore + var self = Error.call(this, message); + // istanbul ignore next Check is a necessary evil to support older environments +// @ts-ignore + if (Object.setPrototypeOf) { +// @ts-ignore + Object.setPrototypeOf(self, peg$SyntaxError.prototype); + } +// @ts-ignore + self.expected = expected; +// @ts-ignore + self.found = found; +// @ts-ignore + self.location = location; +// @ts-ignore + self.name = "SyntaxError"; +// @ts-ignore + return self; +} + +// @ts-ignore +peg$subclass(peg$SyntaxError, Error); + +// @ts-ignore +function peg$padEnd(str, targetLength, padString) { +// @ts-ignore + padString = padString || " "; +// @ts-ignore + if (str.length > targetLength) { return str; } +// @ts-ignore + targetLength -= str.length; +// @ts-ignore + padString += padString.repeat(targetLength); +// @ts-ignore + return str + padString.slice(0, targetLength); +} + +// @ts-ignore +peg$SyntaxError.prototype.format = function(sources) { +// @ts-ignore + var str = "Error: " + this.message; +// @ts-ignore + if (this.location) { +// @ts-ignore + var src = null; +// @ts-ignore + var k; +// @ts-ignore + for (k = 0; k < sources.length; k++) { +// @ts-ignore + if (sources[k].source === this.location.source) { +// @ts-ignore + src = sources[k].text.split(/\r\n|\n|\r/g); +// @ts-ignore + break; + } + } +// @ts-ignore + var s = this.location.start; +// @ts-ignore + var offset_s = (this.location.source && (typeof this.location.source.offset === "function")) +// @ts-ignore + ? this.location.source.offset(s) +// @ts-ignore + : s; +// @ts-ignore + var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column; +// @ts-ignore + if (src) { +// @ts-ignore + var e = this.location.end; +// @ts-ignore + var filler = peg$padEnd("", offset_s.line.toString().length, ' '); +// @ts-ignore + var line = src[s.line - 1]; +// @ts-ignore + var last = s.line === e.line ? e.column : line.length + 1; +// @ts-ignore + var hatLen = (last - s.column) || 1; +// @ts-ignore + str += "\n --> " + loc + "\n" +// @ts-ignore + + filler + " |\n" +// @ts-ignore + + offset_s.line + " | " + line + "\n" +// @ts-ignore + + filler + " | " + peg$padEnd("", s.column - 1, ' ') +// @ts-ignore + + peg$padEnd("", hatLen, "^"); +// @ts-ignore + } else { +// @ts-ignore + str += "\n at " + loc; + } + } +// @ts-ignore + return str; +}; + +// @ts-ignore +peg$SyntaxError.buildMessage = function(expected, found) { +// @ts-ignore + var DESCRIBE_EXPECTATION_FNS = { +// @ts-ignore + literal: function(expectation) { +// @ts-ignore + return "\"" + literalEscape(expectation.text) + "\""; + }, + +// @ts-ignore + class: function(expectation) { +// @ts-ignore + var escapedParts = expectation.parts.map(function(part) { +// @ts-ignore + return Array.isArray(part) +// @ts-ignore + ? classEscape(part[0]) + "-" + classEscape(part[1]) +// @ts-ignore + : classEscape(part); + }); + +// @ts-ignore + return "[" + (expectation.inverted ? "^" : "") + escapedParts.join("") + "]"; + }, + +// @ts-ignore + any: function() { +// @ts-ignore + return "any character"; + }, + +// @ts-ignore + end: function() { +// @ts-ignore + return "end of input"; + }, + +// @ts-ignore + other: function(expectation) { +// @ts-ignore + return expectation.description; + } + }; + +// @ts-ignore + function hex(ch) { +// @ts-ignore + return ch.charCodeAt(0).toString(16).toUpperCase(); + } + +// @ts-ignore + function literalEscape(s) { +// @ts-ignore + return s +// @ts-ignore + .replace(/\\/g, "\\\\") +// @ts-ignore + .replace(/"/g, "\\\"") +// @ts-ignore + .replace(/\0/g, "\\0") +// @ts-ignore + .replace(/\t/g, "\\t") +// @ts-ignore + .replace(/\n/g, "\\n") +// @ts-ignore + .replace(/\r/g, "\\r") +// @ts-ignore + .replace(/[\x00-\x0F]/g, function(ch) { return "\\x0" + hex(ch); }) +// @ts-ignore + .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x" + hex(ch); }); + } + +// @ts-ignore + function classEscape(s) { +// @ts-ignore + return s +// @ts-ignore + .replace(/\\/g, "\\\\") +// @ts-ignore + .replace(/\]/g, "\\]") +// @ts-ignore + .replace(/\^/g, "\\^") +// @ts-ignore + .replace(/-/g, "\\-") +// @ts-ignore + .replace(/\0/g, "\\0") +// @ts-ignore + .replace(/\t/g, "\\t") +// @ts-ignore + .replace(/\n/g, "\\n") +// @ts-ignore + .replace(/\r/g, "\\r") +// @ts-ignore + .replace(/[\x00-\x0F]/g, function(ch) { return "\\x0" + hex(ch); }) +// @ts-ignore + .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x" + hex(ch); }); + } + +// @ts-ignore + function describeExpectation(expectation) { +// @ts-ignore + return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation); + } + +// @ts-ignore + function describeExpected(expected) { +// @ts-ignore + var descriptions = expected.map(describeExpectation); +// @ts-ignore + var i, j; + +// @ts-ignore + descriptions.sort(); + +// @ts-ignore + if (descriptions.length > 0) { +// @ts-ignore + for (i = 1, j = 1; i < descriptions.length; i++) { +// @ts-ignore + if (descriptions[i - 1] !== descriptions[i]) { +// @ts-ignore + descriptions[j] = descriptions[i]; +// @ts-ignore + j++; + } + } +// @ts-ignore + descriptions.length = j; + } + +// @ts-ignore + switch (descriptions.length) { +// @ts-ignore + case 1: +// @ts-ignore + return descriptions[0]; + +// @ts-ignore + case 2: +// @ts-ignore + return descriptions[0] + " or " + descriptions[1]; + +// @ts-ignore + default: +// @ts-ignore + return descriptions.slice(0, -1).join(", ") +// @ts-ignore + + ", or " +// @ts-ignore + + descriptions[descriptions.length - 1]; + } + } + +// @ts-ignore + function describeFound(found) { +// @ts-ignore + return found ? "\"" + literalEscape(found) + "\"" : "end of input"; + } + +// @ts-ignore + return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found."; +}; + +// @ts-ignore +function peg$parse(input, options) { +// @ts-ignore + options = options !== undefined ? options : {}; + +// @ts-ignore + var peg$FAILED = {}; +// @ts-ignore + var peg$source = options.grammarSource; + +// @ts-ignore + var peg$startRuleFunctions = { VersionRange: peg$parseVersionRange, Or: peg$parseOr, And: peg$parseAnd, VersionRangeAtom: peg$parseVersionRangeAtom, Parens: peg$parseParens, Anchor: peg$parseAnchor, VersionSpec: peg$parseVersionSpec, Not: peg$parseNot, Any: peg$parseAny, None: peg$parseNone, CmpOp: peg$parseCmpOp, ExtendedVersion: peg$parseExtendedVersion, EmVer: peg$parseEmVer, Flavor: peg$parseFlavor, Lowercase: peg$parseLowercase, String: peg$parseString, Version: peg$parseVersion, PreRelease: peg$parsePreRelease, PreReleaseSegment: peg$parsePreReleaseSegment, VersionNumber: peg$parseVersionNumber, Digit: peg$parseDigit, _: peg$parse_ }; +// @ts-ignore + var peg$startRuleFunction = peg$parseVersionRange; + +// @ts-ignore + var peg$c0 = "||"; + var peg$c1 = "&&"; + var peg$c2 = "("; + var peg$c3 = ")"; + var peg$c4 = ":"; + var peg$c5 = "!"; + var peg$c6 = "*"; + var peg$c7 = ">="; + var peg$c8 = "<="; + var peg$c9 = ">"; + var peg$c10 = "<"; + var peg$c11 = "="; + var peg$c12 = "!="; + var peg$c13 = "^"; + var peg$c14 = "~"; + var peg$c15 = "."; + var peg$c16 = "#"; + var peg$c17 = "-"; + + var peg$r0 = /^[a-z]/; + var peg$r1 = /^[a-zA-Z]/; + var peg$r2 = /^[0-9]/; + var peg$r3 = /^[ \t\n\r]/; + + var peg$e0 = peg$literalExpectation("||", false); + var peg$e1 = peg$literalExpectation("&&", false); + var peg$e2 = peg$literalExpectation("(", false); + var peg$e3 = peg$literalExpectation(")", false); + var peg$e4 = peg$literalExpectation(":", false); + var peg$e5 = peg$literalExpectation("!", false); + var peg$e6 = peg$literalExpectation("*", false); + var peg$e7 = peg$literalExpectation(">=", false); + var peg$e8 = peg$literalExpectation("<=", false); + var peg$e9 = peg$literalExpectation(">", false); + var peg$e10 = peg$literalExpectation("<", false); + var peg$e11 = peg$literalExpectation("=", false); + var peg$e12 = peg$literalExpectation("!=", false); + var peg$e13 = peg$literalExpectation("^", false); + var peg$e14 = peg$literalExpectation("~", false); + var peg$e15 = peg$literalExpectation(".", false); + var peg$e16 = peg$literalExpectation("#", false); + var peg$e17 = peg$classExpectation([["a", "z"]], false, false); + var peg$e18 = peg$classExpectation([["a", "z"], ["A", "Z"]], false, false); + var peg$e19 = peg$literalExpectation("-", false); + var peg$e20 = peg$classExpectation([["0", "9"]], false, false); + var peg$e21 = peg$otherExpectation("whitespace"); + var peg$e22 = peg$classExpectation([" ", "\t", "\n", "\r"], false, false); +// @ts-ignore + + var peg$f0 = function(expr) {// @ts-ignore + return { type: "Parens", expr } };// @ts-ignore + + var peg$f1 = function(operator, version) {// @ts-ignore + return { type: "Anchor", operator, version } };// @ts-ignore + + var peg$f2 = function(flavor, upstream, downstream) {// @ts-ignore + return { flavor: flavor || null, upstream, downstream: downstream ? downstream[1] : { number: [0], prerelease: [] } } };// @ts-ignore + + var peg$f3 = function(value) {// @ts-ignore + return { type: "Not", value: value }};// @ts-ignore + + var peg$f4 = function() {// @ts-ignore + return { type: "Any" } };// @ts-ignore + + var peg$f5 = function() {// @ts-ignore + return { type: "None" } };// @ts-ignore + + var peg$f6 = function() {// @ts-ignore + return ">="; };// @ts-ignore + + var peg$f7 = function() {// @ts-ignore + return "<="; };// @ts-ignore + + var peg$f8 = function() {// @ts-ignore + return ">"; };// @ts-ignore + + var peg$f9 = function() {// @ts-ignore + return "<"; };// @ts-ignore + + var peg$f10 = function() {// @ts-ignore + return "="; };// @ts-ignore + + var peg$f11 = function() {// @ts-ignore + return "!="; };// @ts-ignore + + var peg$f12 = function() {// @ts-ignore + return "^"; };// @ts-ignore + + var peg$f13 = function() {// @ts-ignore + return "~"; };// @ts-ignore + + var peg$f14 = function(flavor, upstream, downstream) { +// @ts-ignore + return { flavor: flavor || null, upstream, downstream } + };// @ts-ignore + + var peg$f15 = function(major, minor, patch) { +// @ts-ignore + return { +// @ts-ignore + flavor: null, +// @ts-ignore + upstream: { +// @ts-ignore + number: [major, minor, patch], +// @ts-ignore + prerelease: [], + }, +// @ts-ignore + downstream: { +// @ts-ignore + number: [revision || 0], +// @ts-ignore + prerelease: [], + }, + } + };// @ts-ignore + + var peg$f16 = function(flavor) {// @ts-ignore + return flavor };// @ts-ignore + + var peg$f17 = function() {// @ts-ignore + return text() };// @ts-ignore + + var peg$f18 = function() {// @ts-ignore + return text(); };// @ts-ignore + + var peg$f19 = function(number, prerelease) { +// @ts-ignore + return { +// @ts-ignore + number, +// @ts-ignore + prerelease: prerelease || [] + }; + };// @ts-ignore + + var peg$f20 = function(first, rest) { +// @ts-ignore + return [first].concat(rest.map(r => r[1])); + };// @ts-ignore + + var peg$f21 = function(segment) { +// @ts-ignore + return segment; + };// @ts-ignore + + var peg$f22 = function(first, rest) { +// @ts-ignore + return [first].concat(rest.map(r => r[1])); + };// @ts-ignore + + var peg$f23 = function() {// @ts-ignore + return parseInt(text(), 10); }; +// @ts-ignore + var peg$currPos = 0; +// @ts-ignore + var peg$savedPos = 0; +// @ts-ignore + var peg$posDetailsCache = [{ line: 1, column: 1 }]; +// @ts-ignore + var peg$maxFailPos = 0; +// @ts-ignore + var peg$maxFailExpected = []; +// @ts-ignore + var peg$silentFails = 0; + +// @ts-ignore + var peg$result; + +// @ts-ignore + if ("startRule" in options) { +// @ts-ignore + if (!(options.startRule in peg$startRuleFunctions)) { +// @ts-ignore + throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); + } + +// @ts-ignore + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + +// @ts-ignore + function text() { +// @ts-ignore + return input.substring(peg$savedPos, peg$currPos); + } + +// @ts-ignore + function offset() { +// @ts-ignore + return peg$savedPos; + } + +// @ts-ignore + function range() { +// @ts-ignore + return { +// @ts-ignore + source: peg$source, +// @ts-ignore + start: peg$savedPos, +// @ts-ignore + end: peg$currPos + }; + } + +// @ts-ignore + function location() { +// @ts-ignore + return peg$computeLocation(peg$savedPos, peg$currPos); + } + +// @ts-ignore + function expected(description, location) { +// @ts-ignore + location = location !== undefined +// @ts-ignore + ? location +// @ts-ignore + : peg$computeLocation(peg$savedPos, peg$currPos); + +// @ts-ignore + throw peg$buildStructuredError( +// @ts-ignore + [peg$otherExpectation(description)], +// @ts-ignore + input.substring(peg$savedPos, peg$currPos), +// @ts-ignore + location + ); + } + +// @ts-ignore + function error(message, location) { +// @ts-ignore + location = location !== undefined +// @ts-ignore + ? location +// @ts-ignore + : peg$computeLocation(peg$savedPos, peg$currPos); + +// @ts-ignore + throw peg$buildSimpleError(message, location); + } + +// @ts-ignore + function peg$literalExpectation(text, ignoreCase) { +// @ts-ignore + return { type: "literal", text: text, ignoreCase: ignoreCase }; + } + +// @ts-ignore + function peg$classExpectation(parts, inverted, ignoreCase) { +// @ts-ignore + return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase }; + } + +// @ts-ignore + function peg$anyExpectation() { +// @ts-ignore + return { type: "any" }; + } + +// @ts-ignore + function peg$endExpectation() { +// @ts-ignore + return { type: "end" }; + } + +// @ts-ignore + function peg$otherExpectation(description) { +// @ts-ignore + return { type: "other", description: description }; + } + +// @ts-ignore + function peg$computePosDetails(pos) { +// @ts-ignore + var details = peg$posDetailsCache[pos]; +// @ts-ignore + var p; + +// @ts-ignore + if (details) { +// @ts-ignore + return details; +// @ts-ignore + } else { +// @ts-ignore + p = pos - 1; +// @ts-ignore + while (!peg$posDetailsCache[p]) { +// @ts-ignore + p--; + } + +// @ts-ignore + details = peg$posDetailsCache[p]; +// @ts-ignore + details = { +// @ts-ignore + line: details.line, +// @ts-ignore + column: details.column + }; + +// @ts-ignore + while (p < pos) { +// @ts-ignore + if (input.charCodeAt(p) === 10) { +// @ts-ignore + details.line++; +// @ts-ignore + details.column = 1; +// @ts-ignore + } else { +// @ts-ignore + details.column++; + } + +// @ts-ignore + p++; + } + +// @ts-ignore + peg$posDetailsCache[pos] = details; + +// @ts-ignore + return details; + } + } + +// @ts-ignore + function peg$computeLocation(startPos, endPos, offset) { +// @ts-ignore + var startPosDetails = peg$computePosDetails(startPos); +// @ts-ignore + var endPosDetails = peg$computePosDetails(endPos); + +// @ts-ignore + var res = { +// @ts-ignore + source: peg$source, +// @ts-ignore + start: { +// @ts-ignore + offset: startPos, +// @ts-ignore + line: startPosDetails.line, +// @ts-ignore + column: startPosDetails.column + }, +// @ts-ignore + end: { +// @ts-ignore + offset: endPos, +// @ts-ignore + line: endPosDetails.line, +// @ts-ignore + column: endPosDetails.column + } + }; +// @ts-ignore + if (offset && peg$source && (typeof peg$source.offset === "function")) { +// @ts-ignore + res.start = peg$source.offset(res.start); +// @ts-ignore + res.end = peg$source.offset(res.end); + } +// @ts-ignore + return res; + } + +// @ts-ignore + function peg$fail(expected) { +// @ts-ignore + if (peg$currPos < peg$maxFailPos) { return; } + +// @ts-ignore + if (peg$currPos > peg$maxFailPos) { +// @ts-ignore + peg$maxFailPos = peg$currPos; +// @ts-ignore + peg$maxFailExpected = []; + } + +// @ts-ignore + peg$maxFailExpected.push(expected); + } + +// @ts-ignore + function peg$buildSimpleError(message, location) { +// @ts-ignore + return new peg$SyntaxError(message, null, null, location); + } + +// @ts-ignore + function peg$buildStructuredError(expected, found, location) { +// @ts-ignore + return new peg$SyntaxError( +// @ts-ignore + peg$SyntaxError.buildMessage(expected, found), +// @ts-ignore + expected, +// @ts-ignore + found, +// @ts-ignore + location + ); + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersionRange() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5, s6, s7; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseVersionRangeAtom(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = []; +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + s5 = peg$currPos; +// @ts-ignore + s6 = peg$parseOr(); +// @ts-ignore + if (s6 === peg$FAILED) { +// @ts-ignore + s6 = peg$parseAnd(); + } +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s7 = peg$parse_(); +// @ts-ignore + s6 = [s6, s7]; +// @ts-ignore + s5 = s6; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s5; +// @ts-ignore + s5 = peg$FAILED; + } +// @ts-ignore + if (s5 === peg$FAILED) { +// @ts-ignore + s5 = null; + } +// @ts-ignore + s6 = peg$parseVersionRangeAtom(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5, s6]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + while (s3 !== peg$FAILED) { +// @ts-ignore + s2.push(s3); +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + s5 = peg$currPos; +// @ts-ignore + s6 = peg$parseOr(); +// @ts-ignore + if (s6 === peg$FAILED) { +// @ts-ignore + s6 = peg$parseAnd(); + } +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s7 = peg$parse_(); +// @ts-ignore + s6 = [s6, s7]; +// @ts-ignore + s5 = s6; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s5; +// @ts-ignore + s5 = peg$FAILED; + } +// @ts-ignore + if (s5 === peg$FAILED) { +// @ts-ignore + s5 = null; + } +// @ts-ignore + s6 = peg$parseVersionRangeAtom(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5, s6]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } + } +// @ts-ignore + s1 = [s1, s2]; +// @ts-ignore + s0 = s1; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseOr() { +// @ts-ignore + var s0; + +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c0) { +// @ts-ignore + s0 = peg$c0; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s0 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e0); } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseAnd() { +// @ts-ignore + var s0; + +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c1) { +// @ts-ignore + s0 = peg$c1; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s0 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e1); } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersionRangeAtom() { +// @ts-ignore + var s0; + +// @ts-ignore + s0 = peg$parseParens(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseAnchor(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseNot(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseAny(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseNone(); + } + } + } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseParens() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 40) { +// @ts-ignore + s1 = peg$c2; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e2); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + s3 = peg$parseVersionRange(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 41) { +// @ts-ignore + s5 = peg$c3; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s5 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e3); } + } +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f0(s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseAnchor() { +// @ts-ignore + var s0, s1, s2, s3; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseCmpOp(); +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + s3 = peg$parseVersionSpec(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f1(s1, s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersionSpec() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseFlavor(); +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parseVersion(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 58) { +// @ts-ignore + s4 = peg$c4; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + s5 = peg$parseVersion(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + if (s3 === peg$FAILED) { +// @ts-ignore + s3 = null; + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f2(s1, s2, s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseNot() { +// @ts-ignore + var s0, s1, s2, s3; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 33) { +// @ts-ignore + s1 = peg$c5; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e5); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + s3 = peg$parseVersionRangeAtom(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f3(s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseAny() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 42) { +// @ts-ignore + s1 = peg$c6; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e6); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f4(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseNone() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 33) { +// @ts-ignore + s1 = peg$c5; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e5); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f5(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseCmpOp() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c7) { +// @ts-ignore + s1 = peg$c7; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e7); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f6(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c8) { +// @ts-ignore + s1 = peg$c8; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e8); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f7(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 62) { +// @ts-ignore + s1 = peg$c9; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e9); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f8(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 60) { +// @ts-ignore + s1 = peg$c10; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e10); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f9(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 61) { +// @ts-ignore + s1 = peg$c11; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e11); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f10(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c12) { +// @ts-ignore + s1 = peg$c12; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e12); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f11(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 94) { +// @ts-ignore + s1 = peg$c13; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e13); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f12(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 126) { +// @ts-ignore + s1 = peg$c14; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e14); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f13(); + } +// @ts-ignore + s0 = s1; + } + } + } + } + } + } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseExtendedVersion() { +// @ts-ignore + var s0, s1, s2, s3, s4; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseFlavor(); +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parseVersion(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 58) { +// @ts-ignore + s3 = peg$c4; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s3 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + s4 = peg$parseVersion(); +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f14(s1, s2, s4); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseEmVer() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5, s6, s7, s8; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseDigit(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s2 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + s3 = peg$parseDigit(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s4 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + s5 = peg$parseDigit(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s6 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s7 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s7 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s7 !== peg$FAILED) { +// @ts-ignore + s8 = peg$parseDigit(); +// @ts-ignore + if (s8 !== peg$FAILED) { +// @ts-ignore + s7 = [s7, s8]; +// @ts-ignore + s6 = s7; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s6; +// @ts-ignore + s6 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s6; +// @ts-ignore + s6 = peg$FAILED; + } +// @ts-ignore + if (s6 === peg$FAILED) { +// @ts-ignore + s6 = null; + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f15(s1, s3, s5); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseFlavor() { +// @ts-ignore + var s0, s1, s2, s3; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 35) { +// @ts-ignore + s1 = peg$c16; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e16); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parseLowercase(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 58) { +// @ts-ignore + s3 = peg$c4; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s3 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f16(s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseLowercase() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = []; +// @ts-ignore + if (peg$r0.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e17); } + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + while (s2 !== peg$FAILED) { +// @ts-ignore + s1.push(s2); +// @ts-ignore + if (peg$r0.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e17); } + } + } +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f17(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseString() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = []; +// @ts-ignore + if (peg$r1.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e18); } + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + while (s2 !== peg$FAILED) { +// @ts-ignore + s1.push(s2); +// @ts-ignore + if (peg$r1.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e18); } + } + } +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f18(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersion() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseVersionNumber(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parsePreRelease(); +// @ts-ignore + if (s2 === peg$FAILED) { +// @ts-ignore + s2 = null; + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f19(s1, s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parsePreRelease() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5, s6; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 45) { +// @ts-ignore + s1 = peg$c17; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e19); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parsePreReleaseSegment(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + s3 = []; +// @ts-ignore + s4 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s5 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s5 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s6 = peg$parsePreReleaseSegment(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s5 = [s5, s6]; +// @ts-ignore + s4 = s5; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s4; +// @ts-ignore + s4 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s4; +// @ts-ignore + s4 = peg$FAILED; + } +// @ts-ignore + while (s4 !== peg$FAILED) { +// @ts-ignore + s3.push(s4); +// @ts-ignore + s4 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s5 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s5 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s6 = peg$parsePreReleaseSegment(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s5 = [s5, s6]; +// @ts-ignore + s4 = s5; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s4; +// @ts-ignore + s4 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s4; +// @ts-ignore + s4 = peg$FAILED; + } + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f20(s2, s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parsePreReleaseSegment() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s1 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parseDigit(); +// @ts-ignore + if (s2 === peg$FAILED) { +// @ts-ignore + s2 = peg$parseString(); + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f21(s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersionNumber() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseDigit(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = []; +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s4 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + s5 = peg$parseDigit(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + while (s3 !== peg$FAILED) { +// @ts-ignore + s2.push(s3); +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s4 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + s5 = peg$parseDigit(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f22(s1, s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseDigit() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = []; +// @ts-ignore + if (peg$r2.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e20); } + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + while (s2 !== peg$FAILED) { +// @ts-ignore + s1.push(s2); +// @ts-ignore + if (peg$r2.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e20); } + } + } +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f23(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parse_() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + peg$silentFails++; +// @ts-ignore + s0 = []; +// @ts-ignore + if (peg$r3.test(input.charAt(peg$currPos))) { +// @ts-ignore + s1 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e22); } + } +// @ts-ignore + while (s1 !== peg$FAILED) { +// @ts-ignore + s0.push(s1); +// @ts-ignore + if (peg$r3.test(input.charAt(peg$currPos))) { +// @ts-ignore + s1 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e22); } + } + } +// @ts-ignore + peg$silentFails--; +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e21); } + +// @ts-ignore + return s0; + } + +// @ts-ignore + peg$result = peg$startRuleFunction(); + +// @ts-ignore + if (peg$result !== peg$FAILED && peg$currPos === input.length) { +// @ts-ignore + return peg$result; +// @ts-ignore + } else { +// @ts-ignore + if (peg$result !== peg$FAILED && peg$currPos < input.length) { +// @ts-ignore + peg$fail(peg$endExpectation()); + } + +// @ts-ignore + throw peg$buildStructuredError( +// @ts-ignore + peg$maxFailExpected, +// @ts-ignore + peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, +// @ts-ignore + peg$maxFailPos < input.length +// @ts-ignore + ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) +// @ts-ignore + : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) + ); + } +} + +// @ts-ignore + return { + SyntaxError: peg$SyntaxError, + parse: peg$parse + }; +})() + +export interface FilePosition { + offset: number; + line: number; + column: number; +} + +export interface FileRange { + start: FilePosition; + end: FilePosition; + source: string; +} + +export interface LiteralExpectation { + type: "literal"; + text: string; + ignoreCase: boolean; +} + +export interface ClassParts extends Array {} + +export interface ClassExpectation { + type: "class"; + parts: ClassParts; + inverted: boolean; + ignoreCase: boolean; +} + +export interface AnyExpectation { + type: "any"; +} + +export interface EndExpectation { + type: "end"; +} + +export interface OtherExpectation { + type: "other"; + description: string; +} + +export type Expectation = LiteralExpectation | ClassExpectation | AnyExpectation | EndExpectation | OtherExpectation; + +declare class _PeggySyntaxError extends Error { + public static buildMessage(expected: Expectation[], found: string | null): string; + public message: string; + public expected: Expectation[]; + public found: string | null; + public location: FileRange; + public name: string; + constructor(message: string, expected: Expectation[], found: string | null, location: FileRange); + format(sources: { + source?: any; + text: string; + }[]): string; +} + +export interface TraceEvent { + type: string; + rule: string; + result?: any; + location: FileRange; + } + +declare class _DefaultTracer { + private indentLevel: number; + public trace(event: TraceEvent): void; +} + +peggyParser.SyntaxError.prototype.name = "PeggySyntaxError"; + +export interface ParseOptions { + filename?: string; + startRule?: "VersionRange" | "Or" | "And" | "VersionRangeAtom" | "Parens" | "Anchor" | "VersionSpec" | "Not" | "Any" | "None" | "CmpOp" | "ExtendedVersion" | "EmVer" | "Flavor" | "Lowercase" | "String" | "Version" | "PreRelease" | "PreReleaseSegment" | "VersionNumber" | "Digit" | "_"; + tracer?: any; + [key: string]: any; +} +export type ParseFunction = ( + input: string, + options?: Options + ) => Options extends { startRule: infer StartRule } ? + StartRule extends "VersionRange" ? VersionRange : + StartRule extends "Or" ? Or : + StartRule extends "And" ? And : + StartRule extends "VersionRangeAtom" ? VersionRangeAtom : + StartRule extends "Parens" ? Parens : + StartRule extends "Anchor" ? Anchor : + StartRule extends "VersionSpec" ? VersionSpec : + StartRule extends "Not" ? Not : + StartRule extends "Any" ? Any : + StartRule extends "None" ? None : + StartRule extends "CmpOp" ? CmpOp : + StartRule extends "ExtendedVersion" ? ExtendedVersion : + StartRule extends "EmVer" ? EmVer : + StartRule extends "Flavor" ? Flavor : + StartRule extends "Lowercase" ? Lowercase_1 : + StartRule extends "String" ? String_1 : + StartRule extends "Version" ? Version : + StartRule extends "PreRelease" ? PreRelease : + StartRule extends "PreReleaseSegment" ? PreReleaseSegment : + StartRule extends "VersionNumber" ? VersionNumber : + StartRule extends "Digit" ? Digit : + StartRule extends "_" ? _ : VersionRange + : VersionRange; +export const parse: ParseFunction = peggyParser.parse; + +export const PeggySyntaxError = peggyParser.SyntaxError as typeof _PeggySyntaxError; + +export type PeggySyntaxError = _PeggySyntaxError; + +// These types were autogenerated by ts-pegjs +export type VersionRange = [ + VersionRangeAtom, + [_, [Or | And, _] | null, VersionRangeAtom][] +]; +export type Or = "||"; +export type And = "&&"; +export type VersionRangeAtom = Parens | Anchor | Not | Any | None; +export type Parens = { type: "Parens"; expr: VersionRange }; +export type Anchor = { + type: "Anchor"; + operator: CmpOp | null; + version: VersionSpec; +}; +export type VersionSpec = { + flavor: NonNullable | null; + upstream: Version; + downstream: any; +}; +export type Not = { type: "Not"; value: VersionRangeAtom }; +export type Any = { type: "Any" }; +export type None = { type: "None" }; +export type CmpOp = ">=" | "<=" | ">" | "<" | "=" | "!=" | "^" | "~"; +export type ExtendedVersion = { + flavor: NonNullable | null; + upstream: Version; + downstream: Version; +}; +export type EmVer = { + flavor: null; + upstream: { number: [Digit, Digit, Digit]; prerelease: [] }; + downstream: { number: [any]; prerelease: [] }; +}; +export type Flavor = Lowercase_1; +export type Lowercase_1 = string; +export type String_1 = string; +export type Version = { + number: VersionNumber; + prerelease: never[] | NonNullable; +}; +export type PreRelease = PreReleaseSegment[]; +export type PreReleaseSegment = Digit | String_1; +export type VersionNumber = Digit[]; +export type Digit = number; +export type _ = string[]; diff --git a/sdk/lib/exver/index.ts b/sdk/lib/exver/index.ts new file mode 100644 index 000000000..913194875 --- /dev/null +++ b/sdk/lib/exver/index.ts @@ -0,0 +1,443 @@ +import * as P from "./exver" + +// prettier-ignore +export type ValidateVersion = +T extends `-${infer A}` ? never : +T extends `${infer A}-${infer B}` ? ValidateVersion & ValidateVersion : + T extends `${bigint}` ? unknown : + T extends `${bigint}.${infer A}` ? ValidateVersion : + never + +// prettier-ignore +export type ValidateExVer = + T extends `#${string}:${infer A}:${infer B}` ? ValidateVersion & ValidateVersion : + T extends `${infer A}:${infer B}` ? ValidateVersion & ValidateVersion : + never + +// prettier-ignore +export type ValidateExVers = + T extends [] ? unknown : + T extends [infer A, ...infer B] ? ValidateExVer & ValidateExVers : + never + +type Anchor = { + type: "Anchor" + operator: P.CmpOp + version: ExtendedVersion +} + +type And = { + type: "And" + left: VersionRange + right: VersionRange +} + +type Or = { + type: "Or" + left: VersionRange + right: VersionRange +} + +type Not = { + type: "Not" + value: VersionRange +} + +export class VersionRange { + private constructor(private atom: Anchor | And | Or | Not | P.Any | P.None) {} + + toString(): string { + switch (this.atom.type) { + case "Anchor": + return `${this.atom.operator}${this.atom.version}` + case "And": + return `(${this.atom.left.toString()}) && (${this.atom.right.toString()})` + case "Or": + return `(${this.atom.left.toString()}) || (${this.atom.right.toString()})` + case "Not": + return `!(${this.atom.value.toString()})` + case "Any": + return "*" + case "None": + return "!" + } + } + + /** + * Returns a boolean indicating whether a given version satisfies the VersionRange + * !( >= 1:1 <= 2:2) || <=#bitcoin:1.2.0-alpha:0 + */ + satisfiedBy(version: ExtendedVersion): boolean { + switch (this.atom.type) { + case "Anchor": + const otherVersion = this.atom.version + switch (this.atom.operator) { + case "=": + return version.equals(otherVersion) + case ">": + return version.greaterThan(otherVersion) + case "<": + return version.lessThan(otherVersion) + case ">=": + return version.greaterThanOrEqual(otherVersion) + case "<=": + return version.lessThanOrEqual(otherVersion) + case "!=": + return !version.equals(otherVersion) + case "^": + const nextMajor = this.atom.version.incrementMajor() + if ( + version.greaterThanOrEqual(otherVersion) && + version.lessThan(nextMajor) + ) { + return true + } else { + return false + } + case "~": + const nextMinor = this.atom.version.incrementMinor() + if ( + version.greaterThanOrEqual(otherVersion) && + version.lessThan(nextMinor) + ) { + return true + } else { + return false + } + } + case "And": + return ( + this.atom.left.satisfiedBy(version) && + this.atom.right.satisfiedBy(version) + ) + case "Or": + return ( + this.atom.left.satisfiedBy(version) || + this.atom.right.satisfiedBy(version) + ) + case "Not": + return !this.atom.value.satisfiedBy(version) + case "Any": + return true + case "None": + return false + } + } + + private static parseAtom(atom: P.VersionRangeAtom): VersionRange { + switch (atom.type) { + case "Not": + return new VersionRange({ + type: "Not", + value: VersionRange.parseAtom(atom.value), + }) + case "Parens": + return VersionRange.parseRange(atom.expr) + case "Anchor": + return new VersionRange({ + type: "Anchor", + operator: atom.operator || "^", + version: new ExtendedVersion( + atom.version.flavor, + new Version( + atom.version.upstream.number, + atom.version.upstream.prerelease, + ), + new Version( + atom.version.downstream.number, + atom.version.downstream.prerelease, + ), + ), + }) + default: + return new VersionRange(atom) + } + } + + private static parseRange(range: P.VersionRange): VersionRange { + let result = VersionRange.parseAtom(range[0]) + for (const next of range[1]) { + switch (next[1]?.[0]) { + case "||": + result = new VersionRange({ + type: "Or", + left: result, + right: VersionRange.parseAtom(next[2]), + }) + break + case "&&": + default: + result = new VersionRange({ + type: "And", + left: result, + right: VersionRange.parseAtom(next[2]), + }) + break + } + } + return result + } + + static parse(range: string): VersionRange { + return VersionRange.parseRange( + P.parse(range, { startRule: "VersionRange" }), + ) + } + + and(right: VersionRange) { + return new VersionRange({ type: "And", left: this, right }) + } + + or(right: VersionRange) { + return new VersionRange({ type: "Or", left: this, right }) + } + + not() { + return new VersionRange({ type: "Not", value: this }) + } + + static anchor(operator: P.CmpOp, version: ExtendedVersion) { + return new VersionRange({ type: "Anchor", operator, version }) + } + + static any() { + return new VersionRange({ type: "Any" }) + } + + static none() { + return new VersionRange({ type: "None" }) + } +} + +export class Version { + constructor( + public number: number[], + public prerelease: (string | number)[], + ) {} + + toString(): string { + return `${this.number.join(".")}${this.prerelease.length > 0 ? `-${this.prerelease.join(".")}` : ""}` + } + + compare(other: Version): "greater" | "equal" | "less" { + const numLen = Math.max(this.number.length, other.number.length) + for (let i = 0; i < numLen; i++) { + if ((this.number[i] || 0) > (other.number[i] || 0)) { + return "greater" + } else if ((this.number[i] || 0) < (other.number[i] || 0)) { + return "less" + } + } + + if (this.prerelease.length === 0 && other.prerelease.length !== 0) { + return "greater" + } else if (this.prerelease.length !== 0 && other.prerelease.length === 0) { + return "less" + } + + const prereleaseLen = Math.max(this.number.length, other.number.length) + for (let i = 0; i < prereleaseLen; i++) { + if (typeof this.prerelease[i] === typeof other.prerelease[i]) { + if (this.prerelease[i] > other.prerelease[i]) { + return "greater" + } else if (this.prerelease[i] < other.prerelease[i]) { + return "less" + } + } else { + switch (`${typeof this.prerelease[1]}:${typeof other.prerelease[i]}`) { + case "number:string": + return "less" + case "string:number": + return "greater" + case "number:undefined": + case "string:undefined": + return "greater" + case "undefined:number": + case "undefined:string": + return "less" + } + } + } + + return "equal" + } + + static parse(version: string): Version { + const parsed = P.parse(version, { startRule: "Version" }) + return new Version(parsed.number, parsed.prerelease) + } +} + +// #flavor:0.1.2-beta.1:0 +export class ExtendedVersion { + constructor( + public flavor: string | null, + public upstream: Version, + public downstream: Version, + ) {} + + toString(): string { + return `${this.flavor ? `#${this.flavor}:` : ""}${this.upstream.toString()}:${this.downstream.toString()}` + } + + compare(other: ExtendedVersion): "greater" | "equal" | "less" | null { + if (this.flavor !== other.flavor) { + return null + } + const upstreamCmp = this.upstream.compare(other.upstream) + if (upstreamCmp !== "equal") { + return upstreamCmp + } + return this.downstream.compare(other.downstream) + } + + compareLexicographic(other: ExtendedVersion): "greater" | "equal" | "less" { + if ((this.flavor || "") > (other.flavor || "")) { + return "greater" + } else if ((this.flavor || "") > (other.flavor || "")) { + return "less" + } else { + return this.compare(other)! + } + } + + compareForSort(other: ExtendedVersion): 1 | 0 | -1 { + switch (this.compareLexicographic(other)) { + case "greater": + return 1 + case "equal": + return 0 + case "less": + return -1 + } + } + + greaterThan(other: ExtendedVersion): boolean { + return this.compare(other) === "greater" + } + + greaterThanOrEqual(other: ExtendedVersion): boolean { + return ["greater", "equal"].includes(this.compare(other) as string) + } + + equals(other: ExtendedVersion): boolean { + return this.compare(other) === "equal" + } + + lessThan(other: ExtendedVersion): boolean { + return this.compare(other) === "less" + } + + lessThanOrEqual(other: ExtendedVersion): boolean { + return ["less", "equal"].includes(this.compare(other) as string) + } + + static parse(extendedVersion: string): ExtendedVersion { + const parsed = P.parse(extendedVersion, { startRule: "ExtendedVersion" }) + return new ExtendedVersion( + parsed.flavor, + new Version(parsed.upstream.number, parsed.upstream.prerelease), + new Version(parsed.downstream.number, parsed.downstream.prerelease), + ) + } + + static parseEmver(extendedVersion: string): ExtendedVersion { + const parsed = P.parse(extendedVersion, { startRule: "EmVer" }) + return new ExtendedVersion( + parsed.flavor, + new Version(parsed.upstream.number, parsed.upstream.prerelease), + new Version(parsed.downstream.number, parsed.downstream.prerelease), + ) + } + + /** + * Returns an ExtendedVersion with the Upstream major version version incremented by 1 + * and sets subsequent digits to zero. + * If no non-zero upstream digit can be found the last upstream digit will be incremented. + */ + incrementMajor(): ExtendedVersion { + const majorIdx = this.upstream.number.findIndex((num: number) => num !== 0) + + const majorNumber = this.upstream.number.map((num, idx): number => { + if (idx > majorIdx) { + return 0 + } else if (idx === majorIdx) { + return num + 1 + } + return num + }) + + const incrementedUpstream = new Version(majorNumber, []) + const updatedDownstream = new Version([0], []) + + return new ExtendedVersion( + this.flavor, + incrementedUpstream, + updatedDownstream, + ) + } + + /** + * Returns an ExtendedVersion with the Upstream minor version version incremented by 1 + * also sets subsequent digits to zero. + * If no non-zero upstream digit can be found the last digit will be incremented. + */ + incrementMinor(): ExtendedVersion { + const majorIdx = this.upstream.number.findIndex((num: number) => num !== 0) + let minorIdx = majorIdx === -1 ? majorIdx : majorIdx + 1 + + const majorNumber = this.upstream.number.map((num, idx): number => { + if (idx > minorIdx) { + return 0 + } else if (idx === minorIdx) { + return num + 1 + } + return num + }) + + const incrementedUpstream = new Version(majorNumber, []) + const updatedDownstream = new Version([0], []) + + return new ExtendedVersion( + this.flavor, + incrementedUpstream, + updatedDownstream, + ) + } +} + +export const testTypeExVer = (t: T & ValidateExVer) => t + +export const testTypeVersion = (t: T & ValidateVersion) => + t +function tests() { + testTypeVersion("1.2.3") + testTypeVersion("1") + testTypeVersion("12.34.56") + testTypeVersion("1.2-3") + testTypeVersion("1-3") + // @ts-expect-error + testTypeVersion("-3") + // @ts-expect-error + testTypeVersion("1.2.3:1") + // @ts-expect-error + testTypeVersion("#cat:1:1") + + testTypeExVer("1.2.3:1.2.3") + testTypeExVer("1.2.3.4.5.6.7.8.9.0:1") + testTypeExVer("100:1") + testTypeExVer("#cat:1:1") + testTypeExVer("1.2.3.4.5.6.7.8.9.11.22.33:1") + testTypeExVer("1-0:1") + testTypeExVer("1-0:1") + // @ts-expect-error + testTypeExVer("1.2-3") + // @ts-expect-error + testTypeExVer("1-3") + // @ts-expect-error + testTypeExVer("1.2.3.4.5.6.7.8.9.0.10:1" as string) + // @ts-expect-error + testTypeExVer("1.-2:1") + // @ts-expect-error + testTypeExVer("1..2.3:3") +} diff --git a/sdk/lib/health/HealthCheck.ts b/sdk/lib/health/HealthCheck.ts index 1ed8652bf..4b72dbf61 100644 --- a/sdk/lib/health/HealthCheck.ts +++ b/sdk/lib/health/HealthCheck.ts @@ -1,5 +1,4 @@ -import { InterfaceReceipt } from "../interfaces/interfaceReceipt" -import { Daemon, Effects, SDKManifest } from "../types" +import { Effects } from "../types" import { CheckResult } from "./checkFns/CheckResult" import { HealthReceipt } from "./HealthReceipt" import { Trigger } from "../trigger" @@ -8,9 +7,9 @@ import { defaultTrigger } from "../trigger/defaultTrigger" import { once } from "../util/once" import { Overlay } from "../util/Overlay" import { object, unknown } from "ts-matches" -import { T } from ".." +import * as T from "../types" -export type HealthCheckParams = { +export type HealthCheckParams = { effects: Effects name: string image: { @@ -22,7 +21,7 @@ export type HealthCheckParams = { onFirstSuccess?: () => unknown | Promise } -export function healthCheck( +export function healthCheck( o: HealthCheckParams, ) { new Promise(async () => { diff --git a/sdk/lib/index.browser.ts b/sdk/lib/index.browser.ts index c7ab45e60..f7d645133 100644 --- a/sdk/lib/index.browser.ts +++ b/sdk/lib/index.browser.ts @@ -1,12 +1,10 @@ -export { EmVer } from "./emverLite/mod" -export { setupManifest } from "./manifest/setupManifest" -export { setupExposeStore } from "./store/setupExposeStore" export { S9pk } from "./s9pk" +export { VersionRange, ExtendedVersion, Version } from "./exver" + export * as config from "./config" export * as CB from "./config/builder" export * as CT from "./config/configTypes" export * as dependencyConfig from "./dependencies" -export * as manifest from "./manifest" export * as types from "./types" export * as T from "./types" export * as yaml from "yaml" diff --git a/sdk/lib/index.ts b/sdk/lib/index.ts index c99798d72..935ffc023 100644 --- a/sdk/lib/index.ts +++ b/sdk/lib/index.ts @@ -1,5 +1,4 @@ export { Daemons } from "./mainFn/Daemons" -export { EmVer } from "./emverLite/mod" export { Overlay } from "./util/Overlay" export { StartSdk } from "./StartSdk" export { setupManifest } from "./manifest/setupManifest" @@ -7,6 +6,7 @@ export { FileHelper } from "./util/fileHelper" export { setupExposeStore } from "./store/setupExposeStore" export { pathBuilder } from "./store/PathBuilder" export { S9pk } from "./s9pk" +export { VersionRange, ExtendedVersion, Version } from "./exver" export * as actions from "./actions" export * as backup from "./backup" diff --git a/sdk/lib/inits/migrations/Migration.ts b/sdk/lib/inits/migrations/Migration.ts index 119271aea..16be93dbd 100644 --- a/sdk/lib/inits/migrations/Migration.ts +++ b/sdk/lib/inits/migrations/Migration.ts @@ -1,35 +1,35 @@ -import { ManifestVersion, SDKManifest } from "../../manifest/ManifestTypes" -import { Effects } from "../../types" +import { ValidateExVer } from "../../exver" +import * as T from "../../types" export class Migration< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, - Version extends ManifestVersion, + Version extends string, > { constructor( readonly options: { - version: Version - up: (opts: { effects: Effects }) => Promise - down: (opts: { effects: Effects }) => Promise + version: Version & ValidateExVer + up: (opts: { effects: T.Effects }) => Promise + down: (opts: { effects: T.Effects }) => Promise }, ) {} static of< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, - Version extends ManifestVersion, + Version extends string, >(options: { - version: Version - up: (opts: { effects: Effects }) => Promise - down: (opts: { effects: Effects }) => Promise + version: Version & ValidateExVer + up: (opts: { effects: T.Effects }) => Promise + down: (opts: { effects: T.Effects }) => Promise }) { return new Migration(options) } - async up(opts: { effects: Effects }) { + async up(opts: { effects: T.Effects }) { this.up(opts) } - async down(opts: { effects: Effects }) { + async down(opts: { effects: T.Effects }) { this.down(opts) } } diff --git a/sdk/lib/inits/migrations/setupMigrations.ts b/sdk/lib/inits/migrations/setupMigrations.ts index 288b2b9d7..6d690b239 100644 --- a/sdk/lib/inits/migrations/setupMigrations.ts +++ b/sdk/lib/inits/migrations/setupMigrations.ts @@ -1,27 +1,31 @@ -import { EmVer } from "../../emverLite/mod" -import { SDKManifest } from "../../manifest/ManifestTypes" -import { ExpectedExports } from "../../types" +import { ExtendedVersion } from "../../exver" + +import * as T from "../../types" import { once } from "../../util/once" import { Migration } from "./Migration" -export class Migrations { +export class Migrations { private constructor( - readonly manifest: SDKManifest, + readonly manifest: T.Manifest, readonly migrations: Array>, ) {} private sortedMigrations = once(() => { const migrationsAsVersions = ( this.migrations as Array> - ).map((x) => [EmVer.parse(x.options.version), x] as const) + ) + .map((x) => [ExtendedVersion.parse(x.options.version), x] as const) + .filter(([v, _]) => v.flavor === this.currentVersion().flavor) migrationsAsVersions.sort((a, b) => a[0].compareForSort(b[0])) return migrationsAsVersions }) - private currentVersion = once(() => EmVer.parse(this.manifest.version)) + private currentVersion = once(() => + ExtendedVersion.parse(this.manifest.version), + ) static of< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, Migrations extends Array>, - >(manifest: SDKManifest, ...migrations: EnsureUniqueId) { + >(manifest: T.Manifest, ...migrations: EnsureUniqueId) { return new Migrations( manifest, migrations as Array>, @@ -30,11 +34,11 @@ export class Migrations { async init({ effects, previousVersion, - }: Parameters[0]) { + }: Parameters[0]) { if (!!previousVersion) { - const previousVersionEmVer = EmVer.parse(previousVersion) + const previousVersionExVer = ExtendedVersion.parse(previousVersion) for (const [_, migration] of this.sortedMigrations() - .filter((x) => x[0].greaterThan(previousVersionEmVer)) + .filter((x) => x[0].greaterThan(previousVersionExVer)) .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { await migration.up({ effects }) } @@ -43,12 +47,12 @@ export class Migrations { async uninit({ effects, nextVersion, - }: Parameters[0]) { + }: Parameters[0]) { if (!!nextVersion) { - const nextVersionEmVer = EmVer.parse(nextVersion) + const nextVersionExVer = ExtendedVersion.parse(nextVersion) const reversed = [...this.sortedMigrations()].reverse() for (const [_, migration] of reversed - .filter((x) => x[0].greaterThan(nextVersionEmVer)) + .filter((x) => x[0].greaterThan(nextVersionExVer)) .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { await migration.down({ effects }) } @@ -57,10 +61,10 @@ export class Migrations { } export function setupMigrations< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, Migrations extends Array>, ->(manifest: SDKManifest, ...migrations: EnsureUniqueId) { +>(manifest: T.Manifest, ...migrations: EnsureUniqueId) { return Migrations.of(manifest, ...migrations) } diff --git a/sdk/lib/inits/setupInit.ts b/sdk/lib/inits/setupInit.ts index 03a7085c5..5718caa58 100644 --- a/sdk/lib/inits/setupInit.ts +++ b/sdk/lib/inits/setupInit.ts @@ -1,25 +1,25 @@ import { DependenciesReceipt } from "../config/setupConfig" import { SetInterfaces } from "../interfaces/setupInterfaces" -import { SDKManifest } from "../manifest/ManifestTypes" + import { ExposedStorePaths } from "../store/setupExposeStore" -import { Effects, ExpectedExports } from "../types" +import * as T from "../types" import { Migrations } from "./migrations/setupMigrations" import { Install } from "./setupInstall" import { Uninstall } from "./setupUninstall" -export function setupInit( +export function setupInit( migrations: Migrations, install: Install, uninstall: Uninstall, setInterfaces: SetInterfaces, setDependencies: (options: { - effects: Effects + effects: T.Effects input: any }) => Promise, exposedStore: ExposedStorePaths, ): { - init: ExpectedExports.init - uninit: ExpectedExports.uninit + init: T.ExpectedExports.init + uninit: T.ExpectedExports.uninit } { return { init: async (opts) => { diff --git a/sdk/lib/inits/setupInstall.ts b/sdk/lib/inits/setupInstall.ts index 3990be0ca..7b51a22ea 100644 --- a/sdk/lib/inits/setupInstall.ts +++ b/sdk/lib/inits/setupInstall.ts @@ -1,12 +1,11 @@ -import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects, ExpectedExports } from "../types" +import * as T from "../types" -export type InstallFn = (opts: { - effects: Effects +export type InstallFn = (opts: { + effects: T.Effects }) => Promise -export class Install { +export class Install { private constructor(readonly fn: InstallFn) {} - static of( + static of( fn: InstallFn, ) { return new Install(fn) @@ -15,7 +14,7 @@ export class Install { async init({ effects, previousVersion, - }: Parameters[0]) { + }: Parameters[0]) { if (!previousVersion) await this.fn({ effects, @@ -23,7 +22,7 @@ export class Install { } } -export function setupInstall( +export function setupInstall( fn: InstallFn, ) { return Install.of(fn) diff --git a/sdk/lib/inits/setupUninstall.ts b/sdk/lib/inits/setupUninstall.ts index 812848c8f..c8c3e490f 100644 --- a/sdk/lib/inits/setupUninstall.ts +++ b/sdk/lib/inits/setupUninstall.ts @@ -1,12 +1,11 @@ -import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects, ExpectedExports } from "../types" +import * as T from "../types" -export type UninstallFn = (opts: { - effects: Effects +export type UninstallFn = (opts: { + effects: T.Effects }) => Promise -export class Uninstall { +export class Uninstall { private constructor(readonly fn: UninstallFn) {} - static of( + static of( fn: UninstallFn, ) { return new Uninstall(fn) @@ -15,7 +14,7 @@ export class Uninstall { async uninit({ effects, nextVersion, - }: Parameters[0]) { + }: Parameters[0]) { if (!nextVersion) await this.fn({ effects, @@ -23,7 +22,7 @@ export class Uninstall { } } -export function setupUninstall( +export function setupUninstall( fn: UninstallFn, ) { return Uninstall.of(fn) diff --git a/sdk/lib/interfaces/setupInterfaces.ts b/sdk/lib/interfaces/setupInterfaces.ts index 5ad8d8a7d..c82b69e0b 100644 --- a/sdk/lib/interfaces/setupInterfaces.ts +++ b/sdk/lib/interfaces/setupInterfaces.ts @@ -1,17 +1,17 @@ import { Config } from "../config/builder/config" -import { SDKManifest } from "../manifest/ManifestTypes" -import { AddressInfo, Effects } from "../types" + +import * as T from "../types" import { AddressReceipt } from "./AddressReceipt" -export type InterfacesReceipt = Array +export type InterfacesReceipt = Array export type SetInterfaces< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, ConfigInput extends Record, Output extends InterfacesReceipt, -> = (opts: { effects: Effects; input: null | ConfigInput }) => Promise +> = (opts: { effects: T.Effects; input: null | ConfigInput }) => Promise export type SetupInterfaces = < - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, ConfigInput extends Record, Output extends InterfacesReceipt, diff --git a/sdk/lib/mainFn/CommandController.ts b/sdk/lib/mainFn/CommandController.ts index 3af01e915..40f787f86 100644 --- a/sdk/lib/mainFn/CommandController.ts +++ b/sdk/lib/mainFn/CommandController.ts @@ -1,7 +1,7 @@ import { DEFAULT_SIGTERM_TIMEOUT } from "." import { NO_TIMEOUT, SIGKILL, SIGTERM } from "../StartSdk" -import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects, ImageId, ValidIfNoStupidEscape } from "../types" + +import * as T from "../types" import { MountOptions, Overlay } from "../util/Overlay" import { splitCommand } from "../util/splitCommand" import { cpExecFile, cpExec } from "./Daemons" @@ -13,14 +13,14 @@ export class CommandController { readonly pid: number | undefined, readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT, ) {} - static of() { + static of() { return async ( - effects: Effects, + effects: T.Effects, imageId: { - id: keyof Manifest["images"] & ImageId + id: keyof Manifest["images"] & T.ImageId sharedRun?: boolean }, - command: ValidIfNoStupidEscape | [string, ...string[]], + command: T.CommandType, options: { // Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms sigtermTimeout?: number diff --git a/sdk/lib/mainFn/Daemon.ts b/sdk/lib/mainFn/Daemon.ts index 90882418c..6dceda951 100644 --- a/sdk/lib/mainFn/Daemon.ts +++ b/sdk/lib/mainFn/Daemon.ts @@ -1,5 +1,4 @@ -import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects, ImageId, ValidIfNoStupidEscape } from "../types" +import * as T from "../types" import { MountOptions, Overlay } from "../util/Overlay" import { CommandController } from "./CommandController" @@ -14,14 +13,14 @@ export class Daemon { private commandController: CommandController | null = null private shouldBeRunning = false private constructor(private startCommand: () => Promise) {} - static of() { + static of() { return async ( - effects: Effects, + effects: T.Effects, imageId: { - id: keyof Manifest["images"] & ImageId + id: keyof Manifest["images"] & T.ImageId sharedRun?: boolean }, - command: ValidIfNoStupidEscape | [string, ...string[]], + command: T.CommandType, options: { mounts?: { path: string; options: MountOptions }[] overlay?: Overlay diff --git a/sdk/lib/mainFn/Daemons.ts b/sdk/lib/mainFn/Daemons.ts index fbda547aa..c766e2f2e 100644 --- a/sdk/lib/mainFn/Daemons.ts +++ b/sdk/lib/mainFn/Daemons.ts @@ -1,16 +1,11 @@ import { NO_TIMEOUT, SIGKILL, SIGTERM, Signals } from "../StartSdk" import { HealthReceipt } from "../health/HealthReceipt" import { CheckResult } from "../health/checkFns" -import { SDKManifest } from "../manifest/ManifestTypes" + import { Trigger } from "../trigger" import { TriggerInput } from "../trigger/TriggerInput" import { defaultTrigger } from "../trigger/defaultTrigger" -import { - DaemonReturned, - Effects, - ImageId, - ValidIfNoStupidEscape, -} from "../types" +import * as T from "../types" import { Mounts } from "./Mounts" import { CommandOptions, MountOptions, Overlay } from "../util/Overlay" import { splitCommand } from "../util/splitCommand" @@ -33,13 +28,13 @@ export type Ready = { } type DaemonsParams< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Ids extends string, Command extends string, Id extends string, > = { - command: ValidIfNoStupidEscape | [string, ...string[]] - image: { id: keyof Manifest["images"] & ImageId; sharedRun?: boolean } + command: T.CommandType + image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean } mounts: Mounts env?: Record ready: Ready @@ -49,7 +44,7 @@ type DaemonsParams< type ErrorDuplicateId = `The id '${Id}' is already used` -export const runCommand = () => +export const runCommand = () => CommandController.of() /** @@ -75,9 +70,9 @@ Daemons.of({ }) ``` */ -export class Daemons { +export class Daemons { private constructor( - readonly effects: Effects, + readonly effects: T.Effects, readonly started: (onTerm: () => PromiseLike) => PromiseLike, readonly daemons: Promise[], readonly ids: Ids[], @@ -93,8 +88,8 @@ export class Daemons { * @param config * @returns */ - static of(config: { - effects: Effects + static of(config: { + effects: T.Effects started: (onTerm: () => PromiseLike) => PromiseLike healthReceipts: HealthReceipt[] }) { diff --git a/sdk/lib/mainFn/Mounts.ts b/sdk/lib/mainFn/Mounts.ts index eeedc79c6..968b77b4e 100644 --- a/sdk/lib/mainFn/Mounts.ts +++ b/sdk/lib/mainFn/Mounts.ts @@ -1,10 +1,9 @@ -import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects } from "../types" +import * as T from "../types" import { MountOptions } from "../util/Overlay" type MountArray = { path: string; options: MountOptions }[] -export class Mounts { +export class Mounts { private constructor( readonly volumes: { id: Manifest["volumes"][number] @@ -26,7 +25,7 @@ export class Mounts { }[], ) {} - static of() { + static of() { return new Mounts([], [], []) } @@ -58,7 +57,7 @@ export class Mounts { return this } - addDependency( + addDependency( dependencyId: keyof Manifest["dependencies"] & string, volumeId: DependencyManifest["volumes"][number], subpath: string | null, diff --git a/sdk/lib/mainFn/index.ts b/sdk/lib/mainFn/index.ts index c4f74764c..7a094a31a 100644 --- a/sdk/lib/mainFn/index.ts +++ b/sdk/lib/mainFn/index.ts @@ -1,10 +1,10 @@ -import { ExpectedExports } from "../types" +import * as T from "../types" import { Daemons } from "./Daemons" import "../interfaces/ServiceInterfaceBuilder" import "../interfaces/Origin" import "./Daemons" -import { SDKManifest } from "../manifest/ManifestTypes" + import { MainEffects } from "../StartSdk" export const DEFAULT_SIGTERM_TIMEOUT = 30_000 @@ -18,12 +18,12 @@ export const DEFAULT_SIGTERM_TIMEOUT = 30_000 * @param fn * @returns */ -export const setupMain = ( +export const setupMain = ( fn: (o: { effects: MainEffects started(onTerm: () => PromiseLike): PromiseLike }) => Promise>, -): ExpectedExports.main => { +): T.ExpectedExports.main => { return async (options) => { const result = await fn(options) return result diff --git a/sdk/lib/manifest/ManifestTypes.ts b/sdk/lib/manifest/ManifestTypes.ts index 8e6b572d3..ea349710f 100644 --- a/sdk/lib/manifest/ManifestTypes.ts +++ b/sdk/lib/manifest/ManifestTypes.ts @@ -1,20 +1,16 @@ -import { ValidEmVer } from "../emverLite/mod" -import { ActionMetadata, ImageConfig, ImageId } from "../types" +import { ValidateExVer, ValidateExVers } from "../exver" +import { + ActionMetadata, + HardwareRequirements, + ImageConfig, + ImageId, + ImageSource, +} from "../types" -export type Container = { - /** This should be pointing to a docker container name */ - image: string - /** These should match the manifest data volumes */ - mounts: Record - /** Default is 64mb */ - shmSizeMb?: `${number}${"mb" | "gb" | "b" | "kb"}` - /** if more than 30s to shutdown */ - sigtermTimeout?: `${number}${"s" | "m" | "h"}` -} - -export type ManifestVersion = ValidEmVer - -export type SDKManifest = { +export type SDKManifest< + Version extends string, + Satisfies extends string[] = [], +> = { /** The package identifier used by the OS. This must be unique amongst all other known packages */ readonly id: string /** A human readable service title */ @@ -23,7 +19,8 @@ export type SDKManifest = { * - see documentation: https://github.com/Start9Labs/emver-rs. This value will change with each release of * the service */ - readonly version: ManifestVersion + readonly version: Version & ValidateExVer + readonly satisfies?: Satisfies & ValidateExVers /** Release notes for the update - can be a string, paragraph or URL */ readonly releaseNotes: string /** The type of license for the project. Include the LICENSE in the root of the project directory. A license is required for a Start9 package.*/ @@ -50,36 +47,49 @@ export type SDKManifest = { } /** Defines the os images needed to run the container processes */ - readonly images: Record + readonly images: Record /** This denotes readonly asset directories that should be available to mount to the container. - * Assuming that there will be three files with names along the lines: - * icon.* : the icon that will be this packages icon on the ui - * LICENSE : What the license is for this service - * Instructions : to be seen in the ui section of the package - * */ + * These directories are expected to be found in `assets/` at pack time. + **/ readonly assets: string[] /** This denotes any data volumes that should be available to mount to the container */ readonly volumes: string[] - readonly alerts: { - readonly install: string | null - readonly update: string | null - readonly uninstall: string | null - readonly restore: string | null - readonly start: string | null - readonly stop: string | null + readonly alerts?: { + readonly install?: string | null + readonly update?: string | null + readonly uninstall?: string | null + readonly restore?: string | null + readonly start?: string | null + readonly stop?: string | null } + readonly hasConfig?: boolean readonly dependencies: Readonly> + readonly hardwareRequirements?: { + readonly device?: { display?: RegExp; processor?: RegExp } + readonly ram?: number | null + readonly arch?: string[] | null + } +} + +export type SDKImageConfig = { + source: Exclude + arch?: string[] + emulateMissingAs?: string | null } export type ManifestDependency = { /** * A human readable explanation on what the dependency is used for */ - description: string | null + readonly description: string | null /** * Determines if the dependency is optional or not. Times that optional that are good include such situations * such as being able to toggle other services or to use a different service for the same purpose. */ - optional: boolean + readonly optional: boolean + /** + * A url or local path for an s9pk that satisfies this dependency + */ + readonly s9pk: string } diff --git a/sdk/lib/manifest/setupManifest.ts b/sdk/lib/manifest/setupManifest.ts index 8bd39a7aa..e3b746874 100644 --- a/sdk/lib/manifest/setupManifest.ts +++ b/sdk/lib/manifest/setupManifest.ts @@ -1,21 +1,71 @@ +import * as T from "../types" import { ImageConfig, ImageId, VolumeId } from "../osBindings" -import { SDKManifest, ManifestVersion } from "./ManifestTypes" +import { SDKManifest, SDKImageConfig } from "./ManifestTypes" +import { SDKVersion } from "../StartSdk" export function setupManifest< Id extends string, - Version extends ManifestVersion, + Version extends string, Dependencies extends Record, VolumesTypes extends VolumeId, AssetTypes extends VolumeId, ImagesTypes extends ImageId, - Manifest extends SDKManifest & { + Manifest extends SDKManifest & { dependencies: Dependencies id: Id - version: Version assets: AssetTypes[] - images: Record + images: Record volumes: VolumesTypes[] }, ->(manifest: Manifest): Manifest { - return manifest + Satisfies extends string[] = [], +>(manifest: Manifest & { version: Version }): Manifest & T.Manifest { + const images = Object.entries(manifest.images).reduce( + (images, [k, v]) => { + v.arch = v.arch || ["aarch64", "x86_64"] + if (v.emulateMissingAs === undefined) + v.emulateMissingAs = v.arch[0] || null + images[k] = v as ImageConfig + return images + }, + {} as { [k: string]: ImageConfig }, + ) + return { + ...manifest, + gitHash: null, + osVersion: SDKVersion, + satisfies: manifest.satisfies || [], + images, + alerts: { + install: manifest.alerts?.install || null, + update: manifest.alerts?.update || null, + uninstall: manifest.alerts?.uninstall || null, + restore: manifest.alerts?.restore || null, + start: manifest.alerts?.start || null, + stop: manifest.alerts?.stop || null, + }, + hasConfig: manifest.hasConfig === undefined ? true : manifest.hasConfig, + hardwareRequirements: { + device: Object.fromEntries( + Object.entries(manifest.hardwareRequirements?.device || {}).map( + ([k, v]) => [k, v.source], + ), + ), + ram: manifest.hardwareRequirements?.ram || null, + arch: + manifest.hardwareRequirements?.arch === undefined + ? Object.values(images).reduce( + (arch, config) => { + if (config.emulateMissingAs) { + return arch + } + if (arch === null) { + return config.arch + } + return arch.filter((a) => config.arch.includes(a)) + }, + null as string[] | null, + ) + : manifest.hardwareRequirements?.arch, + }, + } } diff --git a/sdk/lib/osBindings/CheckDependenciesResult.ts b/sdk/lib/osBindings/CheckDependenciesResult.ts index c102c733a..d349bdf18 100644 --- a/sdk/lib/osBindings/CheckDependenciesResult.ts +++ b/sdk/lib/osBindings/CheckDependenciesResult.ts @@ -1,4 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HealthCheckId } from "./HealthCheckId" import type { HealthCheckResult } from "./HealthCheckResult" import type { PackageId } from "./PackageId" @@ -6,6 +7,7 @@ export type CheckDependenciesResult = { packageId: PackageId isInstalled: boolean isRunning: boolean - healthChecks: Array + configSatisfied: boolean + healthChecks: { [key: HealthCheckId]: HealthCheckResult } version: string | null } diff --git a/sdk/lib/osBindings/CurrentDependencyInfo.ts b/sdk/lib/osBindings/CurrentDependencyInfo.ts index de46e4b52..2096a0113 100644 --- a/sdk/lib/osBindings/CurrentDependencyInfo.ts +++ b/sdk/lib/osBindings/CurrentDependencyInfo.ts @@ -2,9 +2,8 @@ import type { DataUrl } from "./DataUrl" export type CurrentDependencyInfo = { - title: string - icon: DataUrl - registryUrl: string - versionSpec: string + title: string | null + icon: DataUrl | null + versionRange: string configSatisfied: boolean } & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] }) diff --git a/sdk/lib/osBindings/DepInfo.ts b/sdk/lib/osBindings/DepInfo.ts index 20b9eb4cd..d635cca3b 100644 --- a/sdk/lib/osBindings/DepInfo.ts +++ b/sdk/lib/osBindings/DepInfo.ts @@ -1,3 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PathOrUrl } from "./PathOrUrl" -export type DepInfo = { description: string | null; optional: boolean } +export type DepInfo = { + description: string | null + optional: boolean + s9pk: PathOrUrl | null +} diff --git a/sdk/lib/osBindings/DependencyMetadata.ts b/sdk/lib/osBindings/DependencyMetadata.ts new file mode 100644 index 000000000..3d56ef052 --- /dev/null +++ b/sdk/lib/osBindings/DependencyMetadata.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DataUrl } from "./DataUrl" + +export type DependencyMetadata = { + title: string | null + icon: DataUrl | null + description: string | null + optional: boolean +} diff --git a/sdk/lib/osBindings/DependencyRequirement.ts b/sdk/lib/osBindings/DependencyRequirement.ts index d0415bee9..01b8e12ce 100644 --- a/sdk/lib/osBindings/DependencyRequirement.ts +++ b/sdk/lib/osBindings/DependencyRequirement.ts @@ -5,7 +5,6 @@ export type DependencyRequirement = kind: "running" id: string healthChecks: string[] - versionSpec: string - registryUrl: string + versionRange: string } - | { kind: "exists"; id: string; versionSpec: string; registryUrl: string } + | { kind: "exists"; id: string; versionRange: string } diff --git a/sdk/lib/osBindings/FullIndex.ts b/sdk/lib/osBindings/FullIndex.ts index 4d9914015..c7889760a 100644 --- a/sdk/lib/osBindings/FullIndex.ts +++ b/sdk/lib/osBindings/FullIndex.ts @@ -6,6 +6,7 @@ import type { PackageIndex } from "./PackageIndex" import type { SignerInfo } from "./SignerInfo" export type FullIndex = { + name: string | null icon: DataUrl | null package: PackageIndex os: OsIndex diff --git a/sdk/lib/osBindings/GetVersionParams.ts b/sdk/lib/osBindings/GetOsVersionParams.ts similarity index 85% rename from sdk/lib/osBindings/GetVersionParams.ts rename to sdk/lib/osBindings/GetOsVersionParams.ts index 853d76022..de0458645 100644 --- a/sdk/lib/osBindings/GetVersionParams.ts +++ b/sdk/lib/osBindings/GetOsVersionParams.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type GetVersionParams = { +export type GetOsVersionParams = { source: string | null target: string | null serverId: string | null diff --git a/sdk/lib/osBindings/GetPackageParams.ts b/sdk/lib/osBindings/GetPackageParams.ts index 8b852c35c..3dde55b28 100644 --- a/sdk/lib/osBindings/GetPackageParams.ts +++ b/sdk/lib/osBindings/GetPackageParams.ts @@ -7,5 +7,5 @@ export type GetPackageParams = { id: PackageId | null version: string | null sourceVersion: Version | null - otherVersions: PackageDetailLevel | null + otherVersions: PackageDetailLevel } diff --git a/sdk/lib/osBindings/HardwareRequirements.ts b/sdk/lib/osBindings/HardwareRequirements.ts index 0e1da1f36..3579e9524 100644 --- a/sdk/lib/osBindings/HardwareRequirements.ts +++ b/sdk/lib/osBindings/HardwareRequirements.ts @@ -1,7 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type HardwareRequirements = { - device: { [key: string]: string } + device: { device?: string; processor?: string } ram: number | null arch: string[] | null } diff --git a/sdk/lib/osBindings/InstallParams.ts b/sdk/lib/osBindings/InstallParams.ts new file mode 100644 index 000000000..2b70ad593 --- /dev/null +++ b/sdk/lib/osBindings/InstallParams.ts @@ -0,0 +1,9 @@ +// 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 InstallParams = { + registry: string + id: PackageId + version: Version +} diff --git a/sdk/lib/osBindings/Manifest.ts b/sdk/lib/osBindings/Manifest.ts index d808f47a2..51f14935a 100644 --- a/sdk/lib/osBindings/Manifest.ts +++ b/sdk/lib/osBindings/Manifest.ts @@ -13,6 +13,7 @@ export type Manifest = { id: PackageId title: string version: Version + satisfies: Array releaseNotes: string license: string wrapperRepo: string diff --git a/sdk/lib/osBindings/PackageDetailLevel.ts b/sdk/lib/osBindings/PackageDetailLevel.ts index b5e1ae42b..f2016f632 100644 --- a/sdk/lib/osBindings/PackageDetailLevel.ts +++ b/sdk/lib/osBindings/PackageDetailLevel.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type PackageDetailLevel = "short" | "full" +export type PackageDetailLevel = "none" | "short" | "full" diff --git a/sdk/lib/osBindings/PackageVersionInfo.ts b/sdk/lib/osBindings/PackageVersionInfo.ts index 364c530f2..80481acb3 100644 --- a/sdk/lib/osBindings/PackageVersionInfo.ts +++ b/sdk/lib/osBindings/PackageVersionInfo.ts @@ -1,8 +1,11 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Alerts } from "./Alerts" import type { DataUrl } from "./DataUrl" +import type { DependencyMetadata } from "./DependencyMetadata" import type { Description } from "./Description" import type { HardwareRequirements } from "./HardwareRequirements" import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" +import type { PackageId } from "./PackageId" import type { RegistryAsset } from "./RegistryAsset" export type PackageVersionInfo = { @@ -16,6 +19,9 @@ export type PackageVersionInfo = { upstreamRepo: string supportSite: string marketingSite: string + donationUrl: string | null + alerts: Alerts + dependencyMetadata: { [key: PackageId]: DependencyMetadata } osVersion: string hardwareRequirements: HardwareRequirements sourceVersion: string | null diff --git a/sdk/lib/osBindings/PathOrUrl.ts b/sdk/lib/osBindings/PathOrUrl.ts new file mode 100644 index 000000000..9c4ff1e28 --- /dev/null +++ b/sdk/lib/osBindings/PathOrUrl.ts @@ -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 PathOrUrl = string diff --git a/sdk/lib/osBindings/RegistryAsset.ts b/sdk/lib/osBindings/RegistryAsset.ts index 3eb13e8a4..41f09431f 100644 --- a/sdk/lib/osBindings/RegistryAsset.ts +++ b/sdk/lib/osBindings/RegistryAsset.ts @@ -3,6 +3,7 @@ import type { AnySignature } from "./AnySignature" import type { AnyVerifyingKey } from "./AnyVerifyingKey" export type RegistryAsset = { + publishedAt: string url: string commitment: Commitment signatures: { [key: AnyVerifyingKey]: AnySignature } diff --git a/sdk/lib/osBindings/RegistryInfo.ts b/sdk/lib/osBindings/RegistryInfo.ts new file mode 100644 index 000000000..f9265fdec --- /dev/null +++ b/sdk/lib/osBindings/RegistryInfo.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Category } from "./Category" +import type { DataUrl } from "./DataUrl" + +export type RegistryInfo = { + name: string | null + icon: DataUrl | null + categories: { [key: string]: Category } +} diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts index 32e57956a..70ce65f29 100644 --- a/sdk/lib/osBindings/index.ts +++ b/sdk/lib/osBindings/index.ts @@ -37,6 +37,7 @@ export { CurrentDependencyInfo } from "./CurrentDependencyInfo" export { DataUrl } from "./DataUrl" export { Dependencies } from "./Dependencies" export { DependencyKind } from "./DependencyKind" +export { DependencyMetadata } from "./DependencyMetadata" export { DependencyRequirement } from "./DependencyRequirement" export { DepInfo } from "./DepInfo" export { Description } from "./Description" @@ -51,6 +52,7 @@ export { FullIndex } from "./FullIndex" export { FullProgress } from "./FullProgress" export { GetHostInfoParams } from "./GetHostInfoParams" export { GetOsAssetParams } from "./GetOsAssetParams" +export { GetOsVersionParams } from "./GetOsVersionParams" export { GetPackageParams } from "./GetPackageParams" export { GetPackageResponseFull } from "./GetPackageResponseFull" export { GetPackageResponse } from "./GetPackageResponse" @@ -61,7 +63,6 @@ export { GetSslCertificateParams } from "./GetSslCertificateParams" export { GetSslKeyParams } from "./GetSslKeyParams" export { GetStoreParams } from "./GetStoreParams" export { GetSystemSmtpParams } from "./GetSystemSmtpParams" -export { GetVersionParams } from "./GetVersionParams" export { Governor } from "./Governor" export { Guid } from "./Guid" export { HardwareRequirements } from "./HardwareRequirements" @@ -82,6 +83,7 @@ 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 { LanInfo } from "./LanInfo" @@ -109,11 +111,13 @@ export { PackageVersionInfo } from "./PackageVersionInfo" export { ParamsMaybePackageId } from "./ParamsMaybePackageId" export { ParamsPackageId } from "./ParamsPackageId" export { PasswordType } from "./PasswordType" +export { PathOrUrl } from "./PathOrUrl" export { ProcedureId } from "./ProcedureId" export { Progress } from "./Progress" export { Public } from "./Public" export { RecoverySource } from "./RecoverySource" export { RegistryAsset } from "./RegistryAsset" +export { RegistryInfo } from "./RegistryInfo" export { RemoveActionParams } from "./RemoveActionParams" export { RemoveAddressParams } from "./RemoveAddressParams" export { RemoveVersionParams } from "./RemoveVersionParams" diff --git a/sdk/lib/test/configBuilder.test.ts b/sdk/lib/test/configBuilder.test.ts index c5003c55b..a413d76b8 100644 --- a/sdk/lib/test/configBuilder.test.ts +++ b/sdk/lib/test/configBuilder.test.ts @@ -369,7 +369,7 @@ describe("values", () => { setupManifest({ id: "testOutput", title: "", - version: "1.0", + version: "1.0.0:0", releaseNotes: "", license: "", replaces: [], @@ -395,9 +395,10 @@ describe("values", () => { stop: null, }, dependencies: { - remoteTest: { + "remote-test": { description: "", optional: true, + s9pk: "https://example.com/remote-test.s9pk", }, }, }), diff --git a/sdk/lib/test/emverList.test.ts b/sdk/lib/test/emverList.test.ts deleted file mode 100644 index 07dbb5aaf..000000000 --- a/sdk/lib/test/emverList.test.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { EmVer, notRange, rangeAnd, rangeOf, rangeOr } from "../emverLite/mod" -describe("EmVer", () => { - { - { - const checker = rangeOf("*") - test("rangeOf('*')", () => { - checker.check("1") - checker.check("1.2") - checker.check("1.2.3") - checker.check("1.2.3.4") - checker.check("1.2.3.4.5") - checker.check("1.2.3.4.5.6") - expect(checker.check("1")).toEqual(true) - expect(checker.check("1.2")).toEqual(true) - expect(checker.check("1.2.3.4")).toEqual(true) - }) - test("rangeOf('*') invalid", () => { - expect(() => checker.check("a")).toThrow() - expect(() => checker.check("")).toThrow() - expect(() => checker.check("1..3")).toThrow() - }) - } - - { - const checker = rangeOf(">1.2.3.4") - test(`rangeOf(">1.2.3.4") valid`, () => { - expect(checker.check("2-beta123")).toEqual(true) - expect(checker.check("2")).toEqual(true) - expect(checker.check("1.2.3.5")).toEqual(true) - expect(checker.check("1.2.3.4.1")).toEqual(true) - }) - - test(`rangeOf(">1.2.3.4") invalid`, () => { - expect(checker.check("1.2.3.4")).toEqual(false) - expect(checker.check("1.2.3")).toEqual(false) - expect(checker.check("1")).toEqual(false) - }) - } - { - const checker = rangeOf("=1.2.3") - test(`rangeOf("=1.2.3") valid`, () => { - expect(checker.check("1.2.3")).toEqual(true) - }) - - test(`rangeOf("=1.2.3") invalid`, () => { - expect(checker.check("2")).toEqual(false) - expect(checker.check("1.2.3.1")).toEqual(false) - expect(checker.check("1.2")).toEqual(false) - }) - } - { - const checker = rangeOf(">=1.2.3.4") - test(`rangeOf(">=1.2.3.4") valid`, () => { - expect(checker.check("2")).toEqual(true) - expect(checker.check("1.2.3.5")).toEqual(true) - expect(checker.check("1.2.3.4.1")).toEqual(true) - expect(checker.check("1.2.3.4")).toEqual(true) - }) - - test(`rangeOf(">=1.2.3.4") invalid`, () => { - expect(checker.check("1.2.3")).toEqual(false) - expect(checker.check("1")).toEqual(false) - }) - } - { - const checker = rangeOf("<1.2.3.4") - test(`rangeOf("<1.2.3.4") invalid`, () => { - expect(checker.check("2")).toEqual(false) - expect(checker.check("1.2.3.5")).toEqual(false) - expect(checker.check("1.2.3.4.1")).toEqual(false) - expect(checker.check("1.2.3.4")).toEqual(false) - }) - - test(`rangeOf("<1.2.3.4") valid`, () => { - expect(checker.check("1.2.3")).toEqual(true) - expect(checker.check("1")).toEqual(true) - }) - } - { - const checker = rangeOf("<=1.2.3.4") - test(`rangeOf("<=1.2.3.4") invalid`, () => { - expect(checker.check("2")).toEqual(false) - expect(checker.check("1.2.3.5")).toEqual(false) - expect(checker.check("1.2.3.4.1")).toEqual(false) - }) - - test(`rangeOf("<=1.2.3.4") valid`, () => { - expect(checker.check("1.2.3")).toEqual(true) - expect(checker.check("1")).toEqual(true) - expect(checker.check("1.2.3.4")).toEqual(true) - }) - } - - { - const checkA = rangeOf(">1") - const checkB = rangeOf("<=2") - - const checker = rangeAnd(checkA, checkB) - test(`simple and(checkers) valid`, () => { - expect(checker.check("2")).toEqual(true) - - expect(checker.check("1.1")).toEqual(true) - }) - test(`simple and(checkers) invalid`, () => { - expect(checker.check("2.1")).toEqual(false) - expect(checker.check("1")).toEqual(false) - expect(checker.check("0")).toEqual(false) - }) - } - { - const checkA = rangeOf("<1") - const checkB = rangeOf("=2") - - const checker = rangeOr(checkA, checkB) - test(`simple or(checkers) valid`, () => { - expect(checker.check("2")).toEqual(true) - expect(checker.check("0.1")).toEqual(true) - }) - test(`simple or(checkers) invalid`, () => { - expect(checker.check("2.1")).toEqual(false) - expect(checker.check("1")).toEqual(false) - expect(checker.check("1.1")).toEqual(false) - }) - } - - { - const checker = rangeOf("1.2.*") - test(`rangeOf(1.2.*) valid`, () => { - expect(checker.check("1.2")).toEqual(true) - expect(checker.check("1.2.1")).toEqual(true) - }) - test(`rangeOf(1.2.*) invalid`, () => { - expect(checker.check("1.3")).toEqual(false) - expect(checker.check("1.3.1")).toEqual(false) - - expect(checker.check("1.1.1")).toEqual(false) - expect(checker.check("1.1")).toEqual(false) - expect(checker.check("1")).toEqual(false) - - expect(checker.check("2")).toEqual(false) - }) - } - - { - const checker = notRange(rangeOf("1.2.*")) - test(`notRange(rangeOf(1.2.*)) valid`, () => { - expect(checker.check("1.3")).toEqual(true) - expect(checker.check("1.3.1")).toEqual(true) - - expect(checker.check("1.1.1")).toEqual(true) - expect(checker.check("1.1")).toEqual(true) - expect(checker.check("1")).toEqual(true) - - expect(checker.check("2")).toEqual(true) - }) - test(`notRange(rangeOf(1.2.*)) invalid `, () => { - expect(checker.check("1.2")).toEqual(false) - expect(checker.check("1.2.1")).toEqual(false) - }) - } - { - const checker = rangeOf("!1.2.*") - test(`!(rangeOf(1.2.*)) valid`, () => { - expect(checker.check("1.3")).toEqual(true) - expect(checker.check("1.3.1")).toEqual(true) - - expect(checker.check("1.1.1")).toEqual(true) - expect(checker.check("1.1")).toEqual(true) - expect(checker.check("1")).toEqual(true) - - expect(checker.check("2")).toEqual(true) - }) - test(`!(rangeOf(1.2.*)) invalid `, () => { - expect(checker.check("1.2")).toEqual(false) - expect(checker.check("1.2.1")).toEqual(false) - }) - } - { - test(`no and ranges`, () => { - expect(() => rangeAnd()).toThrow() - }) - test(`no or ranges`, () => { - expect(() => rangeOr()).toThrow() - }) - } - { - const checker = rangeOf("!>1.2.3.4") - test(`rangeOf("!>1.2.3.4") invalid`, () => { - expect(checker.check("2")).toEqual(false) - expect(checker.check("1.2.3.5")).toEqual(false) - expect(checker.check("1.2.3.4.1")).toEqual(false) - }) - - test(`rangeOf("!>1.2.3.4") valid`, () => { - expect(checker.check("1.2.3.4")).toEqual(true) - expect(checker.check("1.2.3")).toEqual(true) - expect(checker.check("1")).toEqual(true) - }) - } - - { - test(">1 && =1.2", () => { - const checker = rangeOf(">1 && =1.2") - - expect(checker.check("1.2")).toEqual(true) - expect(checker.check("1.2.1")).toEqual(false) - }) - test("=1 || =2", () => { - const checker = rangeOf("=1 || =2") - - expect(checker.check("1")).toEqual(true) - expect(checker.check("2")).toEqual(true) - expect(checker.check("3")).toEqual(false) - }) - - test(">1 && =1.2 || =2", () => { - const checker = rangeOf(">1 && =1.2 || =2") - - expect(checker.check("1.2")).toEqual(true) - expect(checker.check("1")).toEqual(false) - expect(checker.check("2")).toEqual(true) - expect(checker.check("3")).toEqual(false) - }) - - test("&& before || order of operationns: <1.5 && >1 || >1.5 && <3", () => { - const checker = rangeOf("<1.5 && >1 || >1.5 && <3") - expect(checker.check("1.1")).toEqual(true) - expect(checker.check("2")).toEqual(true) - - expect(checker.check("1.5")).toEqual(false) - expect(checker.check("1")).toEqual(false) - expect(checker.check("3")).toEqual(false) - }) - - test("Compare function on the emver", () => { - const a = EmVer.from("1.2.3") - const b = EmVer.from("1.2.4") - - expect(a.compare(b)).toEqual("less") - expect(b.compare(a)).toEqual("greater") - expect(a.compare(a)).toEqual("equal") - }) - test("Compare for sort function on the emver", () => { - const a = EmVer.from("1.2.3") - const b = EmVer.from("1.2.4") - - expect(a.compareForSort(b)).toEqual(-1) - expect(b.compareForSort(a)).toEqual(1) - expect(a.compareForSort(a)).toEqual(0) - }) - } - } -}) diff --git a/sdk/lib/test/exverList.test.ts b/sdk/lib/test/exverList.test.ts new file mode 100644 index 000000000..e29a9f0d1 --- /dev/null +++ b/sdk/lib/test/exverList.test.ts @@ -0,0 +1,355 @@ +import { VersionRange, ExtendedVersion } from "../exver" +describe("ExVer", () => { + { + { + const checker = VersionRange.parse("*") + test("VersionRange.parse('*')", () => { + checker.satisfiedBy(ExtendedVersion.parse("1:0")) + checker.satisfiedBy(ExtendedVersion.parse("1.2:0")) + checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0")) + checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4")) + checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.5")) + checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.5.6")) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + true, + ) + }) + test("VersionRange.parse('*') invalid", () => { + expect(() => checker.satisfiedBy(ExtendedVersion.parse("a"))).toThrow() + expect(() => checker.satisfiedBy(ExtendedVersion.parse(""))).toThrow() + expect(() => + checker.satisfiedBy(ExtendedVersion.parse("1..3")), + ).toThrow() + }) + } + + { + const checker = VersionRange.parse(">1.2.3:4") + test(`VersionRange.parse(">1.2.3:4") valid`, () => { + expect( + checker.satisfiedBy(ExtendedVersion.parse("2-beta.123:0")), + ).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual( + true, + ) + }) + + test(`VersionRange.parse(">1.2.3:4") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + }) + } + { + const checker = VersionRange.parse("=1.2.3") + test(`VersionRange.parse("=1.2.3") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + true, + ) + }) + + test(`VersionRange.parse("=1.2.3") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:1"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + false, + ) + }) + } + { + const checker = VersionRange.parse(">=1.2.3:4") + test(`VersionRange.parse(">=1.2.3:4") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + true, + ) + }) + + test(`VersionRange.parse(">=1.2.3:4") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + }) + } + { + const checker = VersionRange.parse("<1.2.3:4") + test(`VersionRange.parse("<1.2.3:4") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + false, + ) + }) + + test(`VersionRange.parse("<1.2.3:4") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + }) + } + { + const checker = VersionRange.parse("<=1.2.3:4") + test(`VersionRange.parse("<=1.2.3:4") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual( + false, + ) + }) + + test(`VersionRange.parse("<=1.2.3:4") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + true, + ) + }) + } + + { + const checkA = VersionRange.parse(">1") + const checkB = VersionRange.parse("<=2") + + const checker = checkA.and(checkB) + test(`simple and(checkers) valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + true, + ) + }) + test(`simple and(checkers) invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2.1:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + }) + } + { + const checkA = VersionRange.parse("<1") + const checkB = VersionRange.parse("=2") + + const checker = checkA.or(checkB) + test(`simple or(checkers) valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("0.1:0"))).toEqual( + true, + ) + }) + test(`simple or(checkers) invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2.1:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + false, + ) + }) + } + + { + const checker = VersionRange.parse("~1.2") + test(`VersionRange.parse(~1.2) valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.1:0"))).toEqual( + true, + ) + }) + test(`VersionRange.parse(~1.2) invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3.1:0"))).toEqual( + false, + ) + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1.1:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false) + }) + } + + { + const checker = VersionRange.parse("~1.2").not() + test(`VersionRange.parse(~1.2).not() valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3.1:0"))).toEqual( + true, + ) + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1.1:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + }) + test(`VersionRange.parse(~1.2).not() invalid `, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.1:0"))).toEqual( + false, + ) + }) + } + { + const checker = VersionRange.parse("!~1.2") + test(`!(VersionRange.parse(~1.2)) valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3.1:0"))).toEqual( + true, + ) + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1.1:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + }) + test(`!(VersionRange.parse(~1.2)) invalid `, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.1:0"))).toEqual( + false, + ) + }) + } + { + const checker = VersionRange.parse("!>1.2.3:4") + test(`VersionRange.parse("!>1.2.3:4") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual( + false, + ) + }) + + test(`VersionRange.parse("!>1.2.3:4") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + }) + } + + { + test(">1 && =1.2", () => { + const checker = VersionRange.parse(">1 && =1.2") + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.1:0"))).toEqual( + false, + ) + }) + test("=1 || =2", () => { + const checker = VersionRange.parse("=1 || =2") + + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("3:0"))).toEqual(false) + }) + + test(">1 && =1.2 || =2", () => { + const checker = VersionRange.parse(">1 && =1.2 || =2") + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("3:0"))).toEqual(false) + }) + + test("&& before || order of operationns: <1.5 && >1 || >1.5 && <3", () => { + const checker = VersionRange.parse("<1.5 && >1 || >1.5 && <3") + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.5:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("3:0"))).toEqual(false) + }) + + test("Compare function on the emver", () => { + const a = ExtendedVersion.parse("1.2.3:0") + const b = ExtendedVersion.parse("1.2.4:0") + + expect(a.compare(b)).toEqual("less") + expect(b.compare(a)).toEqual("greater") + expect(a.compare(a)).toEqual("equal") + }) + test("Compare for sort function on the emver", () => { + const a = ExtendedVersion.parse("1.2.3:0") + const b = ExtendedVersion.parse("1.2.4:0") + + expect(a.compareForSort(b)).toEqual(-1) + expect(b.compareForSort(a)).toEqual(1) + expect(a.compareForSort(a)).toEqual(0) + }) + } + } +}) diff --git a/sdk/lib/test/output.sdk.ts b/sdk/lib/test/output.sdk.ts index 189491be5..c56e05e60 100644 --- a/sdk/lib/test/output.sdk.ts +++ b/sdk/lib/test/output.sdk.ts @@ -7,7 +7,7 @@ export const sdk = StartSdk.of() setupManifest({ id: "testOutput", title: "", - version: "1.0", + version: "1.0:0", releaseNotes: "", license: "", replaces: [], @@ -33,9 +33,10 @@ export const sdk = StartSdk.of() stop: null, }, dependencies: { - remoteTest: { + "remote-test": { description: "", optional: false, + s9pk: "https://example.com/remote-test.s9pk", }, }, }), diff --git a/sdk/lib/test/setupDependencyConfig.test.ts b/sdk/lib/test/setupDependencyConfig.test.ts index 5fa4a0ddf..622559eb6 100644 --- a/sdk/lib/test/setupDependencyConfig.test.ts +++ b/sdk/lib/test/setupDependencyConfig.test.ts @@ -21,7 +21,7 @@ describe("setupDependencyConfig", () => { dependencyConfig: async ({}) => {}, }) sdk.setupDependencyConfig(testConfig, { - remoteTest, + "remote-test": remoteTest, }) }) }) diff --git a/sdk/lib/test/utils.splitCommand.test.ts b/sdk/lib/test/utils.splitCommand.test.ts deleted file mode 100644 index aafddb177..000000000 --- a/sdk/lib/test/utils.splitCommand.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getHostname } from "../util/getServiceInterface" -import { splitCommand } from "../util/splitCommand" - -describe("splitCommand ", () => { - const inputToExpected = [ - ["cat", ["cat"]], - [["cat"], ["cat"]], - [ - ["cat", "hello all my homies"], - ["cat", "hello all my homies"], - ], - ["cat hello world", ["cat", "hello", "world"]], - ["cat hello 'big world'", ["cat", "hello", "big world"]], - [`cat hello "big world"`, ["cat", "hello", "big world"]], - [ - `cat hello "big world's are the greatest"`, - ["cat", "hello", "big world's are the greatest"], - ], - // Too many spaces - ["cat ", ["cat"]], - [["cat "], ["cat "]], - [ - ["cat ", "hello all my homies "], - ["cat ", "hello all my homies "], - ], - ["cat hello world ", ["cat", "hello", "world"]], - [ - " cat hello 'big world' ", - ["cat", "hello", "big world"], - ], - [ - ` cat hello "big world" `, - ["cat", "hello", "big world"], - ], - ] - - for (const [input, expectValue] of inputToExpected) { - test(`should return ${expectValue} for ${input}`, () => { - expect(splitCommand(input as any)).toEqual(expectValue) - }) - } -}) diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index 686cb4750..e9c766448 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -12,6 +12,7 @@ import { LanInfo, BindParams, Manifest, + CheckDependenciesResult, } from "./osBindings" import { MainEffects, ServiceInterfaceType, Signals } from "./StartSdk" @@ -154,14 +155,6 @@ export type DependencyConfig = { }): Promise } -export type ValidIfNoStupidEscape = A extends - | `${string}'"'"'${string}` - | `${string}\\"${string}` - ? never - : "" extends A & "" - ? never - : A - export type ConfigRes = { /** This should be the previous config, that way during set config we start with the previous */ config?: null | Record @@ -188,9 +181,7 @@ export type SmtpValue = { password: string | null | undefined } -export type CommandType = - | ValidIfNoStupidEscape - | [string, ...string[]] +export type CommandType = string | [string, ...string[]] export type DaemonReturned = { wait(): Promise @@ -470,7 +461,7 @@ export type Effects = { */ checkDependencies(options: { packageIds: PackageId[] | null - }): Promise + }): Promise /** Exists could be useful during the runtime to know if some service exists, option dep */ exists(options: { packageId: PackageId }): Promise /** Exists could be useful during the runtime to know if some service is running, option dep */ @@ -554,12 +545,3 @@ export type Dependencies = Array export type DeepPartial = T extends {} ? { [P in keyof T]?: DeepPartial } : T - -export type CheckDependencyResult = { - packageId: PackageId - isInstalled: boolean - isRunning: boolean - healthChecks: SetHealth[] - version: string | null -} -export type CheckResults = CheckDependencyResult[] diff --git a/sdk/lib/util/splitCommand.ts b/sdk/lib/util/splitCommand.ts index 69f00a5a7..ac1237574 100644 --- a/sdk/lib/util/splitCommand.ts +++ b/sdk/lib/util/splitCommand.ts @@ -1,17 +1,8 @@ import { arrayOf, string } from "ts-matches" -import { ValidIfNoStupidEscape } from "../types" export const splitCommand = ( command: string | [string, ...string[]], ): string[] => { if (arrayOf(string).test(command)) return command - return String(command) - .split('"') - .flatMap((x, i) => - i % 2 !== 0 - ? [x] - : x.split("'").flatMap((x, i) => (i % 2 !== 0 ? [x] : x.split(" "))), - ) - .map((x) => x.trim()) - .filter(Boolean) + return ["sh", "-c", command] } diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 0a3655a7d..06d456019 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -22,9 +22,11 @@ "@types/jest": "^29.4.0", "@types/lodash.merge": "^4.6.2", "jest": "^29.4.3", + "peggy": "^3.0.2", "prettier": "^3.2.5", "ts-jest": "^29.0.5", "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", "tsx": "^4.7.1", "typescript": "^5.0.4" } @@ -1030,6 +1032,41 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", @@ -1054,6 +1091,42 @@ "@sinonjs/commons": "^2.0.0" } }, + "node_modules/@ts-morph/common": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.19.0.tgz", + "integrity": "sha512-Unz/WHmd4pGax91rdIKWi51wnVUW11QttMEPpBiBgIewnc9UQIX7UDLxr5vRlqeByXCwhkF6VabSsI0raWcyAQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.12", + "minimatch": "^7.4.3", + "mkdirp": "^2.1.6", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -1605,6 +1678,12 @@ "node": ">= 0.12.0" } }, + "node_modules/code-block-writer": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", + "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", + "dev": true + }, "node_modules/collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -1629,6 +1708,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1826,12 +1914,37 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -1963,6 +2076,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -2076,6 +2201,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-generator-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", @@ -2085,6 +2219,18 @@ "node": ">=6" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2938,6 +3084,15 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -2986,6 +3141,21 @@ "node": "*" } }, + "node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3124,6 +3294,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3157,6 +3333,22 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/peggy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/peggy/-/peggy-3.0.2.tgz", + "integrity": "sha512-n7chtCbEoGYRwZZ0i/O3t1cPr6o+d9Xx4Zwy2LYfzv0vjchMBU0tO+qYYyvZloBPcgRgzYvALzGWHe609JjEpg==", + "dev": true, + "dependencies": { + "commander": "^10.0.0", + "source-map-generator": "0.8.0" + }, + "bin": { + "peggy": "bin/peggy.js" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -3266,6 +3458,26 @@ } ] }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -3337,6 +3549,39 @@ "node": ">=10" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -3397,6 +3642,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-generator": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map-generator/-/source-map-generator-0.8.0.tgz", + "integrity": "sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -3631,6 +3885,16 @@ "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.5.1.tgz", "integrity": "sha512-UFYaKgfqlg9FROK7bdpYqFwG1CJvP4kOJdjXuWoqxo9jCmANoDw1GxkSCpJgoTeIiSTaTH5Qr1klSspb8c+ydg==" }, + "node_modules/ts-morph": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-18.0.0.tgz", + "integrity": "sha512-Kg5u0mk19PIIe4islUI/HWRvm9bC1lHejK4S0oh1zaZ77TMZAEmQC0sHQYiu2RgCQFZKXz1fMVi/7nOOeirznA==", + "dev": true, + "dependencies": { + "@ts-morph/common": "~0.19.0", + "code-block-writer": "^12.0.0" + } + }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -3674,6 +3938,37 @@ } } }, + "node_modules/ts-pegjs": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ts-pegjs/-/ts-pegjs-4.2.1.tgz", + "integrity": "sha512-mK/O2pu6lzWUeKpEMA/wsa0GdYblfjJI1y0s0GqH6xCTvugQDOWPJbm5rY6AHivpZICuXIriCb+a7Cflbdtc2w==", + "dev": true, + "dependencies": { + "prettier": "^2.8.8", + "ts-morph": "^18.0.0" + }, + "bin": { + "tspegjs": "dist/cli.mjs" + }, + "peerDependencies": { + "peggy": "^3.0.2" + } + }, + "node_modules/ts-pegjs/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/tsx": { "version": "4.7.1", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.1.tgz", diff --git a/sdk/package.json b/sdk/package.json index 8ed984a82..e03153722 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -49,9 +49,11 @@ "@types/jest": "^29.4.0", "@types/lodash.merge": "^4.6.2", "jest": "^29.4.3", + "peggy": "^3.0.2", "prettier": "^3.2.5", "ts-jest": "^29.0.5", "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", "tsx": "^4.7.1", "typescript": "^5.0.4" } diff --git a/web/angular.json b/web/angular.json index cf6c113a4..e3153139b 100644 --- a/web/angular.json +++ b/web/angular.json @@ -74,7 +74,8 @@ "with": "projects/ui/src/environments/environment.prod.ts" } ], - "outputHashing": "all" + "outputHashing": "all", + "extractLicenses": false }, "development": { "buildOptimizer": false, diff --git a/web/package-lock.json b/web/package-lock.json index cf32bbc01..ac1f77a6b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "startos-ui", "version": "0.3.6", + "license": "MIT", "dependencies": { "@angular/animations": "^14.1.0", "@angular/common": "^14.1.0", @@ -114,6 +115,7 @@ } }, "../sdk/dist": { + "name": "@start9labs/start-sdk", "version": "0.3.6-alpha5", "license": "MIT", "dependencies": { @@ -130,9 +132,11 @@ "@types/jest": "^29.4.0", "@types/lodash.merge": "^4.6.2", "jest": "^29.4.3", + "peggy": "^3.0.2", "prettier": "^3.2.5", "ts-jest": "^29.0.5", "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", "tsx": "^4.7.1", "typescript": "^5.0.4" } diff --git a/web/package.json b/web/package.json index a75701333..b304d4f94 100644 --- a/web/package.json +++ b/web/package.json @@ -3,6 +3,7 @@ "version": "0.3.6", "author": "Start9 Labs, Inc", "homepage": "https://start9.com/", + "license": "MIT", "scripts": { "ng": "ng", "check": "npm run check:shared && npm run check:marketplace && npm run check:ui && npm run check:install-wiz && npm run check:setup && npm run check:dui", diff --git a/web/patchdb-ui-seed.json b/web/patchdb-ui-seed.json index 7bea3ea14..6236275cd 100644 --- a/web/patchdb-ui-seed.json +++ b/web/patchdb-ui-seed.json @@ -1,20 +1,24 @@ { "name": null, - "ack-welcome": "0.3.6", + "ackWelcome": "0.3.6", "marketplace": { - "selected-url": "https://registry.start9.com/", - "known-hosts": { - "https://registry.start9.com/": {}, - "https://community-registry.start9.com/": {} + "selectedUrl": "https://registry.start9.com/", + "knownHosts": { + "https://registry.start9.com/": { + "name": "Start9 Registry" + }, + "https://community-registry.start9.com/": { + "name": "Community Registry" + } } }, "dev": {}, "gaming": { "snake": { - "high-score": 0 + "highScore": 0 } }, - "ack-instructions": {}, + "ackInstructions": {}, "theme": "Dark", "widgets": [] } diff --git a/web/projects/marketplace/src/pages/release-notes/release-notes.component.html b/web/projects/marketplace/src/modals/release-notes/release-notes.component.html similarity index 63% rename from web/projects/marketplace/src/pages/release-notes/release-notes.component.html rename to web/projects/marketplace/src/modals/release-notes/release-notes.component.html index 74e34c88f..74661827d 100644 --- a/web/projects/marketplace/src/pages/release-notes/release-notes.component.html +++ b/web/projects/marketplace/src/modals/release-notes/release-notes.component.html @@ -1,6 +1,17 @@ - + + + Past Release Notes + + + + + + + + + -
+
-

{{ note.key | displayEmver }}

+

{{ note.key }}

{ + return Object.entries(this.pkg.otherVersions) + .filter( + ([v, _]) => + this.exver.getFlavor(v) === this.pkg.flavor && + this.exver.compareExver(this.pkg.version, v) === 1, + ) + .reduce( + (obj, [version, info]) => ({ + ...obj, + [version]: info.releaseNotes, + }), + { + [`${this.pkg.version} (current)`]: this.pkg.releaseNotes, + }, + ) + }), + ) + + constructor( + private readonly marketplaceService: AbstractMarketplaceService, + private readonly exver: Exver, + private readonly modalCtrl: ModalController, + ) {} + + async dismiss() { + return this.modalCtrl.dismiss() + } + + isSelected(key: string): boolean { + return this.selected === key + } + + setSelected(selected: string) { + this.selected = this.isSelected(selected) ? null : selected + } + + getDocSize(key: string, { nativeElement }: ElementRef) { + return this.isSelected(key) ? nativeElement.scrollHeight : 0 + } + + asIsOrder(a: any, b: any) { + return 0 + } +} diff --git a/web/projects/marketplace/src/pages/release-notes/release-notes.module.ts b/web/projects/marketplace/src/modals/release-notes/release-notes.module.ts similarity index 86% rename from web/projects/marketplace/src/pages/release-notes/release-notes.module.ts rename to web/projects/marketplace/src/modals/release-notes/release-notes.module.ts index 583631dc4..41055a314 100644 --- a/web/projects/marketplace/src/pages/release-notes/release-notes.module.ts +++ b/web/projects/marketplace/src/modals/release-notes/release-notes.module.ts @@ -2,12 +2,11 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, TextSpinnerComponentModule, } from '@start9labs/shared' import { TuiElementModule } from '@taiga-ui/cdk' - import { ReleaseNotesComponent } from './release-notes.component' @NgModule({ @@ -15,11 +14,11 @@ import { ReleaseNotesComponent } from './release-notes.component' CommonModule, IonicModule, TextSpinnerComponentModule, - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, TuiElementModule, ], declarations: [ReleaseNotesComponent], exports: [ReleaseNotesComponent], }) -export class ReleaseNotesModule {} +export class ReleaseNotesComponentModule {} diff --git a/web/projects/marketplace/src/pages/list/categories/categories.component.html b/web/projects/marketplace/src/pages/list/categories/categories.component.html index 4e99a21c2..29613813b 100644 --- a/web/projects/marketplace/src/pages/list/categories/categories.component.html +++ b/web/projects/marketplace/src/pages/list/categories/categories.component.html @@ -1,9 +1,9 @@ - {{ cat }} + {{ cat.value.name }} diff --git a/web/projects/marketplace/src/pages/list/categories/categories.component.ts b/web/projects/marketplace/src/pages/list/categories/categories.component.ts index b34761079..349a3bd18 100644 --- a/web/projects/marketplace/src/pages/list/categories/categories.component.ts +++ b/web/projects/marketplace/src/pages/list/categories/categories.component.ts @@ -5,6 +5,7 @@ import { Input, Output, } from '@angular/core' +import { T } from '@start9labs/start-sdk' @Component({ selector: 'marketplace-categories', @@ -17,7 +18,7 @@ import { }) export class CategoriesComponent { @Input() - categories: readonly string[] = [] + categories!: Map @Input() category = '' @@ -29,4 +30,8 @@ export class CategoriesComponent { this.category = category this.categoryChange.emit(category) } + + originalOrder() { + return 0 + } } diff --git a/web/projects/marketplace/src/pages/list/item/item.component.html b/web/projects/marketplace/src/pages/list/item/item.component.html index 9a88d0869..0a22b8699 100644 --- a/web/projects/marketplace/src/pages/list/item/item.component.html +++ b/web/projects/marketplace/src/pages/list/item/item.component.html @@ -1,12 +1,16 @@ - +

- {{ pkg.manifest.title }} + {{ pkg.title }}

-

{{ pkg.manifest.description.short }}

+

{{ pkg.description.short }}

diff --git a/web/projects/marketplace/src/pages/release-notes/release-notes.component.ts b/web/projects/marketplace/src/pages/release-notes/release-notes.component.ts deleted file mode 100644 index 49da475d9..000000000 --- a/web/projects/marketplace/src/pages/release-notes/release-notes.component.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { getPkgId } from '@start9labs/shared' -import { AbstractMarketplaceService } from '../../services/marketplace.service' - -@Component({ - selector: 'release-notes', - templateUrl: './release-notes.component.html', - styleUrls: ['./release-notes.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ReleaseNotesComponent { - private readonly pkgId = getPkgId(this.route) - - private selected: string | null = null - - readonly notes$ = this.marketplaceService.fetchReleaseNotes$(this.pkgId) - - constructor( - private readonly route: ActivatedRoute, - private readonly marketplaceService: AbstractMarketplaceService, - ) {} - - isSelected(key: string): boolean { - return this.selected === key - } - - setSelected(selected: string) { - this.selected = this.isSelected(selected) ? null : selected - } - - getDocSize(key: string, { nativeElement }: ElementRef) { - return this.isSelected(key) ? nativeElement.scrollHeight : 0 - } - - asIsOrder(a: any, b: any) { - return 0 - } -} diff --git a/web/projects/marketplace/src/pages/show/about/about.component.html b/web/projects/marketplace/src/pages/show/about/about.component.html index 1126f8b54..56d363071 100644 --- a/web/projects/marketplace/src/pages/show/about/about.component.html +++ b/web/projects/marketplace/src/pages/show/about/about.component.html @@ -1,13 +1,11 @@ - - New in {{ pkg.manifest.version | displayEmver }} - +New in {{ pkg.version }} -
+
- + Past Release Notes @@ -15,10 +13,10 @@ Description -

{{ pkg.manifest.description.long }}

+

{{ pkg.description.long }}

-
+
View website diff --git a/web/projects/marketplace/src/pages/show/about/about.component.ts b/web/projects/marketplace/src/pages/show/about/about.component.ts index 6626d4fbe..e9d69ebb3 100644 --- a/web/projects/marketplace/src/pages/show/about/about.component.ts +++ b/web/projects/marketplace/src/pages/show/about/about.component.ts @@ -1,5 +1,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { MarketplacePkg } from '../../../types' +import { ModalController } from '@ionic/angular' +import { ReleaseNotesComponent } from '../../../modals/release-notes/release-notes.component' @Component({ selector: 'marketplace-about', @@ -10,4 +12,15 @@ import { MarketplacePkg } from '../../../types' export class AboutComponent { @Input() pkg!: MarketplacePkg + + constructor(private readonly modalCtrl: ModalController) {} + + async presentModalNotes() { + const modal = await this.modalCtrl.create({ + componentProps: { pkg: this.pkg }, + component: ReleaseNotesComponent, + }) + + await modal.present() + } } diff --git a/web/projects/marketplace/src/pages/show/about/about.module.ts b/web/projects/marketplace/src/pages/show/about/about.module.ts index b48bbcbaa..a110cd300 100644 --- a/web/projects/marketplace/src/pages/show/about/about.module.ts +++ b/web/projects/marketplace/src/pages/show/about/about.module.ts @@ -2,9 +2,9 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' -import { EmverPipesModule, MarkdownPipeModule } from '@start9labs/shared' - +import { ExverPipesModule, MarkdownPipeModule } from '@start9labs/shared' import { AboutComponent } from './about.component' +import { ReleaseNotesComponentModule } from '../../../modals/release-notes/release-notes.module' @NgModule({ imports: [ @@ -12,7 +12,8 @@ import { AboutComponent } from './about.component' RouterModule, IonicModule, MarkdownPipeModule, - EmverPipesModule, + ExverPipesModule, + ReleaseNotesComponentModule, ], declarations: [AboutComponent], exports: [AboutComponent], diff --git a/web/projects/marketplace/src/pages/show/additional/additional.component.html b/web/projects/marketplace/src/pages/show/additional/additional.component.html index a75daa3db..03a52cc94 100644 --- a/web/projects/marketplace/src/pages/show/additional/additional.component.html +++ b/web/projects/marketplace/src/pages/show/additional/additional.component.html @@ -1,10 +1,10 @@ Additional Info - +

License

-

{{ manifest.license }}

+

{{ pkg.license }}

@@ -53,39 +53,39 @@

Source Repository

-

{{ manifest.upstreamRepo }}

+

{{ pkg.upstreamRepo }}

Wrapper Repository

-

{{ manifest.wrapperRepo }}

+

{{ pkg.wrapperRepo }}

Support Site

-

{{ manifest.supportSite || 'Not provided' }}

+

{{ pkg.supportSite || 'Not provided' }}

diff --git a/web/projects/marketplace/src/pages/show/additional/additional.component.ts b/web/projects/marketplace/src/pages/show/additional/additional.component.ts index 778ea6c54..92ad42bff 100644 --- a/web/projects/marketplace/src/pages/show/additional/additional.component.ts +++ b/web/projects/marketplace/src/pages/show/additional/additional.component.ts @@ -10,15 +10,9 @@ import { ModalController, ToastController, } from '@ionic/angular' -import { - copyToClipboard, - displayEmver, - Emver, - MarkdownComponent, -} from '@start9labs/shared' +import { copyToClipboard, Exver, MarkdownComponent } from '@start9labs/shared' import { MarketplacePkg } from '../../../types' import { AbstractMarketplaceService } from '../../../services/marketplace.service' -import { ActivatedRoute } from '@angular/router' @Component({ selector: 'marketplace-additional', @@ -32,15 +26,12 @@ export class AdditionalComponent { @Output() version = new EventEmitter() - readonly url = this.route.snapshot.queryParamMap.get('url') || undefined - constructor( private readonly alertCtrl: AlertController, private readonly modalCtrl: ModalController, - private readonly emver: Emver, + private readonly exver: Exver, private readonly marketplaceService: AbstractMarketplaceService, private readonly toastCtrl: ToastController, - private readonly route: ActivatedRoute, ) {} async copy(address: string): Promise { @@ -58,41 +49,53 @@ export class AdditionalComponent { } async presentAlertVersions() { - const alert = await this.alertCtrl.create({ - header: 'Versions', - inputs: this.pkg.versions - .sort((a, b) => -1 * (this.emver.compare(a, b) || 0)) - .map(v => ({ - name: v, // for CSS - type: 'radio', - label: displayEmver(v), // appearance on screen - value: v, // literal SEM version value - checked: this.pkg.manifest.version === v, - })), - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Ok', - handler: (version: string) => this.version.emit(version), - }, - ], - }) + const versions = Object.keys(this.pkg.otherVersions).filter( + v => this.exver.getFlavor(v) === this.pkg.flavor, + ) - await alert.present() + if (!versions.length) { + const alert = await this.alertCtrl.create({ + header: 'Versions', + message: 'No other versions', + }) + + await alert.present() + } else { + const alert = await this.alertCtrl.create({ + header: 'Versions', + inputs: versions + .sort((a, b) => -1 * (this.exver.compareExver(a, b) || 0)) + .map(v => ({ + name: v, // for CSS + type: 'radio', + label: v, // appearance on screen + value: v, // literal SEM version value + checked: this.pkg.version === v, + })), + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Ok', + handler: (version: string) => this.version.emit(version), + }, + ], + }) + + await alert.present() + } } - async presentModalMd(title: string) { + async presentModalMd(asset: 'license' | 'instructions') { const content = this.marketplaceService.fetchStatic$( - this.pkg.manifest.id, - title, - this.url, + this.pkg, + asset === 'license' ? 'LICENSE.md' : 'instructions.md', ) const modal = await this.modalCtrl.create({ - componentProps: { title, content }, + componentProps: { title: asset, content }, component: MarkdownComponent, }) diff --git a/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.html b/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.html index 4c1bfcc65..b5d3fb4db 100644 --- a/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.html +++ b/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.html @@ -2,7 +2,7 @@

- {{ pkg.dependencyMetadata[dep.key].title }} + {{ + pkg.dependencyMetadata[dep.key].title + ? pkg.dependencyMetadata[dep.key].title + : dep.key + }} (optional) (Required) diff --git a/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts b/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts index a1fed6cca..7b7f2efe7 100644 --- a/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts +++ b/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts @@ -11,6 +11,7 @@ export class DependenciesComponent { pkg!: MarketplacePkg getImg(key: string): string { - return this.pkg.dependencyMetadata[key].icon + const icon = this.pkg.dependencyMetadata[key]?.icon + return icon ? icon : 'assets/img/service-icons/fallback.png' } } diff --git a/web/projects/marketplace/src/pages/show/dependencies/dependencies.module.ts b/web/projects/marketplace/src/pages/show/dependencies/dependencies.module.ts index abb3032e9..55b220d08 100644 --- a/web/projects/marketplace/src/pages/show/dependencies/dependencies.module.ts +++ b/web/projects/marketplace/src/pages/show/dependencies/dependencies.module.ts @@ -2,11 +2,7 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' -import { - EmverPipesModule, - ResponsiveColModule, - SharedPipesModule, -} from '@start9labs/shared' +import { ResponsiveColModule, SharedPipesModule } from '@start9labs/shared' import { DependenciesComponent } from './dependencies.component' @@ -16,7 +12,6 @@ import { DependenciesComponent } from './dependencies.component' RouterModule, IonicModule, SharedPipesModule, - EmverPipesModule, ResponsiveColModule, ], declarations: [DependenciesComponent], diff --git a/web/projects/marketplace/src/pages/show/flavors/flavors.component.html b/web/projects/marketplace/src/pages/show/flavors/flavors.component.html new file mode 100644 index 000000000..7a0f64aff --- /dev/null +++ b/web/projects/marketplace/src/pages/show/flavors/flavors.component.html @@ -0,0 +1,21 @@ +Alternative Implementations + + + + + + + + +

+ {{ pkg.title }} +

+

{{ pkg.version }}

+
+
+
+
+
diff --git a/web/projects/marketplace/src/pages/show/flavors/flavors.component.ts b/web/projects/marketplace/src/pages/show/flavors/flavors.component.ts new file mode 100644 index 000000000..4927f47a1 --- /dev/null +++ b/web/projects/marketplace/src/pages/show/flavors/flavors.component.ts @@ -0,0 +1,12 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { MarketplacePkg } from '../../../types' + +@Component({ + selector: 'marketplace-flavors', + templateUrl: 'flavors.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FlavorsComponent { + @Input() + pkgs!: MarketplacePkg[] +} diff --git a/web/projects/marketplace/src/pages/show/flavors/flavors.module.ts b/web/projects/marketplace/src/pages/show/flavors/flavors.module.ts new file mode 100644 index 000000000..662a914fd --- /dev/null +++ b/web/projects/marketplace/src/pages/show/flavors/flavors.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common' +import { NgModule } from '@angular/core' +import { RouterModule } from '@angular/router' +import { IonicModule } from '@ionic/angular' +import { ResponsiveColModule, SharedPipesModule } from '@start9labs/shared' +import { FlavorsComponent } from './flavors.component' + +@NgModule({ + imports: [ + CommonModule, + RouterModule, + IonicModule, + SharedPipesModule, + ResponsiveColModule, + ], + declarations: [FlavorsComponent], + exports: [FlavorsComponent], +}) +export class FlavorsModule {} diff --git a/web/projects/marketplace/src/pages/show/package/package.component.html b/web/projects/marketplace/src/pages/show/package/package.component.html index 94e7006b1..aa5fcf0d0 100644 --- a/web/projects/marketplace/src/pages/show/package/package.component.html +++ b/web/projects/marketplace/src/pages/show/package/package.component.html @@ -1,9 +1,11 @@
-

{{ pkg.manifest.title }}

-

{{ pkg.manifest.version | displayEmver }}

-

Released: {{ pkg.publishedAt | date: 'medium' }}

+

{{ pkg.title }}

+

{{ pkg.version }}

+

+ Released: {{ pkg.s9pk.publishedAt | date : 'medium' }} +

diff --git a/web/projects/marketplace/src/pages/show/package/package.module.ts b/web/projects/marketplace/src/pages/show/package/package.module.ts index 502ee82bb..8565a2352 100644 --- a/web/projects/marketplace/src/pages/show/package/package.module.ts +++ b/web/projects/marketplace/src/pages/show/package/package.module.ts @@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { IonicModule } from '@ionic/angular' import { - EmverPipesModule, + ExverPipesModule, SharedPipesModule, TickerModule, } from '@start9labs/shared' @@ -16,7 +16,7 @@ import { PackageComponent } from './package.component' CommonModule, IonicModule, SharedPipesModule, - EmverPipesModule, + ExverPipesModule, TickerModule, ], }) diff --git a/web/projects/marketplace/src/pipes/filter-packages.pipe.ts b/web/projects/marketplace/src/pipes/filter-packages.pipe.ts index 48313376f..ed546ada9 100644 --- a/web/projects/marketplace/src/pipes/filter-packages.pipe.ts +++ b/web/projects/marketplace/src/pipes/filter-packages.pipe.ts @@ -26,11 +26,11 @@ export class FilterPackagesPipe implements PipeTransform { distance: 16, keys: [ { - name: 'manifest.title', + name: 'title', weight: 1, }, { - name: 'manifest.id', + name: 'id', weight: 0.5, }, ], @@ -42,19 +42,19 @@ export class FilterPackagesPipe implements PipeTransform { useExtendedSearch: true, keys: [ { - name: 'manifest.title', + name: 'title', weight: 1, }, { - name: 'manifest.id', + name: 'id', weight: 0.5, }, { - name: 'manifest.description.short', + name: 'description.short', weight: 0.4, }, { - name: 'manifest.description.long', + name: 'description.long', weight: 0.1, }, ], @@ -71,7 +71,8 @@ export class FilterPackagesPipe implements PipeTransform { .filter(p => category === 'all' || p.categories.includes(category)) .sort((a, b) => { return ( - new Date(b.publishedAt).valueOf() - new Date(a.publishedAt).valueOf() + new Date(b.s9pk.publishedAt).valueOf() - + new Date(a.s9pk.publishedAt).valueOf() ) }) } diff --git a/web/projects/marketplace/src/public-api.ts b/web/projects/marketplace/src/public-api.ts index b9e8c4686..600ceabfe 100644 --- a/web/projects/marketplace/src/public-api.ts +++ b/web/projects/marketplace/src/public-api.ts @@ -10,8 +10,6 @@ export * from './pages/list/search/search.component' export * from './pages/list/search/search.module' export * from './pages/list/skeleton/skeleton.component' export * from './pages/list/skeleton/skeleton.module' -export * from './pages/release-notes/release-notes.component' -export * from './pages/release-notes/release-notes.module' export * from './pages/show/about/about.component' export * from './pages/show/about/about.module' export * from './pages/show/additional/additional.component' @@ -20,6 +18,8 @@ export * from './pages/show/dependencies/dependencies.component' export * from './pages/show/dependencies/dependencies.module' export * from './pages/show/package/package.component' export * from './pages/show/package/package.module' +export * from './pages/show/flavors/flavors.component' +export * from './pages/show/flavors/flavors.module' export * from './pipes/filter-packages.pipe' diff --git a/web/projects/marketplace/src/services/marketplace.service.ts b/web/projects/marketplace/src/services/marketplace.service.ts index af1b473d5..3fdabc426 100644 --- a/web/projects/marketplace/src/services/marketplace.service.ts +++ b/web/projects/marketplace/src/services/marketplace.service.ts @@ -12,18 +12,13 @@ export abstract class AbstractMarketplaceService { abstract getPackage$( id: string, - version: string, + version: string | null, + flavor: string | null, url?: string, - ): Observable // could be {} so need to check in show page - - abstract fetchReleaseNotes$( - id: string, - url?: string, - ): Observable> + ): Observable abstract fetchStatic$( - id: string, - type: string, - url?: string, + pkg: MarketplacePkg, + type: 'LICENSE.md' | 'instructions.md', ): Observable } diff --git a/web/projects/marketplace/src/types.ts b/web/projects/marketplace/src/types.ts index 7b99c7b80..e25aa59eb 100644 --- a/web/projects/marketplace/src/types.ts +++ b/web/projects/marketplace/src/types.ts @@ -1,48 +1,40 @@ -import { Url } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' -export type StoreURL = string -export type StoreName = string - -export interface StoreIdentity { - url: StoreURL - name?: StoreName +export type GetPackageReq = { + id: string + version: string | null + sourceVersion: null // @TODO what is this? + otherVersions: 'short' +} +export type GetPackageRes = T.GetPackageResponse & { + otherVersions: { [version: string]: T.PackageInfoShort } } -export type Marketplace = Record -export interface StoreData { - info: StoreInfo +export type GetPackagesReq = { + id: null + version: null + sourceVersion: null + otherVersions: 'short' +} +export type GetPackagesRes = { + [id: T.PackageId]: GetPackageRes +} + +export type StoreIdentity = { + url: string + name?: string +} + +export type Marketplace = Record + +export type StoreData = { + info: T.RegistryInfo packages: MarketplacePkg[] } -export interface StoreInfo { - name: StoreName - categories: string[] -} - -export type StoreIdentityWithData = StoreData & StoreIdentity - -export interface MarketplacePkg { - icon: Url - license: Url - instructions: Url - manifest: T.Manifest - categories: string[] - versions: string[] - dependencyMetadata: { - [id: string]: DependencyMetadata +export type MarketplacePkg = T.PackageVersionInfo & + Omit & { + id: T.PackageId + version: string + flavor: string | null } - publishedAt: string -} - -export interface DependencyMetadata { - title: string - icon: Url - optional: boolean - hidden: boolean -} - -export interface Dependency { - description: string | null - optional: boolean -} diff --git a/web/projects/shared/assets/img/service-icons/fallback.png b/web/projects/shared/assets/img/service-icons/fallback.png new file mode 100644 index 0000000000000000000000000000000000000000..75f97cc58c1e08e82be6ae6dcff8cc7e6b3296e4 GIT binary patch literal 30439 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 Object.entries(marketplace).reduce((list, [_, store]) => { - store?.packages.forEach(({ manifest: { id, version } }) => { + store?.packages.forEach(({ id, version }) => { if ( local[id] && - this.emver.compare( + this.exver.compareExver( version, getManifest(local[id]).version || '', ) === 1 @@ -125,7 +125,7 @@ export class MenuComponent { @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, private readonly splitPane: SplitPaneTracker, - private readonly emver: Emver, + private readonly exver: Exver, private readonly connection$: ConnectionService, ) {} } diff --git a/web/projects/ui/src/app/components/backup-drives/backup.service.ts b/web/projects/ui/src/app/components/backup-drives/backup.service.ts index 7e05178bf..837bebf9b 100644 --- a/web/projects/ui/src/app/components/backup-drives/backup.service.ts +++ b/web/projects/ui/src/app/components/backup-drives/backup.service.ts @@ -7,7 +7,8 @@ import { DiskBackupTarget, } from 'src/app/services/api/api.types' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' -import { getErrorMessage, Emver } from '@start9labs/shared' +import { Exver, getErrorMessage } from '@start9labs/shared' +import { Version } from '@start9labs/start-sdk' @Injectable({ providedIn: 'root', @@ -20,7 +21,7 @@ export class BackupService { constructor( private readonly embassyApi: ApiService, - private readonly emver: Emver, + private readonly exver: Exver, ) {} async getBackupTargets(): Promise { @@ -57,14 +58,15 @@ export class BackupService { hasAnyBackup(target: BackupTarget): boolean { return Object.values(target.startOs).some( - s => this.emver.compare(s.version, '0.3.6') !== -1, + s => this.exver.compareOsVersion(s.version, '0.3.6') !== 'less', ) } hasThisBackup(target: BackupTarget, id: string): boolean { return ( target.startOs[id] && - this.emver.compare(target.startOs[id].version, '0.3.6') !== -1 + this.exver.compareOsVersion(target.startOs[id].version, '0.3.6') !== + 'less' ) } } diff --git a/web/projects/ui/src/app/components/form/form-array/form-array.component.html b/web/projects/ui/src/app/components/form/form-array/form-array.component.html index 76c67f837..d66387fb5 100644 --- a/web/projects/ui/src/app/components/form/form-array/form-array.component.html +++ b/web/projects/ui/src/app/components/form/form-array/form-array.component.html @@ -28,7 +28,7 @@ [open]="!!open.get(item)" (openChange)="open.set(item, $event)" > - {{ item.value | mustache: $any(spec.spec).displayAs }} + {{ item.value | mustache : $any(spec.spec).displayAs }} diff --git a/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html b/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html index 05ffa69f3..37387a338 100644 --- a/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html +++ b/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html @@ -18,8 +18,8 @@ [disabled]="!!spec.disabled" [readOnly]="readOnly" [pseudoInvalid]="invalid" - [min]="spec.min ? (spec.min | tuiMapper: getLimit)[0] : min" - [max]="spec.max ? (spec.max | tuiMapper: getLimit)[0] : max" + [min]="spec.min ? (spec.min | tuiMapper : getLimit)[0] : min" + [max]="spec.max ? (spec.max | tuiMapper : getLimit)[0] : max" [(ngModel)]="value" (focusedChange)="onFocus($event)" > @@ -32,8 +32,8 @@ [disabled]="!!spec.disabled" [readOnly]="readOnly" [pseudoInvalid]="invalid" - [min]="spec.min ? (spec.min | tuiMapper: getLimit) : min" - [max]="spec.max ? (spec.max | tuiMapper: getLimit) : max" + [min]="spec.min ? (spec.min | tuiMapper : getLimit) : min" + [max]="spec.max ? (spec.max | tuiMapper : getLimit) : max" [(ngModel)]="value" (focusedChange)="onFocus($event)" > diff --git a/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.service.ts b/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.service.ts index 50cbc5a56..8cf5ce76d 100644 --- a/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.service.ts +++ b/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core' import { endWith, Observable } from 'rxjs' import { map } from 'rxjs/operators' -import { Emver } from '@start9labs/shared' +import { Exver } from '@start9labs/shared' import { PatchDB } from 'patch-db-client' import { ConfigService } from '../../../services/config.service' import { DataModel } from 'src/app/services/patch-db/data-model' @@ -9,13 +9,16 @@ import { DataModel } from 'src/app/services/patch-db/data-model' @Injectable({ providedIn: 'root' }) export class RefreshAlertService extends Observable { private readonly stream$ = this.patch.watch$('serverInfo', 'version').pipe( - map(version => !!this.emver.compare(this.config.version, version)), + map( + version => + this.exver.compareOsVersion(this.config.version, version) !== 'equal', + ), endWith(false), ) constructor( private readonly patch: PatchDB, - private readonly emver: Emver, + private readonly exver: Exver, private readonly config: ConfigService, ) { super(subscriber => this.stream$.subscribe(subscriber)) diff --git a/web/projects/ui/src/app/modals/app-recover-select/to-options.pipe.ts b/web/projects/ui/src/app/modals/app-recover-select/to-options.pipe.ts index 8a607896e..1c2d99f53 100644 --- a/web/projects/ui/src/app/modals/app-recover-select/to-options.pipe.ts +++ b/web/projects/ui/src/app/modals/app-recover-select/to-options.pipe.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core' -import { Emver } from '@start9labs/shared' +import { Exver } from '@start9labs/shared' import { PackageBackupInfo } from 'src/app/services/api/api.types' import { ConfigService } from 'src/app/services/config.service' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' @@ -19,7 +19,7 @@ export interface AppRecoverOption extends PackageBackupInfo { export class ToOptionsPipe implements PipeTransform { constructor( private readonly config: ConfigService, - private readonly emver: Emver, + private readonly exver: Exver, ) {} transform( @@ -44,7 +44,9 @@ export class ToOptionsPipe implements PipeTransform { } private compare(version: string): boolean { - // checks to see if backup was made on a newer version of eOS - return this.emver.compare(version, this.config.version) === 1 + // checks to see if backup was made on a newer version of startOS + return ( + this.exver.compareOsVersion(version, this.config.version) === 'greater' + ) } } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index dd4e4b36e..63c534bb6 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -150,7 +150,7 @@ export class AppActionsPage { try { await this.embassyApi.uninstallPackage({ id: this.pkgId }) this.embassyApi - .setDbValue(['ack-instructions', this.pkgId], false) + .setDbValue(['ackInstructions', this.pkgId], false) .catch(e => console.error('Failed to mark instructions as unseen', e)) this.navCtrl.navigateRoot('/services') } catch (e: any) { diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html index 9173de39f..e353cf9b2 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html @@ -11,7 +11,7 @@

{{ manifest.title }}

-

{{ manifest.version | displayEmver }}

+

{{ manifest.version }}

diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.module.ts index 89998c9bf..aa4c5fcd6 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.module.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.module.ts @@ -4,7 +4,7 @@ import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { AppListPage } from './app-list.page' import { - EmverPipesModule, + ExverPipesModule, ResponsiveColModule, TextSpinnerComponentModule, TickerModule, @@ -29,7 +29,7 @@ const routes: Routes = [ imports: [ CommonModule, StatusComponentModule, - EmverPipesModule, + ExverPipesModule, TextSpinnerComponentModule, LaunchablePipeModule, UiPipeModule, diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts index 86744cc91..8db082f42 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts @@ -4,7 +4,7 @@ import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { AppShowPage } from './app-show.page' import { - EmverPipesModule, + ExverPipesModule, ResponsiveColModule, SharedPipesModule, } from '@start9labs/shared' @@ -49,7 +49,7 @@ const routes: Routes = [ InstallingProgressPipeModule, IonicModule, RouterModule.forChild(routes), - EmverPipesModule, + ExverPipesModule, LaunchablePipeModule, UiPipeModule, ResponsiveColModule, diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html index 5b3123131..390c6c642 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html @@ -7,8 +7,7 @@ @@ -34,9 +33,7 @@ - +
diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts index cb1f6f5f9..52855be3c 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { NavController } from '@ionic/angular' import { PatchDB } from 'patch-db-client' import { + AllPackageData, DataModel, InstallingState, PackageDataEntry, @@ -47,17 +48,19 @@ export class AppShowPage { private readonly pkgId = getPkgId(this.route) readonly pkgPlus$ = combineLatest([ - this.patch.watch$('packageData', this.pkgId), + this.patch.watch$('packageData'), this.depErrorService.getPkgDepErrors$(this.pkgId), ]).pipe( - tap(([pkg, _]) => { + tap(([allPkgs, _]) => { + const pkg = allPkgs[this.pkgId] // if package disappears, navigate to list page if (!pkg) this.navCtrl.navigateRoot('/services') }), - map(([pkg, depErrors]) => { + map(([allPkgs, depErrors]) => { + const pkg = allPkgs[this.pkgId] return { pkg, - dependencies: this.getDepInfo(pkg, depErrors), + dependencies: this.getDepInfo(pkg, allPkgs, depErrors), status: renderPkgStatus(pkg, depErrors), } }), @@ -81,17 +84,45 @@ export class AppShowPage { private getDepInfo( pkg: PackageDataEntry, + allPkgs: AllPackageData, depErrors: PkgDependencyErrors, ): DependencyInfo[] { const manifest = getManifest(pkg) return Object.keys(pkg.currentDependencies) .filter(id => !!manifest.dependencies[id]) - .map(id => this.getDepValues(pkg, manifest, id, depErrors)) + .map(id => this.getDepValues(pkg, allPkgs, manifest, id, depErrors)) + } + + private getDepDetails( + pkg: PackageDataEntry, + allPkgs: AllPackageData, + depId: string, + ) { + const { title, icon, versionRange } = pkg.currentDependencies[depId] + + if ( + allPkgs[depId] && + (allPkgs[depId].stateInfo.state === 'installed' || + allPkgs[depId].stateInfo.state === 'updating') + ) { + return { + title: allPkgs[depId].stateInfo.manifest!.title, + icon: allPkgs[depId].icon, + versionRange, + } + } else { + return { + title: title ? title : depId, + icon: icon ? icon : 'assets/img/service-icons/fallback.png', + versionRange, + } + } } private getDepValues( pkg: PackageDataEntry, + allPkgs: AllPackageData, manifest: T.Manifest, depId: string, depErrors: PkgDependencyErrors, @@ -103,11 +134,15 @@ export class AppShowPage { depErrors, ) - const { title, icon, versionSpec } = pkg.currentDependencies[depId] + const { title, icon, versionRange } = this.getDepDetails( + pkg, + allPkgs, + depId, + ) return { id: depId, - version: versionSpec, + version: versionRange, title, icon, errorText: errorText @@ -190,7 +225,7 @@ export class AppShowPage { const dependentInfo: DependentInfo = { id: pkgManifest.id, title: pkgManifest.title, - version: pkg.currentDependencies[depId].versionSpec, + version: pkg.currentDependencies[depId].versionRange, } const navigationExtras: NavigationExtras = { state: { dependentInfo }, diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.html index 73fc2158b..e524e13b5 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.html @@ -6,11 +6,11 @@

Version

-

{{ manifest.version | displayEmver }}

+

{{ pkg.stateInfo.manifest.version }}

License

-

{{ manifest.license }}

+

{{ pkg.stateInfo.manifest.license }}

Marketing Site

-

{{ manifest.marketingSite || 'Not provided' }}

+

{{ pkg.stateInfo.manifest.marketingSite || 'Not provided' }}

@@ -54,52 +54,52 @@

Source Repository

-

{{ manifest.upstreamRepo }}

+

{{ pkg.stateInfo.manifest.upstreamRepo }}

Wrapper Repository

-

{{ manifest.wrapperRepo }}

+

{{ pkg.stateInfo.manifest.wrapperRepo }}

Support Site

-

{{ manifest.supportSite || 'Not provided' }}

+

{{ pkg.stateInfo.manifest.supportSite || 'Not provided' }}

Donation Link

-

{{ manifest.donationUrl || 'Not provided' }}

+

{{ pkg.stateInfo.manifest.donationUrl || 'Not provided' }}

diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.ts index 278fa3d88..9aa152917 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.ts @@ -1,9 +1,12 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ModalController, ToastController } from '@ionic/angular' import { copyToClipboard, MarkdownComponent } from '@start9labs/shared' -import { T } from '@start9labs/start-sdk' import { from } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' +import { + InstalledState, + PackageDataEntry, +} from 'src/app/services/patch-db/data-model' @Component({ selector: 'app-show-additional', @@ -12,7 +15,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' }) export class AppShowAdditionalComponent { @Input() - manifest!: T.Manifest + pkg!: PackageDataEntry constructor( private readonly modalCtrl: ModalController, @@ -35,16 +38,12 @@ export class AppShowAdditionalComponent { } async presentModalLicense() { - const { id, version } = this.manifest + const { id } = this.pkg.stateInfo.manifest const modal = await this.modalCtrl.create({ componentProps: { title: 'License', - content: from( - this.api.getStatic( - `/public/package-data/${id}/${version}/LICENSE.md`, - ), - ), + content: from(this.api.getStaticInstalled(id, 'LICENSE.md')), }, component: MarkdownComponent, }) diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html index e9f7b97d7..b184b83ff 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html @@ -15,7 +15,7 @@ > {{ dep.title }}

-

{{ dep.version | displayEmver }}

+

{{ dep.version }}

{{ dep.errorText || 'satisfied' }} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.html index 06f517222..efe34b5ec 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.html @@ -9,7 +9,7 @@

{{ manifest.title }}

-

{{ manifest.version | displayEmver }}

+

{{ manifest.version }}

diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html index 9b62a6402..ea80b3003 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html @@ -7,10 +7,12 @@

, private readonly formDialog: FormDialogService, ) {} @@ -42,7 +42,7 @@ export class ToButtonsPipe implements PipeTransform { return [ // instructions { - action: () => this.presentModalInstructions(manifest), + action: () => this.presentModalInstructions(pkg), title: 'Instructions', description: `Understand how to use ${manifest.title}`, icon: 'list-outline', @@ -103,17 +103,20 @@ export class ToButtonsPipe implements PipeTransform { ] } - private async presentModalInstructions(manifest: T.Manifest) { + private async presentModalInstructions( + pkg: PackageDataEntry, + ) { this.apiService - .setDbValue(['ack-instructions', manifest.id], true) + .setDbValue(['ackInstructions', pkg.stateInfo.manifest.id], true) .catch(e => console.error('Failed to mark instructions as seen', e)) const modal = await this.modalCtrl.create({ componentProps: { title: 'Instructions', content: from( - this.apiService.getStatic( - `/public/package-data/${manifest.id}/${manifest.version}/INSTRUCTIONS.md`, + this.api.getStaticInstalled( + pkg.stateInfo.manifest.id, + 'instructions.md', ), ), }, diff --git a/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.html b/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.html index 49f65cc14..8f1582db1 100644 --- a/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.html +++ b/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.html @@ -97,7 +97,4 @@
-
+ diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts index c10ce42aa..b86d5df12 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts @@ -5,7 +5,7 @@ import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { SharedPipesModule, - EmverPipesModule, + ExverPipesModule, ResponsiveColModule, } from '@start9labs/shared' import { @@ -34,7 +34,7 @@ const routes: Routes = [ FormsModule, RouterModule.forChild(routes), SharedPipesModule, - EmverPipesModule, + ExverPipesModule, FilterPackagesPipeModule, MarketplaceStatusModule, BadgeMenuComponentModule, diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html index 734cb8910..32a6120ec 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html @@ -62,9 +62,10 @@ > diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts index e1f13883a..0f0fa5752 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { AbstractMarketplaceService } from '@start9labs/marketplace' +import { T } from '@start9labs/start-sdk' import { TuiDialogService } from '@taiga-ui/core' import { PatchDB } from 'patch-db-client' import { map } from 'rxjs' @@ -20,12 +21,21 @@ export class MarketplaceListPage { readonly store$ = this.marketplaceService.getSelectedStore$().pipe( map(({ info, packages }) => { - const categories = new Set() - if (info.categories.includes('featured')) categories.add('featured') - info.categories.forEach(c => categories.add(c)) - categories.add('all') + const categories = new Map() + if (info.categories['featured']) + categories.set('featured', info.categories['featured']) + Object.keys(info.categories).forEach(c => + categories.set(c, info.categories[c]), + ) + categories.set('all', { + name: 'All', + description: { + short: 'All registry packages', + long: 'An unfiltered list of all packages available on this registry.', + }, + }) - return { categories: Array.from(categories), packages } + return { categories, packages } }), ) diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-routing.module.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-routing.module.ts index d5d6304e4..cc4946dc1 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-routing.module.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-routing.module.ts @@ -17,13 +17,6 @@ const routes: Routes = [ m => m.MarketplaceShowPageModule, ), }, - { - path: ':pkgId/notes', - loadChildren: () => - import('./release-notes/release-notes.module').then( - m => m.ReleaseNotesPageModule, - ), - }, ] @NgModule({ diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html index 6995dde5e..797a2fb13 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html @@ -1,11 +1,15 @@
- View Installed + {{ + localPkg.stateInfo.state === 'installed' + ? 'View Installed' + : 'View Installing' + }} - - Install + + {{ localFlavor ? 'Switch' : 'Install' }}
diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts index 5c83c536f..0b76579bc 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts @@ -10,7 +10,7 @@ import { MarketplacePkg, } from '@start9labs/marketplace' import { - Emver, + Exver, ErrorService, isEmptyObject, LoadingService, @@ -44,6 +44,9 @@ export class MarketplaceShowControlsComponent { @Input() localPkg!: PackageDataEntry | null + @Input() + localFlavor!: boolean + readonly showDevTools$ = this.ClientStorageService.showDevTools$ constructor( @@ -52,7 +55,7 @@ export class MarketplaceShowControlsComponent { @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, private readonly loader: LoadingService, - private readonly emver: Emver, + private readonly exver: Exver, private readonly errorService: ErrorService, private readonly patch: PatchDB, ) {} @@ -79,7 +82,7 @@ export class MarketplaceShowControlsComponent { const localManifest = getManifest(this.localPkg) if ( - this.emver.compare(localManifest.version, this.pkg.manifest.version) !== + this.exver.compareExver(localManifest.version, this.pkg.version) !== 0 && hasCurrentDeps(localManifest.id, await getAllPackages(this.patch)) ) { @@ -136,9 +139,9 @@ export class MarketplaceShowControlsComponent { private async dryInstall(url: string) { const breakages = dryUpdate( - this.pkg.manifest, + this.pkg, await getAllPackages(this.patch), - this.emver, + this.exver, ) if (isEmptyObject(breakages)) { @@ -152,7 +155,7 @@ export class MarketplaceShowControlsComponent { } private async alertInstall(url: string) { - const installAlert = this.pkg.manifest.alerts.install + const installAlert = this.pkg.alerts.install if (!installAlert) return this.install(url) @@ -179,7 +182,7 @@ export class MarketplaceShowControlsComponent { private async install(url: string) { const loader = this.loader.open('Beginning Install...').subscribe() - const { id, version } = this.pkg.manifest + const { id, version } = this.pkg try { await this.marketplaceService.installPackage(id, version, url) diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html index a7e5f9cb6..631b45018 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html @@ -13,16 +13,16 @@

- {{ title }} version {{ version | displayEmver }} is compatible. + {{ title }} version {{ version }} is compatible. - {{ title }} version {{ version | displayEmver }} is NOT compatible. + {{ title }} version {{ version }} is NOT compatible.

diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.ts index 76c648867..7c39714f5 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.ts @@ -24,10 +24,10 @@ export class MarketplaceShowDependentComponent { constructor(@Inject(DOCUMENT) private readonly document: Document) {} get title(): string { - return this.pkg.manifest.title + return this.pkg.title } get version(): string { - return this.pkg.manifest.version + return this.pkg.version } } diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts index 058cbb158..7d0d3995b 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common' import { RouterModule, Routes } from '@angular/router' import { IonicModule } from '@ionic/angular' import { - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, SharedPipesModule, TextSpinnerComponentModule, @@ -13,6 +13,7 @@ import { AdditionalModule, DependenciesModule, PackageModule, + FlavorsModule, } from '@start9labs/marketplace' import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module' import { MarketplaceShowPage } from './marketplace-show.page' @@ -35,13 +36,14 @@ const routes: Routes = [ RouterModule.forChild(routes), TextSpinnerComponentModule, SharedPipesModule, - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, MarketplaceStatusModule, PackageModule, AboutModule, DependenciesModule, AdditionalModule, + FlavorsModule, UiPipeModule, ], declarations: [ diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html index f563e88d6..a0f7622ad 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html @@ -1,21 +1,14 @@ - + -
+
-

- {{ pkgId }} @{{ version === '*' ? 'latest' : version }} not found in - this registry -

+

{{ pkgId }} not found in this registry

@@ -25,21 +18,26 @@ [url]="url" [pkg]="pkg" [localPkg]="localPkg$ | async" + [localFlavor]="!!(localFlavor$ | async)" > + diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts index 876e71924..018ddbb64 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts @@ -1,11 +1,15 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { getPkgId } from '@start9labs/shared' -import { AbstractMarketplaceService } from '@start9labs/marketplace' +import { ActivatedRoute, Router } from '@angular/router' +import { Exver, getPkgId } from '@start9labs/shared' +import { + AbstractMarketplaceService, + MarketplacePkg, +} from '@start9labs/marketplace' import { PatchDB } from 'patch-db-client' -import { BehaviorSubject } from 'rxjs' -import { filter, shareReplay, switchMap } from 'rxjs/operators' +import { combineLatest, Observable } from 'rxjs' +import { filter, map, shareReplay, startWith, switchMap } from 'rxjs/operators' import { DataModel } from 'src/app/services/patch-db/data-model' +import { getManifest } from 'src/app/util/get-package-data' @Component({ selector: 'marketplace-show', @@ -17,21 +21,61 @@ export class MarketplaceShowPage { readonly pkgId = getPkgId(this.route) readonly url = this.route.snapshot.queryParamMap.get('url') || undefined - readonly loadVersion$ = new BehaviorSubject('*') + readonly localPkg$ = combineLatest([ + this.patch.watch$('packageData', this.pkgId).pipe(filter(Boolean)), + this.route.queryParamMap, + ]).pipe( + map(([pkg, paramMap]) => + this.exver.getFlavor(getManifest(pkg).version) === paramMap.get('flavor') + ? pkg + : null, + ), + shareReplay({ bufferSize: 1, refCount: true }), + ) - readonly localPkg$ = this.patch - .watch$('packageData', this.pkgId) - .pipe(filter(Boolean), shareReplay({ bufferSize: 1, refCount: true })) + readonly localFlavor$ = this.localPkg$.pipe( + map(pkg => !pkg), + startWith(false), + ) - readonly pkg$ = this.loadVersion$.pipe( - switchMap(version => - this.marketplaceService.getPackage$(this.pkgId, version, this.url), + readonly pkg$: Observable = this.route.queryParamMap.pipe( + switchMap(paramMap => + this.marketplaceService.getPackage$( + this.pkgId, + paramMap.get('version'), + paramMap.get('flavor'), + this.url, + ), + ), + ) + + readonly flavors$ = this.route.queryParamMap.pipe( + switchMap(paramMap => + this.marketplaceService + .getSelectedStore$() + .pipe( + map(s => + s.packages.filter( + p => p.id === this.pkgId && p.flavor !== paramMap.get('flavor'), + ), + ), + ), ), ) constructor( private readonly route: ActivatedRoute, + private readonly router: Router, private readonly patch: PatchDB, private readonly marketplaceService: AbstractMarketplaceService, + private readonly exver: Exver, ) {} + + updateVersion(version: string) { + this.router.navigate([], { + relativeTo: this.route, + queryParams: { version }, + queryParamsHandling: 'merge', + }) + } } diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.html b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.html index 8958cdda2..173fda66d 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.html +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.html @@ -1,13 +1,13 @@ - +
Installed Update Available diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts index 9db50dc29..6686b2842 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts @@ -8,6 +8,7 @@ import { isRestoring, getManifest, } from 'src/app/util/get-package-data' +import { Exver } from '@start9labs/shared' @Component({ selector: 'marketplace-status', @@ -16,8 +17,7 @@ import { }) export class MarketplaceStatusComponent { @Input() version!: string - - @Input() localPkg?: PackageDataEntry + @Input() localPkg!: PackageDataEntry isInstalled = isInstalled isInstalling = isInstalling @@ -26,6 +26,15 @@ export class MarketplaceStatusComponent { isRestoring = isRestoring get localVersion(): string { - return this.localPkg ? getManifest(this.localPkg).version : '' + return getManifest(this.localPkg).version } + + get sameFlavor(): boolean { + return ( + this.exver.getFlavor(this.version) === + this.exver.getFlavor(this.localVersion) + ) + } + + constructor(private readonly exver: Exver) {} } diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts index c31f4fd16..d95b919a2 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { IonicModule } from '@ionic/angular' -import { EmverPipesModule } from '@start9labs/shared' +import { ExverPipesModule } from '@start9labs/shared' import { InstallingProgressPipeModule } from '../../../pipes/install-progress/install-progress.module' import { MarketplaceStatusComponent } from './marketplace-status.component' @@ -9,7 +9,7 @@ import { MarketplaceStatusComponent } from './marketplace-status.component' imports: [ CommonModule, IonicModule, - EmverPipesModule, + ExverPipesModule, InstallingProgressPipeModule, ], declarations: [MarketplaceStatusComponent], diff --git a/web/projects/ui/src/app/pages/server-routes/lan/lan.page.html b/web/projects/ui/src/app/pages/server-routes/lan/lan.page.html index b61412445..c6c16d28e 100644 --- a/web/projects/ui/src/app/pages/server-routes/lan/lan.page.html +++ b/web/projects/ui/src/app/pages/server-routes/lan/lan.page.html @@ -35,5 +35,5 @@ - + diff --git a/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts b/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts index eff288eb2..503aa57ad 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common' import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { ServerSpecsPage } from './server-specs.page' -import { EmverPipesModule } from '@start9labs/shared' +import { ExverPipesModule } from '@start9labs/shared' import { TuiLetModule } from '@taiga-ui/cdk' import { QRComponentModule } from 'src/app/components/qr/qr.component.module' @@ -20,7 +20,7 @@ const routes: Routes = [ IonicModule, RouterModule.forChild(routes), QRComponentModule, - EmverPipesModule, + ExverPipesModule, TuiLetModule, ], declarations: [ServerSpecsPage], diff --git a/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html b/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html index f9df6eb37..03d7ef3d7 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html +++ b/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html @@ -13,7 +13,7 @@

Version

-

{{ server.version | displayEmver }}

+

{{ server.version }}

diff --git a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts index 863b2d127..27a3757dd 100644 --- a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts +++ b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { SideloadPage } from './sideload.page' import { Routes, RouterModule } from '@angular/router' -import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared' +import { ExverPipesModule, SharedPipesModule } from '@start9labs/shared' import { DragNDropDirective } from './dnd.directive' const routes: Routes = [ @@ -19,7 +19,7 @@ const routes: Routes = [ IonicModule, RouterModule.forChild(routes), SharedPipesModule, - EmverPipesModule, + ExverPipesModule, ], declarations: [SideloadPage, DragNDropDirective], }) diff --git a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html index 6a54fe82e..0d413075e 100644 --- a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html +++ b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html @@ -77,7 +77,7 @@ [src]="toUpload.icon | trustUrl" />

{{ toUpload.manifest.title }}

-

{{ toUpload.manifest.version | displayEmver }}

+

{{ toUpload.manifest.version }}

diff --git a/web/projects/ui/src/app/pages/updates/updates.module.ts b/web/projects/ui/src/app/pages/updates/updates.module.ts index 7b6c6594c..4930463b0 100644 --- a/web/projects/ui/src/app/pages/updates/updates.module.ts +++ b/web/projects/ui/src/app/pages/updates/updates.module.ts @@ -5,7 +5,7 @@ import { RouterModule, Routes } from '@angular/router' import { FilterUpdatesPipe, UpdatesPage } from './updates.page' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, SharedPipesModule, } from '@start9labs/shared' @@ -34,7 +34,7 @@ const routes: Routes = [ RoundProgressModule, InstallingProgressPipeModule, StoreIconComponentModule, - EmverPipesModule, + ExverPipesModule, ], }) export class UpdatesPageModule {} diff --git a/web/projects/ui/src/app/pages/updates/updates.page.html b/web/projects/ui/src/app/pages/updates/updates.page.html index 2bb3860c6..fd4d19133 100644 --- a/web/projects/ui/src/app/pages/updates/updates.page.html +++ b/web/projects/ui/src/app/pages/updates/updates.page.html @@ -30,27 +30,21 @@ >
- + -

{{ pkg.manifest.title }}

+

{{ pkg.title }}

- - {{ local.stateInfo.manifest.version | displayEmver }} - + {{ local.stateInfo.manifest.version }}     - - {{ pkg.manifest.version | displayEmver }} - + {{ pkg.version }}

-

+

{{ error }}

@@ -69,17 +63,17 @@ - {{ marketplaceService.updateErrors[pkg.manifest.id] ? - 'Retry' : 'Update' }} + {{ marketplaceService.updateErrors[pkg.id] ? 'Retry' : + 'Update' }} @@ -88,12 +82,12 @@
What's new
-

+

View listing diff --git a/web/projects/ui/src/app/pages/updates/updates.page.ts b/web/projects/ui/src/app/pages/updates/updates.page.ts index d1ffc4829..e25632bc8 100644 --- a/web/projects/ui/src/app/pages/updates/updates.page.ts +++ b/web/projects/ui/src/app/pages/updates/updates.page.ts @@ -13,7 +13,7 @@ import { MarketplacePkg, StoreIdentity, } from '@start9labs/marketplace' -import { Emver, isEmptyObject } from '@start9labs/shared' +import { Exver, isEmptyObject } from '@start9labs/shared' import { Pipe, PipeTransform } from '@angular/core' import { combineLatest, map, Observable } from 'rxjs' import { AlertController, NavController } from '@ionic/angular' @@ -24,7 +24,6 @@ import { isUpdating, } from 'src/app/util/get-package-data' import { dryUpdate } from 'src/app/util/dry-update' -import { T } from '@start9labs/start-sdk' interface UpdatesData { hosts: StoreIdentity[] @@ -59,7 +58,7 @@ export class UpdatesPage { private readonly patch: PatchDB, private readonly navCtrl: NavController, private readonly alertCtrl: AlertController, - private readonly emver: Emver, + private readonly exver: Exver, ) {} viewInMarketplace(event: Event, url: string, id: string) { @@ -70,29 +69,29 @@ export class UpdatesPage { }) } - async tryUpdate(manifest: T.Manifest, url: string, e: Event): Promise { + async tryUpdate(pkg: MarketplacePkg, url: string, e: Event): Promise { e.stopPropagation() - const { id, version } = manifest + const { id, version } = pkg delete this.marketplaceService.updateErrors[id] this.marketplaceService.updateQueue[id] = true - // manifest.id OK because same as local id for update - if (hasCurrentDeps(manifest.id, await getAllPackages(this.patch))) { - this.dryInstall(manifest, url) + // id OK because same as local id for update + if (hasCurrentDeps(id, await getAllPackages(this.patch))) { + this.dryInstall(pkg, url) } else { this.install(id, version, url) } } - private async dryInstall(manifest: T.Manifest, url: string) { - const { id, version, title } = manifest + private async dryInstall(pkg: MarketplacePkg, url: string) { + const { id, version, title } = pkg const breakages = dryUpdate( - manifest, + pkg, await getAllPackages(this.patch), - this.emver, + this.exver, ) if (isEmptyObject(breakages)) { @@ -159,18 +158,19 @@ export class UpdatesPage { name: 'filterUpdates', }) export class FilterUpdatesPipe implements PipeTransform { - constructor(private readonly emver: Emver) {} + constructor(private readonly exver: Exver) {} transform( pkgs: MarketplacePkg[], local: Record>, ): MarketplacePkg[] { - return pkgs.filter(({ manifest }) => { - const localPkg = local[manifest.id] + return pkgs.filter(({ id, version, flavor }) => { + const localPkg = local[id] return ( localPkg && - this.emver.compare( - manifest.version, + this.exver.getFlavor(localPkg.stateInfo.manifest.version) === flavor && + this.exver.compareExver( + version, localPkg.stateInfo.manifest.version, ) === 1 ) diff --git a/web/projects/ui/src/app/services/api/api-icons.ts b/web/projects/ui/src/app/services/api/api-icons.ts index 3b0b08b33..55c3172da 100644 --- a/web/projects/ui/src/app/services/api/api-icons.ts +++ b/web/projects/ui/src/app/services/api/api-icons.ts @@ -1,8 +1,10 @@ +const REGISTRY_ICON = + 'data:image/png;base64,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' const BTC_ICON = - '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' + 'data:image/png;base64,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' const LND_ICON = - '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' + 'data:image/png;base64,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' const PROXY_ICON = - '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' + 'data:image/png;base64,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' -export { BTC_ICON, LND_ICON, PROXY_ICON } +export { REGISTRY_ICON, BTC_ICON, LND_ICON, PROXY_ICON } diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index c1f98dcd4..4ce3f670a 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -3,11 +3,26 @@ import { PackageDataEntry, } from 'src/app/services/patch-db/data-model' import { Metric, NotificationLevel, RR, ServerNotifications } from './api.types' -import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons' -import { DependencyMetadata, MarketplacePkg } from '@start9labs/marketplace' +import { BTC_ICON, LND_ICON, PROXY_ICON, REGISTRY_ICON } from './api-icons' import { Log } from '@start9labs/shared' import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec' import { T, CB } from '@start9labs/start-sdk' +import { GetPackagesRes } from '@start9labs/marketplace' + +const mockBlake3Commitment: T.Blake3Commitment = { + hash: 'fakehash', + size: 0, +} + +const mockMerkleArchiveCommitment: T.MerkleArchiveCommitment = { + rootSighash: 'fakehash', + rootMaxsize: 0, +} + +const mockDescription = { + short: 'Lorem ipsum dolor sit amet', + long: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', +} export module Mock { export const ServerUpdated: T.ServerStatus = { @@ -37,17 +52,42 @@ export module Mock { }, } - export const ReleaseNotes: RR.GetReleaseNotesRes = { - '0.19.2': - 'Contrary to popular belief, Lorem Ipsum is not simply random text.', - '0.19.1': 'release notes for Bitcoin 0.19.1', - '0.19.0': 'release notes for Bitcoin 0.19.0', + export const RegistryInfo: T.RegistryInfo = { + name: 'Start9 Registry', + icon: REGISTRY_ICON, + 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, + }, + }, } export const MockManifestBitcoind: T.Manifest = { id: 'bitcoind', title: 'Bitcoin Core', - version: '0.21.0', + version: '0.21.0:0', + satisfies: [], gitHash: 'abcdefgh', description: { short: 'A Bitcoin full node by Bitcoin Core.', @@ -90,7 +130,8 @@ export module Mock { export const MockManifestLnd: T.Manifest = { id: 'lnd', title: 'Lightning Network Daemon', - version: '0.11.1', + version: '0.11.1:0', + satisfies: [], gitHash: 'abcdefgh', description: { short: 'A bolt spec compliant client.', @@ -116,11 +157,13 @@ export module Mock { bitcoind: { description: 'LND needs bitcoin to live.', optional: true, + s9pk: '', }, 'btc-rpc-proxy': { description: 'As long as Bitcoin is pruned, LND needs Bitcoin Proxy to fetch block over the P2P network.', optional: true, + s9pk: '', }, }, hasConfig: true, @@ -143,7 +186,8 @@ export module Mock { export const MockManifestBitcoinProxy: T.Manifest = { id: 'btc-rpc-proxy', title: 'Bitcoin Proxy', - version: '0.2.2', + version: '0.2.2:0', + satisfies: [], gitHash: 'lmnopqrx', description: { short: 'A super charger for your Bitcoin node.', @@ -168,6 +212,7 @@ export module Mock { bitcoind: { description: 'Bitcoin Proxy requires a Bitcoin node.', optional: false, + s9pk: '', }, }, hasConfig: false, @@ -187,149 +232,509 @@ export module Mock { }, } - export const BitcoinDep: DependencyMetadata = { + export const BitcoinDep: T.DependencyMetadata = { title: 'Bitcoin Core', icon: BTC_ICON, optional: false, - hidden: true, + description: 'Needed to run', } - export const ProxyDep: DependencyMetadata = { + export const ProxyDep: T.DependencyMetadata = { title: 'Bitcoin Proxy', icon: PROXY_ICON, optional: true, - hidden: false, + description: 'Needed to run', } - export const MarketplacePkgs: { - [id: string]: { - [version: string]: MarketplacePkg - } + export const OtherPackageVersions: { + [id: T.PackageId]: GetPackagesRes } = { bitcoind: { - '0.19.0': { - icon: BTC_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestBitcoind, - version: '0.19.0', + '=26.1.0:0.1.0': { + best: { + '26.1.0:0.1.0': { + title: 'Bitcoin Core', + description: mockDescription, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoind-startos', + upstreamRepo: 'https://github.com/bitcoin/bitcoin', + supportSite: 'https://bitcoin.org', + marketingSite: 'https://bitcoin.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v26.1.0/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + '#knots:26.1.20240325:0': { + title: 'Bitcoin Knots', + description: { + short: 'An alternate fully verifying implementation of Bitcoin', + long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.', + }, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos', + upstreamRepo: 'https://github.com/bitcoinknots/bitcoin', + supportSite: 'https://bitcoinknots.org', + marketingSite: 'https://bitcoinknots.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['bitcoin', 'featured'], + otherVersions: { + '27.0.0:1.0.0': { + releaseNotes: 'Even better support for Bitcoin and wallets!', + }, + '#knots:27.1.0:0': { + releaseNotes: 'Even better support for Bitcoin and wallets!', + }, }, - categories: ['bitcoin', 'cryptocurrency'], - versions: ['0.19.0', '0.20.0', '0.21.0'], - dependencyMetadata: {}, - publishedAt: new Date().toISOString(), }, - '0.20.0': { - icon: BTC_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestBitcoind, - version: '0.20.0', + '=#knots:26.1.20240325:0': { + best: { + '26.1.0:0.1.0': { + title: 'Bitcoin Core', + description: mockDescription, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoind-startos', + upstreamRepo: 'https://github.com/bitcoin/bitcoin', + supportSite: 'https://bitcoin.org', + marketingSite: 'https://bitcoin.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v26.1.0/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + '#knots:26.1.20240325:0': { + title: 'Bitcoin Knots', + description: { + short: 'An alternate fully verifying implementation of Bitcoin', + long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.', + }, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos', + upstreamRepo: 'https://github.com/bitcoinknots/bitcoin', + supportSite: 'https://bitcoinknots.org', + marketingSite: 'https://bitcoinknots.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, }, - categories: ['bitcoin', 'cryptocurrency'], - versions: ['0.19.0', '0.20.0', '0.21.0'], - dependencyMetadata: {}, - publishedAt: new Date().toISOString(), - }, - '0.21.0': { - icon: BTC_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestBitcoind, - version: '0.21.0', - releaseNotes: - 'For a complete list of changes, please visit https://bitcoincore.org/en/releases/0.21.0/
  • Taproot!
  • New RPCs
  • Experimental Descriptor Wallets
', + categories: ['bitcoin', 'featured'], + otherVersions: { + '27.0.0:1.0.0': { + releaseNotes: 'Even better support for Bitcoin and wallets!', + }, + '#knots:27.1.0:0': { + releaseNotes: 'Even better support for Bitcoin and wallets!', + }, }, - categories: ['bitcoin', 'cryptocurrency'], - versions: ['0.19.0', '0.20.0', '0.21.0'], - dependencyMetadata: {}, - publishedAt: new Date().toISOString(), - }, - latest: { - icon: BTC_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestBitcoind, - releaseNotes: - 'For a complete list of changes, please visit https://bitcoincore.org/en/releases/0.21.0/
Or in [markdown](https://bitcoincore.org/en/releases/0.21.0/)
  • Taproot!
  • New RPCs
  • Experimental Descriptor Wallets
', - }, - categories: ['bitcoin', 'cryptocurrency'], - versions: ['0.19.0', '0.20.0', '0.21.0'], - dependencyMetadata: {}, - publishedAt: new Date().toISOString(), }, }, lnd: { - '0.11.0': { - icon: LND_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestLnd, - version: '0.11.0', - releaseNotes: 'release notes for LND 0.11.0', + '=0.17.5:0': { + best: { + '0.17.5:0': { + title: 'LND', + description: mockDescription, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/lnd-startos', + upstreamRepo: 'https://github.com/lightningnetwork/lnd', + supportSite: 'https://lightning.engineering/slack.html', + marketingSite: 'https://lightning.engineering/', + releaseNotes: 'Upstream release to 0.17.5', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: LND_ICON, + sourceVersion: null, + dependencyMetadata: { + bitcoind: { + title: 'Bitcoin Core', + icon: BTC_ICON, + description: 'Used for RPC requests', + optional: false, + }, + 'btc-rpc-proxy': { + title: 'Bitcoin Proxy', + icon: PROXY_ICON, + description: 'Used for authorized proxying of RPC requests', + optional: true, + }, + }, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.17.5/lnd.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, }, - categories: ['bitcoin', 'lightning', 'cryptocurrency'], - versions: ['0.11.0', '0.11.1'], - dependencyMetadata: { - bitcoind: BitcoinDep, - 'btc-rpc-proxy': ProxyDep, + categories: ['lightning', 'featured'], + otherVersions: { + '0.18.0:0.0.1': { + releaseNotes: 'Upstream release and minor fixes.', + }, + '0.17.4-beta:1.0-alpha': { + releaseNotes: 'Upstream release to 0.17.4', + }, }, - publishedAt: new Date().toISOString(), }, - '0.11.1': { - icon: LND_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestLnd, - version: '0.11.1', - releaseNotes: 'release notes for LND 0.11.1', + '=0.17.4-beta:1.0-alpha': { + best: { + '0.17.4-beta:1.0-alpha': { + title: 'LND', + description: mockDescription, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/lnd-startos', + upstreamRepo: 'https://github.com/lightningnetwork/lnd', + supportSite: 'https://lightning.engineering/slack.html', + marketingSite: 'https://lightning.engineering/', + releaseNotes: 'Upstream release to 0.17.4', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: LND_ICON, + sourceVersion: null, + dependencyMetadata: { + bitcoind: { + title: 'Bitcoin Core', + icon: BTC_ICON, + description: 'Used for RPC requests', + optional: false, + }, + 'btc-rpc-proxy': { + title: 'Bitcoin Proxy', + icon: PROXY_ICON, + description: 'Used for authorized proxying of RPC requests', + optional: true, + }, + }, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.17.4/lnd.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, }, - categories: ['bitcoin', 'lightning', 'cryptocurrency'], - versions: ['0.11.0', '0.11.1'], - dependencyMetadata: { - bitcoind: BitcoinDep, - 'btc-rpc-proxy': ProxyDep, + categories: ['lightning', 'featured'], + otherVersions: { + '0.18.0:0.0.1': { + releaseNotes: 'Upstream release and minor fixes.', + }, + '0.17.5:0': { + releaseNotes: 'Upstream release to 0.17.5', + }, }, - publishedAt: new Date().toISOString(), - }, - latest: { - icon: LND_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: Mock.MockManifestLnd, - categories: ['bitcoin', 'lightning', 'cryptocurrency'], - versions: ['0.11.0', '0.11.1'], - dependencyMetadata: { - bitcoind: BitcoinDep, - 'btc-rpc-proxy': ProxyDep, - }, - publishedAt: new Date(new Date().valueOf() + 10).toISOString(), }, }, 'btc-rpc-proxy': { - latest: { - icon: PROXY_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: Mock.MockManifestBitcoinProxy, - categories: ['bitcoin'], - versions: ['0.2.2'], - dependencyMetadata: { - bitcoind: BitcoinDep, + '=0.3.2.6:0': { + best: { + '0.3.2.6:0': { + title: 'Bitcoin Proxy', + description: mockDescription, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers', + upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy', + supportSite: 'https://github.com/Kixunil/btc-rpc-proxy/issues', + marketingSite: '', + releaseNotes: 'Upstream release and minor fixes.', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: PROXY_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/btc-rpc-proxy-startos/releases/download/v0.3.2.7.1/btc-rpc-proxy.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['bitcoin'], + otherVersions: { + '0.3.2.7:0': { + releaseNotes: 'Upstream release and minor fixes.', + }, }, - publishedAt: new Date().toISOString(), }, }, } - export const MarketplacePkgsList: RR.GetMarketplacePackagesRes = - Object.values(Mock.MarketplacePkgs).map(service => service['latest']) + export const RegistryPackages: GetPackagesRes = { + bitcoind: { + best: { + '27.0.0:1.0.0': { + title: 'Bitcoin Core', + description: mockDescription, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoind-startos', + upstreamRepo: 'https://github.com/bitcoin/bitcoin', + supportSite: 'https://bitcoin.org', + marketingSite: 'https://bitcoin.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v27.0.0/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + '#knots:27.1.0:0': { + title: 'Bitcoin Knots', + description: { + short: 'An alternate fully verifying implementation of Bitcoin', + long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.', + }, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos', + upstreamRepo: 'https://github.com/bitcoinknots/bitcoin', + supportSite: 'https://bitcoinknots.org', + marketingSite: 'https://bitcoinknots.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['bitcoin', 'featured'], + otherVersions: { + '26.1.0:0.1.0': { + releaseNotes: 'Even better support for Bitcoin and wallets!', + }, + '#knots:26.1.20240325:0': { + releaseNotes: 'Even better Knots support for Bitcoin and wallets!', + }, + }, + }, + lnd: { + best: { + '0.18.0:0.0.1': { + title: 'LND', + description: mockDescription, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/lnd-startos', + upstreamRepo: 'https://github.com/lightningnetwork/lnd', + supportSite: 'https://lightning.engineering/slack.html', + marketingSite: 'https://lightning.engineering/', + releaseNotes: 'Upstream release and minor fixes.', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: LND_ICON, + sourceVersion: null, + dependencyMetadata: { + bitcoind: { + title: 'Bitcoin Core', + icon: BTC_ICON, + description: 'Used for RPC requests', + optional: false, + }, + 'btc-rpc-proxy': { + title: 'Bitcoin Proxy', + icon: null, + description: 'Used for authorized RPC requests', + optional: true, + }, + }, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.18.0.1/lnd.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['lightning', 'featured'], + otherVersions: { + '0.17.5:0': { + releaseNotes: 'Upstream release to 0.17.5', + }, + '0.17.4-beta:1.0-alpha': { + releaseNotes: 'Upstream release to 0.17.4', + }, + }, + }, + 'btc-rpc-proxy': { + best: { + '0.3.2.7:0': { + title: 'Bitcoin Proxy', + description: mockDescription, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers', + upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy', + supportSite: 'https://github.com/Kixunil/btc-rpc-proxy/issues', + marketingSite: '', + releaseNotes: 'Upstream release and minor fixes.', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: PROXY_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/btc-rpc-proxy-startos/releases/download/v0.3.2.7/btc-rpc-proxy.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['bitcoin'], + otherVersions: { + '0.3.2.6:0': { + releaseNotes: 'Upstream release and minor fixes.', + }, + }, + }, + } export const Notifications: ServerNotifications = [ { @@ -658,13 +1063,13 @@ export module Mock { packageBackups: { bitcoind: { title: 'Bitcoin Core', - version: '0.21.0', + version: '0.21.0:0', osVersion: '0.3.6', timestamp: new Date().toISOString(), }, 'btc-rpc-proxy': { title: 'Bitcoin Proxy', - version: '0.2.2', + version: '0.2.2:0', osVersion: '0.3.6', timestamp: new Date().toISOString(), }, @@ -1489,8 +1894,7 @@ export module Mock { title: Mock.MockManifestBitcoind.title, icon: 'assets/img/service-icons/bitcoind.svg', kind: 'running', - registryUrl: '', - versionSpec: '>=26.0.0', + versionRange: '>=26.0.0', healthChecks: [], configSatisfied: true, }, @@ -1576,8 +1980,7 @@ export module Mock { title: Mock.MockManifestBitcoind.title, icon: 'assets/img/service-icons/bitcoind.svg', kind: 'running', - registryUrl: 'https://registry.start9.com', - versionSpec: '>=26.0.0', + versionRange: '>=26.0.0', healthChecks: [], configSatisfied: true, }, @@ -1585,8 +1988,7 @@ export module Mock { title: Mock.MockManifestBitcoinProxy.title, icon: 'assets/img/service-icons/btc-rpc-proxy.png', kind: 'exists', - registryUrl: 'https://community-registry.start9.com', - versionSpec: '>2.0.0', + versionRange: '>2.0.0', configSatisfied: false, }, }, diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index 5ada03cad..770792328 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -1,5 +1,4 @@ import { Dump } from 'patch-db-client' -import { MarketplacePkg, StoreInfo } from '@start9labs/marketplace' import { PackagePropertiesVersioned } from 'src/app/util/properties.util' import { DataModel } from 'src/app/services/patch-db/data-model' import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared' @@ -223,12 +222,7 @@ export module RR { export type GetPackageMetricsReq = { id: string } // package.metrics export type GetPackageMetricsRes = Metric - export type InstallPackageReq = { - id: string - versionSpec?: string - versionPriority?: 'min' | 'max' - registry: string - } // package.install + export type InstallPackageReq = T.InstallParams export type InstallPackageRes = null export type GetPackageConfigReq = { id: string } // package.config.get @@ -287,26 +281,13 @@ export module RR { progress: string // guid } - // marketplace + // registry - export type GetMarketplaceInfoReq = { serverId: string } - export type GetMarketplaceInfoRes = StoreInfo + /** these are returned in ASCENDING order. the newest available version will be the LAST in the object */ + export type GetRegistryOsUpdateRes = { [version: string]: T.OsVersionInfo } export type CheckOSUpdateReq = { serverId: string } export type CheckOSUpdateRes = OSUpdate - - export type GetMarketplacePackagesReq = { - ids?: { id: string; version: string }[] - // iff !ids - category?: string - query?: string - page?: number - perPage?: number - } - export type GetMarketplacePackagesRes = MarketplacePkg[] - - export type GetReleaseNotesReq = { id: string } - export type GetReleaseNotesRes = { [version: string]: string } } export interface OSUpdate { diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index a5bce8c62..579292300 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -1,17 +1,32 @@ import { Observable } from 'rxjs' import { RR } from './api.types' +import { RPCOptions } from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' +import { + GetPackageRes, + GetPackagesRes, + MarketplacePkg, +} from '@start9labs/marketplace' export abstract class ApiService { // http - // for getting static files: ex icons, instructions, licenses - abstract getStatic(url: string): Promise - // for sideloading packages abstract uploadPackage(guid: string, body: Blob): Promise abstract uploadFile(body: Blob): Promise + // for getting static files: ex icons, instructions, licenses + abstract getStaticProxy( + pkg: MarketplacePkg, + path: 'LICENSE.md' | 'instructions.md', + ): Promise + + abstract getStaticInstalled( + id: T.PackageId, + path: 'LICENSE.md' | 'instructions.md', + ): Promise + // websocket abstract openWebsocket$( @@ -120,14 +135,23 @@ export abstract class ApiService { // marketplace URLs - abstract marketplaceProxy( - path: string, - params: Record, - url: string, + abstract registryRequest( + registryUrl: string, + options: RPCOptions, ): Promise abstract checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise + abstract getRegistryInfo(registryUrl: string): Promise + + abstract getRegistryPackage( + url: string, + id: string, + versionRange: string | null, + ): Promise + + abstract getRegistryPackages(registryUrl: string): Promise + // notification abstract getNotifications( diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index cef2c13ff..87be49e36 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -18,6 +18,15 @@ import { AuthService } from '../auth.service' 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 { blake3 } from '@noble/hashes/blake3' @Injectable() export class LiveApiService extends ApiService { @@ -29,17 +38,7 @@ export class LiveApiService extends ApiService { @Inject(PATCH_CACHE) private readonly cache$: Observable>, ) { super() - ; (window as any).rpcClient = this - } - - // for getting static files: ex icons, instructions, licenses - - async getStatic(url: string): Promise { - return this.httpRequest({ - method: Method.GET, - url, - responseType: 'text', - }) + ;(window as any).rpcClient = this } // for sideloading packages @@ -62,6 +61,36 @@ export class LiveApiService extends ApiService { }) } + // for getting static files: ex. instructions, licenses + + async getStaticProxy( + pkg: MarketplacePkg, + path: 'LICENSE.md' | 'instructions.md', + ): Promise { + const encodedUrl = encodeURIComponent(pkg.s9pk.url) + + return this.httpRequest({ + method: Method.GET, + url: `/s9pk/proxy/${encodedUrl}/${path}`, + params: { + rootSighash: pkg.s9pk.commitment.rootSighash, + rootMaxsize: pkg.s9pk.commitment.rootMaxsize, + }, + responseType: 'text', + }) + } + + async getStaticInstalled( + id: T.PackageId, + path: 'LICENSE.md' | 'instructions.md', + ): Promise { + return this.httpRequest({ + method: Method.GET, + url: `/s9pk/installed/${id}.s9pk/${path}`, + responseType: 'text', + }) + } + // websocket openWebsocket$( @@ -257,24 +286,63 @@ export class LiveApiService extends ApiService { // marketplace URLs - async marketplaceProxy( - path: string, - qp: Record, - baseUrl: string, + async registryRequest( + registryUrl: string, + options: RPCOptions, ): Promise { - const fullUrl = `${baseUrl}${path}?${new URLSearchParams(qp).toString()}` return this.rpcRequest({ - method: 'marketplace.get', - params: { url: fullUrl }, + ...options, + method: `registry.${options.method}`, + params: { registry: registryUrl, ...options.params }, }) } async checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise { - return this.marketplaceProxy( - '/eos/v0/latest', - qp, - this.config.marketplace.start9, - ) + const { serverId } = qp + + return this.registryRequest(this.config.marketplace.start9, { + method: 'os.version.get', + params: { serverId }, + }) + } + + async getRegistryInfo(registryUrl: string): Promise { + return this.registryRequest(registryUrl, { + method: 'info', + params: {}, + }) + } + + async getRegistryPackage( + registryUrl: string, + id: string, + versionRange: string | null, + ): Promise { + const params: GetPackageReq = { + id, + version: versionRange, + sourceVersion: null, + otherVersions: 'short', + } + + return this.registryRequest(registryUrl, { + method: 'package.get', + params, + }) + } + + async getRegistryPackages(registryUrl: string): Promise { + const params: GetPackagesReq = { + id: null, + version: null, + sourceVersion: null, + otherVersions: 'short', + } + + return this.registryRequest(registryUrl, { + method: 'package.get', + params, + }) } // notification @@ -504,6 +572,29 @@ export class LiveApiService extends ApiService { private async httpRequest(opts: HttpOptions): Promise { const res = await this.http.httpRequest(opts) + if (res.headers.get('Repr-Digest')) { + // verify + const digest = res.headers.get('Repr-Digest')! + let data: Uint8Array + if (opts.responseType === 'arrayBuffer') { + data = Buffer.from(res.body as ArrayBuffer) + } else if (opts.responseType === 'text') { + data = Buffer.from(res.body as string) + } else if ((opts.responseType as string) === 'blob') { + data = Buffer.from(await (res.body as Blob).arrayBuffer()) + } else { + console.warn( + `could not verify Repr-Digest for responseType ${ + opts.responseType || 'json' + }`, + ) + return res.body + } + const computedDigest = Buffer.from(blake3(data)).toString('base64') + if (`blake3=:${computedDigest}:` === digest) return res.body + console.debug(computedDigest, digest) + throw new Error('File digest mismatch.') + } return res.body } } diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 21491dc52..e063fb6e4 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core' -import { Log, RPCErrorDetails, pauseFor } from '@start9labs/shared' +import { Log, RPCErrorDetails, RPCOptions, pauseFor } from '@start9labs/shared' import { ApiService } from './embassy-api.service' import { Operation, @@ -30,8 +30,12 @@ import { } from 'rxjs' import { mockPatchData } from './mock-patch' import { AuthService } from '../auth.service' -import { StoreInfo } from '@start9labs/marketplace' import { T } from '@start9labs/start-sdk' +import { + GetPackageRes, + GetPackagesRes, + MarketplacePkg, +} from '@start9labs/marketplace' const PROGRESS: T.FullProgress = { overall: { @@ -48,10 +52,7 @@ const PROGRESS: T.FullProgress = { }, { name: 'Validating', - progress: { - done: 0, - total: 40, - }, + progress: null, }, { name: 'Installing', @@ -80,14 +81,25 @@ export class MockApiService extends ApiService { .subscribe() } - async getStatic(url: string): Promise { + async uploadPackage(guid: string, body: Blob): Promise { + await pauseFor(2000) + return 'success' + } + + async getStaticProxy( + pkg: MarketplacePkg, + path: 'LICENSE.md' | 'instructions.md', + ): Promise { await pauseFor(2000) return markdown } - async uploadPackage(guid: string, body: Blob): Promise { + async getStaticInstalled( + id: T.PackageId, + path: 'LICENSE.md' | 'instructions.md', + ): Promise { await pauseFor(2000) - return 'success' + return markdown } // websocket @@ -136,7 +148,7 @@ export class MockApiService extends ApiService { this.stateIndex++ - return this.stateIndex === 1 ? 'initializing' : 'running' + return this.stateIndex === 1 ? 'running' : 'running' } // db @@ -448,34 +460,13 @@ export class MockApiService extends ApiService { // marketplace URLs - async marketplaceProxy( - path: string, - params: Record, - url: string, + async registryRequest( + registryUrl: string, + options: RPCOptions, ): Promise { await pauseFor(2000) - if (path === '/package/v0/info') { - const info: StoreInfo = { - name: 'Start9 Registry', - categories: [ - 'bitcoin', - 'lightning', - 'data', - 'featured', - 'messaging', - 'social', - 'alt coin', - ], - } - return info - } else if (path === '/package/v0/index') { - return Mock.MarketplacePkgsList - } else if (path.startsWith('/package/v0/release-notes')) { - return Mock.ReleaseNotes - } else if (path.includes('instructions') || path.includes('license')) { - return markdown - } + return Error('do not call directly') } async checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise { @@ -483,6 +474,29 @@ export class MockApiService extends ApiService { return Mock.MarketplaceEos } + async getRegistryInfo(registryUrl: string): Promise { + await pauseFor(2000) + return Mock.RegistryInfo + } + + async getRegistryPackage( + url: string, + id: string, + versionRange: string, + ): Promise { + await pauseFor(2000) + if (!versionRange) { + return Mock.RegistryPackages[id] + } else { + return Mock.OtherPackageVersions[id][versionRange] + } + } + + async getRegistryPackages(registryUrl: string): Promise { + await pauseFor(2000) + return Mock.RegistryPackages + } + // notification async getNotifications( @@ -742,11 +756,11 @@ export class MockApiService extends ApiService { ...Mock.LocalPkgs[params.id], stateInfo: { // if installing - // state: PackageState.Installing, + state: 'installing', // if updating - state: 'updating', - manifest: mockPatchData.packageData[params.id].stateInfo.manifest!, + // state: 'updating', + // manifest: mockPatchData.packageData[params.id].stateInfo.manifest!, // both installingInfo: { @@ -1129,11 +1143,7 @@ export class MockApiService extends ApiService { const progress = JSON.parse(JSON.stringify(PROGRESS)) for (let [i, phase] of progress.phases.entries()) { - if ( - !phase.progress || - typeof phase.progress !== 'object' || - !phase.progress.total - ) { + if (!phase.progress || phase.progress === true || !phase.progress.total) { await pauseFor(2000) const patches: Operation[] = [ diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 8411b7b7c..7b0d6b1df 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -88,7 +88,7 @@ export const mockPatchData: DataModel = { state: 'installed', manifest: { ...Mock.MockManifestBitcoind, - version: '0.20.0', + version: '0.20.0:0', }, }, icon: '/assets/img/service-icons/bitcoind.svg', @@ -295,7 +295,7 @@ export const mockPatchData: DataModel = { state: 'installed', manifest: { ...Mock.MockManifestLnd, - version: '0.11.0', + version: '0.11.0:0.0.1', }, }, icon: '/assets/img/service-icons/lnd.png', @@ -368,8 +368,7 @@ export const mockPatchData: DataModel = { title: 'Bitcoin Core', icon: 'assets/img/service-icons/bitcoind.svg', kind: 'running', - registryUrl: 'https://registry.start9.com', - versionSpec: '>=26.0.0', + versionRange: '>=26.0.0', healthChecks: [], configSatisfied: true, }, @@ -377,8 +376,7 @@ export const mockPatchData: DataModel = { title: 'Bitcoin Proxy', icon: 'assets/img/service-icons/btc-rpc-proxy.png', kind: 'running', - registryUrl: 'https://community-registry.start9.com', - versionSpec: '>2.0.0', + versionRange: '>2.0.0', healthChecks: [], configSatisfied: false, }, diff --git a/web/projects/ui/src/app/services/dep-error.service.ts b/web/projects/ui/src/app/services/dep-error.service.ts index 5abee0d6b..a46a5123e 100644 --- a/web/projects/ui/src/app/services/dep-error.service.ts +++ b/web/projects/ui/src/app/services/dep-error.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core' -import { Emver } from '@start9labs/shared' +import { Exver } from '@start9labs/shared' import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators' import { PatchDB } from 'patch-db-client' import { @@ -39,7 +39,7 @@ export class DepErrorService { ) constructor( - private readonly emver: Emver, + private readonly exver: Exver, private readonly patch: PatchDB, ) {} @@ -87,11 +87,17 @@ export class DepErrorService { const depManifest = dep.stateInfo.manifest // incorrect version - if (!this.emver.satisfies(depManifest.version, currentDep.versionSpec)) { - return { - type: 'incorrectVersion', - expected: currentDep.versionSpec, - received: depManifest.version, + if (!this.exver.satisfies(depManifest.version, currentDep.versionRange)) { + if ( + depManifest.satisfies.some( + v => !this.exver.satisfies(v, currentDep.versionRange), + ) + ) { + return { + type: 'incorrectVersion', + expected: currentDep.versionRange, + received: depManifest.version, + } } } diff --git a/web/projects/ui/src/app/services/eos.service.ts b/web/projects/ui/src/app/services/eos.service.ts index 81b97bf11..c61c667e3 100644 --- a/web/projects/ui/src/app/services/eos.service.ts +++ b/web/projects/ui/src/app/services/eos.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core' -import { Emver } from '@start9labs/shared' import { BehaviorSubject, combineLatest } from 'rxjs' import { distinctUntilChanged, map } from 'rxjs/operators' import { OSUpdate } from 'src/app/services/api/api.types' @@ -7,6 +6,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' import { PatchDB } from 'patch-db-client' import { getServerInfo } from 'src/app/util/get-server-info' import { DataModel } from './patch-db/data-model' +import { Exver } from '@start9labs/shared' @Injectable({ providedIn: 'root', @@ -47,15 +47,15 @@ export class EOSService { constructor( private readonly api: ApiService, - private readonly emver: Emver, private readonly patch: PatchDB, + private readonly exver: Exver, ) {} async loadEos(): Promise { const { version, id } = await getServerInfo(this.patch) this.osUpdate = await this.api.checkOSUpdate({ serverId: id }) const updateAvailable = - this.emver.compare(this.osUpdate.version, version) === 1 + this.exver.compareOsVersion(this.osUpdate.version, version) === 'greater' this.updateAvailable$.next(updateAvailable) } } diff --git a/web/projects/ui/src/app/services/marketplace.service.ts b/web/projects/ui/src/app/services/marketplace.service.ts index 40d2247e7..6ce3b136f 100644 --- a/web/projects/ui/src/app/services/marketplace.service.ts +++ b/web/projects/ui/src/app/services/marketplace.service.ts @@ -1,11 +1,11 @@ -import { Injectable } from '@angular/core' +import { Inject, Injectable } from '@angular/core' import { - MarketplacePkg, AbstractMarketplaceService, StoreData, Marketplace, - StoreInfo, StoreIdentity, + MarketplacePkg, + GetPackageRes, } from '@start9labs/marketplace' import { BehaviorSubject, @@ -37,8 +37,9 @@ import { tap, } from 'rxjs/operators' import { ConfigService } from './config.service' -import { sameUrl } from '@start9labs/shared' +import { Exver, sameUrl } from '@start9labs/shared' import { ClientStorageService } from './client-storage.service' +import { ExtendedVersion, T } from '@start9labs/start-sdk' @Injectable() export class MarketplaceService implements AbstractMarketplaceService { @@ -93,11 +94,9 @@ export class MarketplaceService implements AbstractMarketplaceService { mergeMap(({ url, name }) => this.fetchStore$(url).pipe( tap(data => { - if (data?.info) this.updateStoreName(url, name, data.info.name) - }), - map(data => { - return [url, data] + if (data?.info.name) this.updateStoreName(url, name, data.info.name) }), + map(data => [url, data]), startWith<[string, StoreData | null]>([url, null]), ), ), @@ -148,6 +147,7 @@ export class MarketplaceService implements AbstractMarketplaceService { private readonly patch: PatchDB, private readonly config: ConfigService, private readonly clientStorageService: ClientStorageService, + private readonly exver: Exver, ) {} getKnownHosts$(filtered = false): Observable { @@ -170,28 +170,29 @@ export class MarketplaceService implements AbstractMarketplaceService { getPackage$( id: string, - version: string, - optionalUrl?: string, + version: string | null, + flavor: string | null, + registryUrl?: string, ): Observable { - return this.patch.watch$('ui', 'marketplace').pipe( - switchMap(uiMarketplace => { - const url = optionalUrl || uiMarketplace.selectedUrl + return this.selectedHost$.pipe( + switchMap(selected => + this.marketplace$.pipe( + switchMap(m => { + const url = registryUrl || selected.url - if (version !== '*' || !uiMarketplace.knownHosts[url]) { - return this.fetchPackage$(id, version, url) - } + const pkg = m[url]?.packages.find( + p => + p.id === id && + p.flavor === flavor && + (!version || this.exver.compareExver(p.version, version) === 0), + ) - return this.marketplace$.pipe( - map(m => m[url]), - filter(Boolean), - take(1), - map( - store => - store.packages.find(p => p.manifest.id === id) || - ({} as MarketplacePkg), - ), - ) - }), + return !!pkg + ? of(pkg) + : this.fetchPackage$(url, id, version, flavor) + }), + ), + ), ) } @@ -210,56 +211,22 @@ export class MarketplaceService implements AbstractMarketplaceService { ): Promise { const params: RR.InstallPackageReq = { id, - versionSpec: `=${version}`, + version, registry: url, } await this.api.installPackage(params) } - fetchInfo$(url: string): Observable { - return this.patch.watch$('serverInfo').pipe( - take(1), - switchMap(serverInfo => { - const qp: RR.GetMarketplaceInfoReq = { serverId: serverInfo.id } - return this.api.marketplaceProxy( - '/package/v0/info', - qp, - url, - ) - }), - ) + fetchInfo$(url: string): Observable { + return from(this.api.getRegistryInfo(url)) } - fetchReleaseNotes$( - id: string, - url?: string, - ): Observable> { - return this.selectedHost$.pipe( - switchMap(m => { - return from( - this.api.marketplaceProxy>( - `/package/v0/release-notes/${id}`, - {}, - url || m.url, - ), - ) - }), - ) - } - - fetchStatic$(id: string, type: string, url?: string): Observable { - return this.selectedHost$.pipe( - switchMap(m => { - return from( - this.api.marketplaceProxy( - `/package/v0/${type}/${id}`, - {}, - url || m.url, - ), - ) - }), - ) + fetchStatic$( + pkg: MarketplacePkg, + type: 'LICENSE.md' | 'instructions.md', + ): Observable { + return from(this.api.getStaticProxy(pkg, type)) } private fetchStore$(url: string): Observable { @@ -273,33 +240,57 @@ export class MarketplaceService implements AbstractMarketplaceService { ) } - private fetchPackages$( - url: string, - params: Omit = {}, - ): Observable { - const qp: RR.GetMarketplacePackagesReq = { - ...params, - page: 1, - perPage: 100, - } - if (qp.ids) qp.ids = JSON.stringify(qp.ids) - - return from( - this.api.marketplaceProxy( - '/package/v0/index', - qp, - url, - ), + private fetchPackages$(url: string): Observable { + return from(this.api.getRegistryPackages(url)).pipe( + map(packages => { + return Object.entries(packages).flatMap(([id, pkgInfo]) => + Object.keys(pkgInfo.best).map(version => + this.convertToMarketplacePkg( + id, + version, + this.exver.getFlavor(version), + pkgInfo, + ), + ), + ) + }), ) } - private fetchPackage$( + convertToMarketplacePkg( id: string, - version: string, + version: string | null, + flavor: string | null, + pkgInfo: GetPackageRes, + ): MarketplacePkg { + version = + version || + Object.keys(pkgInfo.best).find(v => this.exver.getFlavor(v) === flavor) || + null + + return !version || !pkgInfo.best[version] + ? ({} as MarketplacePkg) + : { + id, + version, + flavor, + ...pkgInfo, + ...pkgInfo.best[version], + } + } + + private fetchPackage$( url: string, + id: string, + version: string | null, + flavor: string | null, ): Observable { - return this.fetchPackages$(url, { ids: [{ id, version }] }).pipe( - map(pkgs => pkgs[0] || {}), + return from( + this.api.getRegistryPackage(url, id, version ? `=${version}` : null), + ).pipe( + map(pkgInfo => + this.convertToMarketplacePkg(id, version, flavor, pkgInfo), + ), ) } diff --git a/web/projects/ui/src/app/services/patch-db/data-model.ts b/web/projects/ui/src/app/services/patch-db/data-model.ts index 33daecda7..d7deb31df 100644 --- a/web/projects/ui/src/app/services/patch-db/data-model.ts +++ b/web/projects/ui/src/app/services/patch-db/data-model.ts @@ -50,6 +50,10 @@ export type PackageDataEntry = stateInfo: T } +export type AllPackageData = NonNullable< + T.AllPackageData & Record> +> + export type StateInfo = InstalledState | InstallingState | UpdatingState export type InstalledState = { diff --git a/web/projects/ui/src/app/util/dry-update.ts b/web/projects/ui/src/app/util/dry-update.ts index 2d4d1aa10..e9386df3c 100644 --- a/web/projects/ui/src/app/util/dry-update.ts +++ b/web/projects/ui/src/app/util/dry-update.ts @@ -1,18 +1,19 @@ -import { Emver } from '@start9labs/shared' +import { Exver } from '@start9labs/shared' import { DataModel } from '../services/patch-db/data-model' import { getManifest } from './get-package-data' export function dryUpdate( { id, version }: { id: string; version: string }, pkgs: DataModel['packageData'], - emver: Emver, + exver: Exver, ): string[] { return Object.values(pkgs) .filter( pkg => Object.keys(pkg.currentDependencies || {}).some( pkgId => pkgId === id, - ) && !emver.satisfies(version, pkg.currentDependencies[id].versionSpec), + ) && + !exver.satisfies(version, pkg.currentDependencies[id].versionRange), ) .map(pkg => getManifest(pkg).title) } diff --git a/web/projects/ui/src/manifest.webmanifest b/web/projects/ui/src/manifest.webmanifest index fee3469fc..cecc31bfe 100644 --- a/web/projects/ui/src/manifest.webmanifest +++ b/web/projects/ui/src/manifest.webmanifest @@ -5,8 +5,8 @@ "background_color": "#1e1e1e", "display": "standalone", "scope": ".", - "start_url": "/?version=0351", - "id": "/?version=0351", + "start_url": "/?version=036", + "id": "/?version=036", "icons": [ { "src": "assets/img/icon.png", diff --git a/web/projects/ui/src/polyfills.ts b/web/projects/ui/src/polyfills.ts index 67caa24e8..813796e34 100644 --- a/web/projects/ui/src/polyfills.ts +++ b/web/projects/ui/src/polyfills.ts @@ -52,8 +52,8 @@ * */ -;(window as any).global = window -;(window as any).process = { env: { DEBUG: undefined }, browser: true } +(window as any).global = window +; (window as any).process = { env: { DEBUG: undefined }, browser: true } import { Buffer } from 'buffer' window.Buffer = Buffer