Refactor/actions (#2733)

* store, properties, manifest

* interfaces

* init and backups

* fix init and backups

* file models

* more versions

* dependencies

* config except dynamic types

* clean up config

* remove disabled from non-dynamic vaues

* actions

* standardize example code block formats

* wip: actions refactor

Co-authored-by: Jade <Blu-J@users.noreply.github.com>

* commit types

* fix types

* update types

* update action request type

* update apis

* add description to actionrequest

* clean up imports

* revert package json

* chore: Remove the recursive to the index

* chore: Remove the other thing I was testing

* flatten action requests

* update container runtime with new config paradigm

* new actions strategy

* seems to be working

* misc backend fixes

* fix fe bugs

* only show breakages if breakages

* only show success modal if result

* don't panic on failed removal

* hide config from actions page

* polyfill autoconfig

* use metadata strategy for actions instead of prev

* misc fixes

* chore: split the sdk into 2 libs (#2736)

* follow sideload progress (#2718)

* follow sideload progress

* small bugfix

* shareReplay with no refcount false

* don't wrap sideload progress in RPCResult

* dont present toast

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>

* chore: Add the initial of the creation of the two sdk

* chore: Add in the baseDist

* chore: Add in the baseDist

* chore: Get the web and the runtime-container running

* chore: Remove the empty file

* chore: Fix it so the container-runtime works

---------

Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
Co-authored-by: Aiden McClelland <me@drbonez.dev>

* misc fixes

* update todos

* minor clean up

* fix link script

* update node version in CI test

* fix node version syntax in ci build

* wip: fixing callbacks

* fix sdk makefile dependencies

* add support for const outside of main

* update apis

* don't panic!

* Chore: Capture weird case on rpc, and log that

* fix procedure id issue

* pass input value for dep auto config

* handle disabled and warning for actions

* chore: Fix for link not having node_modules

* sdk fixes

* fix build

* fix build

* fix build

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: Jade <Blu-J@users.noreply.github.com>
Co-authored-by: J H <dragondef@gmail.com>
Co-authored-by: Jade <2364004+Blu-J@users.noreply.github.com>
Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
This commit is contained in:
Aiden McClelland
2024-09-25 16:12:52 -06:00
committed by GitHub
parent eec5cf6b65
commit db0695126f
469 changed files with 16218 additions and 10485 deletions

View File

@@ -1,18 +0,0 @@
import { VersionRange } from "./exver"
export class Dependency {
constructor(
readonly data:
| {
type: "running"
versionRange: VersionRange
registryUrl: string
healthChecks: string[]
}
| {
type: "exists"
versionRange: VersionRange
registryUrl: string
},
) {}
}

View File

@@ -1,805 +0,0 @@
import { RequiredDefault, Value } from "./config/builder/value"
import { Config, ExtractConfigType, LazyBuild } from "./config/builder/config"
import {
DefaultString,
ListValueSpecText,
Pattern,
RandomString,
UniqueBy,
ValueSpecDatetime,
ValueSpecText,
} from "./config/configTypes"
import { Variants } from "./config/builder/variants"
import { CreatedAction, createAction } from "./actions/createAction"
import {
ActionMetadata,
Effects,
ActionResult,
BackupOptions,
DeepPartial,
MaybePromise,
ServiceInterfaceId,
PackageId,
} from "./types"
import * as patterns from "./util/patterns"
import { DependencyConfig, Update } from "./dependencies/DependencyConfig"
import { BackupSet, Backups } from "./backup/Backups"
import { smtpConfig } from "./config/configConstants"
import { Daemons } from "./mainFn/Daemons"
import { healthCheck, HealthCheckParams } from "./health/HealthCheck"
import { checkPortListening } from "./health/checkFns/checkPortListening"
import { checkWebUrl, runHealthScript } from "./health/checkFns"
import { List } from "./config/builder/list"
import { Install, InstallFn } from "./inits/setupInstall"
import { setupActions } from "./actions/setupActions"
import { setupDependencyConfig } from "./dependencies/setupDependencyConfig"
import { SetupBackupsParams, setupBackups } from "./backup/setupBackups"
import { setupInit } from "./inits/setupInit"
import { Uninstall, UninstallFn, setupUninstall } from "./inits/setupUninstall"
import { setupMain } from "./mainFn"
import { defaultTrigger } from "./trigger/defaultTrigger"
import { changeOnFirstSuccess, cooldownTrigger } from "./trigger"
import setupConfig, {
DependenciesReceipt,
Read,
Save,
} from "./config/setupConfig"
import {
InterfacesReceipt,
SetInterfaces,
setupInterfaces,
} from "./interfaces/setupInterfaces"
import { successFailure } from "./trigger/successFailure"
import { HealthReceipt } from "./health/HealthReceipt"
import { MultiHost, Scheme } from "./interfaces/Host"
import { ServiceInterfaceBuilder } from "./interfaces/ServiceInterfaceBuilder"
import { GetSystemSmtp } from "./util/GetSystemSmtp"
import nullIfEmpty from "./util/nullIfEmpty"
import {
GetServiceInterface,
getServiceInterface,
} from "./util/getServiceInterface"
import { getServiceInterfaces } from "./util/getServiceInterfaces"
import { getStore } from "./store/getStore"
import { CommandOptions, MountOptions, SubContainer } from "./util/SubContainer"
import { splitCommand } from "./util/splitCommand"
import { Mounts } from "./mainFn/Mounts"
import { Dependency } from "./Dependency"
import * as T from "./types"
import { testTypeVersion, ValidateExVer } from "./exver"
import { ExposedStorePaths } from "./store/setupExposeStore"
import { PathBuilder, extractJsonPath, pathBuilder } from "./store/PathBuilder"
import {
CheckDependencies,
checkDependencies,
} from "./dependencies/dependencies"
import { health } from "."
import { GetSslCertificate } from "./util/GetSslCertificate"
import { VersionGraph } from "./version"
export const SDKVersion = testTypeVersion("0.3.6")
// prettier-ignore
type AnyNeverCond<T extends any[], Then, Else> =
T extends [] ? Else :
T extends [never, ...Array<any>] ? Then :
T extends [any, ...infer U] ? AnyNeverCond<U,Then, Else> :
never
export type ServiceInterfaceType = "ui" | "p2p" | "api"
export type MainEffects = Effects & {
_type: "main"
clearCallbacks: () => Promise<void>
}
export type Signals = NodeJS.Signals
export const SIGTERM: Signals = "SIGTERM"
export const SIGKILL: Signals = "SIGKILL"
export const NO_TIMEOUT = -1
function removeCallbackTypes<E extends Effects>(effects: E) {
return <T extends object>(t: T) => {
if ("_type" in effects && effects._type === "main") {
return t as E extends MainEffects ? T : Omit<T, "const" | "watch">
} else {
if ("const" in t) {
delete t.const
}
if ("watch" in t) {
delete t.watch
}
return t as E extends MainEffects ? T : Omit<T, "const" | "watch">
}
}
}
export class StartSdk<Manifest extends T.Manifest, Store> {
private constructor(readonly manifest: Manifest) {}
static of() {
return new StartSdk<never, never>(null as never)
}
withManifest<Manifest extends T.Manifest = never>(manifest: Manifest) {
return new StartSdk<Manifest, Store>(manifest)
}
withStore<Store extends Record<string, any>>() {
return new StartSdk<Manifest, Store>(this.manifest)
}
build(isReady: AnyNeverCond<[Manifest, Store], "Build not ready", true>) {
type DependencyType = {
[K in keyof {
[K in keyof Manifest["dependencies"]]: Manifest["dependencies"][K]["optional"] extends false
? K
: never
}]: Dependency
} & {
[K in keyof {
[K in keyof Manifest["dependencies"]]: Manifest["dependencies"][K]["optional"] extends true
? K
: never
}]?: Dependency
}
type NestedEffects = "subcontainer" | "store"
type InterfaceEffects =
| "getServiceInterface"
| "listServiceInterfaces"
| "exportServiceInterface"
| "clearServiceInterfaces"
| "bind"
| "getHostInfo"
| "getPrimaryUrl"
type MainUsedEffects = "setMainStatus" | "setHealth"
type AlreadyExposed = "getSslCertificate" | "getSystemSmtp"
// prettier-ignore
type StartSdkEffectWrapper = {
[K in keyof Omit<Effects, NestedEffects | InterfaceEffects | MainUsedEffects| AlreadyExposed>]: (effects: Effects, ...args: Parameters<Effects[K]>) => ReturnType<Effects[K]>
}
const startSdkEffectWrapper: StartSdkEffectWrapper = {
executeAction: (effects, ...args) => effects.executeAction(...args),
exportAction: (effects, ...args) => effects.exportAction(...args),
clearActions: (effects, ...args) => effects.clearActions(...args),
getConfigured: (effects, ...args) => effects.getConfigured(...args),
setConfigured: (effects, ...args) => effects.setConfigured(...args),
restart: (effects, ...args) => effects.restart(...args),
setDependencies: (effects, ...args) => effects.setDependencies(...args),
checkDependencies: (effects, ...args) =>
effects.checkDependencies(...args),
mount: (effects, ...args) => effects.mount(...args),
getInstalledPackages: (effects, ...args) =>
effects.getInstalledPackages(...args),
exposeForDependents: (effects, ...args) =>
effects.exposeForDependents(...args),
getServicePortForward: (effects, ...args) =>
effects.getServicePortForward(...args),
clearBindings: (effects, ...args) => effects.clearBindings(...args),
getContainerIp: (effects, ...args) => effects.getContainerIp(...args),
getSslKey: (effects, ...args) => effects.getSslKey(...args),
setDataVersion: (effects, ...args) => effects.setDataVersion(...args),
getDataVersion: (effects, ...args) => effects.getDataVersion(...args),
shutdown: (effects, ...args) => effects.shutdown(...args),
getDependencies: (effects, ...args) => effects.getDependencies(...args),
}
return {
...startSdkEffectWrapper,
checkDependencies: checkDependencies as <
DependencyId extends keyof Manifest["dependencies"] &
PackageId = keyof Manifest["dependencies"] & PackageId,
>(
effects: Effects,
packageIds?: DependencyId[],
) => Promise<CheckDependencies<DependencyId>>,
serviceInterface: {
getOwn: <E extends Effects>(effects: E, id: ServiceInterfaceId) =>
removeCallbackTypes<E>(effects)(
getServiceInterface(effects, {
id,
}),
),
get: <E extends Effects>(
effects: E,
opts: { id: ServiceInterfaceId; packageId: PackageId },
) =>
removeCallbackTypes<E>(effects)(getServiceInterface(effects, opts)),
getAllOwn: <E extends Effects>(effects: E) =>
removeCallbackTypes<E>(effects)(getServiceInterfaces(effects, {})),
getAll: <E extends Effects>(
effects: E,
opts: { packageId: PackageId },
) =>
removeCallbackTypes<E>(effects)(getServiceInterfaces(effects, opts)),
},
store: {
get: <E extends Effects, StoreValue = unknown>(
effects: E,
packageId: string,
path: PathBuilder<Store, StoreValue>,
) =>
removeCallbackTypes<E>(effects)(
getStore<Store, StoreValue>(effects, path, {
packageId,
}),
),
getOwn: <E extends Effects, StoreValue = unknown>(
effects: E,
path: PathBuilder<Store, StoreValue>,
) =>
removeCallbackTypes<E>(effects)(
getStore<Store, StoreValue>(effects, path),
),
setOwn: <E extends Effects, Path extends PathBuilder<Store, unknown>>(
effects: E,
path: Path,
value: Path extends PathBuilder<Store, infer Value> ? Value : never,
) =>
effects.store.set<Store>({
value,
path: extractJsonPath(path),
}),
},
host: {
// static: (effects: Effects, id: string) =>
// new StaticHost({ id, effects }),
// single: (effects: Effects, id: string) =>
// new SingleHost({ id, effects }),
multi: (effects: Effects, id: string) => new MultiHost({ id, effects }),
},
nullIfEmpty,
runCommand: async <A extends string>(
effects: Effects,
image: {
id: keyof Manifest["images"] & T.ImageId
sharedRun?: boolean
},
command: T.CommandType,
options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
},
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> => {
return runCommand<Manifest>(effects, image, command, options)
},
createAction: <
ConfigType extends
| Record<string, any>
| Config<any, any>
| Config<any, never>,
Type extends Record<string, any> = ExtractConfigType<ConfigType>,
>(
id: string,
metaData: Omit<ActionMetadata, "input"> & {
input: Config<Type, Store> | Config<Type, never>
},
fn: (options: {
effects: Effects
input: Type
}) => Promise<ActionResult>,
) => {
const { input, ...rest } = metaData
return createAction<Manifest, Store, ConfigType, Type>(
id,
rest,
fn,
input,
)
},
configConstants: { smtpConfig },
createInterface: (
effects: Effects,
options: {
name: string
id: string
description: string
hasPrimary: boolean
type: ServiceInterfaceType
username: null | string
path: string
search: Record<string, string>
schemeOverride: { ssl: Scheme; noSsl: Scheme } | null
masked: boolean
},
) => new ServiceInterfaceBuilder({ ...options, effects }),
getSystemSmtp: <E extends Effects>(effects: E) =>
removeCallbackTypes<E>(effects)(new GetSystemSmtp(effects)),
getSslCerificate: <E extends Effects>(
effects: E,
hostnames: string[],
algorithm?: T.Algorithm,
) =>
removeCallbackTypes<E>(effects)(
new GetSslCertificate(effects, hostnames, algorithm),
),
createDynamicAction: <
ConfigType extends
| Record<string, any>
| Config<any, any>
| Config<any, never>,
Type extends Record<string, any> = ExtractConfigType<ConfigType>,
>(
id: string,
metaData: (options: {
effects: Effects
}) => MaybePromise<Omit<ActionMetadata, "input">>,
fn: (options: {
effects: Effects
input: Type
}) => Promise<ActionResult>,
input: Config<Type, Store> | Config<Type, never>,
) => {
return createAction<Manifest, Store, ConfigType, Type>(
id,
metaData,
fn,
input,
)
},
HealthCheck: {
of(o: HealthCheckParams) {
return healthCheck(o)
},
},
Dependency: {
of(data: Dependency["data"]) {
return new Dependency({ ...data })
},
},
healthCheck: {
checkPortListening,
checkWebUrl,
runHealthScript,
},
patterns,
setupActions: (...createdActions: CreatedAction<any, any, any>[]) =>
setupActions<Manifest, Store>(...createdActions),
setupBackups: (...args: SetupBackupsParams<Manifest>) =>
setupBackups<Manifest>(this.manifest, ...args),
setupConfig: <
ConfigType extends Config<any, Store> | Config<any, never>,
Type extends Record<string, any> = ExtractConfigType<ConfigType>,
>(
spec: ConfigType,
write: Save<Type>,
read: Read<Manifest, Store, Type>,
) => setupConfig<Store, ConfigType, Manifest, Type>(spec, write, read),
setupConfigRead: <
ConfigSpec extends
| Config<Record<string, any>, any>
| Config<Record<string, never>, never>,
>(
_configSpec: ConfigSpec,
fn: Read<Manifest, Store, ConfigSpec>,
) => fn,
setupConfigSave: <
ConfigSpec extends
| Config<Record<string, any>, any>
| Config<Record<string, never>, never>,
>(
_configSpec: ConfigSpec,
fn: Save<ConfigSpec>,
) => fn,
setupDependencyConfig: <Input extends Record<string, any>>(
config: Config<Input, Store> | Config<Input, never>,
autoConfigs: {
[K in keyof Manifest["dependencies"]]: DependencyConfig<
Manifest,
Store,
Input,
any
> | null
},
) => setupDependencyConfig<Store, Input, Manifest>(config, autoConfigs),
setupDependencies: <Input extends Record<string, any>>(
fn: (options: {
effects: Effects
input: Input | null
}) => Promise<DependencyType>,
) => {
return async (options: { effects: Effects; input: Input }) => {
const dependencyType = await fn(options)
return await options.effects.setDependencies({
dependencies: Object.entries(dependencyType).map(
([
id,
{
data: { versionRange, ...x },
},
]) => ({
id,
...x,
...(x.type === "running"
? {
kind: "running",
healthChecks: x.healthChecks,
}
: {
kind: "exists",
}),
versionRange: versionRange.toString(),
}),
),
})
}
},
setupInit: (
versions: VersionGraph<Manifest["version"]>,
install: Install<Manifest, Store>,
uninstall: Uninstall<Manifest, Store>,
setInterfaces: SetInterfaces<Manifest, Store, any, any>,
setDependencies: (options: {
effects: Effects
input: any
}) => Promise<DependenciesReceipt>,
exposedStore: ExposedStorePaths,
) =>
setupInit<Manifest, Store>(
versions,
install,
uninstall,
setInterfaces,
setDependencies,
exposedStore,
),
setupInstall: (fn: InstallFn<Manifest, Store>) => Install.of(fn),
setupInterfaces: <
ConfigInput extends Record<string, any>,
Output extends InterfacesReceipt,
>(
config: Config<ConfigInput, Store>,
fn: SetInterfaces<Manifest, Store, ConfigInput, Output>,
) => setupInterfaces(config, fn),
setupMain: (
fn: (o: {
effects: MainEffects
started(onTerm: () => PromiseLike<void>): PromiseLike<void>
}) => Promise<Daemons<Manifest, any>>,
) => setupMain<Manifest, Store>(fn),
setupProperties:
(
fn: (options: { effects: Effects }) => Promise<T.SdkPropertiesReturn>,
): T.ExpectedExports.properties =>
(options) =>
fn(options).then(nullifyProperties),
setupUninstall: (fn: UninstallFn<Manifest, Store>) =>
setupUninstall<Manifest, Store>(fn),
trigger: {
defaultTrigger,
cooldownTrigger,
changeOnFirstSuccess,
successFailure,
},
Mounts: {
of() {
return Mounts.of<Manifest>()
},
},
Backups: {
volumes: (
...volumeNames: Array<Manifest["volumes"][number] & string>
) => Backups.volumes<Manifest>(...volumeNames),
addSets: (
...options: BackupSet<Manifest["volumes"][number] & string>[]
) => Backups.addSets<Manifest>(...options),
withOptions: (options?: Partial<BackupOptions>) =>
Backups.with_options<Manifest>(options),
},
Config: {
of: <
Spec extends Record<string, Value<any, Store> | Value<any, never>>,
>(
spec: Spec,
) => Config.of<Spec, Store>(spec),
},
Daemons: {
of(config: {
effects: Effects
started: (onTerm: () => PromiseLike<void>) => PromiseLike<void>
healthReceipts: HealthReceipt[]
}) {
return Daemons.of<Manifest>(config)
},
},
DependencyConfig: {
of<
LocalConfig extends Record<string, any>,
RemoteConfig extends Record<string, any>,
>({
localConfigSpec,
remoteConfigSpec,
dependencyConfig,
update,
}: {
localConfigSpec:
| Config<LocalConfig, Store>
| Config<LocalConfig, never>
remoteConfigSpec:
| Config<RemoteConfig, any>
| Config<RemoteConfig, never>
dependencyConfig: (options: {
effects: Effects
localConfig: LocalConfig
}) => Promise<void | DeepPartial<RemoteConfig>>
update?: Update<void | DeepPartial<RemoteConfig>, RemoteConfig>
}) {
return new DependencyConfig<
Manifest,
Store,
LocalConfig,
RemoteConfig
>(dependencyConfig, update)
},
},
List: {
text: List.text,
obj: <Type extends Record<string, any>>(
a: {
name: string
description?: string | null
warning?: string | null
/** Default [] */
default?: []
minLength?: number | null
maxLength?: number | null
},
aSpec: {
spec: Config<Type, Store>
displayAs?: null | string
uniqueBy?: null | UniqueBy
},
) => List.obj<Type, Store>(a, aSpec),
dynamicText: (
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
/** Default = [] */
default?: string[]
minLength?: number | null
maxLength?: number | null
disabled?: false | string
generate?: null | RandomString
spec: {
/** Default = false */
masked?: boolean
placeholder?: string | null
minLength?: number | null
maxLength?: number | null
patterns: Pattern[]
/** Default = "text" */
inputmode?: ListValueSpecText["inputmode"]
}
}
>,
) => List.dynamicText<Store>(getA),
},
StorePath: pathBuilder<Store>(),
Value: {
toggle: Value.toggle,
text: Value.text,
textarea: Value.textarea,
number: Value.number,
color: Value.color,
datetime: Value.datetime,
select: Value.select,
multiselect: Value.multiselect,
object: Value.object,
union: Value.union,
list: Value.list,
dynamicToggle: (
a: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
default: boolean
disabled?: false | string
}
>,
) => Value.dynamicToggle<Store>(a),
dynamicText: (
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<DefaultString>
/** Default = false */
masked?: boolean
placeholder?: string | null
minLength?: number | null
maxLength?: number | null
patterns?: Pattern[]
/** Default = 'text' */
inputmode?: ValueSpecText["inputmode"]
generate?: null | RandomString
}
>,
) => Value.dynamicText<Store>(getA),
dynamicTextarea: (
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: boolean
minLength?: number | null
maxLength?: number | null
placeholder?: string | null
disabled?: false | string
generate?: null | RandomString
}
>,
) => Value.dynamicTextarea<Store>(getA),
dynamicNumber: (
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<number>
min?: number | null
max?: number | null
/** Default = '1' */
step?: number | null
integer: boolean
units?: string | null
placeholder?: string | null
disabled?: false | string
}
>,
) => Value.dynamicNumber<Store>(getA),
dynamicColor: (
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<string>
disabled?: false | string
}
>,
) => Value.dynamicColor<Store>(getA),
dynamicDatetime: (
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<string>
/** Default = 'datetime-local' */
inputmode?: ValueSpecDatetime["inputmode"]
min?: string | null
max?: string | null
disabled?: false | string
}
>,
) => Value.dynamicDatetime<Store>(getA),
dynamicSelect: (
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<string>
values: Record<string, string>
disabled?: false | string
}
>,
) => Value.dynamicSelect<Store>(getA),
dynamicMultiselect: (
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
default: string[]
values: Record<string, string>
minLength?: number | null
maxLength?: number | null
disabled?: false | string
}
>,
) => Value.dynamicMultiselect<Store>(getA),
filteredUnion: <
Required extends RequiredDefault<string>,
Type extends Record<string, any>,
>(
getDisabledFn: LazyBuild<Store, string[]>,
a: {
name: string
description?: string | null
warning?: string | null
required: Required
},
aVariants: Variants<Type, Store> | Variants<Type, never>,
) =>
Value.filteredUnion<Required, Type, Store>(
getDisabledFn,
a,
aVariants,
),
dynamicUnion: <
Required extends RequiredDefault<string>,
Type extends Record<string, any>,
>(
getA: LazyBuild<
Store,
{
disabled: string[] | false | string
name: string
description?: string | null
warning?: string | null
required: Required
}
>,
aVariants: Variants<Type, Store> | Variants<Type, never>,
) => Value.dynamicUnion<Required, Type, Store>(getA, aVariants),
},
Variants: {
of: <
VariantValues extends {
[K in string]: {
name: string
spec: Config<any, Store>
}
},
>(
a: VariantValues,
) => Variants.of<VariantValues, Store>(a),
},
}
}
}
export async function runCommand<Manifest extends T.Manifest>(
effects: Effects,
image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean },
command: string | [string, ...string[]],
options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
},
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> {
const commands = splitCommand(command)
return SubContainer.with(
effects,
image,
options.mounts || [],
commands.join(" "),
(subcontainer) => subcontainer.exec(commands),
)
}
function nullifyProperties(value: T.SdkPropertiesReturn): T.PropertiesReturn {
return Object.fromEntries(
Object.entries(value).map(([k, v]) => [k, nullifyProperties_(v)]),
)
}
function nullifyProperties_(value: T.SdkPropertiesValue): T.PropertiesValue {
if (value.type === "string") {
return { description: null, copyable: null, qr: null, ...value }
}
return {
description: null,
...value,
value: Object.fromEntries(
Object.entries(value.value).map(([k, v]) => [k, nullifyProperties_(v)]),
),
}
}

View File

@@ -1,89 +0,0 @@
import * as T from "../types"
import { Config, ExtractConfigType } from "../config/builder/config"
import { ActionMetadata, ActionResult, Effects, ExportedAction } from "../types"
export type MaybeFn<Manifest extends T.Manifest, Store, Value> =
| Value
| ((options: { effects: Effects }) => Promise<Value> | Value)
export class CreatedAction<
Manifest extends T.Manifest,
Store,
ConfigType extends
| Record<string, any>
| Config<any, Store>
| Config<any, never>,
Type extends Record<string, any> = ExtractConfigType<ConfigType>,
> {
private constructor(
public readonly id: string,
public readonly myMetadata: MaybeFn<
Manifest,
Store,
Omit<ActionMetadata, "input">
>,
readonly fn: (options: {
effects: Effects
input: Type
}) => Promise<ActionResult>,
readonly input: Config<Type, Store>,
public validator = input.validator,
) {}
static of<
Manifest extends T.Manifest,
Store,
ConfigType extends
| Record<string, any>
| Config<any, any>
| Config<any, never>,
Type extends Record<string, any> = ExtractConfigType<ConfigType>,
>(
id: string,
metadata: MaybeFn<Manifest, Store, Omit<ActionMetadata, "input">>,
fn: (options: { effects: Effects; input: Type }) => Promise<ActionResult>,
inputConfig: Config<Type, Store> | Config<Type, never>,
) {
return new CreatedAction<Manifest, Store, ConfigType, Type>(
id,
metadata,
fn,
inputConfig as Config<Type, Store>,
)
}
exportedAction: ExportedAction = ({ effects, input }) => {
return this.fn({
effects,
input: this.validator.unsafeCast(input),
})
}
run = async ({ effects, input }: { effects: Effects; input?: Type }) => {
return this.fn({
effects,
input: this.validator.unsafeCast(input),
})
}
async metadata(options: { effects: Effects }) {
if (this.myMetadata instanceof Function)
return await this.myMetadata(options)
return this.myMetadata
}
async ActionMetadata(options: { effects: Effects }): Promise<ActionMetadata> {
return {
...(await this.metadata(options)),
input: await this.input.build(options),
}
}
async getConfig({ effects }: { effects: Effects }) {
return this.input.build({
effects,
})
}
}
export const createAction = CreatedAction.of

View File

@@ -1,3 +0,0 @@
import "./createAction"
import "./setupActions"

View File

@@ -1,29 +0,0 @@
import * as T from "../types"
import { Effects, ExpectedExports } from "../types"
import { CreatedAction } from "./createAction"
export function setupActions<Manifest extends T.Manifest, Store>(
...createdActions: CreatedAction<Manifest, Store, any>[]
) {
const myActions = async (options: { effects: Effects }) => {
const actions: Record<string, CreatedAction<Manifest, Store, any>> = {}
for (const action of createdActions) {
actions[action.id] = action
}
return actions
}
const answer: {
actions: ExpectedExports.actions
actionsMetadata: ExpectedExports.actionsMetadata
} = {
actions(options: { effects: Effects }) {
return myActions(options)
},
async actionsMetadata({ effects }: { effects: Effects }) {
return Promise.all(
createdActions.map((x) => x.ActionMetadata({ effects })),
)
},
}
return answer
}

View File

@@ -1,209 +0,0 @@
import * as T from "../types"
import * as child_process from "child_process"
import { promises as fsPromises } from "fs"
import { asError } from "../util"
export type BACKUP = "BACKUP"
export const DEFAULT_OPTIONS: T.BackupOptions = {
delete: true,
force: true,
ignoreExisting: false,
exclude: [],
}
export type BackupSet<Volumes extends string> = {
srcPath: string
srcVolume: Volumes | BACKUP
dstPath: string
dstVolume: Volumes | BACKUP
options?: Partial<T.BackupOptions>
}
/**
* This utility simplifies the volume backup process.
* ```ts
* export const { createBackup, restoreBackup } = Backups.volumes("main").build();
* ```
*
* Changing the options of the rsync, (ie exludes) use either
* ```ts
* Backups.volumes("main").set_options({exclude: ['bigdata/']}).volumes('excludedVolume').build()
* // or
* Backups.with_options({exclude: ['bigdata/']}).volumes('excludedVolume').build()
* ```
*
* Using the more fine control, using the addSets for more control
* ```ts
* Backups.addSets({
* srcVolume: 'main', srcPath:'smallData/', dstPath: 'main/smallData/', dstVolume: : Backups.BACKUP
* }, {
* srcVolume: 'main', srcPath:'bigData/', dstPath: 'main/bigData/', dstVolume: : Backups.BACKUP, options: {exclude:['bigData/excludeThis']}}
* ).build()q
* ```
*/
export class Backups<M extends T.Manifest> {
static BACKUP: BACKUP = "BACKUP"
private constructor(
private options = DEFAULT_OPTIONS,
private backupSet = [] as BackupSet<M["volumes"][number]>[],
) {}
static volumes<M extends T.Manifest = never>(
...volumeNames: Array<M["volumes"][0]>
): Backups<M> {
return new Backups<M>().addSets(
...volumeNames.map((srcVolume) => ({
srcVolume,
srcPath: "./",
dstPath: `./${srcVolume}/`,
dstVolume: Backups.BACKUP,
})),
)
}
static addSets<M extends T.Manifest = never>(
...options: BackupSet<M["volumes"][0]>[]
) {
return new Backups().addSets(...options)
}
static with_options<M extends T.Manifest = never>(
options?: Partial<T.BackupOptions>,
) {
return new Backups({ ...DEFAULT_OPTIONS, ...options })
}
static withOptions = Backups.with_options
setOptions(options?: Partial<T.BackupOptions>) {
this.options = {
...this.options,
...options,
}
return this
}
volumes(...volumeNames: Array<M["volumes"][0]>) {
return this.addSets(
...volumeNames.map((srcVolume) => ({
srcVolume,
srcPath: "./",
dstPath: `./${srcVolume}/`,
dstVolume: Backups.BACKUP,
})),
)
}
addSets(...options: BackupSet<M["volumes"][0]>[]) {
options.forEach((x) =>
this.backupSet.push({ ...x, options: { ...this.options, ...x.options } }),
)
return this
}
build(pathMaker: T.PathMaker) {
const createBackup: T.ExpectedExports.createBackup = async ({
effects,
}) => {
for (const item of this.backupSet) {
const rsyncResults = await runRsync(
{
dstPath: item.dstPath,
dstVolume: item.dstVolume,
options: { ...this.options, ...item.options },
srcPath: item.srcPath,
srcVolume: item.srcVolume,
},
pathMaker,
)
await rsyncResults.wait()
}
return
}
const restoreBackup: T.ExpectedExports.restoreBackup = async ({
effects,
}) => {
for (const item of this.backupSet) {
const rsyncResults = await runRsync(
{
dstPath: item.dstPath,
dstVolume: item.dstVolume,
options: { ...this.options, ...item.options },
srcPath: item.srcPath,
srcVolume: item.srcVolume,
},
pathMaker,
)
await rsyncResults.wait()
}
return
}
return { createBackup, restoreBackup }
}
}
function notEmptyPath(file: string) {
return ["", ".", "./"].indexOf(file) === -1
}
async function runRsync(
rsyncOptions: {
srcVolume: string
dstVolume: string
srcPath: string
dstPath: string
options: T.BackupOptions
},
pathMaker: T.PathMaker,
): Promise<{
id: () => Promise<string>
wait: () => Promise<null>
progress: () => Promise<number>
}> {
const { srcVolume, dstVolume, srcPath, dstPath, options } = rsyncOptions
const command = "rsync"
const args: string[] = []
if (options.delete) {
args.push("--delete")
}
if (options.force) {
args.push("--force")
}
if (options.ignoreExisting) {
args.push("--ignore-existing")
}
for (const exclude of options.exclude) {
args.push(`--exclude=${exclude}`)
}
args.push("-actAXH")
args.push("--info=progress2")
args.push("--no-inc-recursive")
args.push(pathMaker({ volume: srcVolume, path: srcPath }))
args.push(pathMaker({ volume: dstVolume, path: dstPath }))
const spawned = child_process.spawn(command, args, { detached: true })
let percentage = 0.0
spawned.stdout.on("data", (data: unknown) => {
const lines = String(data).replace("\r", "\n").split("\n")
for (const line of lines) {
const parsed = /$([0-9.]+)%/.exec(line)?.[1]
if (!parsed) continue
percentage = Number.parseFloat(parsed)
}
})
spawned.stderr.on("data", (data: unknown) => {
console.error(`Backups.runAsync`, asError(data))
})
const id = async () => {
const pid = spawned.pid
if (pid === undefined) {
throw new Error("rsync process has no pid")
}
return String(pid)
}
const waitPromise = new Promise<null>((resolve, reject) => {
spawned.on("exit", (code: any) => {
if (code === 0) {
resolve(null)
} else {
reject(new Error(`rsync exited with code ${code}`))
}
})
})
const wait = () => waitPromise
const progress = () => Promise.resolve(percentage)
return { id, wait, progress }
}

View File

@@ -1,3 +0,0 @@
import "./Backups"
import "./setupBackups"

View File

@@ -1,45 +0,0 @@
import { Backups } from "./Backups"
import * as T from "../types"
import { _ } from "../util"
export type SetupBackupsParams<M extends T.Manifest> = Array<
M["volumes"][number] | Backups<M>
>
export function setupBackups<M extends T.Manifest>(
manifest: M,
...args: _<SetupBackupsParams<M>>
) {
const backups = Array<Backups<M>>()
const volumes = new Set<M["volumes"][0]>()
for (const arg of args) {
if (arg instanceof Backups) {
backups.push(arg)
} else {
volumes.add(arg)
}
}
backups.push(Backups.volumes(...volumes))
const answer: {
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 T.ExpectedExports.createBackup
},
get restoreBackup() {
return (async (options) => {
for (const backup of backups) {
await backup.build(options.pathMaker).restoreBackup(options)
}
await options.effects.setDataVersion({ version: manifest.version })
}) as T.ExpectedExports.restoreBackup
},
}
return answer
}

View File

@@ -1,137 +0,0 @@
import { ValueSpec } from "../configTypes"
import { Value } from "./value"
import { _ } from "../../util"
import { Effects } from "../../types"
import { Parser, object } from "ts-matches"
export type LazyBuildOptions<Store> = {
effects: Effects
}
export type LazyBuild<Store, ExpectedOut> = (
options: LazyBuildOptions<Store>,
) => Promise<ExpectedOut> | ExpectedOut
// prettier-ignore
export type ExtractConfigType<A extends Record<string, any> | Config<Record<string, any>, any> | Config<Record<string, any>, never>> =
A extends Config<infer B, any> | Config<infer B, never> ? B :
A
export type ConfigSpecOf<A extends Record<string, any>, Store = never> = {
[K in keyof A]: Value<A[K], Store>
}
export type MaybeLazyValues<A> = LazyBuild<any, A> | A
/**
* Configs are the specs that are used by the os configuration form for this service.
* Here is an example of a simple configuration
```ts
const smallConfig = Config.of({
test: Value.boolean({
name: "Test",
description: "This is the description for the test",
warning: null,
default: false,
}),
});
```
The idea of a config is that now the form is going to ask for
Test: [ ] and the value is going to be checked as a boolean.
There are more complex values like selects, lists, and objects. See {@link Value}
Also, there is the ability to get a validator/parser from this config spec.
```ts
const matchSmallConfig = smallConfig.validator();
type SmallConfig = typeof matchSmallConfig._TYPE;
```
Here is an example of a more complex configuration which came from a configuration for a service
that works with bitcoin, like c-lightning.
```ts
export const hostname = Value.string({
name: "Hostname",
default: null,
description: "Domain or IP address of bitcoin peer",
warning: null,
required: true,
masked: false,
placeholder: null,
pattern:
"(^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))",
patternDescription:
"Must be either a domain name, or an IPv4 or IPv6 address. Do not include protocol scheme (eg 'http://') or port.",
});
export const port = Value.number({
name: "Port",
default: null,
description: "Port that peer is listening on for inbound p2p connections",
warning: null,
required: false,
range: "[0,65535]",
integral: true,
units: null,
placeholder: null,
});
export const addNodesSpec = Config.of({ hostname: hostname, port: port });
```
*/
export class Config<Type extends Record<string, any>, Store = never> {
private constructor(
private readonly spec: {
[K in keyof Type]: Value<Type[K], Store> | Value<Type[K], never>
},
public validator: Parser<unknown, Type>,
) {}
async build(options: LazyBuildOptions<Store>) {
const answer = {} as {
[K in keyof Type]: ValueSpec
}
for (const k in this.spec) {
answer[k] = await this.spec[k].build(options as any)
}
return answer
}
static of<
Spec extends Record<string, Value<any, Store> | Value<any, never>>,
Store = never,
>(spec: Spec) {
const validatorObj = {} as {
[K in keyof Spec]: Parser<unknown, any>
}
for (const key in spec) {
validatorObj[key] = spec[key].validator
}
const validator = object(validatorObj)
return new Config<
{
[K in keyof Spec]: Spec[K] extends
| Value<infer T, Store>
| Value<infer T, never>
? T
: never
},
Store
>(spec, validator as any)
}
/**
* Use this during the times that the input needs a more specific type.
* Used in types that the value/ variant/ list/ config is constructed somewhere else.
```ts
const a = Config.text({
name: "a",
required: false,
})
return Config.of<Store>()({
myValue: a.withStore(),
})
```
*/
withStore<NewStore extends Store extends never ? any : Store>() {
return this as any as Config<Type, NewStore>
}
}

View File

@@ -1,6 +0,0 @@
import { Config } from "./config"
import { List } from "./list"
import { Value } from "./value"
import { Variants } from "./variants"
export { Config, List, Value, Variants }

View File

@@ -1,188 +0,0 @@
import { Config, LazyBuild } from "./config"
import {
ListValueSpecText,
Pattern,
RandomString,
UniqueBy,
ValueSpecList,
ValueSpecListOf,
} from "../configTypes"
import { Parser, arrayOf, number, string } from "ts-matches"
/**
* Used as a subtype of Value.list
```ts
export const authorizationList = List.string({
"name": "Authorization",
"range": "[0,*)",
"default": [],
"description": "Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.",
"warning": null
}, {"masked":false,"placeholder":null,"pattern":"^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$","patternDescription":"Each item must be of the form \"<USERNAME>:<SALT>$<HASH>\"."});
export const auth = Value.list(authorizationList);
```
*/
export class List<Type, Store> {
private constructor(
public build: LazyBuild<Store, ValueSpecList>,
public validator: Parser<unknown, Type>,
) {}
static text(
a: {
name: string
description?: string | null
warning?: string | null
/** Default = [] */
default?: string[]
minLength?: number | null
maxLength?: number | null
},
aSpec: {
/** Default = false */
masked?: boolean
placeholder?: string | null
minLength?: number | null
maxLength?: number | null
patterns: Pattern[]
/** Default = "text" */
inputmode?: ListValueSpecText["inputmode"]
generate?: null | RandomString
},
) {
return new List<string[], never>(() => {
const spec = {
type: "text" as const,
placeholder: null,
minLength: null,
maxLength: null,
masked: false,
inputmode: "text" as const,
generate: null,
...aSpec,
}
const built: ValueSpecListOf<"text"> = {
description: null,
warning: null,
default: [],
type: "list" as const,
minLength: null,
maxLength: null,
disabled: false,
...a,
spec,
}
return built
}, arrayOf(string))
}
static dynamicText<Store = never>(
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
/** Default = [] */
default?: string[]
minLength?: number | null
maxLength?: number | null
disabled?: false | string
generate?: null | RandomString
spec: {
/** Default = false */
masked?: boolean
placeholder?: string | null
minLength?: number | null
maxLength?: number | null
patterns: Pattern[]
/** Default = "text" */
inputmode?: ListValueSpecText["inputmode"]
}
}
>,
) {
return new List<string[], Store>(async (options) => {
const { spec: aSpec, ...a } = await getA(options)
const spec = {
type: "text" as const,
placeholder: null,
minLength: null,
maxLength: null,
masked: false,
inputmode: "text" as const,
generate: null,
...aSpec,
}
const built: ValueSpecListOf<"text"> = {
description: null,
warning: null,
default: [],
type: "list" as const,
minLength: null,
maxLength: null,
disabled: false,
...a,
spec,
}
return built
}, arrayOf(string))
}
static obj<Type extends Record<string, any>, Store>(
a: {
name: string
description?: string | null
warning?: string | null
/** Default [] */
default?: []
minLength?: number | null
maxLength?: number | null
},
aSpec: {
spec: Config<Type, Store>
displayAs?: null | string
uniqueBy?: null | UniqueBy
},
) {
return new List<Type[], Store>(async (options) => {
const { spec: previousSpecSpec, ...restSpec } = aSpec
const specSpec = await previousSpecSpec.build(options)
const spec = {
type: "object" as const,
displayAs: null,
uniqueBy: null,
...restSpec,
spec: specSpec,
}
const value = {
spec,
default: [],
...a,
}
return {
description: null,
warning: null,
minLength: null,
maxLength: null,
type: "list" as const,
disabled: false,
...value,
}
}, arrayOf(aSpec.spec.validator))
}
/**
* Use this during the times that the input needs a more specific type.
* Used in types that the value/ variant/ list/ config is constructed somewhere else.
```ts
const a = Config.text({
name: "a",
required: false,
})
return Config.of<Store>()({
myValue: a.withStore(),
})
```
*/
withStore<NewStore extends Store extends never ? any : Store>() {
return this as any as List<Type, NewStore>
}
}

View File

@@ -1,783 +0,0 @@
import { Config, LazyBuild, LazyBuildOptions } from "./config"
import { List } from "./list"
import { Variants } from "./variants"
import {
FilePath,
Pattern,
RandomString,
ValueSpec,
ValueSpecDatetime,
ValueSpecText,
ValueSpecTextarea,
} from "../configTypes"
import { DefaultString } from "../configTypes"
import { _ } from "../../util"
import {
Parser,
anyOf,
arrayOf,
boolean,
literal,
literals,
number,
object,
string,
unknown,
} from "ts-matches"
import { once } from "../../util/once"
export type RequiredDefault<A> =
| false
| {
default: A | null
}
function requiredLikeToAbove<Input extends RequiredDefault<A>, A>(
requiredLike: Input,
) {
// prettier-ignore
return {
required: (typeof requiredLike === 'object' ? true : requiredLike) as (
Input extends { default: unknown} ? true:
Input extends true ? true :
false
),
default:(typeof requiredLike === 'object' ? requiredLike.default : null) as (
Input extends { default: infer Default } ? Default :
null
)
};
}
type AsRequired<Type, MaybeRequiredType> = MaybeRequiredType extends
| { default: unknown }
| never
? Type
: Type | null | undefined
type InputAsRequired<A, Type> = A extends
| { required: { default: any } | never }
| never
? Type
: Type | null | undefined
const testForAsRequiredParser = once(
() => object({ required: object({ default: unknown }) }).test,
)
function asRequiredParser<
Type,
Input,
Return extends
| Parser<unknown, Type>
| Parser<unknown, Type | null | undefined>,
>(parser: Parser<unknown, Type>, input: Input): Return {
if (testForAsRequiredParser()(input)) return parser as any
return parser.optional() as any
}
/**
* A value is going to be part of the form in the FE of the OS.
* Something like a boolean, a string, a number, etc.
* in the fe it will ask for the name of value, and use the rest of the value to determine how to render it.
* While writing with a value, you will start with `Value.` then let the IDE suggest the rest.
* for things like string, the options are going to be in {}.
* Keep an eye out for another config builder types as params.
* Note, usually this is going to be used in a `Config` {@link Config} builder.
```ts
const username = Value.string({
name: "Username",
default: "bitcoin",
description: "The username for connecting to Bitcoin over RPC.",
warning: null,
required: true,
masked: true,
placeholder: null,
pattern: "^[a-zA-Z0-9_]+$",
patternDescription: "Must be alphanumeric (can contain underscore).",
});
```
*/
export class Value<Type, Store> {
protected constructor(
public build: LazyBuild<Store, ValueSpec>,
public validator: Parser<unknown, Type>,
) {}
static toggle(a: {
name: string
description?: string | null
warning?: string | null
default: boolean
/** Immutable means it can only be configed at the first config then never again
Default is false */
immutable?: boolean
}) {
return new Value<boolean, never>(
async () => ({
description: null,
warning: null,
type: "toggle" as const,
disabled: false,
immutable: a.immutable ?? false,
...a,
}),
boolean,
)
}
static dynamicToggle<Store = never>(
a: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
default: boolean
disabled?: false | string
}
>,
) {
return new Value<boolean, Store>(
async (options) => ({
description: null,
warning: null,
type: "toggle" as const,
disabled: false,
immutable: false,
...(await a(options)),
}),
boolean,
)
}
static text<Required extends RequiredDefault<DefaultString>>(a: {
name: string
description?: string | null
warning?: string | null
required: Required
/** Default = false */
masked?: boolean
placeholder?: string | null
minLength?: number | null
maxLength?: number | null
patterns?: Pattern[]
/** Default = 'text' */
inputmode?: ValueSpecText["inputmode"]
/** Immutable means it can only be configured at the first config then never again
* Default is false
*/
immutable?: boolean
generate?: null | RandomString
}) {
return new Value<AsRequired<string, Required>, never>(
async () => ({
type: "text" as const,
description: null,
warning: null,
masked: false,
placeholder: null,
minLength: null,
maxLength: null,
patterns: [],
inputmode: "text",
disabled: false,
immutable: a.immutable ?? false,
generate: a.generate ?? null,
...a,
...requiredLikeToAbove(a.required),
}),
asRequiredParser(string, a),
)
}
static dynamicText<Store = never>(
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<DefaultString>
/** Default = false */
masked?: boolean
placeholder?: string | null
minLength?: number | null
maxLength?: number | null
patterns?: Pattern[]
/** Default = 'text' */
inputmode?: ValueSpecText["inputmode"]
disabled?: string | false
/** Immutable means it can only be configured at the first config then never again
* Default is false
*/
generate?: null | RandomString
}
>,
) {
return new Value<string | null | undefined, Store>(async (options) => {
const a = await getA(options)
return {
type: "text" as const,
description: null,
warning: null,
masked: false,
placeholder: null,
minLength: null,
maxLength: null,
patterns: [],
inputmode: "text",
disabled: false,
immutable: false,
generate: a.generate ?? null,
...a,
...requiredLikeToAbove(a.required),
}
}, string.optional())
}
static textarea(a: {
name: string
description?: string | null
warning?: string | null
required: boolean
minLength?: number | null
maxLength?: number | null
placeholder?: string | null
/** Immutable means it can only be configed at the first config then never again
Default is false */
immutable?: boolean
}) {
return new Value<string, never>(async () => {
const built: ValueSpecTextarea = {
description: null,
warning: null,
minLength: null,
maxLength: null,
placeholder: null,
type: "textarea" as const,
disabled: false,
immutable: a.immutable ?? false,
...a,
}
return built
}, string)
}
static dynamicTextarea<Store = never>(
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: boolean
minLength?: number | null
maxLength?: number | null
placeholder?: string | null
disabled?: false | string
}
>,
) {
return new Value<string, Store>(async (options) => {
const a = await getA(options)
return {
description: null,
warning: null,
minLength: null,
maxLength: null,
placeholder: null,
type: "textarea" as const,
disabled: false,
immutable: false,
...a,
}
}, string)
}
static number<Required extends RequiredDefault<number>>(a: {
name: string
description?: string | null
warning?: string | null
required: Required
min?: number | null
max?: number | null
/** Default = '1' */
step?: number | null
integer: boolean
units?: string | null
placeholder?: string | null
/** Immutable means it can only be configed at the first config then never again
Default is false */
immutable?: boolean
}) {
return new Value<AsRequired<number, Required>, never>(
() => ({
type: "number" as const,
description: null,
warning: null,
min: null,
max: null,
step: null,
units: null,
placeholder: null,
disabled: false,
immutable: a.immutable ?? false,
...a,
...requiredLikeToAbove(a.required),
}),
asRequiredParser(number, a),
)
}
static dynamicNumber<Store = never>(
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<number>
min?: number | null
max?: number | null
/** Default = '1' */
step?: number | null
integer: boolean
units?: string | null
placeholder?: string | null
disabled?: false | string
}
>,
) {
return new Value<number | null | undefined, Store>(async (options) => {
const a = await getA(options)
return {
type: "number" as const,
description: null,
warning: null,
min: null,
max: null,
step: null,
units: null,
placeholder: null,
disabled: false,
immutable: false,
...a,
...requiredLikeToAbove(a.required),
}
}, number.optional())
}
static color<Required extends RequiredDefault<string>>(a: {
name: string
description?: string | null
warning?: string | null
required: Required
/** Immutable means it can only be configed at the first config then never again
Default is false */
immutable?: boolean
}) {
return new Value<AsRequired<string, Required>, never>(
() => ({
type: "color" as const,
description: null,
warning: null,
disabled: false,
immutable: a.immutable ?? false,
...a,
...requiredLikeToAbove(a.required),
}),
asRequiredParser(string, a),
)
}
static dynamicColor<Store = never>(
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<string>
disabled?: false | string
}
>,
) {
return new Value<string | null | undefined, Store>(async (options) => {
const a = await getA(options)
return {
type: "color" as const,
description: null,
warning: null,
disabled: false,
immutable: false,
...a,
...requiredLikeToAbove(a.required),
}
}, string.optional())
}
static datetime<Required extends RequiredDefault<string>>(a: {
name: string
description?: string | null
warning?: string | null
required: Required
/** Default = 'datetime-local' */
inputmode?: ValueSpecDatetime["inputmode"]
min?: string | null
max?: string | null
/** Immutable means it can only be configed at the first config then never again
Default is false */
immutable?: boolean
}) {
return new Value<AsRequired<string, Required>, never>(
() => ({
type: "datetime" as const,
description: null,
warning: null,
inputmode: "datetime-local",
min: null,
max: null,
step: null,
disabled: false,
immutable: a.immutable ?? false,
...a,
...requiredLikeToAbove(a.required),
}),
asRequiredParser(string, a),
)
}
static dynamicDatetime<Store = never>(
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<string>
/** Default = 'datetime-local' */
inputmode?: ValueSpecDatetime["inputmode"]
min?: string | null
max?: string | null
disabled?: false | string
}
>,
) {
return new Value<string | null | undefined, Store>(async (options) => {
const a = await getA(options)
return {
type: "datetime" as const,
description: null,
warning: null,
inputmode: "datetime-local",
min: null,
max: null,
disabled: false,
immutable: false,
...a,
...requiredLikeToAbove(a.required),
}
}, string.optional())
}
static select<
Required extends RequiredDefault<string>,
B extends Record<string, string>,
>(a: {
name: string
description?: string | null
warning?: string | null
required: Required
values: B
/**
* Disabled: false means that there is nothing disabled, good to modify
* string means that this is the message displayed and the whole thing is disabled
* string[] means that the options are disabled
*/
disabled?: false | string | (string & keyof B)[]
/** Immutable means it can only be configed at the first config then never again
Default is false */
immutable?: boolean
}) {
return new Value<AsRequired<keyof B, Required>, never>(
() => ({
description: null,
warning: null,
type: "select" as const,
disabled: false,
immutable: a.immutable ?? false,
...a,
...requiredLikeToAbove(a.required),
}),
asRequiredParser(
anyOf(
...Object.keys(a.values).map((x: keyof B & string) => literal(x)),
),
a,
) as any,
)
}
static dynamicSelect<Store = never>(
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<string>
values: Record<string, string>
/**
* Disabled: false means that there is nothing disabled, good to modify
* string means that this is the message displayed and the whole thing is disabled
* string[] means that the options are disabled
*/
disabled?: false | string | string[]
}
>,
) {
return new Value<string | null | undefined, Store>(async (options) => {
const a = await getA(options)
return {
description: null,
warning: null,
type: "select" as const,
disabled: false,
immutable: false,
...a,
...requiredLikeToAbove(a.required),
}
}, string.optional())
}
static multiselect<Values extends Record<string, string>>(a: {
name: string
description?: string | null
warning?: string | null
default: string[]
values: Values
minLength?: number | null
maxLength?: number | null
/** Immutable means it can only be configed at the first config then never again
Default is false */
immutable?: boolean
/**
* Disabled: false means that there is nothing disabled, good to modify
* string means that this is the message displayed and the whole thing is disabled
* string[] means that the options are disabled
*/
disabled?: false | string | (string & keyof Values)[]
}) {
return new Value<(keyof Values)[], never>(
() => ({
type: "multiselect" as const,
minLength: null,
maxLength: null,
warning: null,
description: null,
disabled: false,
immutable: a.immutable ?? false,
...a,
}),
arrayOf(
literals(...(Object.keys(a.values) as any as [keyof Values & string])),
),
)
}
static dynamicMultiselect<Store = never>(
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
default: string[]
values: Record<string, string>
minLength?: number | null
maxLength?: number | null
/**
* Disabled: false means that there is nothing disabled, good to modify
* string means that this is the message displayed and the whole thing is disabled
* string[] means that the options are disabled
*/
disabled?: false | string | string[]
}
>,
) {
return new Value<string[], Store>(async (options) => {
const a = await getA(options)
return {
type: "multiselect" as const,
minLength: null,
maxLength: null,
warning: null,
description: null,
disabled: false,
immutable: false,
...a,
}
}, arrayOf(string))
}
static object<Type extends Record<string, any>, Store>(
a: {
name: string
description?: string | null
warning?: string | null
},
spec: Config<Type, Store>,
) {
return new Value<Type, Store>(async (options) => {
const built = await spec.build(options as any)
return {
type: "object" as const,
description: null,
warning: null,
...a,
spec: built,
}
}, spec.validator)
}
static file<Required extends RequiredDefault<string>, Store>(a: {
name: string
description?: string | null
warning?: string | null
extensions: string[]
required: Required
}) {
const buildValue = {
type: "file" as const,
description: null,
warning: null,
...a,
}
return new Value<AsRequired<FilePath, Required>, Store>(
() => ({
...buildValue,
...requiredLikeToAbove(a.required),
}),
asRequiredParser(object({ filePath: string }), a),
)
}
static dynamicFile<Required extends boolean, Store>(
a: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
extensions: string[]
required: Required
}
>,
) {
return new Value<string | null | undefined, Store>(
async (options) => ({
type: "file" as const,
description: null,
warning: null,
...(await a(options)),
}),
string.optional(),
)
}
static union<Required extends RequiredDefault<string>, Type, Store>(
a: {
name: string
description?: string | null
warning?: string | null
required: Required
/** Immutable means it can only be configed at the first config then never again
Default is false */
immutable?: boolean
/**
* Disabled: false means that there is nothing disabled, good to modify
* string means that this is the message displayed and the whole thing is disabled
* string[] means that the options are disabled
*/
disabled?: false | string | string[]
},
aVariants: Variants<Type, Store>,
) {
return new Value<AsRequired<Type, Required>, Store>(
async (options) => ({
type: "union" as const,
description: null,
warning: null,
disabled: false,
...a,
variants: await aVariants.build(options as any),
...requiredLikeToAbove(a.required),
immutable: a.immutable ?? false,
}),
asRequiredParser(aVariants.validator, a),
)
}
static filteredUnion<
Required extends RequiredDefault<string>,
Type extends Record<string, any>,
Store = never,
>(
getDisabledFn: LazyBuild<Store, string[] | false | string>,
a: {
name: string
description?: string | null
warning?: string | null
required: Required
},
aVariants: Variants<Type, Store> | Variants<Type, never>,
) {
return new Value<AsRequired<Type, Required>, Store>(
async (options) => ({
type: "union" as const,
description: null,
warning: null,
...a,
variants: await aVariants.build(options as any),
...requiredLikeToAbove(a.required),
disabled: (await getDisabledFn(options)) || false,
immutable: false,
}),
asRequiredParser(aVariants.validator, a),
)
}
static dynamicUnion<
Required extends RequiredDefault<string>,
Type extends Record<string, any>,
Store = never,
>(
getA: LazyBuild<
Store,
{
disabled: string[] | false | string
name: string
description?: string | null
warning?: string | null
required: Required
}
>,
aVariants: Variants<Type, Store> | Variants<Type, never>,
) {
return new Value<Type | null | undefined, Store>(async (options) => {
const newValues = await getA(options)
return {
type: "union" as const,
description: null,
warning: null,
...newValues,
variants: await aVariants.build(options as any),
...requiredLikeToAbove(newValues.required),
immutable: false,
}
}, aVariants.validator.optional())
}
static list<Type, Store>(a: List<Type, Store>) {
return new Value<Type, Store>((options) => a.build(options), a.validator)
}
/**
* Use this during the times that the input needs a more specific type.
* Used in types that the value/ variant/ list/ config is constructed somewhere else.
```ts
const a = Config.text({
name: "a",
required: false,
})
return Config.of<Store>()({
myValue: a.withStore(),
})
```
*/
withStore<NewStore extends Store extends never ? any : Store>() {
return this as any as Value<Type, NewStore>
}
}

View File

@@ -1,120 +0,0 @@
import { InputSpec, ValueSpecUnion } from "../configTypes"
import { LazyBuild, Config } from "./config"
import { Parser, anyOf, literals, object } from "ts-matches"
/**
* Used in the the Value.select { @link './value.ts' }
* to indicate the type of select variants that are available. The key for the record passed in will be the
* key to the tag.id in the Value.select
```ts
export const disabled = Config.of({});
export const size = Value.number({
name: "Max Chain Size",
default: 550,
description: "Limit of blockchain size on disk.",
warning: "Increasing this value will require re-syncing your node.",
required: true,
range: "[550,1000000)",
integral: true,
units: "MiB",
placeholder: null,
});
export const automatic = Config.of({ size: size });
export const size1 = Value.number({
name: "Failsafe Chain Size",
default: 65536,
description: "Prune blockchain if size expands beyond this.",
warning: null,
required: true,
range: "[550,1000000)",
integral: true,
units: "MiB",
placeholder: null,
});
export const manual = Config.of({ size: size1 });
export const pruningSettingsVariants = Variants.of({
disabled: { name: "Disabled", spec: disabled },
automatic: { name: "Automatic", spec: automatic },
manual: { name: "Manual", spec: manual },
});
export const pruning = Value.union(
{
name: "Pruning Settings",
description:
'- Disabled: Disable pruning\n- Automatic: Limit blockchain size on disk to a certain number of megabytes\n- Manual: Prune blockchain with the "pruneblockchain" RPC\n',
warning: null,
required: true,
default: "disabled",
},
pruningSettingsVariants
);
```
*/
export class Variants<Type, Store> {
static text: any
private constructor(
public build: LazyBuild<Store, ValueSpecUnion["variants"]>,
public validator: Parser<unknown, Type>,
) {}
static of<
VariantValues extends {
[K in string]: {
name: string
spec: Config<any, Store> | Config<any, never>
}
},
Store = never,
>(a: VariantValues) {
const validator = anyOf(
...Object.entries(a).map(([name, { spec }]) =>
object({
selection: literals(name),
value: spec.validator,
}),
),
) as Parser<unknown, any>
return new Variants<
{
[K in keyof VariantValues]: {
selection: K
// prettier-ignore
value:
VariantValues[K]["spec"] extends (Config<infer B, Store> | Config<infer B, never>) ? B :
never
}
}[keyof VariantValues],
Store
>(async (options) => {
const variants = {} as {
[K in keyof VariantValues]: { name: string; spec: InputSpec }
}
for (const key in a) {
const value = a[key]
variants[key] = {
name: value.name,
spec: await value.spec.build(options as any),
}
}
return variants
}, validator)
}
/**
* Use this during the times that the input needs a more specific type.
* Used in types that the value/ variant/ list/ config is constructed somewhere else.
```ts
const a = Config.text({
name: "a",
required: false,
})
return Config.of<Store>()({
myValue: a.withStore(),
})
```
*/
withStore<NewStore extends Store extends never ? any : Store>() {
return this as any as Variants<Type, NewStore>
}
}

View File

@@ -1,81 +0,0 @@
import { SmtpValue } from "../types"
import { GetSystemSmtp } from "../util/GetSystemSmtp"
import { email } from "../util/patterns"
import { Config, ConfigSpecOf } from "./builder/config"
import { Value } from "./builder/value"
import { Variants } from "./builder/variants"
/**
* Base SMTP settings, to be used by StartOS for system wide SMTP
*/
export const customSmtp = Config.of<ConfigSpecOf<SmtpValue>, never>({
server: Value.text({
name: "SMTP Server",
required: {
default: null,
},
}),
port: Value.number({
name: "Port",
required: { default: 587 },
min: 1,
max: 65535,
integer: true,
}),
from: Value.text({
name: "From Address",
required: {
default: null,
},
placeholder: "<name>test@example.com",
inputmode: "email",
patterns: [email],
}),
login: Value.text({
name: "Login",
required: {
default: null,
},
}),
password: Value.text({
name: "Password",
required: false,
masked: true,
}),
})
/**
* For service config. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings
*/
export const smtpConfig = Value.filteredUnion(
async ({ effects }) => {
const smtp = await new GetSystemSmtp(effects).once()
return smtp ? [] : ["system"]
},
{
name: "SMTP",
description: "Optionally provide an SMTP server for sending emails",
required: { default: "disabled" },
},
Variants.of({
disabled: { name: "Disabled", spec: Config.of({}) },
system: {
name: "System Credentials",
spec: Config.of({
customFrom: Value.text({
name: "Custom From Address",
description:
"A custom from address for this service. If not provided, the system from address will be used.",
required: false,
placeholder: "<name>test@example.com",
inputmode: "email",
patterns: [email],
}),
}),
},
custom: {
name: "Custom Credentials",
spec: customSmtp,
},
}),
)

View File

@@ -1,28 +0,0 @@
import * as T from "../types"
export type ConfigDependencies<T extends T.Manifest> = {
exists(id: keyof T["dependencies"]): T.Dependencies[number]
running(
id: keyof T["dependencies"],
healthChecks: string[],
): T.Dependencies[number]
}
export const configDependenciesSet = <
T extends T.Manifest,
>(): ConfigDependencies<T> => ({
exists(id: keyof T["dependencies"]) {
return {
id,
kind: "exists",
} as T.Dependencies[number]
},
running(id: keyof T["dependencies"], healthChecks: string[]) {
return {
id,
kind: "running",
healthChecks,
} as T.Dependencies[number]
},
})

View File

@@ -1,272 +0,0 @@
export type InputSpec = Record<string, ValueSpec>
export type ValueType =
| "text"
| "textarea"
| "number"
| "color"
| "datetime"
| "toggle"
| "select"
| "multiselect"
| "list"
| "object"
| "file"
| "union"
export type ValueSpec = ValueSpecOf<ValueType>
/** core spec types. These types provide the metadata for performing validations */
// prettier-ignore
export type ValueSpecOf<T extends ValueType> =
T extends "text" ? ValueSpecText :
T extends "textarea" ? ValueSpecTextarea :
T extends "number" ? ValueSpecNumber :
T extends "color" ? ValueSpecColor :
T extends "datetime" ? ValueSpecDatetime :
T extends "toggle" ? ValueSpecToggle :
T extends "select" ? ValueSpecSelect :
T extends "multiselect" ? ValueSpecMultiselect :
T extends "list" ? ValueSpecList :
T extends "object" ? ValueSpecObject :
T extends "file" ? ValueSpecFile :
T extends "union" ? ValueSpecUnion :
never
export type ValueSpecText = {
name: string
description: string | null
warning: string | null
type: "text"
patterns: Pattern[]
minLength: number | null
maxLength: number | null
masked: boolean
inputmode: "text" | "email" | "tel" | "url"
placeholder: string | null
required: boolean
default: DefaultString | null
disabled: false | string
generate: null | RandomString
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type ValueSpecTextarea = {
name: string
description: string | null
warning: string | null
type: "textarea"
placeholder: string | null
minLength: number | null
maxLength: number | null
required: boolean
disabled: false | string
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type FilePath = {
filePath: string
}
export type ValueSpecNumber = {
type: "number"
min: number | null
max: number | null
integer: boolean
step: number | null
units: string | null
placeholder: string | null
name: string
description: string | null
warning: string | null
required: boolean
default: number | null
disabled: false | string
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type ValueSpecColor = {
name: string
description: string | null
warning: string | null
type: "color"
required: boolean
default: string | null
disabled: false | string
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type ValueSpecDatetime = {
name: string
description: string | null
warning: string | null
type: "datetime"
required: boolean
inputmode: "date" | "time" | "datetime-local"
min: string | null
max: string | null
default: string | null
disabled: false | string
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type ValueSpecSelect = {
values: Record<string, string>
name: string
description: string | null
warning: string | null
type: "select"
required: boolean
default: string | null
/**
* Disabled: false means that there is nothing disabled, good to modify
* string means that this is the message displayed and the whole thing is disabled
* string[] means that the options are disabled
*/
disabled: false | string | string[]
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type ValueSpecMultiselect = {
values: Record<string, string>
name: string
description: string | null
warning: string | null
type: "multiselect"
minLength: number | null
maxLength: number | null
/**
* Disabled: false means that there is nothing disabled, good to modify
* string means that this is the message displayed and the whole thing is disabled
* string[] means that the options are disabled
*/
disabled: false | string | string[]
default: string[]
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type ValueSpecToggle = {
name: string
description: string | null
warning: string | null
type: "toggle"
default: boolean | null
disabled: false | string
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type ValueSpecUnion = {
name: string
description: string | null
warning: string | null
type: "union"
variants: Record<
string,
{
name: string
spec: InputSpec
}
>
/**
* Disabled: false means that there is nothing disabled, good to modify
* string means that this is the message displayed and the whole thing is disabled
* string[] means that the options are disabled
*/
disabled: false | string | string[]
required: boolean
default: string | null
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type ValueSpecFile = {
name: string
description: string | null
warning: string | null
type: "file"
extensions: string[]
required: boolean
}
export type ValueSpecObject = {
name: string
description: string | null
warning: string | null
type: "object"
spec: InputSpec
}
export type ListValueSpecType = "text" | "object"
/** represents a spec for the values of a list */
// prettier-ignore
export type ListValueSpecOf<T extends ListValueSpecType> =
T extends "text" ? ListValueSpecText :
T extends "object" ? ListValueSpecObject :
never
/** represents a spec for a list */
export type ValueSpecList = ValueSpecListOf<ListValueSpecType>
export type ValueSpecListOf<T extends ListValueSpecType> = {
name: string
description: string | null
warning: string | null
type: "list"
spec: ListValueSpecOf<T>
minLength: number | null
maxLength: number | null
disabled: false | string
default:
| string[]
| DefaultString[]
| Record<string, unknown>[]
| readonly string[]
| readonly DefaultString[]
| readonly Record<string, unknown>[]
}
export type Pattern = {
regex: string
description: string
}
export type ListValueSpecText = {
type: "text"
patterns: Pattern[]
minLength: number | null
maxLength: number | null
masked: boolean
generate: null | RandomString
inputmode: "text" | "email" | "tel" | "url"
placeholder: string | null
}
export type ListValueSpecObject = {
type: "object"
/** this is a mapped type of the config object at this level, replacing the object's values with specs on those values */
spec: InputSpec
/** indicates whether duplicates can be permitted in the list */
uniqueBy: UniqueBy
/** this should be a handlebars template which can make use of the entire config which corresponds to 'spec' */
displayAs: string | null
}
export type UniqueBy =
| null
| string
| {
any: readonly UniqueBy[] | UniqueBy[]
}
| {
all: readonly UniqueBy[] | UniqueBy[]
}
export type DefaultString = string | RandomString
export type RandomString = {
charset: string
len: number
}
// sometimes the type checker needs just a little bit of help
export function isValueSpecListOf<S extends ListValueSpecType>(
t: ValueSpec,
s: S,
): t is ValueSpecListOf<S> & { spec: ListValueSpecOf<S> } {
return "spec" in t && t.spec.type === s
}

View File

@@ -1,3 +0,0 @@
export * as constants from "./configConstants"
export * as types from "./configTypes"
export * as builder from "./builder"

View File

@@ -1,87 +0,0 @@
import * as T from "../types"
import * as D from "./configDependencies"
import { Config, ExtractConfigType } from "./builder/config"
import nullIfEmpty from "../util/nullIfEmpty"
import { InterfacesReceipt as InterfacesReceipt } from "../interfaces/setupInterfaces"
declare const dependencyProof: unique symbol
export type DependenciesReceipt = void & {
[dependencyProof]: never
}
export type Save<
A extends
| Record<string, any>
| Config<Record<string, any>, any>
| Config<Record<string, never>, never>,
> = (options: {
effects: T.Effects
input: ExtractConfigType<A> & Record<string, any>
}) => Promise<{
dependenciesReceipt: DependenciesReceipt
interfacesReceipt: InterfacesReceipt
restart: boolean
}>
export type Read<
Manifest extends T.Manifest,
Store,
A extends
| Record<string, any>
| Config<Record<string, any>, any>
| Config<Record<string, any>, never>,
> = (options: {
effects: T.Effects
}) => Promise<void | (ExtractConfigType<A> & Record<string, any>)>
/**
* We want to setup a config export with a get and set, this
* is going to be the default helper to setup config, because it will help
* enforce that we have a spec, write, and reading.
* @param options
* @returns
*/
export function setupConfig<
Store,
ConfigType extends
| Record<string, any>
| Config<any, any>
| Config<any, never>,
Manifest extends T.Manifest,
Type extends Record<string, any> = ExtractConfigType<ConfigType>,
>(
spec: Config<Type, Store> | Config<Type, never>,
write: Save<Type>,
read: Read<Manifest, Store, Type>,
) {
const validator = spec.validator
return {
setConfig: (async ({ effects, input }) => {
if (!validator.test(input)) {
await console.error(
new Error(validator.errorMessage(input)?.toString()),
)
return { error: "Set config type error for config" }
}
await effects.clearBindings()
await effects.clearServiceInterfaces()
const { restart } = await write({
input: JSON.parse(JSON.stringify(input)) as any,
effects,
})
if (restart) {
await effects.restart()
}
}) as T.ExpectedExports.setConfig,
getConfig: (async ({ effects }) => {
const configValue = nullIfEmpty((await read({ effects })) || null)
return {
spec: await spec.build({
effects,
}),
config: configValue,
}
}) as T.ExpectedExports.getConfig,
}
}
export default setupConfig

View File

@@ -1,39 +0,0 @@
import * as T from "../types"
import { deepEqual } from "../util/deepEqual"
import { deepMerge } from "../util/deepMerge"
export type Update<QueryResults, RemoteConfig> = (options: {
remoteConfig: RemoteConfig
queryResults: QueryResults
}) => Promise<RemoteConfig>
export class DependencyConfig<
Manifest extends T.Manifest,
Store,
Input extends Record<string, any>,
RemoteConfig extends Record<string, any>,
> {
static defaultUpdate = async (options: {
queryResults: unknown
remoteConfig: unknown
}): Promise<unknown> => {
return deepMerge({}, options.remoteConfig, options.queryResults || {})
}
constructor(
readonly dependencyConfig: (options: {
effects: T.Effects
localConfig: Input
}) => Promise<void | T.DeepPartial<RemoteConfig>>,
readonly update: Update<
void | T.DeepPartial<RemoteConfig>,
RemoteConfig
> = DependencyConfig.defaultUpdate as any,
) {}
async query(options: { effects: T.Effects; localConfig: unknown }) {
return this.dependencyConfig({
localConfig: options.localConfig as Input,
effects: options.effects,
})
}
}

View File

@@ -1,206 +0,0 @@
import { ExtendedVersion, VersionRange } from "../exver"
import {
Effects,
PackageId,
DependencyRequirement,
SetHealth,
CheckDependenciesResult,
HealthCheckId,
} from "../types"
export type CheckDependencies<DependencyId extends PackageId = PackageId> = {
installedSatisfied: (packageId: DependencyId) => boolean
installedVersionSatisfied: (packageId: DependencyId) => boolean
runningSatisfied: (packageId: DependencyId) => boolean
configSatisfied: (packageId: DependencyId) => boolean
healthCheckSatisfied: (
packageId: DependencyId,
healthCheckId: HealthCheckId,
) => boolean
satisfied: () => boolean
throwIfInstalledNotSatisfied: (packageId: DependencyId) => void
throwIfInstalledVersionNotSatisfied: (packageId: DependencyId) => void
throwIfRunningNotSatisfied: (packageId: DependencyId) => void
throwIfConfigNotSatisfied: (packageId: DependencyId) => void
throwIfHealthNotSatisfied: (
packageId: DependencyId,
healthCheckId?: HealthCheckId,
) => void
throwIfNotSatisfied: (packageId?: DependencyId) => void
}
export async function checkDependencies<
DependencyId extends PackageId = PackageId,
>(
effects: Effects,
packageIds?: DependencyId[],
): Promise<CheckDependencies<DependencyId>> {
let [dependencies, results] = await Promise.all([
effects.getDependencies(),
effects.checkDependencies({
packageIds,
}),
])
if (packageIds) {
dependencies = dependencies.filter((d) =>
(packageIds as PackageId[]).includes(d.id),
)
}
const find = (packageId: DependencyId) => {
const dependencyRequirement = dependencies.find((d) => d.id === packageId)
const dependencyResult = results.find((d) => d.packageId === packageId)
if (!dependencyRequirement || !dependencyResult) {
throw new Error(`Unknown DependencyId ${packageId}`)
}
return { requirement: dependencyRequirement, result: dependencyResult }
}
const installedSatisfied = (packageId: DependencyId) =>
!!find(packageId).result.installedVersion
const installedVersionSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
return (
!!dep.result.installedVersion &&
ExtendedVersion.parse(dep.result.installedVersion).satisfies(
VersionRange.parse(dep.requirement.versionRange),
)
)
}
const runningSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
return dep.requirement.kind !== "running" || dep.result.isRunning
}
const configSatisfied = (packageId: DependencyId) =>
find(packageId).result.configSatisfied
const healthCheckSatisfied = (
packageId: DependencyId,
healthCheckId?: HealthCheckId,
) => {
const dep = find(packageId)
if (
healthCheckId &&
(dep.requirement.kind !== "running" ||
!dep.requirement.healthChecks.includes(healthCheckId))
) {
throw new Error(`Unknown HealthCheckId ${healthCheckId}`)
}
const errors = Object.entries(dep.result.healthChecks)
.filter(([id, _]) => (healthCheckId ? id === healthCheckId : true))
.filter(([_, res]) => res.result !== "success")
return errors.length === 0
}
const pkgSatisfied = (packageId: DependencyId) =>
installedSatisfied(packageId) &&
installedVersionSatisfied(packageId) &&
runningSatisfied(packageId) &&
configSatisfied(packageId) &&
healthCheckSatisfied(packageId)
const satisfied = (packageId?: DependencyId) =>
packageId
? pkgSatisfied(packageId)
: dependencies.every((d) => pkgSatisfied(d.id as DependencyId))
const throwIfInstalledNotSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
if (!dep.result.installedVersion) {
throw new Error(`${dep.result.title || packageId} is not installed`)
}
}
const throwIfInstalledVersionNotSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
if (!dep.result.installedVersion) {
throw new Error(`${dep.result.title || packageId} is not installed`)
}
if (
![dep.result.installedVersion, ...dep.result.satisfies].find((v) =>
ExtendedVersion.parse(v).satisfies(
VersionRange.parse(dep.requirement.versionRange),
),
)
) {
throw new Error(
`Installed version ${dep.result.installedVersion} of ${dep.result.title || packageId} does not match expected version range ${dep.requirement.versionRange}`,
)
}
}
const throwIfRunningNotSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
if (dep.requirement.kind === "running" && !dep.result.isRunning) {
throw new Error(`${dep.result.title || packageId} is not running`)
}
}
const throwIfConfigNotSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
if (!dep.result.configSatisfied) {
throw new Error(
`${dep.result.title || packageId}'s configuration does not satisfy requirements`,
)
}
}
const throwIfHealthNotSatisfied = (
packageId: DependencyId,
healthCheckId?: HealthCheckId,
) => {
const dep = find(packageId)
if (
healthCheckId &&
(dep.requirement.kind !== "running" ||
!dep.requirement.healthChecks.includes(healthCheckId))
) {
throw new Error(`Unknown HealthCheckId ${healthCheckId}`)
}
const errors = Object.entries(dep.result.healthChecks)
.filter(([id, _]) => (healthCheckId ? id === healthCheckId : true))
.filter(([_, res]) => res.result !== "success")
if (errors.length) {
throw new Error(
errors
.map(
([_, e]) =>
`Health Check ${e.name} of ${dep.result.title || packageId} failed with status ${e.result}${e.message ? `: ${e.message}` : ""}`,
)
.join("; "),
)
}
}
const throwIfPkgNotSatisfied = (packageId: DependencyId) => {
throwIfInstalledNotSatisfied(packageId)
throwIfInstalledVersionNotSatisfied(packageId)
throwIfRunningNotSatisfied(packageId)
throwIfConfigNotSatisfied(packageId)
throwIfHealthNotSatisfied(packageId)
}
const throwIfNotSatisfied = (packageId?: DependencyId) =>
packageId
? throwIfPkgNotSatisfied(packageId)
: (() => {
const err = dependencies.flatMap((d) => {
try {
throwIfPkgNotSatisfied(d.id as DependencyId)
} catch (e) {
if (e instanceof Error) return [e.message]
throw e
}
return []
})
if (err.length) {
throw new Error(err.join("; "))
}
})()
return {
installedSatisfied,
installedVersionSatisfied,
runningSatisfied,
configSatisfied,
healthCheckSatisfied,
satisfied,
throwIfInstalledNotSatisfied,
throwIfInstalledVersionNotSatisfied,
throwIfRunningNotSatisfied,
throwIfConfigNotSatisfied,
throwIfHealthNotSatisfied,
throwIfNotSatisfied,
}
}

View File

@@ -1,9 +0,0 @@
// prettier-ignore
export type ReadonlyDeep<A> =
A extends Function ? A :
A extends {} ? { readonly [K in keyof A]: ReadonlyDeep<A[K]> } : A;
export type MaybePromise<A> = Promise<A> | A
export type Message = string
import "./DependencyConfig"
import "./setupDependencyConfig"

View File

@@ -1,22 +0,0 @@
import { Config } from "../config/builder/config"
import * as T from "../types"
import { DependencyConfig } from "./DependencyConfig"
export function setupDependencyConfig<
Store,
Input extends Record<string, any>,
Manifest extends T.Manifest,
>(
_config: Config<Input, Store> | Config<Input, never>,
autoConfigs: {
[key in keyof Manifest["dependencies"] & string]: DependencyConfig<
Manifest,
Store,
Input,
any
> | null
},
): T.ExpectedExports.dependencyConfig {
return autoConfigs
}

View File

@@ -1,99 +0,0 @@
// #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]*

File diff suppressed because it is too large Load Diff

View File

@@ -1,454 +0,0 @@
import * as P from "./exver"
// prettier-ignore
export type ValidateVersion<T extends String> =
T extends `-${infer A}` ? never :
T extends `${infer A}-${string}` ? ValidateVersion<A> :
T extends `${bigint}` ? unknown :
T extends `${bigint}.${infer A}` ? ValidateVersion<A> :
never
// prettier-ignore
export type ValidateExVer<T extends string> =
T extends `#${string}:${infer A}:${infer B}` ? ValidateVersion<A> & ValidateVersion<B> :
T extends `${infer A}:${infer B}` ? ValidateVersion<A> & ValidateVersion<B> :
never
// prettier-ignore
export type ValidateExVers<T> =
T extends [] ? unknown[] :
T extends [infer A, ...infer B] ? ValidateExVer<A & string> & ValidateExVers<B> :
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(public 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 "!"
}
}
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" })
}
satisfiedBy(version: Version | ExtendedVersion) {
return version.satisfies(this)
}
}
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)
}
satisfies(versionRange: VersionRange): boolean {
return new ExtendedVersion(null, this, new Version([0], [])).satisfies(
versionRange,
)
}
}
// #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,
)
}
/**
* Returns a boolean indicating whether a given version satisfies the VersionRange
* !( >= 1:1 <= 2:2) || <=#bitcoin:1.2.0-alpha:0
*/
satisfies(versionRange: VersionRange): boolean {
switch (versionRange.atom.type) {
case "Anchor":
const otherVersion = versionRange.atom.version
switch (versionRange.atom.operator) {
case "=":
return this.equals(otherVersion)
case ">":
return this.greaterThan(otherVersion)
case "<":
return this.lessThan(otherVersion)
case ">=":
return this.greaterThanOrEqual(otherVersion)
case "<=":
return this.lessThanOrEqual(otherVersion)
case "!=":
return !this.equals(otherVersion)
case "^":
const nextMajor = versionRange.atom.version.incrementMajor()
if (
this.greaterThanOrEqual(otherVersion) &&
this.lessThan(nextMajor)
) {
return true
} else {
return false
}
case "~":
const nextMinor = versionRange.atom.version.incrementMinor()
if (
this.greaterThanOrEqual(otherVersion) &&
this.lessThan(nextMinor)
) {
return true
} else {
return false
}
}
case "And":
return (
this.satisfies(versionRange.atom.left) &&
this.satisfies(versionRange.atom.right)
)
case "Or":
return (
this.satisfies(versionRange.atom.left) ||
this.satisfies(versionRange.atom.right)
)
case "Not":
return !this.satisfies(versionRange.atom.value)
case "Any":
return true
case "None":
return false
}
}
}
export const testTypeExVer = <T extends string>(t: T & ValidateExVer<T>) => t
export const testTypeVersion = <T extends string>(t: T & ValidateVersion<T>) =>
t
function tests() {
testTypeVersion("1.2.3")
testTypeVersion("1")
testTypeVersion("12.34.56")
testTypeVersion("1.2-3")
testTypeVersion("1-3")
testTypeVersion("1-alpha")
// @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")
}

View File

@@ -1,68 +0,0 @@
import { Effects } from "../types"
import { HealthCheckResult } from "./checkFns/HealthCheckResult"
import { HealthReceipt } from "./HealthReceipt"
import { Trigger } from "../trigger"
import { TriggerInput } from "../trigger/TriggerInput"
import { defaultTrigger } from "../trigger/defaultTrigger"
import { once } from "../util/once"
import { SubContainer } from "../util/SubContainer"
import { object, unknown } from "ts-matches"
import * as T from "../types"
import { asError } from "../util/asError"
export type HealthCheckParams = {
effects: Effects
name: string
trigger?: Trigger
fn(): Promise<HealthCheckResult> | HealthCheckResult
onFirstSuccess?: () => unknown | Promise<unknown>
}
export function healthCheck(o: HealthCheckParams) {
new Promise(async () => {
let currentValue: TriggerInput = {}
const getCurrentValue = () => currentValue
const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue)
const triggerFirstSuccess = once(() =>
Promise.resolve(
"onFirstSuccess" in o && o.onFirstSuccess
? o.onFirstSuccess()
: undefined,
),
)
for (
let res = await trigger.next();
!res.done;
res = await trigger.next()
) {
try {
const { result, message } = await o.fn()
await o.effects.setHealth({
name: o.name,
id: o.name,
result,
message: message || "",
})
currentValue.lastResult = result
await triggerFirstSuccess().catch((err) => {
console.error(asError(err))
})
} catch (e) {
await o.effects.setHealth({
name: o.name,
id: o.name,
result: "failure",
message: asMessage(e) || "",
})
currentValue.lastResult = "failure"
}
}
})
return {} as HealthReceipt
}
function asMessage(e: unknown) {
if (object({ message: unknown }).test(e)) return String(e.message)
const value = String(e)
if (value.length == null) return null
return value
}

View File

@@ -1,4 +0,0 @@
declare const HealthProof: unique symbol
export type HealthReceipt = {
[HealthProof]: never
}

View File

@@ -1,3 +0,0 @@
import { T } from "../.."
export type HealthCheckResult = Omit<T.NamedHealthCheckResult, "name">

View File

@@ -1,67 +0,0 @@
import { Effects } from "../../types"
import { stringFromStdErrOut } from "../../util/stringFromStdErrOut"
import { HealthCheckResult } from "./HealthCheckResult"
import { promisify } from "node:util"
import * as CP from "node:child_process"
const cpExec = promisify(CP.exec)
const cpExecFile = promisify(CP.execFile)
export function containsAddress(x: string, port: number) {
const readPorts = x
.split("\n")
.filter(Boolean)
.splice(1)
.map((x) => x.split(" ").filter(Boolean)[1]?.split(":")?.[1])
.filter(Boolean)
.map((x) => Number.parseInt(x, 16))
.filter(Number.isFinite)
return readPorts.indexOf(port) >= 0
}
/**
* This is used to check if a port is listening on the system.
* Used during the health check fn or the check main fn.
*/
export async function checkPortListening(
effects: Effects,
port: number,
options: {
errorMessage: string
successMessage: string
timeoutMessage?: string
timeout?: number
},
): Promise<HealthCheckResult> {
return Promise.race<HealthCheckResult>([
Promise.resolve().then(async () => {
const hasAddress =
containsAddress(
await cpExec(`cat /proc/net/tcp`, {}).then(stringFromStdErrOut),
port,
) ||
containsAddress(
await cpExec("cat /proc/net/udp", {}).then(stringFromStdErrOut),
port,
)
if (hasAddress) {
return { result: "success", message: options.successMessage }
}
return {
result: "failure",
message: options.errorMessage,
}
}),
new Promise((resolve) => {
setTimeout(
() =>
resolve({
result: "failure",
message:
options.timeoutMessage || `Timeout trying to check port ${port}`,
}),
options.timeout ?? 1_000,
)
}),
])
}

View File

@@ -1,36 +0,0 @@
import { Effects } from "../../types"
import { asError } from "../../util/asError"
import { HealthCheckResult } from "./HealthCheckResult"
import { timeoutPromise } from "./index"
import "isomorphic-fetch"
/**
* This is a helper function to check if a web url is reachable.
* @param url
* @param createSuccess
* @returns
*/
export const checkWebUrl = async (
effects: Effects,
url: string,
{
timeout = 1000,
successMessage = `Reached ${url}`,
errorMessage = `Error while fetching URL: ${url}`,
} = {},
): Promise<HealthCheckResult> => {
return Promise.race([fetch(url), timeoutPromise(timeout)])
.then(
(x) =>
({
result: "success",
message: successMessage,
}) as const,
)
.catch((e) => {
console.warn(`Error while fetching URL: ${url}`)
console.error(JSON.stringify(e))
console.error(asError(e))
return { result: "failure" as const, message: errorMessage }
})
}

View File

@@ -1,11 +0,0 @@
import { runHealthScript } from "./runHealthScript"
export { checkPortListening } from "./checkPortListening"
export { HealthCheckResult } from "./HealthCheckResult"
export { checkWebUrl } from "./checkWebUrl"
export function timeoutPromise(ms: number, { message = "Timed out" } = {}) {
return new Promise<never>((resolve, reject) =>
setTimeout(() => reject(new Error(message)), ms),
)
}
export { runHealthScript }

View File

@@ -1,37 +0,0 @@
import { Effects } from "../../types"
import { SubContainer } from "../../util/SubContainer"
import { stringFromStdErrOut } from "../../util/stringFromStdErrOut"
import { HealthCheckResult } from "./HealthCheckResult"
import { timeoutPromise } from "./index"
/**
* Running a health script, is used when we want to have a simple
* script in bash or something like that. It should return something that is useful
* in {result: string} else it is considered an error
* @param param0
* @returns
*/
export const runHealthScript = async (
runCommand: string[],
subcontainer: SubContainer,
{
timeout = 30000,
errorMessage = `Error while running command: ${runCommand}`,
message = (res: string) =>
`Have ran script ${runCommand} and the result: ${res}`,
} = {},
): Promise<HealthCheckResult> => {
const res = await Promise.race([
subcontainer.exec(runCommand),
timeoutPromise(timeout),
]).catch((e) => {
console.warn(errorMessage)
console.warn(JSON.stringify(e))
console.warn(e.toString())
throw { result: "failure", message: errorMessage } as HealthCheckResult
})
return {
result: "success",
message: message(res.stdout.toString()),
} as HealthCheckResult
}

View File

@@ -1,3 +0,0 @@
import "./checkFns"
import "./HealthReceipt"

View File

@@ -1,13 +0,0 @@
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 types from "./types"
export * as T from "./types"
export * as yaml from "yaml"
export * as matches from "ts-matches"
export * as utils from "./util/index.browser"

View File

@@ -1,32 +0,0 @@
export { Daemons } from "./mainFn/Daemons"
export { SubContainer } from "./util/SubContainer"
export { StartSdk } from "./StartSdk"
export { setupManifest } from "./manifest/setupManifest"
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"
export * as config from "./config"
export * as CB from "./config/builder"
export * as CT from "./config/configTypes"
export * as dependencyConfig from "./dependencies"
export * as daemons from "./mainFn/Daemons"
export * as health from "./health"
export * as healthFns from "./health/checkFns"
export * as inits from "./inits"
export * as mainFn from "./mainFn"
export * as manifest from "./manifest"
export * as toml from "@iarna/toml"
export * as types from "./types"
export * as T from "./types"
export * as yaml from "yaml"
export * as startSdk from "./StartSdk"
export * as utils from "./util"
export * as matches from "ts-matches"
export * as YAML from "yaml"
export * as TOML from "@iarna/toml"
export * from "./version"

View File

@@ -1,3 +0,0 @@
import "./setupInit"
import "./setupUninstall"
import "./setupInstall"

View File

@@ -1,62 +0,0 @@
import { DependenciesReceipt } from "../config/setupConfig"
import { ExtendedVersion, VersionRange } from "../exver"
import { SetInterfaces } from "../interfaces/setupInterfaces"
import { ExposedStorePaths } from "../store/setupExposeStore"
import * as T from "../types"
import { VersionGraph } from "../version/VersionGraph"
import { Install } from "./setupInstall"
import { Uninstall } from "./setupUninstall"
export function setupInit<Manifest extends T.Manifest, Store>(
versions: VersionGraph<Manifest["version"]>,
install: Install<Manifest, Store>,
uninstall: Uninstall<Manifest, Store>,
setInterfaces: SetInterfaces<Manifest, Store, any, any>,
setDependencies: (options: {
effects: T.Effects
input: any
}) => Promise<DependenciesReceipt>,
exposedStore: ExposedStorePaths,
): {
init: T.ExpectedExports.init
uninit: T.ExpectedExports.uninit
} {
return {
init: async (opts) => {
const prev = await opts.effects.getDataVersion()
if (prev) {
await versions.migrate({
effects: opts.effects,
from: ExtendedVersion.parse(prev),
to: versions.currentVersion(),
})
} else {
await install.install(opts)
await opts.effects.setDataVersion({
version: versions.current.options.version,
})
}
await setInterfaces({
...opts,
input: null,
})
await opts.effects.exposeForDependents({ paths: exposedStore })
await setDependencies({ effects: opts.effects, input: null })
},
uninit: async (opts) => {
if (opts.nextVersion) {
const prev = await opts.effects.getDataVersion()
if (prev) {
await versions.migrate({
effects: opts.effects,
from: ExtendedVersion.parse(prev),
to: ExtendedVersion.parse(opts.nextVersion),
})
}
} else {
await uninstall.uninstall(opts)
}
},
}
}

View File

@@ -1,25 +0,0 @@
import * as T from "../types"
export type InstallFn<Manifest extends T.Manifest, Store> = (opts: {
effects: T.Effects
}) => Promise<void>
export class Install<Manifest extends T.Manifest, Store> {
private constructor(readonly fn: InstallFn<Manifest, Store>) {}
static of<Manifest extends T.Manifest, Store>(
fn: InstallFn<Manifest, Store>,
) {
return new Install(fn)
}
async install({ effects }: Parameters<T.ExpectedExports.init>[0]) {
await this.fn({
effects,
})
}
}
export function setupInstall<Manifest extends T.Manifest, Store>(
fn: InstallFn<Manifest, Store>,
) {
return Install.of(fn)
}

View File

@@ -1,29 +0,0 @@
import * as T from "../types"
export type UninstallFn<Manifest extends T.Manifest, Store> = (opts: {
effects: T.Effects
}) => Promise<void>
export class Uninstall<Manifest extends T.Manifest, Store> {
private constructor(readonly fn: UninstallFn<Manifest, Store>) {}
static of<Manifest extends T.Manifest, Store>(
fn: UninstallFn<Manifest, Store>,
) {
return new Uninstall(fn)
}
async uninstall({
effects,
nextVersion,
}: Parameters<T.ExpectedExports.uninit>[0]) {
if (!nextVersion)
await this.fn({
effects,
})
}
}
export function setupUninstall<Manifest extends T.Manifest, Store>(
fn: UninstallFn<Manifest, Store>,
) {
return Uninstall.of(fn)
}

View File

@@ -1,4 +0,0 @@
declare const AddressProof: unique symbol
export type AddressReceipt = {
[AddressProof]: never
}

View File

@@ -1,194 +0,0 @@
import { number, object, string } from "ts-matches"
import { Effects } from "../types"
import { Origin } from "./Origin"
import { AddSslOptions, BindParams } from ".././osBindings"
import { Security } from ".././osBindings"
import { BindOptions } from ".././osBindings"
import { AlpnInfo } from ".././osBindings"
export { AddSslOptions, Security, BindOptions }
export const knownProtocols = {
http: {
secure: null,
defaultPort: 80,
withSsl: "https",
alpn: { specified: ["http/1.1"] } as AlpnInfo,
},
https: {
secure: { ssl: true },
defaultPort: 443,
},
ws: {
secure: null,
defaultPort: 80,
withSsl: "wss",
alpn: { specified: ["http/1.1"] } as AlpnInfo,
},
wss: {
secure: { ssl: true },
defaultPort: 443,
},
ssh: {
secure: { ssl: false },
defaultPort: 22,
},
bitcoin: {
secure: { ssl: false },
defaultPort: 8333,
},
lightning: {
secure: { ssl: true },
defaultPort: 9735,
},
grpc: {
secure: { ssl: true },
defaultPort: 50051,
},
dns: {
secure: { ssl: false },
defaultPort: 53,
},
} as const
export type Scheme = string | null
type KnownProtocols = typeof knownProtocols
type ProtocolsWithSslVariants = {
[K in keyof KnownProtocols]: KnownProtocols[K] extends {
withSsl: string
}
? K
: never
}[keyof KnownProtocols]
type NotProtocolsWithSslVariants = Exclude<
keyof KnownProtocols,
ProtocolsWithSslVariants
>
type BindOptionsByKnownProtocol =
| {
protocol: ProtocolsWithSslVariants
preferredExternalPort?: number
addSsl?: Partial<AddSslOptions>
}
| {
protocol: NotProtocolsWithSslVariants
preferredExternalPort?: number
addSsl?: AddSslOptions
}
export type BindOptionsByProtocol = BindOptionsByKnownProtocol | BindOptions
export type HostKind = BindParams["kind"]
const hasStringProtocol = object({
protocol: string,
}).test
export class Host {
constructor(
readonly options: {
effects: Effects
kind: HostKind
id: string
},
) {}
async bindPort(
internalPort: number,
options: BindOptionsByProtocol,
): Promise<Origin<this>> {
if (hasStringProtocol(options)) {
return await this.bindPortForKnown(options, internalPort)
} else {
return await this.bindPortForUnknown(internalPort, options)
}
}
private async bindPortForUnknown(
internalPort: number,
options: {
preferredExternalPort: number
addSsl: AddSslOptions | null
secure: { ssl: boolean } | null
},
) {
const binderOptions = {
kind: this.options.kind,
id: this.options.id,
internalPort,
...options,
}
await this.options.effects.bind(binderOptions)
return new Origin(this, internalPort, null, null)
}
private async bindPortForKnown(
options: BindOptionsByKnownProtocol,
internalPort: number,
) {
const protoInfo = knownProtocols[options.protocol]
const preferredExternalPort =
options.preferredExternalPort ||
knownProtocols[options.protocol].defaultPort
const sslProto = this.getSslProto(options, protoInfo)
const addSsl =
sslProto && "alpn" in protoInfo
? {
// addXForwardedHeaders: null,
preferredExternalPort: knownProtocols[sslProto].defaultPort,
scheme: sslProto,
alpn: protoInfo.alpn,
...("addSsl" in options ? options.addSsl : null),
}
: null
const secure: Security | null = !protoInfo.secure ? null : { ssl: false }
await this.options.effects.bind({
kind: this.options.kind,
id: this.options.id,
internalPort,
preferredExternalPort,
addSsl,
secure,
})
return new Origin(this, internalPort, options.protocol, sslProto)
}
private getSslProto(
options: BindOptionsByKnownProtocol,
protoInfo: KnownProtocols[keyof KnownProtocols],
) {
if (inObject("noAddSsl", options) && options.noAddSsl) return null
if ("withSsl" in protoInfo && protoInfo.withSsl) return protoInfo.withSsl
return null
}
}
function inObject<Key extends string>(
key: Key,
obj: any,
): obj is { [K in Key]: unknown } {
return key in obj
}
// export class StaticHost extends Host {
// constructor(options: { effects: Effects; id: string }) {
// super({ ...options, kind: "static" })
// }
// }
// export class SingleHost extends Host {
// constructor(options: { effects: Effects; id: string }) {
// super({ ...options, kind: "single" })
// }
// }
export class MultiHost extends Host {
constructor(options: { effects: Effects; id: string }) {
super({ ...options, kind: "multi" })
}
}

View File

@@ -1,88 +0,0 @@
import { AddressInfo } from "../types"
import { AddressReceipt } from "./AddressReceipt"
import { Host, BindOptions, Scheme } from "./Host"
import { ServiceInterfaceBuilder } from "./ServiceInterfaceBuilder"
export class Origin<T extends Host> {
constructor(
readonly host: T,
readonly internalPort: number,
readonly scheme: string | null,
readonly sslScheme: string | null,
) {}
build({ username, path, search, schemeOverride }: BuildOptions): AddressInfo {
const qpEntries = Object.entries(search)
.map(
([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`,
)
.join("&")
const qp = qpEntries.length ? `?${qpEntries}` : ""
return {
hostId: this.host.options.id,
internalPort: this.internalPort,
scheme: schemeOverride ? schemeOverride.noSsl : this.scheme,
sslScheme: schemeOverride ? schemeOverride.ssl : this.sslScheme,
suffix: `${path}${qp}`,
username,
}
}
/**
* A function to register a group of origins (<PROTOCOL> :// <HOSTNAME> : <PORT>) with StartOS
*
* The returned addressReceipt serves as proof that the addresses were registered
*
* @param addressInfo
* @returns
*/
async export(
serviceInterfaces: ServiceInterfaceBuilder[],
): Promise<AddressInfo[] & AddressReceipt> {
const addressesInfo = []
for (let serviceInterface of serviceInterfaces) {
const {
name,
description,
hasPrimary,
id,
type,
username,
path,
search,
schemeOverride,
masked,
} = serviceInterface.options
const addressInfo = this.build({
username,
path,
search,
schemeOverride,
})
await serviceInterface.options.effects.exportServiceInterface({
id,
name,
description,
hasPrimary,
addressInfo,
type,
masked,
})
addressesInfo.push(addressInfo)
}
return addressesInfo as AddressInfo[] & AddressReceipt
}
}
type BuildOptions = {
schemeOverride: { ssl: Scheme; noSsl: Scheme } | null
username: string | null
path: string
search: Record<string, string>
}

View File

@@ -1,32 +0,0 @@
import { ServiceInterfaceType } from "../StartSdk"
import { Effects } from "../types"
import { Scheme } from "./Host"
/**
* A helper class for creating a Network Interface
*
* Network Interfaces are collections of web addresses that expose the same API or other resource,
* display to the user with under a common name and description.
*
* All URIs on an interface inherit the same ui: bool, basic auth credentials, path, and search (query) params
*
* @param options
* @returns
*/
export class ServiceInterfaceBuilder {
constructor(
readonly options: {
effects: Effects
name: string
id: string
description: string
hasPrimary: boolean
type: ServiceInterfaceType
username: string | null
path: string
search: Record<string, string>
schemeOverride: { ssl: Scheme; noSsl: Scheme } | null
masked: boolean
},
) {}
}

View File

@@ -1,4 +0,0 @@
declare const InterfaceProof: unique symbol
export type InterfaceReceipt = {
[InterfaceProof]: never
}

View File

@@ -1,23 +0,0 @@
import { Config } from "../config/builder/config"
import * as T from "../types"
import { AddressReceipt } from "./AddressReceipt"
export type InterfacesReceipt = Array<T.AddressInfo[] & AddressReceipt>
export type SetInterfaces<
Manifest extends T.Manifest,
Store,
ConfigInput extends Record<string, any>,
Output extends InterfacesReceipt,
> = (opts: { effects: T.Effects; input: null | ConfigInput }) => Promise<Output>
export type SetupInterfaces = <
Manifest extends T.Manifest,
Store,
ConfigInput extends Record<string, any>,
Output extends InterfacesReceipt,
>(
config: Config<ConfigInput, Store>,
fn: SetInterfaces<Manifest, Store, ConfigInput, Output>,
) => SetInterfaces<Manifest, Store, ConfigInput, Output>
export const NO_INTERFACE_CHANGES = [] as InterfacesReceipt
export const setupInterfaces: SetupInterfaces = (_config, fn) => fn

View File

@@ -1,144 +0,0 @@
import { DEFAULT_SIGTERM_TIMEOUT } from "."
import { NO_TIMEOUT, SIGKILL, SIGTERM } from "../StartSdk"
import * as T from "../types"
import { asError } from "../util/asError"
import {
ExecSpawnable,
MountOptions,
SubContainerHandle,
SubContainer,
} from "../util/SubContainer"
import { splitCommand } from "../util/splitCommand"
import * as cp from "child_process"
export class CommandController {
private constructor(
readonly runningAnswer: Promise<unknown>,
private state: { exited: boolean },
private readonly subcontainer: SubContainer,
private process: cp.ChildProcessWithoutNullStreams,
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
) {}
static of<Manifest extends T.Manifest>() {
return async <A extends string>(
effects: T.Effects,
subcontainer:
| {
id: keyof Manifest["images"] & T.ImageId
sharedRun?: boolean
}
| SubContainer,
command: T.CommandType,
options: {
subcontainerName?: string
// Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms
sigtermTimeout?: number
mounts?: { path: string; options: MountOptions }[]
runAsInit?: boolean
env?:
| {
[variable: string]: string
}
| undefined
cwd?: string | undefined
user?: string | undefined
onStdout?: (x: Buffer) => void
onStderr?: (x: Buffer) => void
},
) => {
const commands = splitCommand(command)
const subc =
subcontainer instanceof SubContainer
? subcontainer
: await (async () => {
const subc = await SubContainer.of(
effects,
subcontainer,
options?.subcontainerName || commands.join(" "),
)
for (let mount of options.mounts || []) {
await subc.mount(mount.options, mount.path)
}
return subc
})()
let childProcess: cp.ChildProcessWithoutNullStreams
if (options.runAsInit) {
childProcess = await subc.launch(commands, {
env: options.env,
})
} else {
childProcess = await subc.spawn(commands, {
env: options.env,
})
}
const state = { exited: false }
const answer = new Promise<null>((resolve, reject) => {
childProcess.on("exit", (code) => {
state.exited = true
if (
code === 0 ||
code === 143 ||
(code === null && childProcess.signalCode == "SIGTERM")
) {
return resolve(null)
}
if (code) {
return reject(new Error(`${commands[0]} exited with code ${code}`))
} else {
return reject(
new Error(
`${commands[0]} exited with signal ${childProcess.signalCode}`,
),
)
}
})
})
return new CommandController(
answer,
state,
subc,
childProcess,
options.sigtermTimeout,
)
}
}
get subContainerHandle() {
return new SubContainerHandle(this.subcontainer)
}
async wait({ timeout = NO_TIMEOUT } = {}) {
if (timeout > 0)
setTimeout(() => {
this.term()
}, timeout)
try {
return await this.runningAnswer
} finally {
if (!this.state.exited) {
this.process.kill("SIGKILL")
}
await this.subcontainer.destroy?.().catch((_) => {})
}
}
async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) {
try {
if (!this.state.exited) {
if (signal !== "SIGKILL") {
setTimeout(() => {
if (!this.state.exited) this.process.kill("SIGKILL")
}, timeout)
}
if (!this.process.kill(signal)) {
console.error(
`failed to send signal ${signal} to pid ${this.process.pid}`,
)
}
}
await this.runningAnswer
} finally {
await this.subcontainer.destroy?.()
}
}
}

View File

@@ -1,89 +0,0 @@
import * as T from "../types"
import { asError } from "../util/asError"
import { ExecSpawnable, MountOptions, SubContainer } from "../util/SubContainer"
import { CommandController } from "./CommandController"
const TIMEOUT_INCREMENT_MS = 1000
const MAX_TIMEOUT_MS = 30000
/**
* This is a wrapper around CommandController that has a state of off, where the command shouldn't be running
* and the others state of running, where it will keep a living running command
*/
export class Daemon {
private commandController: CommandController | null = null
private shouldBeRunning = false
constructor(private startCommand: () => Promise<CommandController>) {}
get subContainerHandle(): undefined | ExecSpawnable {
return this.commandController?.subContainerHandle
}
static of<Manifest extends T.Manifest>() {
return async <A extends string>(
effects: T.Effects,
subcontainer:
| {
id: keyof Manifest["images"] & T.ImageId
sharedRun?: boolean
}
| SubContainer,
command: T.CommandType,
options: {
subcontainerName?: string
mounts?: { path: string; options: MountOptions }[]
env?:
| {
[variable: string]: string
}
| undefined
cwd?: string | undefined
user?: string | undefined
onStdout?: (x: Buffer) => void
onStderr?: (x: Buffer) => void
sigtermTimeout?: number
},
) => {
const startCommand = () =>
CommandController.of<Manifest>()(
effects,
subcontainer,
command,
options,
)
return new Daemon(startCommand)
}
}
async start() {
if (this.commandController) {
return
}
this.shouldBeRunning = true
let timeoutCounter = 0
new Promise(async () => {
while (this.shouldBeRunning) {
this.commandController = await this.startCommand()
await this.commandController.wait().catch((err) => console.error(err))
await new Promise((resolve) => setTimeout(resolve, timeoutCounter))
timeoutCounter += TIMEOUT_INCREMENT_MS
timeoutCounter = Math.max(MAX_TIMEOUT_MS, timeoutCounter)
}
}).catch((err) => {
console.error(asError(err))
})
}
async term(termOptions?: {
signal?: NodeJS.Signals | undefined
timeout?: number | undefined
}) {
return this.stop(termOptions)
}
async stop(termOptions?: {
signal?: NodeJS.Signals | undefined
timeout?: number | undefined
}) {
this.shouldBeRunning = false
await this.commandController
?.term({ ...termOptions })
.catch((e) => console.error(asError(e)))
this.commandController = null
}
}

View File

@@ -1,178 +0,0 @@
import { NO_TIMEOUT, SIGKILL, SIGTERM, Signals } from "../StartSdk"
import { HealthReceipt } from "../health/HealthReceipt"
import { HealthCheckResult } from "../health/checkFns"
import { Trigger } from "../trigger"
import { TriggerInput } from "../trigger/TriggerInput"
import { defaultTrigger } from "../trigger/defaultTrigger"
import * as T from "../types"
import { Mounts } from "./Mounts"
import {
CommandOptions,
ExecSpawnable,
MountOptions,
SubContainer,
} from "../util/SubContainer"
import { splitCommand } from "../util/splitCommand"
import { promisify } from "node:util"
import * as CP from "node:child_process"
export { Daemon } from "./Daemon"
export { CommandController } from "./CommandController"
import { HealthDaemon } from "./HealthDaemon"
import { Daemon } from "./Daemon"
import { CommandController } from "./CommandController"
export const cpExec = promisify(CP.exec)
export const cpExecFile = promisify(CP.execFile)
export type Ready = {
display: string | null
fn: (
spawnable: ExecSpawnable,
) => Promise<HealthCheckResult> | HealthCheckResult
trigger?: Trigger
}
type DaemonsParams<
Manifest extends T.Manifest,
Ids extends string,
Command extends string,
Id extends string,
> = {
command: T.CommandType
image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean }
mounts: Mounts<Manifest>
env?: Record<string, string>
ready: Ready
requires: Exclude<Ids, Id>[]
sigtermTimeout?: number
}
type ErrorDuplicateId<Id extends string> = `The id '${Id}' is already used`
export const runCommand = <Manifest extends T.Manifest>() =>
CommandController.of<Manifest>()
/**
* A class for defining and controlling the service daemons
```ts
Daemons.of({
effects,
started,
interfaceReceipt, // Provide the interfaceReceipt to prove it was completed
healthReceipts, // Provide the healthReceipts or [] to prove they were at least considered
}).addDaemon('webui', {
command: 'hello-world', // The command to start the daemon
ready: {
display: 'Web Interface',
// The function to run to determine the health status of the daemon
fn: () =>
checkPortListening(effects, 80, {
successMessage: 'The web interface is ready',
errorMessage: 'The web interface is not ready',
}),
},
requires: [],
})
```
*/
export class Daemons<Manifest extends T.Manifest, Ids extends string> {
private constructor(
readonly effects: T.Effects,
readonly started: (onTerm: () => PromiseLike<void>) => PromiseLike<void>,
readonly daemons: Promise<Daemon>[],
readonly ids: Ids[],
readonly healthDaemons: HealthDaemon[],
) {}
/**
* Returns an empty new Daemons class with the provided config.
*
* Call .addDaemon() on the returned class to add a daemon.
*
* Daemons run in the order they are defined, with latter daemons being capable of
* depending on prior daemons
* @param config
* @returns
*/
static of<Manifest extends T.Manifest>(config: {
effects: T.Effects
started: (onTerm: () => PromiseLike<void>) => PromiseLike<void>
healthReceipts: HealthReceipt[]
}) {
return new Daemons<Manifest, never>(
config.effects,
config.started,
[],
[],
[],
)
}
/**
* Returns the complete list of daemons, including the one defined here
* @param id
* @param newDaemon
* @returns
*/
addDaemon<Id extends string, Command extends string>(
// prettier-ignore
id:
"" extends Id ? never :
ErrorDuplicateId<Id> extends Id ? never :
Id extends Ids ? ErrorDuplicateId<Id> :
Id,
options: DaemonsParams<Manifest, Ids, Command, Id>,
) {
const daemonIndex = this.daemons.length
const daemon = Daemon.of()(this.effects, options.image, options.command, {
...options,
mounts: options.mounts.build(),
subcontainerName: id,
})
const healthDaemon = new HealthDaemon(
daemon,
daemonIndex,
options.requires
.map((x) => this.ids.indexOf(id as any))
.filter((x) => x >= 0)
.map((id) => this.healthDaemons[id]),
id,
this.ids,
options.ready,
this.effects,
options.sigtermTimeout,
)
const daemons = this.daemons.concat(daemon)
const ids = [...this.ids, id] as (Ids | Id)[]
const healthDaemons = [...this.healthDaemons, healthDaemon]
return new Daemons<Manifest, Ids | Id>(
this.effects,
this.started,
daemons,
ids,
healthDaemons,
)
}
async build() {
this.updateMainHealth()
this.healthDaemons.forEach((x) =>
x.addWatcher(() => this.updateMainHealth()),
)
const built = {
term: async (options?: { signal?: Signals; timeout?: number }) => {
try {
await Promise.all(this.healthDaemons.map((x) => x.term(options)))
} finally {
this.effects.setMainStatus({ status: "stopped" })
}
},
}
this.started(() => built.term())
return built
}
private updateMainHealth() {
this.effects.setMainStatus({ status: "running" })
}
}

View File

@@ -1,150 +0,0 @@
import { HealthCheckResult } from "../health/checkFns"
import { defaultTrigger } from "../trigger/defaultTrigger"
import { Ready } from "./Daemons"
import { Daemon } from "./Daemon"
import { Effects, SetHealth } from "../types"
import { DEFAULT_SIGTERM_TIMEOUT } from "."
import { asError } from "../util/asError"
const oncePromise = <T>() => {
let resolve: (value: T) => void
const promise = new Promise<T>((res) => {
resolve = res
})
return { resolve: resolve!, promise }
}
/**
* Wanted a structure that deals with controlling daemons by their health status
* States:
* -- Waiting for dependencies to be success
* -- Running: Daemon is running and the status is in the health
*
*/
export class HealthDaemon {
private _health: HealthCheckResult = { result: "starting", message: null }
private healthWatchers: Array<() => unknown> = []
private running = false
constructor(
private readonly daemon: Promise<Daemon>,
readonly daemonIndex: number,
private readonly dependencies: HealthDaemon[],
readonly id: string,
readonly ids: string[],
readonly ready: Ready,
readonly effects: Effects,
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
) {
this.updateStatus()
this.dependencies.forEach((d) => d.addWatcher(() => this.updateStatus()))
}
/** Run after we want to do cleanup */
async term(termOptions?: {
signal?: NodeJS.Signals | undefined
timeout?: number | undefined
}) {
this.healthWatchers = []
this.running = false
this.healthCheckCleanup?.()
await this.daemon.then((d) =>
d.term({
timeout: this.sigtermTimeout,
...termOptions,
}),
)
}
/** Want to add another notifier that the health might have changed */
addWatcher(watcher: () => unknown) {
this.healthWatchers.push(watcher)
}
get health() {
return Object.freeze(this._health)
}
private async changeRunning(newStatus: boolean) {
if (this.running === newStatus) return
this.running = newStatus
if (newStatus) {
;(await this.daemon).start()
this.setupHealthCheck()
} else {
;(await this.daemon).stop()
this.turnOffHealthCheck()
this.setHealth({ result: "starting", message: null })
}
}
private healthCheckCleanup: (() => void) | null = null
private turnOffHealthCheck() {
this.healthCheckCleanup?.()
}
private async setupHealthCheck() {
if (this.healthCheckCleanup) return
const trigger = (this.ready.trigger ?? defaultTrigger)(() => ({
lastResult: this._health.result,
}))
const { promise: status, resolve: setStatus } = oncePromise<{
done: true
}>()
new Promise(async () => {
for (
let res = await Promise.race([status, trigger.next()]);
!res.done;
res = await Promise.race([status, trigger.next()])
) {
const handle = (await this.daemon).subContainerHandle
if (handle) {
const response: HealthCheckResult = await Promise.resolve(
this.ready.fn(handle),
).catch((err) => {
console.error(asError(err))
return {
result: "failure",
message: "message" in err ? err.message : String(err),
}
})
await this.setHealth(response)
} else {
await this.setHealth({
result: "failure",
message: "Daemon not running",
})
}
}
}).catch((err) => console.error(`Daemon ${this.id} failed: ${err}`))
this.healthCheckCleanup = () => {
setStatus({ done: true })
this.healthCheckCleanup = null
}
}
private async setHealth(health: HealthCheckResult) {
this._health = health
this.healthWatchers.forEach((watcher) => watcher())
const display = this.ready.display
const result = health.result
if (!display) {
return
}
await this.effects.setHealth({
...health,
id: this.id,
name: display,
} as SetHealth)
}
private async updateStatus() {
const healths = this.dependencies.map((d) => d._health)
this.changeRunning(healths.every((x) => x.result === "success"))
}
}

View File

@@ -1,125 +0,0 @@
import * as T from "../types"
import { MountOptions } from "../util/SubContainer"
type MountArray = { path: string; options: MountOptions }[]
export class Mounts<Manifest extends T.Manifest> {
private constructor(
readonly volumes: {
id: Manifest["volumes"][number]
subpath: string | null
mountpoint: string
readonly: boolean
}[],
readonly assets: {
id: Manifest["assets"][number]
subpath: string | null
mountpoint: string
}[],
readonly dependencies: {
dependencyId: string
volumeId: string
subpath: string | null
mountpoint: string
readonly: boolean
}[],
) {}
static of<Manifest extends T.Manifest>() {
return new Mounts<Manifest>([], [], [])
}
addVolume(
id: Manifest["volumes"][number],
subpath: string | null,
mountpoint: string,
readonly: boolean,
) {
this.volumes.push({
id,
subpath,
mountpoint,
readonly,
})
return this
}
addAssets(
id: Manifest["assets"][number],
subpath: string | null,
mountpoint: string,
) {
this.assets.push({
id,
subpath,
mountpoint,
})
return this
}
addDependency<DependencyManifest extends T.Manifest>(
dependencyId: keyof Manifest["dependencies"] & string,
volumeId: DependencyManifest["volumes"][number],
subpath: string | null,
mountpoint: string,
readonly: boolean,
) {
this.dependencies.push({
dependencyId,
volumeId,
subpath,
mountpoint,
readonly,
})
return this
}
build(): MountArray {
const mountpoints = new Set()
for (let mountpoint of this.volumes
.map((v) => v.mountpoint)
.concat(this.assets.map((a) => a.mountpoint))
.concat(this.dependencies.map((d) => d.mountpoint))) {
if (mountpoints.has(mountpoint)) {
throw new Error(
`cannot mount more than once to mountpoint ${mountpoint}`,
)
}
mountpoints.add(mountpoint)
}
return ([] as MountArray)
.concat(
this.volumes.map((v) => ({
path: v.mountpoint,
options: {
type: "volume",
id: v.id,
subpath: v.subpath,
readonly: v.readonly,
},
})),
)
.concat(
this.assets.map((a) => ({
path: a.mountpoint,
options: {
type: "assets",
id: a.id,
subpath: a.subpath,
},
})),
)
.concat(
this.dependencies.map((d) => ({
path: d.mountpoint,
options: {
type: "pointer",
packageId: d.dependencyId,
volumeId: d.volumeId,
subpath: d.subpath,
readonly: d.readonly,
},
})),
)
}
}

View File

@@ -1,31 +0,0 @@
import * as T from "../types"
import { Daemons } from "./Daemons"
import "../interfaces/ServiceInterfaceBuilder"
import "../interfaces/Origin"
import "./Daemons"
import { MainEffects } from "../StartSdk"
export const DEFAULT_SIGTERM_TIMEOUT = 30_000
/**
* Used to ensure that the main function is running with the valid proofs.
* We first do the folowing order of things
* 1. We get the interfaces
* 2. We setup all the commands to setup the system
* 3. We create the health checks
* 4. We setup the daemons init system
* @param fn
* @returns
*/
export const setupMain = <Manifest extends T.Manifest, Store>(
fn: (o: {
effects: MainEffects
started(onTerm: () => PromiseLike<void>): PromiseLike<void>
}) => Promise<Daemons<Manifest, any>>,
): T.ExpectedExports.main => {
return async (options) => {
const result = await fn(options)
return result
}
}

View File

@@ -1,84 +0,0 @@
import { ValidateExVer, ValidateExVers } from "../exver"
import {
ActionMetadata,
HardwareRequirements,
ImageConfig,
ImageId,
ImageSource,
} from "../types"
export type SDKManifest = {
/** The package identifier used by the OS. This must be unique amongst all other known packages */
readonly id: string
/** A human readable service title */
readonly title: 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.*/
readonly license: string // name of license
/** The Start9 wrapper repository URL for the package. This repo contains the manifest file (this),
* any scripts necessary for configuration, backups, actions, or health checks (more below). This key
* must exist. But could be embedded into the source repository
*/
readonly wrapperRepo: string
/** The original project repository URL. There is no upstream repo in this example */
readonly upstreamRepo: string
/** URL to the support site / channel for the project. This key can be omitted if none exists, or it can link to the original project repository issues */
readonly supportSite: string
/** URL to the marketing site for the project. If there is no marketing site, it can link to the original project repository */
readonly marketingSite: string
/** URL where users can donate to the upstream project */
readonly donationUrl: string | null
/**Human readable descriptions for the service. These are used throughout the StartOS user interface, primarily in the marketplace. */
readonly description: {
/**This is the first description visible to the user in the marketplace */
readonly short: string
/** This description will display with additional details in the service's individual marketplace page */
readonly long: string
}
/** Defines the os images needed to run the container processes */
readonly images: Record<ImageId, SDKImageConfig>
/** This denotes readonly asset directories that should be available to mount to the container.
* These directories are expected to be found in `assets/<id>` 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 hasConfig?: boolean
readonly dependencies: Readonly<Record<string, ManifestDependency>>
readonly hardwareRequirements?: {
readonly device?: { display?: RegExp; processor?: RegExp }
readonly ram?: number | null
readonly arch?: string[] | null
}
}
export type SDKImageConfig = {
source: Exclude<ImageSource, "packed">
arch?: string[]
emulateMissingAs?: string | null
}
export type ManifestDependency = {
/**
* A human readable explanation on what the dependency is used for
*/
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.
*/
readonly optional: boolean
/**
* A url or local path for an s9pk that satisfies this dependency
*/
readonly s9pk: string
}

View File

@@ -1,2 +0,0 @@
import "./setupManifest"
import "./ManifestTypes"

View File

@@ -1,84 +0,0 @@
import * as T from "../types"
import { ImageConfig, ImageId, VolumeId } from "../osBindings"
import { SDKManifest, SDKImageConfig } from "./ManifestTypes"
import { SDKVersion } from "../StartSdk"
import { VersionGraph } from "../version/VersionGraph"
/**
* This is an example of a function that takes a manifest and returns a new manifest with additional properties
* @param manifest Manifests are the description of the package
* @returns The manifest with additional properties
*/
export function setupManifest<
Id extends string,
Version extends string,
Dependencies extends Record<string, unknown>,
VolumesTypes extends VolumeId,
AssetTypes extends VolumeId,
ImagesTypes extends ImageId,
Manifest extends {
dependencies: Dependencies
id: Id
assets: AssetTypes[]
images: Record<ImagesTypes, SDKImageConfig>
volumes: VolumesTypes[]
},
Satisfies extends string[] = [],
>(
versions: VersionGraph<Version>,
manifest: SDKManifest & Manifest,
): 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,
version: versions.current.options.version,
releaseNotes: versions.current.options.releaseNotes,
satisfies: versions.current.options.satisfies || [],
canMigrateTo: versions.canMigrateTo().toString(),
canMigrateFrom: versions.canMigrateFrom().toString(),
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,
},
}
}

View File

@@ -1,7 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AnyVerifyingKey } from "./AnyVerifyingKey"
export type AcceptSigners =
| { signer: AnyVerifyingKey }
| { any: Array<AcceptSigners> }
| { all: Array<AcceptSigners> }

View File

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

View File

@@ -1,12 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AllowedStatuses } from "./AllowedStatuses"
export type ActionMetadata = {
name: string
description: string
warning: string | null
input: any
disabled: boolean
allowedStatuses: AllowedStatuses
group: string | null
}

View File

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

View File

@@ -1,11 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AnySignature } from "./AnySignature"
import type { Blake3Commitment } from "./Blake3Commitment"
export type AddAssetParams = {
version: string
platform: string
url: string
signature: AnySignature
commitment: Blake3Commitment
}

View File

@@ -1,9 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AnySignature } from "./AnySignature"
import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment"
export type AddPackageParams = {
url: string
commitment: MerkleArchiveCommitment
signature: AnySignature
}

View File

@@ -1,7 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AlpnInfo } from "./AlpnInfo"
export type AddSslOptions = {
preferredExternalPort: number
alpn: AlpnInfo | null
}

View File

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

View File

@@ -1,11 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { HostId } from "./HostId"
export type AddressInfo = {
username: string | null
hostId: HostId
internalPort: number
scheme: string | null
sslScheme: string | null
suffix: string
}

View File

@@ -1,9 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Alerts = {
install: string | null
uninstall: string | null
restore: string | null
start: string | null
stop: string | null
}

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Algorithm = "ecdsa" | "ed25519"

View File

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

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AllowedStatuses = "onlyRunning" | "onlyStopped" | "any"

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { MaybeUtf8String } from "./MaybeUtf8String"
export type AlpnInfo = "reflect" | { specified: Array<MaybeUtf8String> }

View File

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

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ApiState = "error" | "initializing" | "running"

View File

@@ -1,7 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { EncryptedWire } from "./EncryptedWire"
export type AttachParams = {
startOsPassword: EncryptedWire | null
guid: string
}

View File

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

View File

@@ -1,7 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BlockDev } from "./BlockDev"
import type { Cifs } from "./Cifs"
export type BackupTargetFS =
| ({ type: "disk" } & BlockDev)
| ({ type: "cifs" } & Cifs)

View File

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

View File

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

View File

@@ -1,9 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AddSslOptions } from "./AddSslOptions"
import type { Security } from "./Security"
export type BindOptions = {
preferredExternalPort: number
addSsl: AddSslOptions | null
secure: Security | null
}

View File

@@ -1,14 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AddSslOptions } from "./AddSslOptions"
import type { HostId } from "./HostId"
import type { HostKind } from "./HostKind"
import type { Security } from "./Security"
export type BindParams = {
kind: HostKind
id: HostId
internalPort: number
preferredExternalPort: number
addSsl: AddSslOptions | null
secure: Security | null
}

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CallbackId = number

View File

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

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackageId } from "./PackageId"
export type CheckDependenciesParam = { packageIds?: Array<PackageId> }

View File

@@ -1,14 +0,0 @@
// 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 { NamedHealthCheckResult } from "./NamedHealthCheckResult"
import type { PackageId } from "./PackageId"
export type CheckDependenciesResult = {
packageId: PackageId
title: string | null
installedVersion: string | null
satisfies: string[]
isRunning: boolean
configSatisfied: boolean
healthChecks: { [key: HealthCheckId]: NamedHealthCheckResult }
}

View File

@@ -1,8 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Cifs = {
hostname: string
path: string
username: string
password: string | null
}

View File

@@ -1,6 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ContactInfo =
| { email: string }
| { matrix: string }
| { website: string }

View File

@@ -1,7 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ImageId } from "./ImageId"
export type CreateSubcontainerFsParams = {
imageId: ImageId
name: string | null
}

View File

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

View File

@@ -1,9 +0,0 @@
// 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 CurrentDependencyInfo = {
title: string | null
icon: DataUrl | null
versionRange: string
configSatisfied: boolean
} & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] })

View File

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

View File

@@ -1,8 +0,0 @@
// 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
s9pk: PathOrUrl | null
}

View File

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

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type DependencyKind = "exists" | "running"

View File

@@ -1,9 +0,0 @@
// 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
}

View File

@@ -1,12 +0,0 @@
// 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 { PackageId } from "./PackageId"
export type DependencyRequirement =
| {
kind: "running"
id: PackageId
healthChecks: Array<HealthCheckId>
versionRange: string
}
| { kind: "exists"; id: PackageId; versionRange: string }

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More