mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
add documentation for ai agents (#3115)
* add documentation for ai agents * docs: consolidate CLAUDE.md and CONTRIBUTING.md, add style guidelines - Refactor CLAUDE.md to reference CONTRIBUTING.md for build/test/format info - Expand CONTRIBUTING.md with comprehensive build targets, env vars, and testing - Add code style guidelines section with conventional commits - Standardize SDK prettier config to use single quotes (matching web) - Add project-level Claude Code settings to disable co-author attribution * style(sdk): apply prettier with single quotes Run prettier across sdk/base and sdk/package to apply the standardized quote style (single quotes matching web). * docs: add USER.md for per-developer TODO filtering - Add agents/USER.md to .gitignore (contains user identifier) - Document session startup flow in CLAUDE.md: - Create USER.md if missing, prompting for identifier - Filter TODOs by @username tags - Offer relevant TODOs on session start * docs: add i18n documentation task to agent TODOs * docs: document i18n ID patterns in core/ Add agents/i18n-patterns.md covering rust-i18n setup, translation file format, t!() macro usage, key naming conventions, and locale selection. Remove completed TODO item and add reference in CLAUDE.md. * chore: clarify that all builds work on any OS with Docker
This commit is contained in:
@@ -1,73 +1,73 @@
|
||||
import { Value } from "../../base/lib/actions/input/builder/value"
|
||||
import { InputSpec } from "../../base/lib/actions/input/builder/inputSpec"
|
||||
import { Variants } from "../../base/lib/actions/input/builder/variants"
|
||||
import { Value } from '../../base/lib/actions/input/builder/value'
|
||||
import { InputSpec } from '../../base/lib/actions/input/builder/inputSpec'
|
||||
import { Variants } from '../../base/lib/actions/input/builder/variants'
|
||||
import {
|
||||
Action,
|
||||
ActionInfo,
|
||||
Actions,
|
||||
} from "../../base/lib/actions/setupActions"
|
||||
} from '../../base/lib/actions/setupActions'
|
||||
import {
|
||||
SyncOptions,
|
||||
ServiceInterfaceId,
|
||||
PackageId,
|
||||
ServiceInterfaceType,
|
||||
Effects,
|
||||
} from "../../base/lib/types"
|
||||
import * as patterns from "../../base/lib/util/patterns"
|
||||
import { BackupSync, Backups } from "./backup/Backups"
|
||||
import { smtpInputSpec } from "../../base/lib/actions/input/inputSpecConstants"
|
||||
import { Daemon, Daemons } from "./mainFn/Daemons"
|
||||
import { checkPortListening } from "./health/checkFns/checkPortListening"
|
||||
import { checkWebUrl, runHealthScript } from "./health/checkFns"
|
||||
import { List } from "../../base/lib/actions/input/builder/list"
|
||||
import { SetupBackupsParams, setupBackups } from "./backup/setupBackups"
|
||||
import { setupMain } from "./mainFn"
|
||||
import { defaultTrigger } from "./trigger/defaultTrigger"
|
||||
import { changeOnFirstSuccess, cooldownTrigger } from "./trigger"
|
||||
import { setupServiceInterfaces } from "../../base/lib/interfaces/setupInterfaces"
|
||||
import { successFailure } from "./trigger/successFailure"
|
||||
import { MultiHost, Scheme } from "../../base/lib/interfaces/Host"
|
||||
import { ServiceInterfaceBuilder } from "../../base/lib/interfaces/ServiceInterfaceBuilder"
|
||||
import { GetSystemSmtp } from "./util"
|
||||
import { nullIfEmpty } from "./util"
|
||||
import { getServiceInterface, getServiceInterfaces } from "./util"
|
||||
} from '../../base/lib/types'
|
||||
import * as patterns from '../../base/lib/util/patterns'
|
||||
import { BackupSync, Backups } from './backup/Backups'
|
||||
import { smtpInputSpec } from '../../base/lib/actions/input/inputSpecConstants'
|
||||
import { Daemon, Daemons } from './mainFn/Daemons'
|
||||
import { checkPortListening } from './health/checkFns/checkPortListening'
|
||||
import { checkWebUrl, runHealthScript } from './health/checkFns'
|
||||
import { List } from '../../base/lib/actions/input/builder/list'
|
||||
import { SetupBackupsParams, setupBackups } from './backup/setupBackups'
|
||||
import { setupMain } from './mainFn'
|
||||
import { defaultTrigger } from './trigger/defaultTrigger'
|
||||
import { changeOnFirstSuccess, cooldownTrigger } from './trigger'
|
||||
import { setupServiceInterfaces } from '../../base/lib/interfaces/setupInterfaces'
|
||||
import { successFailure } from './trigger/successFailure'
|
||||
import { MultiHost, Scheme } from '../../base/lib/interfaces/Host'
|
||||
import { ServiceInterfaceBuilder } from '../../base/lib/interfaces/ServiceInterfaceBuilder'
|
||||
import { GetSystemSmtp } from './util'
|
||||
import { nullIfEmpty } from './util'
|
||||
import { getServiceInterface, getServiceInterfaces } from './util'
|
||||
import {
|
||||
CommandOptions,
|
||||
ExitError,
|
||||
SubContainer,
|
||||
SubContainerOwned,
|
||||
} from "./util/SubContainer"
|
||||
import { splitCommand } from "./util"
|
||||
import { Mounts } from "./mainFn/Mounts"
|
||||
import { setupDependencies } from "../../base/lib/dependencies/setupDependencies"
|
||||
import * as T from "../../base/lib/types"
|
||||
import { testTypeVersion } from "../../base/lib/exver"
|
||||
} from './util/SubContainer'
|
||||
import { splitCommand } from './util'
|
||||
import { Mounts } from './mainFn/Mounts'
|
||||
import { setupDependencies } from '../../base/lib/dependencies/setupDependencies'
|
||||
import * as T from '../../base/lib/types'
|
||||
import { testTypeVersion } from '../../base/lib/exver'
|
||||
import {
|
||||
CheckDependencies,
|
||||
checkDependencies,
|
||||
} from "../../base/lib/dependencies/dependencies"
|
||||
import { GetSslCertificate, getServiceManifest } from "./util"
|
||||
import { getDataVersion, setDataVersion } from "./version"
|
||||
import { MaybeFn } from "../../base/lib/actions/setupActions"
|
||||
import { GetInput } from "../../base/lib/actions/setupActions"
|
||||
import { Run } from "../../base/lib/actions/setupActions"
|
||||
import * as actions from "../../base/lib/actions"
|
||||
import * as fs from "node:fs/promises"
|
||||
} from '../../base/lib/dependencies/dependencies'
|
||||
import { GetSslCertificate, getServiceManifest } from './util'
|
||||
import { getDataVersion, setDataVersion } from './version'
|
||||
import { MaybeFn } from '../../base/lib/actions/setupActions'
|
||||
import { GetInput } from '../../base/lib/actions/setupActions'
|
||||
import { Run } from '../../base/lib/actions/setupActions'
|
||||
import * as actions from '../../base/lib/actions'
|
||||
import * as fs from 'node:fs/promises'
|
||||
import {
|
||||
setupInit,
|
||||
setupUninit,
|
||||
setupOnInit,
|
||||
setupOnUninit,
|
||||
} from "../../base/lib/inits"
|
||||
import { DropGenerator } from "../../base/lib/util/Drop"
|
||||
} from '../../base/lib/inits'
|
||||
import { DropGenerator } from '../../base/lib/util/Drop'
|
||||
import {
|
||||
getOwnServiceInterface,
|
||||
ServiceInterfaceFilled,
|
||||
} from "../../base/lib/util/getServiceInterface"
|
||||
import { getOwnServiceInterfaces } from "../../base/lib/util/getServiceInterfaces"
|
||||
import { Volumes, createVolumes } from "./util/Volume"
|
||||
} from '../../base/lib/util/getServiceInterface'
|
||||
import { getOwnServiceInterfaces } from '../../base/lib/util/getServiceInterfaces'
|
||||
import { Volumes, createVolumes } from './util/Volume'
|
||||
|
||||
export const OSVersion = testTypeVersion("0.4.0-alpha.19")
|
||||
export const OSVersion = testTypeVersion('0.4.0-alpha.19')
|
||||
|
||||
// prettier-ignore
|
||||
type AnyNeverCond<T extends any[], Then, Else> =
|
||||
@@ -85,29 +85,29 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
return new StartSdk<Manifest>(manifest)
|
||||
}
|
||||
|
||||
build(isReady: AnyNeverCond<[Manifest], "Build not ready", true>) {
|
||||
type NestedEffects = "subcontainer" | "store" | "action"
|
||||
build(isReady: AnyNeverCond<[Manifest], 'Build not ready', true>) {
|
||||
type NestedEffects = 'subcontainer' | 'store' | 'action'
|
||||
type InterfaceEffects =
|
||||
| "getServiceInterface"
|
||||
| "listServiceInterfaces"
|
||||
| "exportServiceInterface"
|
||||
| "clearServiceInterfaces"
|
||||
| "bind"
|
||||
| "getHostInfo"
|
||||
type MainUsedEffects = "setMainStatus"
|
||||
| 'getServiceInterface'
|
||||
| 'listServiceInterfaces'
|
||||
| 'exportServiceInterface'
|
||||
| 'clearServiceInterfaces'
|
||||
| 'bind'
|
||||
| 'getHostInfo'
|
||||
type MainUsedEffects = 'setMainStatus'
|
||||
type CallbackEffects =
|
||||
| "child"
|
||||
| "constRetry"
|
||||
| "isInContext"
|
||||
| "onLeaveContext"
|
||||
| "clearCallbacks"
|
||||
| 'child'
|
||||
| 'constRetry'
|
||||
| 'isInContext'
|
||||
| 'onLeaveContext'
|
||||
| 'clearCallbacks'
|
||||
type AlreadyExposed =
|
||||
| "getSslCertificate"
|
||||
| "getSystemSmtp"
|
||||
| "getContainerIp"
|
||||
| "getDataVersion"
|
||||
| "setDataVersion"
|
||||
| "getServiceManifest"
|
||||
| 'getSslCertificate'
|
||||
| 'getSystemSmtp'
|
||||
| 'getContainerIp'
|
||||
| 'getDataVersion'
|
||||
| 'setDataVersion'
|
||||
| 'getServiceManifest'
|
||||
|
||||
// prettier-ignore
|
||||
type StartSdkEffectWrapper = {
|
||||
@@ -171,8 +171,8 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
effects.action.clearTasks({ only: replayIds }),
|
||||
},
|
||||
checkDependencies: checkDependencies as <
|
||||
DependencyId extends keyof Manifest["dependencies"] &
|
||||
PackageId = keyof Manifest["dependencies"] & PackageId,
|
||||
DependencyId extends keyof Manifest['dependencies'] &
|
||||
PackageId = keyof Manifest['dependencies'] & PackageId,
|
||||
>(
|
||||
effects: Effects,
|
||||
packageIds?: DependencyId[],
|
||||
@@ -186,8 +186,8 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
getContainerIp: (
|
||||
effects: T.Effects,
|
||||
options: Omit<
|
||||
Parameters<T.Effects["getContainerIp"]>[0],
|
||||
"callback"
|
||||
Parameters<T.Effects['getContainerIp']>[0],
|
||||
'callback'
|
||||
> = {},
|
||||
) => {
|
||||
async function* watch(abort?: AbortSignal) {
|
||||
@@ -195,7 +195,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
effects.onLeaveContext(() => {
|
||||
resolveCell.resolve()
|
||||
})
|
||||
abort?.addEventListener("abort", () => resolveCell.resolve())
|
||||
abort?.addEventListener('abort', () => resolveCell.resolve())
|
||||
while (effects.isInContext && !abort?.aborted) {
|
||||
let callback: () => void = () => {}
|
||||
const waitForNext = new Promise<void>((resolve) => {
|
||||
@@ -217,7 +217,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
once: () => effects.getContainerIp(options),
|
||||
watch: (abort?: AbortSignal) => {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
abort?.addEventListener('abort', () => ctrl.abort())
|
||||
return DropGenerator.of(watch(ctrl.signal), () => ctrl.abort())
|
||||
},
|
||||
onChange: (
|
||||
@@ -237,7 +237,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"callback function threw an error @ getContainerIp.onChange",
|
||||
'callback function threw an error @ getContainerIp.onChange',
|
||||
e,
|
||||
)
|
||||
}
|
||||
@@ -246,7 +246,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
.catch((e) => callback(null, e))
|
||||
.catch((e) =>
|
||||
console.error(
|
||||
"callback function threw an error @ getContainerIp.onChange",
|
||||
'callback function threw an error @ getContainerIp.onChange',
|
||||
e,
|
||||
),
|
||||
)
|
||||
@@ -388,7 +388,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
*/
|
||||
withoutInput: <Id extends T.ActionId>(
|
||||
id: Id,
|
||||
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
|
||||
metadata: MaybeFn<Omit<T.ActionMetadata, 'hasInput'>>,
|
||||
run: Run<{}>,
|
||||
) => Action.withoutInput(id, metadata, run),
|
||||
},
|
||||
@@ -701,7 +701,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
of(
|
||||
effects: Effects,
|
||||
image: {
|
||||
imageId: T.ImageId & keyof Manifest["images"]
|
||||
imageId: T.ImageId & keyof Manifest['images']
|
||||
sharedRun?: boolean
|
||||
},
|
||||
mounts: Mounts<Manifest> | null,
|
||||
@@ -724,7 +724,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
withTemp<T>(
|
||||
effects: T.Effects,
|
||||
image: {
|
||||
imageId: T.ImageId & keyof Manifest["images"]
|
||||
imageId: T.ImageId & keyof Manifest['images']
|
||||
sharedRun?: boolean
|
||||
},
|
||||
mounts: Mounts<Manifest> | null,
|
||||
@@ -743,7 +743,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
|
||||
export async function runCommand<Manifest extends T.SDKManifest>(
|
||||
effects: Effects,
|
||||
image: { imageId: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean },
|
||||
image: { imageId: keyof Manifest['images'] & T.ImageId; sharedRun?: boolean },
|
||||
command: T.CommandType,
|
||||
options: CommandOptions & {
|
||||
mounts: Mounts<Manifest> | null
|
||||
@@ -754,9 +754,9 @@ export async function runCommand<Manifest extends T.SDKManifest>(
|
||||
if (T.isUseEntrypoint(command)) {
|
||||
const imageMeta: T.ImageMetadata = await fs
|
||||
.readFile(`/media/startos/images/${image.imageId}.json`, {
|
||||
encoding: "utf8",
|
||||
encoding: 'utf8',
|
||||
})
|
||||
.catch(() => "{}")
|
||||
.catch(() => '{}')
|
||||
.then(JSON.parse)
|
||||
commands = imageMeta.entrypoint ?? []
|
||||
commands = commands.concat(...(command.overridCmd ?? imageMeta.cmd ?? []))
|
||||
@@ -768,13 +768,13 @@ export async function runCommand<Manifest extends T.SDKManifest>(
|
||||
name ||
|
||||
commands
|
||||
.map((c) => {
|
||||
if (c.includes(" ")) {
|
||||
if (c.includes(' ')) {
|
||||
return `"${c.replace(/"/g, `\"`)}"`
|
||||
} else {
|
||||
return c
|
||||
}
|
||||
})
|
||||
.join(" "),
|
||||
.join(' '),
|
||||
async (subcontainer) => {
|
||||
const res = await subcontainer.exec(commands)
|
||||
if (res.exitCode || res.exitSignal) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
import * as child_process from "child_process"
|
||||
import * as fs from "fs/promises"
|
||||
import { Affine, asError } from "../util"
|
||||
import { ExtendedVersion, VersionRange } from "../../../base/lib"
|
||||
import { InitKind, InitScript } from "../../../base/lib/inits"
|
||||
import * as T from '../../../base/lib/types'
|
||||
import * as child_process from 'child_process'
|
||||
import * as fs from 'fs/promises'
|
||||
import { Affine, asError } from '../util'
|
||||
import { ExtendedVersion, VersionRange } from '../../../base/lib'
|
||||
import { InitKind, InitScript } from '../../../base/lib/inits'
|
||||
|
||||
export const DEFAULT_OPTIONS: T.SyncOptions = {
|
||||
delete: true,
|
||||
@@ -17,14 +17,14 @@ export type BackupSync<Volumes extends string> = {
|
||||
restoreOptions?: Partial<T.SyncOptions>
|
||||
}
|
||||
|
||||
export type BackupEffects = T.Effects & Affine<"Backups">
|
||||
export type BackupEffects = T.Effects & Affine<'Backups'>
|
||||
|
||||
export class Backups<M extends T.SDKManifest> implements InitScript {
|
||||
private constructor(
|
||||
private options = DEFAULT_OPTIONS,
|
||||
private restoreOptions: Partial<T.SyncOptions> = {},
|
||||
private backupOptions: Partial<T.SyncOptions> = {},
|
||||
private backupSet = [] as BackupSync<M["volumes"][number]>[],
|
||||
private backupSet = [] as BackupSync<M['volumes'][number]>[],
|
||||
private preBackup = async (effects: BackupEffects) => {},
|
||||
private postBackup = async (effects: BackupEffects) => {},
|
||||
private preRestore = async (effects: BackupEffects) => {},
|
||||
@@ -32,7 +32,7 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
||||
) {}
|
||||
|
||||
static ofVolumes<M extends T.SDKManifest = never>(
|
||||
...volumeNames: Array<M["volumes"][number]>
|
||||
...volumeNames: Array<M['volumes'][number]>
|
||||
): Backups<M> {
|
||||
return Backups.ofSyncs(
|
||||
...volumeNames.map((srcVolume) => ({
|
||||
@@ -43,7 +43,7 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
||||
}
|
||||
|
||||
static ofSyncs<M extends T.SDKManifest = never>(
|
||||
...syncs: BackupSync<M["volumes"][number]>[]
|
||||
...syncs: BackupSync<M['volumes'][number]>[]
|
||||
) {
|
||||
return syncs.reduce((acc, x) => acc.addSync(x), new Backups<M>())
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
||||
}
|
||||
|
||||
addVolume(
|
||||
volume: M["volumes"][number],
|
||||
volume: M['volumes'][number],
|
||||
options?: Partial<{
|
||||
options: T.SyncOptions
|
||||
backupOptions: T.SyncOptions
|
||||
@@ -113,7 +113,7 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
||||
})
|
||||
}
|
||||
|
||||
addSync(sync: BackupSync<M["volumes"][0]>) {
|
||||
addSync(sync: BackupSync<M['volumes'][0]>) {
|
||||
this.backupSet.push(sync)
|
||||
return this
|
||||
}
|
||||
@@ -136,15 +136,15 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
||||
|
||||
const dataVersion = await effects.getDataVersion()
|
||||
if (dataVersion)
|
||||
await fs.writeFile("/media/startos/backup/dataVersion.txt", dataVersion, {
|
||||
encoding: "utf-8",
|
||||
await fs.writeFile('/media/startos/backup/dataVersion.txt', dataVersion, {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
await this.postBackup(effects as BackupEffects)
|
||||
return
|
||||
}
|
||||
|
||||
async init(effects: T.Effects, kind: InitKind): Promise<void> {
|
||||
if (kind === "restore") {
|
||||
if (kind === 'restore') {
|
||||
await this.restoreBackup(effects)
|
||||
}
|
||||
}
|
||||
@@ -166,8 +166,8 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
||||
await rsyncResults.wait()
|
||||
}
|
||||
const dataVersion = await fs
|
||||
.readFile("/media/startos/backup/dataVersion.txt", {
|
||||
encoding: "utf-8",
|
||||
.readFile('/media/startos/backup/dataVersion.txt', {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
.catch((_) => null)
|
||||
if (dataVersion) await effects.setDataVersion({ version: dataVersion })
|
||||
@@ -189,23 +189,23 @@ async function runRsync(rsyncOptions: {
|
||||
|
||||
await fs.mkdir(dstPath, { recursive: true })
|
||||
|
||||
const command = "rsync"
|
||||
const command = 'rsync'
|
||||
const args: string[] = []
|
||||
if (options.delete) {
|
||||
args.push("--delete")
|
||||
args.push('--delete')
|
||||
}
|
||||
for (const exclude of options.exclude) {
|
||||
args.push(`--exclude=${exclude}`)
|
||||
}
|
||||
args.push("-rlptgocAXH")
|
||||
args.push("--info=progress2")
|
||||
args.push("--no-inc-recursive")
|
||||
args.push('-rlptgocAXH')
|
||||
args.push('--info=progress2')
|
||||
args.push('--no-inc-recursive')
|
||||
args.push(srcPath)
|
||||
args.push(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/g, "\n").split("\n")
|
||||
spawned.stdout.on('data', (data: unknown) => {
|
||||
const lines = String(data).replace(/\r/g, '\n').split('\n')
|
||||
for (const line of lines) {
|
||||
const parsed = /$([0-9.]+)%/.exec(line)?.[1]
|
||||
if (!parsed) {
|
||||
@@ -216,10 +216,10 @@ async function runRsync(rsyncOptions: {
|
||||
}
|
||||
})
|
||||
|
||||
let stderr = ""
|
||||
let stderr = ''
|
||||
|
||||
spawned.stderr.on("data", (data: string | Buffer) => {
|
||||
const errString = data.toString("utf-8")
|
||||
spawned.stderr.on('data', (data: string | Buffer) => {
|
||||
const errString = data.toString('utf-8')
|
||||
stderr += errString
|
||||
console.error(`Backups.runAsync`, asError(errString))
|
||||
})
|
||||
@@ -227,12 +227,12 @@ async function runRsync(rsyncOptions: {
|
||||
const id = async () => {
|
||||
const pid = spawned.pid
|
||||
if (pid === undefined) {
|
||||
throw new Error("rsync process has no pid")
|
||||
throw new Error('rsync process has no pid')
|
||||
}
|
||||
return String(pid)
|
||||
}
|
||||
const waitPromise = new Promise<null>((resolve, reject) => {
|
||||
spawned.on("exit", (code: any) => {
|
||||
spawned.on('exit', (code: any) => {
|
||||
if (code === 0) {
|
||||
resolve(null)
|
||||
} else {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
import "./Backups"
|
||||
import "./setupBackups"
|
||||
import './Backups'
|
||||
import './setupBackups'
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Backups } from "./Backups"
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { _ } from "../util"
|
||||
import { InitScript } from "../../../base/lib/inits"
|
||||
import { Backups } from './Backups'
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { _ } from '../util'
|
||||
import { InitScript } from '../../../base/lib/inits'
|
||||
|
||||
export type SetupBackupsParams<M extends T.SDKManifest> =
|
||||
| M["volumes"][number][]
|
||||
| M['volumes'][number][]
|
||||
| ((_: { effects: T.Effects }) => Promise<Backups<M>>)
|
||||
|
||||
type SetupBackupsRes = {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Effects, HealthCheckId } from "../../../base/lib/types"
|
||||
import { HealthCheckResult } from "./checkFns/HealthCheckResult"
|
||||
import { Trigger } from "../trigger"
|
||||
import { TriggerInput } from "../trigger/TriggerInput"
|
||||
import { defaultTrigger } from "../trigger/defaultTrigger"
|
||||
import { once, asError, Drop } from "../util"
|
||||
import { object, unknown } from "ts-matches"
|
||||
import { Effects, HealthCheckId } from '../../../base/lib/types'
|
||||
import { HealthCheckResult } from './checkFns/HealthCheckResult'
|
||||
import { Trigger } from '../trigger'
|
||||
import { TriggerInput } from '../trigger/TriggerInput'
|
||||
import { defaultTrigger } from '../trigger/defaultTrigger'
|
||||
import { once, asError, Drop } from '../util'
|
||||
import { object, unknown } from 'ts-matches'
|
||||
|
||||
export type HealthCheckParams = {
|
||||
id: HealthCheckId
|
||||
@@ -59,15 +59,15 @@ export class HealthCheck extends Drop {
|
||||
try {
|
||||
let { result, message } = await o.fn()
|
||||
if (
|
||||
result === "failure" &&
|
||||
result === 'failure' &&
|
||||
performance.now() - started <= gracePeriod
|
||||
)
|
||||
result = "starting"
|
||||
result = 'starting'
|
||||
await effects.setHealth({
|
||||
name: o.name,
|
||||
id: o.id,
|
||||
result,
|
||||
message: message || "",
|
||||
message: message || '',
|
||||
})
|
||||
this.currentValue.lastResult = result
|
||||
} catch (e) {
|
||||
@@ -76,11 +76,11 @@ export class HealthCheck extends Drop {
|
||||
id: o.id,
|
||||
result:
|
||||
performance.now() - started <= gracePeriod
|
||||
? "starting"
|
||||
: "failure",
|
||||
message: asMessage(e) || "",
|
||||
? 'starting'
|
||||
: 'failure',
|
||||
message: asMessage(e) || '',
|
||||
})
|
||||
this.currentValue.lastResult = "failure"
|
||||
this.currentValue.lastResult = 'failure'
|
||||
}
|
||||
}
|
||||
} else triggered = false
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { T } from "../../../../base/lib"
|
||||
import { T } from '../../../../base/lib'
|
||||
|
||||
export type HealthCheckResult = Omit<T.NamedHealthCheckResult, "name">
|
||||
export type HealthCheckResult = Omit<T.NamedHealthCheckResult, 'name'>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Effects } from "../../../../base/lib/types"
|
||||
import { stringFromStdErrOut } from "../../util"
|
||||
import { HealthCheckResult } from "./HealthCheckResult"
|
||||
import { promisify } from "node:util"
|
||||
import * as CP from "node:child_process"
|
||||
import { Effects } from '../../../../base/lib/types'
|
||||
import { stringFromStdErrOut } from '../../util'
|
||||
import { HealthCheckResult } from './HealthCheckResult'
|
||||
import { promisify } from 'node:util'
|
||||
import * as CP from 'node:child_process'
|
||||
|
||||
const cpExec = promisify(CP.exec)
|
||||
|
||||
export function containsAddress(x: string, port: number, address?: bigint) {
|
||||
const readPorts = x
|
||||
.split("\n")
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.splice(1)
|
||||
.map((x) => x.split(" ").filter(Boolean)[1]?.split(":"))
|
||||
.map((x) => x.split(' ').filter(Boolean)[1]?.split(':'))
|
||||
.filter((x) => x?.length > 1)
|
||||
.map(([addr, p]) => [BigInt(`0x${addr}`), Number.parseInt(p, 16)] as const)
|
||||
return !!readPorts.find(
|
||||
@@ -46,19 +46,19 @@ export async function checkPortListening(
|
||||
BigInt(0),
|
||||
) ||
|
||||
containsAddress(
|
||||
await cpExec("cat /proc/net/udp", {}).then(stringFromStdErrOut),
|
||||
await cpExec('cat /proc/net/udp', {}).then(stringFromStdErrOut),
|
||||
port,
|
||||
) ||
|
||||
containsAddress(
|
||||
await cpExec("cat /proc/net/udp6", {}).then(stringFromStdErrOut),
|
||||
await cpExec('cat /proc/net/udp6', {}).then(stringFromStdErrOut),
|
||||
port,
|
||||
BigInt(0),
|
||||
)
|
||||
if (hasAddress) {
|
||||
return { result: "success", message: options.successMessage }
|
||||
return { result: 'success', message: options.successMessage }
|
||||
}
|
||||
return {
|
||||
result: "failure",
|
||||
result: 'failure',
|
||||
message: options.errorMessage,
|
||||
}
|
||||
}),
|
||||
@@ -66,7 +66,7 @@ export async function checkPortListening(
|
||||
setTimeout(
|
||||
() =>
|
||||
resolve({
|
||||
result: "failure",
|
||||
result: 'failure',
|
||||
message:
|
||||
options.timeoutMessage || `Timeout trying to check port ${port}`,
|
||||
}),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Effects } from "../../../../base/lib/types"
|
||||
import { asError } from "../../util"
|
||||
import { HealthCheckResult } from "./HealthCheckResult"
|
||||
import { timeoutPromise } from "./index"
|
||||
import "isomorphic-fetch"
|
||||
import { Effects } from '../../../../base/lib/types'
|
||||
import { asError } from '../../util'
|
||||
import { HealthCheckResult } from './HealthCheckResult'
|
||||
import { timeoutPromise } from './index'
|
||||
import 'isomorphic-fetch'
|
||||
|
||||
/**
|
||||
* This is a helper function to check if a web url is reachable.
|
||||
@@ -23,7 +23,7 @@ export const checkWebUrl = async (
|
||||
.then(
|
||||
(x) =>
|
||||
({
|
||||
result: "success",
|
||||
result: 'success',
|
||||
message: successMessage,
|
||||
}) as const,
|
||||
)
|
||||
@@ -31,6 +31,6 @@ export const checkWebUrl = async (
|
||||
console.warn(`Error while fetching URL: ${url}`)
|
||||
console.error(JSON.stringify(e))
|
||||
console.error(asError(e))
|
||||
return { result: "failure" as const, message: errorMessage }
|
||||
return { result: 'failure' as const, message: errorMessage }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { runHealthScript } from "./runHealthScript"
|
||||
export { checkPortListening } from "./checkPortListening"
|
||||
export { HealthCheckResult } from "./HealthCheckResult"
|
||||
export { checkWebUrl } from "./checkWebUrl"
|
||||
import { runHealthScript } from './runHealthScript'
|
||||
export { checkPortListening } from './checkPortListening'
|
||||
export { HealthCheckResult } from './HealthCheckResult'
|
||||
export { checkWebUrl } from './checkWebUrl'
|
||||
|
||||
export function timeoutPromise(ms: number, { message = "Timed out" } = {}) {
|
||||
export function timeoutPromise(ms: number, { message = 'Timed out' } = {}) {
|
||||
return new Promise<never>((resolve, reject) =>
|
||||
setTimeout(() => reject(new Error(message)), ms),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HealthCheckResult } from "./HealthCheckResult"
|
||||
import { timeoutPromise } from "./index"
|
||||
import { SubContainer } from "../../util/SubContainer"
|
||||
import { SDKManifest } from "../../types"
|
||||
import { HealthCheckResult } from './HealthCheckResult'
|
||||
import { timeoutPromise } from './index'
|
||||
import { SubContainer } from '../../util/SubContainer'
|
||||
import { SDKManifest } from '../../types'
|
||||
|
||||
/**
|
||||
* Running a health script, is used when we want to have a simple
|
||||
@@ -27,10 +27,10 @@ export const runHealthScript = async <Manifest extends SDKManifest>(
|
||||
console.warn(errorMessage)
|
||||
console.warn(JSON.stringify(e))
|
||||
console.warn(e.toString())
|
||||
throw { result: "failure", message: errorMessage } as HealthCheckResult
|
||||
throw { result: 'failure', message: errorMessage } as HealthCheckResult
|
||||
})
|
||||
return {
|
||||
result: "success",
|
||||
result: 'success',
|
||||
message: message(res.stdout.toString()),
|
||||
} as HealthCheckResult
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import "./checkFns"
|
||||
import './checkFns'
|
||||
|
||||
export { HealthCheck } from "./HealthCheck"
|
||||
export { HealthCheck } from './HealthCheck'
|
||||
|
||||
@@ -26,10 +26,10 @@ export function setupI18n<
|
||||
Dict extends Record<string, number>,
|
||||
Translations extends Record<string, Record<number, string>>,
|
||||
>(defaultDict: Dict, translations: Translations, defaultLang: string) {
|
||||
const lang = process.env.LANG?.replace(/\.UTF-8$/, "") || defaultLang
|
||||
const lang = process.env.LANG?.replace(/\.UTF-8$/, '') || defaultLang
|
||||
|
||||
// Convert locale format from en_US to en-US for Intl APIs
|
||||
const intlLocale = lang.replace("_", "-")
|
||||
const intlLocale = lang.replace('_', '-')
|
||||
|
||||
function getTranslation(): Record<number, string> | null {
|
||||
if (lang === defaultLang) return null
|
||||
@@ -38,7 +38,7 @@ export function setupI18n<
|
||||
|
||||
const match =
|
||||
availableLangs.find((l) => l === lang) ??
|
||||
availableLangs.find((l) => String(l).startsWith(lang.split("_")[0] + "_"))
|
||||
availableLangs.find((l) => String(l).startsWith(lang.split('_')[0] + '_'))
|
||||
|
||||
return match ? (translations[match] as Record<number, string>) : null
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export function setupI18n<
|
||||
const translation = getTranslation()
|
||||
|
||||
function formatValue(value: ParamValue): string {
|
||||
if (typeof value === "number") {
|
||||
if (typeof value === 'number') {
|
||||
return new Intl.NumberFormat(intlLocale).format(value)
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
types,
|
||||
matches,
|
||||
utils,
|
||||
} from "../../base/lib"
|
||||
} from '../../base/lib'
|
||||
|
||||
export {
|
||||
S9pk,
|
||||
@@ -23,23 +23,23 @@ export {
|
||||
matches,
|
||||
utils,
|
||||
}
|
||||
export { setupI18n } from "./i18n"
|
||||
export * as T from "./types"
|
||||
export { Daemons } from "./mainFn/Daemons"
|
||||
export { SubContainer } from "./util/SubContainer"
|
||||
export { StartSdk } from "./StartSdk"
|
||||
export { setupManifest, buildManifest } from "./manifest/setupManifest"
|
||||
export { FileHelper } from "./util/fileHelper"
|
||||
export { setupI18n } from './i18n'
|
||||
export * as T from './types'
|
||||
export { Daemons } from './mainFn/Daemons'
|
||||
export { SubContainer } from './util/SubContainer'
|
||||
export { StartSdk } from './StartSdk'
|
||||
export { setupManifest, buildManifest } from './manifest/setupManifest'
|
||||
export { FileHelper } from './util/fileHelper'
|
||||
|
||||
export * as actions from "../../base/lib/actions"
|
||||
export * as backup from "./backup"
|
||||
export * as daemons from "./mainFn/Daemons"
|
||||
export * as health from "./health"
|
||||
export * as healthFns from "./health/checkFns"
|
||||
export * as mainFn from "./mainFn"
|
||||
export * as toml from "@iarna/toml"
|
||||
export * as yaml from "yaml"
|
||||
export * as startSdk from "./StartSdk"
|
||||
export * as YAML from "yaml"
|
||||
export * as TOML from "@iarna/toml"
|
||||
export * from "./version"
|
||||
export * as actions from '../../base/lib/actions'
|
||||
export * as backup from './backup'
|
||||
export * as daemons from './mainFn/Daemons'
|
||||
export * as health from './health'
|
||||
export * as healthFns from './health/checkFns'
|
||||
export * as mainFn from './mainFn'
|
||||
export * as toml from '@iarna/toml'
|
||||
export * as yaml from 'yaml'
|
||||
export * as startSdk from './StartSdk'
|
||||
export * as YAML from 'yaml'
|
||||
export * as TOML from '@iarna/toml'
|
||||
export * from './version'
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { DEFAULT_SIGTERM_TIMEOUT } from "."
|
||||
import { NO_TIMEOUT, SIGTERM } from "../../../base/lib/types"
|
||||
import { DEFAULT_SIGTERM_TIMEOUT } from '.'
|
||||
import { NO_TIMEOUT, SIGTERM } from '../../../base/lib/types'
|
||||
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { SubContainer } from "../util/SubContainer"
|
||||
import { Drop, splitCommand } from "../util"
|
||||
import * as cp from "child_process"
|
||||
import * as fs from "node:fs/promises"
|
||||
import { DaemonCommandType, ExecCommandOptions, ExecFnOptions } from "./Daemons"
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { SubContainer } from '../util/SubContainer'
|
||||
import { Drop, splitCommand } from '../util'
|
||||
import * as cp from 'child_process'
|
||||
import * as fs from 'node:fs/promises'
|
||||
import { DaemonCommandType, ExecCommandOptions, ExecFnOptions } from './Daemons'
|
||||
|
||||
export class CommandController<
|
||||
Manifest extends T.SDKManifest,
|
||||
@@ -31,7 +31,7 @@ export class CommandController<
|
||||
exec: DaemonCommandType<Manifest, C>,
|
||||
) => {
|
||||
try {
|
||||
if ("fn" in exec) {
|
||||
if ('fn' in exec) {
|
||||
const abort = new AbortController()
|
||||
const cell: { ctrl: CommandController<Manifest, C> } = {
|
||||
ctrl: new CommandController<Manifest, C>(
|
||||
@@ -63,9 +63,9 @@ export class CommandController<
|
||||
if (T.isUseEntrypoint(exec.command)) {
|
||||
const imageMeta: T.ImageMetadata = await fs
|
||||
.readFile(`/media/startos/images/${subcontainer!.imageId}.json`, {
|
||||
encoding: "utf8",
|
||||
encoding: 'utf8',
|
||||
})
|
||||
.catch(() => "{}")
|
||||
.catch(() => '{}')
|
||||
.then(JSON.parse)
|
||||
commands = imageMeta.entrypoint ?? []
|
||||
commands = commands.concat(
|
||||
@@ -85,21 +85,21 @@ export class CommandController<
|
||||
env: exec.env,
|
||||
user: exec.user,
|
||||
cwd: exec.cwd,
|
||||
stdio: exec.onStdout || exec.onStderr ? "pipe" : "inherit",
|
||||
stdio: exec.onStdout || exec.onStderr ? 'pipe' : 'inherit',
|
||||
})
|
||||
}
|
||||
|
||||
if (exec.onStdout) childProcess.stdout?.on("data", exec.onStdout)
|
||||
if (exec.onStderr) childProcess.stderr?.on("data", exec.onStderr)
|
||||
if (exec.onStdout) childProcess.stdout?.on('data', exec.onStdout)
|
||||
if (exec.onStderr) childProcess.stderr?.on('data', exec.onStderr)
|
||||
|
||||
const state = { exited: false }
|
||||
const answer = new Promise<null>((resolve, reject) => {
|
||||
childProcess.on("exit", (code) => {
|
||||
childProcess.on('exit', (code) => {
|
||||
state.exited = true
|
||||
if (
|
||||
code === 0 ||
|
||||
code === 143 ||
|
||||
(code === null && childProcess.signalCode == "SIGTERM")
|
||||
(code === null && childProcess.signalCode == 'SIGTERM')
|
||||
) {
|
||||
return resolve(null)
|
||||
}
|
||||
@@ -142,7 +142,7 @@ export class CommandController<
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(new Error("Timed out waiting for js command to exit")),
|
||||
reject(new Error('Timed out waiting for js command to exit')),
|
||||
timeout * 2,
|
||||
),
|
||||
),
|
||||
@@ -151,7 +151,7 @@ export class CommandController<
|
||||
} finally {
|
||||
if (!this.state.exited) {
|
||||
if (this.process instanceof AbortController) this.process.abort()
|
||||
else this.process.kill("SIGKILL")
|
||||
else this.process.kill('SIGKILL')
|
||||
}
|
||||
await this.subcontainer?.destroy()
|
||||
}
|
||||
@@ -161,10 +161,10 @@ export class CommandController<
|
||||
if (!this.state.exited) {
|
||||
if (this.process instanceof AbortController) return this.process.abort()
|
||||
|
||||
if (signal !== "SIGKILL") {
|
||||
if (signal !== 'SIGKILL') {
|
||||
setTimeout(() => {
|
||||
if (this.process instanceof AbortController) this.process.abort()
|
||||
else this.process.kill("SIGKILL")
|
||||
else this.process.kill('SIGKILL')
|
||||
}, timeout)
|
||||
}
|
||||
if (!this.process.kill(signal)) {
|
||||
@@ -180,7 +180,7 @@ export class CommandController<
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(new Error("Timed out waiting for js command to exit")),
|
||||
reject(new Error('Timed out waiting for js command to exit')),
|
||||
timeout * 2,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { asError } from "../../../base/lib/util/asError"
|
||||
import { Drop } from "../util"
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { asError } from '../../../base/lib/util/asError'
|
||||
import { Drop } from '../util'
|
||||
import {
|
||||
SubContainer,
|
||||
SubContainerOwned,
|
||||
SubContainerRc,
|
||||
} from "../util/SubContainer"
|
||||
import { CommandController } from "./CommandController"
|
||||
import { DaemonCommandType } from "./Daemons"
|
||||
import { Oneshot } from "./Oneshot"
|
||||
} from '../util/SubContainer'
|
||||
import { CommandController } from './CommandController'
|
||||
import { DaemonCommandType } from './Daemons'
|
||||
import { Oneshot } from './Oneshot'
|
||||
|
||||
const TIMEOUT_INCREMENT_MS = 1000
|
||||
const MAX_TIMEOUT_MS = 30000
|
||||
@@ -87,7 +87,7 @@ export class Daemon<
|
||||
try {
|
||||
fn(success)
|
||||
} catch (e) {
|
||||
console.error("EXIT handler", e)
|
||||
console.error('EXIT handler', e)
|
||||
}
|
||||
}
|
||||
if (success && this.oneshot) {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Signals } from "../../../base/lib/types"
|
||||
import { Signals } from '../../../base/lib/types'
|
||||
|
||||
import { HealthCheckResult } from "../health/checkFns"
|
||||
import { HealthCheckResult } from '../health/checkFns'
|
||||
|
||||
import { Trigger } from "../trigger"
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { SubContainer } from "../util/SubContainer"
|
||||
import { Trigger } from '../trigger'
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { SubContainer } from '../util/SubContainer'
|
||||
|
||||
import { promisify } from "node:util"
|
||||
import * as CP from "node:child_process"
|
||||
import { promisify } from 'node:util'
|
||||
import * as CP from 'node:child_process'
|
||||
|
||||
export { Daemon } from "./Daemon"
|
||||
export { CommandController } from "./CommandController"
|
||||
import { EXIT_SUCCESS, HealthDaemon } from "./HealthDaemon"
|
||||
import { Daemon } from "./Daemon"
|
||||
import { CommandController } from "./CommandController"
|
||||
import { Oneshot } from "./Oneshot"
|
||||
export { Daemon } from './Daemon'
|
||||
export { CommandController } from './CommandController'
|
||||
import { EXIT_SUCCESS, HealthDaemon } from './HealthDaemon'
|
||||
import { Daemon } from './Daemon'
|
||||
import { CommandController } from './CommandController'
|
||||
import { Oneshot } from './Oneshot'
|
||||
|
||||
export const cpExec = promisify(CP.exec)
|
||||
export const cpExecFile = promisify(CP.execFile)
|
||||
@@ -231,7 +231,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
const res = (options: AddDaemonParams<Manifest, Ids, Id, C> | null) => {
|
||||
if (!options) return prev
|
||||
const daemon =
|
||||
"daemon" in options
|
||||
'daemon' in options
|
||||
? options.daemon
|
||||
: Daemon.of<Manifest>()<C>(
|
||||
this.effects,
|
||||
@@ -369,8 +369,8 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
const healthDaemon = new HealthDaemon<Manifest>(
|
||||
daemon,
|
||||
[...this.healthDaemons],
|
||||
"__RUN_UNTIL_SUCCESS",
|
||||
"EXIT_SUCCESS",
|
||||
'__RUN_UNTIL_SUCCESS',
|
||||
'EXIT_SUCCESS',
|
||||
this.effects,
|
||||
)
|
||||
const daemons = await new Daemons<Manifest, Ids>(this.effects, this.ids, [
|
||||
@@ -400,7 +400,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
if (canShutdown.length === 0) {
|
||||
// Dependency cycle that should not happen, just shutdown remaining daemons
|
||||
console.warn(
|
||||
"Dependency cycle detected, shutting down remaining daemons",
|
||||
'Dependency cycle detected, shutting down remaining daemons',
|
||||
)
|
||||
canShutdown.push(...[...remaining].reverse())
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { HealthCheckResult } from "../health/checkFns"
|
||||
import { defaultTrigger } from "../trigger/defaultTrigger"
|
||||
import { Ready } from "./Daemons"
|
||||
import { Daemon } from "./Daemon"
|
||||
import { SetHealth, Effects, SDKManifest } from "../../../base/lib/types"
|
||||
import { HealthCheckResult } from '../health/checkFns'
|
||||
import { defaultTrigger } from '../trigger/defaultTrigger'
|
||||
import { Ready } from './Daemons'
|
||||
import { Daemon } from './Daemon'
|
||||
import { SetHealth, Effects, SDKManifest } from '../../../base/lib/types'
|
||||
|
||||
const oncePromise = <T>() => {
|
||||
let resolve: (value: T) => void
|
||||
@@ -12,7 +12,7 @@ const oncePromise = <T>() => {
|
||||
return { resolve: resolve!, promise }
|
||||
}
|
||||
|
||||
export const EXIT_SUCCESS = "EXIT_SUCCESS" as const
|
||||
export const EXIT_SUCCESS = 'EXIT_SUCCESS' as const
|
||||
|
||||
/**
|
||||
* Wanted a structure that deals with controlling daemons by their health status
|
||||
@@ -22,7 +22,7 @@ export const EXIT_SUCCESS = "EXIT_SUCCESS" as const
|
||||
*
|
||||
*/
|
||||
export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
private _health: HealthCheckResult = { result: "waiting", message: null }
|
||||
private _health: HealthCheckResult = { result: 'waiting', message: null }
|
||||
private healthWatchers: Array<() => unknown> = []
|
||||
private running = false
|
||||
private started?: number
|
||||
@@ -102,21 +102,21 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
}
|
||||
private async setupHealthCheck() {
|
||||
this.daemon?.onExit((success) => {
|
||||
if (success && this.ready === "EXIT_SUCCESS") {
|
||||
this.setHealth({ result: "success", message: null })
|
||||
if (success && this.ready === 'EXIT_SUCCESS') {
|
||||
this.setHealth({ result: 'success', message: null })
|
||||
} else if (!success) {
|
||||
this.setHealth({
|
||||
result: "failure",
|
||||
result: 'failure',
|
||||
message: `${this.id} daemon crashed`,
|
||||
})
|
||||
} else if (!this.daemon?.isOneshot()) {
|
||||
this.setHealth({
|
||||
result: "failure",
|
||||
result: 'failure',
|
||||
message: `${this.id} daemon exited`,
|
||||
})
|
||||
}
|
||||
})
|
||||
if (this.ready === "EXIT_SUCCESS") return
|
||||
if (this.ready === 'EXIT_SUCCESS') return
|
||||
if (this.healthCheckCleanup) return
|
||||
const trigger = (this.ready.trigger ?? defaultTrigger)(() => ({
|
||||
lastResult: this._health.result,
|
||||
@@ -127,7 +127,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
}>()
|
||||
const { promise: exited, resolve: setExited } = oncePromise<null>()
|
||||
new Promise(async () => {
|
||||
if (this.ready === "EXIT_SUCCESS") return
|
||||
if (this.ready === 'EXIT_SUCCESS') return
|
||||
for (
|
||||
let res = await Promise.race([status, trigger.next()]);
|
||||
!res.done;
|
||||
@@ -137,8 +137,8 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
this.ready.fn(),
|
||||
).catch((err) => {
|
||||
return {
|
||||
result: "failure",
|
||||
message: "message" in err ? err.message : String(err),
|
||||
result: 'failure',
|
||||
message: 'message' in err ? err.message : String(err),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -166,23 +166,23 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
private async setHealth(health: HealthCheckResult) {
|
||||
const changed = this._health.result !== health.result
|
||||
this._health = health
|
||||
if (this.resolveReady && health.result === "success") {
|
||||
if (this.resolveReady && health.result === 'success') {
|
||||
this.resolveReady()
|
||||
}
|
||||
if (changed) this.healthWatchers.forEach((watcher) => watcher())
|
||||
if (this.ready === "EXIT_SUCCESS") return
|
||||
if (this.ready === 'EXIT_SUCCESS') return
|
||||
const display = this.ready.display
|
||||
if (!display) {
|
||||
return
|
||||
}
|
||||
let result = health.result
|
||||
if (
|
||||
result === "failure" &&
|
||||
result === 'failure' &&
|
||||
this.started &&
|
||||
performance.now() - this.started <= (this.ready.gracePeriod ?? 10_000)
|
||||
)
|
||||
result = "starting"
|
||||
if (result === "failure") {
|
||||
result = 'starting'
|
||||
if (result === 'failure') {
|
||||
console.error(`Health Check ${this.id} failed:`, health.message)
|
||||
}
|
||||
await this.effects.setHealth({
|
||||
@@ -197,10 +197,10 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
const healths = this.dependencies.map((d) => ({
|
||||
health: d.running && d._health,
|
||||
id: d.id,
|
||||
display: typeof d.ready === "object" ? d.ready.display : null,
|
||||
display: typeof d.ready === 'object' ? d.ready.display : null,
|
||||
}))
|
||||
const waitingOn = healths.filter(
|
||||
(h) => !h.health || h.health.result !== "success",
|
||||
(h) => !h.health || h.health.result !== 'success',
|
||||
)
|
||||
if (waitingOn.length)
|
||||
console.debug(
|
||||
@@ -210,10 +210,10 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
const waitingOnNames = waitingOn.flatMap((w) =>
|
||||
w.display ? [w.display] : [],
|
||||
)
|
||||
const message = waitingOnNames.length ? waitingOnNames.join(", ") : null
|
||||
await this.setHealth({ result: "waiting", message })
|
||||
const message = waitingOnNames.length ? waitingOnNames.join(', ') : null
|
||||
await this.setHealth({ result: 'waiting', message })
|
||||
} else {
|
||||
await this.setHealth({ result: "starting", message: null })
|
||||
await this.setHealth({ result: 'starting', message: null })
|
||||
}
|
||||
await this.changeRunning(!waitingOn.length)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { IdMap, MountOptions } from "../util/SubContainer"
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { IdMap, MountOptions } from '../util/SubContainer'
|
||||
|
||||
type MountArray = { mountpoint: string; options: MountOptions }[]
|
||||
|
||||
@@ -13,7 +13,7 @@ type SharedOptions = {
|
||||
*
|
||||
* defaults to "directory"
|
||||
* */
|
||||
type?: "file" | "directory" | "infer"
|
||||
type?: 'file' | 'directory' | 'infer'
|
||||
// /**
|
||||
// * Whether to map uids/gids for the mount
|
||||
// *
|
||||
@@ -35,16 +35,16 @@ type SharedOptions = {
|
||||
|
||||
type VolumeOpts<Manifest extends T.SDKManifest> = {
|
||||
/** The ID of the volume to mount. Must be one of the volume IDs defined in the manifest */
|
||||
volumeId: Manifest["volumes"][number]
|
||||
volumeId: Manifest['volumes'][number]
|
||||
/** Whether or not the resource should be readonly for this subcontainer */
|
||||
readonly: boolean
|
||||
} & SharedOptions
|
||||
|
||||
type DependencyOpts<Manifest extends T.SDKManifest> = {
|
||||
/** The ID of the dependency */
|
||||
dependencyId: Manifest["id"]
|
||||
dependencyId: Manifest['id']
|
||||
/** The ID of the volume to mount. Must be one of the volume IDs defined in the manifest of the dependency */
|
||||
volumeId: Manifest["volumes"][number]
|
||||
volumeId: Manifest['volumes'][number]
|
||||
/** Whether or not the resource should be readonly for this subcontainer */
|
||||
readonly: boolean
|
||||
} & SharedOptions
|
||||
@@ -126,11 +126,11 @@ export class Mounts<
|
||||
this.volumes.map((v) => ({
|
||||
mountpoint: v.mountpoint,
|
||||
options: {
|
||||
type: "volume",
|
||||
type: 'volume',
|
||||
volumeId: v.volumeId,
|
||||
subpath: v.subpath,
|
||||
readonly: v.readonly,
|
||||
filetype: v.type ?? "directory",
|
||||
filetype: v.type ?? 'directory',
|
||||
idmap: [],
|
||||
},
|
||||
})),
|
||||
@@ -139,9 +139,9 @@ export class Mounts<
|
||||
this.assets.map((a) => ({
|
||||
mountpoint: a.mountpoint,
|
||||
options: {
|
||||
type: "assets",
|
||||
type: 'assets',
|
||||
subpath: a.subpath,
|
||||
filetype: a.type ?? "directory",
|
||||
filetype: a.type ?? 'directory',
|
||||
idmap: [],
|
||||
},
|
||||
})),
|
||||
@@ -150,12 +150,12 @@ export class Mounts<
|
||||
this.dependencies.map((d) => ({
|
||||
mountpoint: d.mountpoint,
|
||||
options: {
|
||||
type: "pointer",
|
||||
type: 'pointer',
|
||||
packageId: d.dependencyId,
|
||||
volumeId: d.volumeId,
|
||||
subpath: d.subpath,
|
||||
readonly: d.readonly,
|
||||
filetype: d.type ?? "directory",
|
||||
filetype: d.type ?? 'directory',
|
||||
idmap: [],
|
||||
},
|
||||
})),
|
||||
@@ -163,6 +163,6 @@ export class Mounts<
|
||||
}
|
||||
}
|
||||
|
||||
const a = Mounts.of().mountBackups({ subpath: null, mountpoint: "" })
|
||||
const a = Mounts.of().mountBackups({ subpath: null, mountpoint: '' })
|
||||
// @ts-expect-error
|
||||
const m: Mounts<T.SDKManifest, never> = a
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { SubContainer, SubContainerOwned } from "../util/SubContainer"
|
||||
import { CommandController } from "./CommandController"
|
||||
import { Daemon } from "./Daemon"
|
||||
import { DaemonCommandType } from "./Daemons"
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { SubContainer, SubContainerOwned } from '../util/SubContainer'
|
||||
import { CommandController } from './CommandController'
|
||||
import { Daemon } from './Daemon'
|
||||
import { DaemonCommandType } from './Daemons'
|
||||
|
||||
/**
|
||||
* This is a wrapper around CommandController that has a state of off, where the command shouldn't be running
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { Daemons } from "./Daemons"
|
||||
import "../../../base/lib/interfaces/ServiceInterfaceBuilder"
|
||||
import "../../../base/lib/interfaces/Origin"
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { Daemons } from './Daemons'
|
||||
import '../../../base/lib/interfaces/ServiceInterfaceBuilder'
|
||||
import '../../../base/lib/interfaces/Origin'
|
||||
|
||||
export const DEFAULT_SIGTERM_TIMEOUT = 60_000
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { ImageConfig, ImageId, VolumeId } from "../../../base/lib/types"
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { ImageConfig, ImageId, VolumeId } from '../../../base/lib/types'
|
||||
import {
|
||||
SDKManifest,
|
||||
SDKImageInputSpec,
|
||||
} from "../../../base/lib/types/ManifestTypes"
|
||||
import { OSVersion } from "../StartSdk"
|
||||
import { VersionGraph } from "../version/VersionGraph"
|
||||
import { version as sdkVersion } from "../../package.json"
|
||||
} from '../../../base/lib/types/ManifestTypes'
|
||||
import { OSVersion } from '../StartSdk'
|
||||
import { VersionGraph } from '../version/VersionGraph'
|
||||
import { version as sdkVersion } from '../../package.json'
|
||||
|
||||
/**
|
||||
* @description Use this function to define critical information about your package
|
||||
@@ -42,10 +42,10 @@ export function buildManifest<
|
||||
): Manifest & T.Manifest {
|
||||
const images = Object.entries(manifest.images).reduce(
|
||||
(images, [k, v]) => {
|
||||
v.arch = v.arch ?? ["aarch64", "x86_64", "riscv64"]
|
||||
v.arch = v.arch ?? ['aarch64', 'x86_64', 'riscv64']
|
||||
if (v.emulateMissingAs === undefined)
|
||||
v.emulateMissingAs = (v.arch as string[]).includes("x86_64")
|
||||
? "x86_64"
|
||||
v.emulateMissingAs = (v.arch as string[]).includes('x86_64')
|
||||
? 'x86_64'
|
||||
: (v.arch[0] ?? null)
|
||||
v.nvidiaContainer = !!v.nvidiaContainer
|
||||
images[k] = v as ImageConfig
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { containsAddress } from "../health/checkFns/checkPortListening"
|
||||
import { containsAddress } from '../health/checkFns/checkPortListening'
|
||||
|
||||
describe("Health ready check", () => {
|
||||
it("Should be able to parse an example information", () => {
|
||||
describe('Health ready check', () => {
|
||||
it('Should be able to parse an example information', () => {
|
||||
let input = `
|
||||
|
||||
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { ServiceInterfaceBuilder } from "../../../base/lib/interfaces/ServiceInterfaceBuilder"
|
||||
import { Effects } from "../../../base/lib/Effects"
|
||||
import { sdk } from "../test/output.sdk"
|
||||
import { ServiceInterfaceBuilder } from '../../../base/lib/interfaces/ServiceInterfaceBuilder'
|
||||
import { Effects } from '../../../base/lib/Effects'
|
||||
import { sdk } from '../test/output.sdk'
|
||||
|
||||
describe("host", () => {
|
||||
test("Testing that the types work", () => {
|
||||
describe('host', () => {
|
||||
test('Testing that the types work', () => {
|
||||
async function test(effects: Effects) {
|
||||
const foo = sdk.MultiHost.of(effects, "foo")
|
||||
const foo = sdk.MultiHost.of(effects, 'foo')
|
||||
const fooOrigin = await foo.bindPort(80, {
|
||||
protocol: "http" as const,
|
||||
protocol: 'http' as const,
|
||||
preferredExternalPort: 80,
|
||||
})
|
||||
const fooInterface = new ServiceInterfaceBuilder({
|
||||
effects,
|
||||
name: "Foo",
|
||||
id: "foo",
|
||||
description: "A Foo",
|
||||
type: "ui",
|
||||
username: "bar",
|
||||
path: "/baz",
|
||||
query: { qux: "yes" },
|
||||
name: 'Foo',
|
||||
id: 'foo',
|
||||
description: 'A Foo',
|
||||
type: 'ui',
|
||||
username: 'bar',
|
||||
path: '/baz',
|
||||
query: { qux: 'yes' },
|
||||
schemeOverride: null,
|
||||
masked: false,
|
||||
})
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { testOutput } from "./output.test"
|
||||
import { InputSpec } from "../../../base/lib/actions/input/builder/inputSpec"
|
||||
import { List } from "../../../base/lib/actions/input/builder/list"
|
||||
import { Value } from "../../../base/lib/actions/input/builder/value"
|
||||
import { Variants } from "../../../base/lib/actions/input/builder/variants"
|
||||
import { ValueSpec } from "../../../base/lib/actions/input/inputSpecTypes"
|
||||
import { setupManifest } from "../manifest/setupManifest"
|
||||
import { StartSdk } from "../StartSdk"
|
||||
import { testOutput } from './output.test'
|
||||
import { InputSpec } from '../../../base/lib/actions/input/builder/inputSpec'
|
||||
import { List } from '../../../base/lib/actions/input/builder/list'
|
||||
import { Value } from '../../../base/lib/actions/input/builder/value'
|
||||
import { Variants } from '../../../base/lib/actions/input/builder/variants'
|
||||
import { ValueSpec } from '../../../base/lib/actions/input/inputSpecTypes'
|
||||
import { setupManifest } from '../manifest/setupManifest'
|
||||
import { StartSdk } from '../StartSdk'
|
||||
|
||||
describe("builder tests", () => {
|
||||
test("text", async () => {
|
||||
describe('builder tests', () => {
|
||||
test('text', async () => {
|
||||
const bitcoinPropertiesBuilt: {
|
||||
"peer-tor-address": ValueSpec
|
||||
'peer-tor-address': ValueSpec
|
||||
} = await InputSpec.of({
|
||||
"peer-tor-address": Value.text({
|
||||
name: "Peer tor address",
|
||||
description: "The Tor address of the peer interface",
|
||||
'peer-tor-address': Value.text({
|
||||
name: 'Peer tor address',
|
||||
description: 'The Tor address of the peer interface',
|
||||
required: true,
|
||||
default: null,
|
||||
}),
|
||||
@@ -22,9 +22,9 @@ describe("builder tests", () => {
|
||||
.build({} as any)
|
||||
.then((a) => a.spec)
|
||||
expect(bitcoinPropertiesBuilt).toMatchObject({
|
||||
"peer-tor-address": {
|
||||
type: "text",
|
||||
description: "The Tor address of the peer interface",
|
||||
'peer-tor-address': {
|
||||
type: 'text',
|
||||
description: 'The Tor address of the peer interface',
|
||||
warning: null,
|
||||
masked: false,
|
||||
placeholder: null,
|
||||
@@ -32,8 +32,8 @@ describe("builder tests", () => {
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
disabled: false,
|
||||
inputmode: "text",
|
||||
name: "Peer tor address",
|
||||
inputmode: 'text',
|
||||
name: 'Peer tor address',
|
||||
required: true,
|
||||
default: null,
|
||||
},
|
||||
@@ -41,10 +41,10 @@ describe("builder tests", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("values", () => {
|
||||
test("toggle", async () => {
|
||||
describe('values', () => {
|
||||
test('toggle', async () => {
|
||||
const value = await Value.toggle({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
description: null,
|
||||
warning: null,
|
||||
default: false,
|
||||
@@ -53,87 +53,87 @@ describe("values", () => {
|
||||
validator.unsafeCast(false)
|
||||
testOutput<typeof validator._TYPE, boolean>()(null)
|
||||
})
|
||||
test("text", async () => {
|
||||
test('text', async () => {
|
||||
const value = await Value.text({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: true,
|
||||
default: null,
|
||||
}).build({} as any)
|
||||
const validator = value.validator
|
||||
const rawIs = value.spec
|
||||
validator.unsafeCast("test text")
|
||||
validator.unsafeCast('test text')
|
||||
expect(() => validator.unsafeCast(null)).toThrowError()
|
||||
testOutput<typeof validator._TYPE, string>()(null)
|
||||
})
|
||||
test("text with default", async () => {
|
||||
test('text with default', async () => {
|
||||
const value = await Value.text({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: true,
|
||||
default: "this is a default value",
|
||||
default: 'this is a default value',
|
||||
}).build({} as any)
|
||||
const validator = value.validator
|
||||
const rawIs = value.spec
|
||||
validator.unsafeCast("test text")
|
||||
validator.unsafeCast('test text')
|
||||
expect(() => validator.unsafeCast(null)).toThrowError()
|
||||
testOutput<typeof validator._TYPE, string>()(null)
|
||||
})
|
||||
test("optional text", async () => {
|
||||
test('optional text', async () => {
|
||||
const value = await Value.text({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: null,
|
||||
}).build({} as any)
|
||||
const validator = value.validator
|
||||
const rawIs = value.spec
|
||||
validator.unsafeCast("test text")
|
||||
validator.unsafeCast('test text')
|
||||
validator.unsafeCast(null)
|
||||
testOutput<typeof validator._TYPE, string | null>()(null)
|
||||
})
|
||||
test("color", async () => {
|
||||
test('color', async () => {
|
||||
const value = await Value.color({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: null,
|
||||
description: null,
|
||||
warning: null,
|
||||
}).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast("#000000")
|
||||
validator.unsafeCast('#000000')
|
||||
testOutput<typeof validator._TYPE, string | null>()(null)
|
||||
})
|
||||
test("datetime", async () => {
|
||||
test('datetime', async () => {
|
||||
const value = await Value.datetime({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: true,
|
||||
default: null,
|
||||
description: null,
|
||||
warning: null,
|
||||
inputmode: "date",
|
||||
inputmode: 'date',
|
||||
min: null,
|
||||
max: null,
|
||||
}).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast("2021-01-01")
|
||||
validator.unsafeCast('2021-01-01')
|
||||
testOutput<typeof validator._TYPE, string>()(null)
|
||||
})
|
||||
test("optional datetime", async () => {
|
||||
test('optional datetime', async () => {
|
||||
const value = await Value.datetime({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: null,
|
||||
description: null,
|
||||
warning: null,
|
||||
inputmode: "date",
|
||||
inputmode: 'date',
|
||||
min: null,
|
||||
max: null,
|
||||
}).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast("2021-01-01")
|
||||
validator.unsafeCast('2021-01-01')
|
||||
testOutput<typeof validator._TYPE, string | null>()(null)
|
||||
})
|
||||
test("textarea", async () => {
|
||||
test('textarea', async () => {
|
||||
const value = await Value.textarea({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: null,
|
||||
description: null,
|
||||
@@ -145,12 +145,12 @@ describe("values", () => {
|
||||
placeholder: null,
|
||||
}).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast("test text")
|
||||
validator.unsafeCast('test text')
|
||||
testOutput<typeof validator._TYPE, string | null>()(null)
|
||||
})
|
||||
test("number", async () => {
|
||||
test('number', async () => {
|
||||
const value = await Value.number({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: true,
|
||||
default: null,
|
||||
integer: false,
|
||||
@@ -166,9 +166,9 @@ describe("values", () => {
|
||||
validator.unsafeCast(2)
|
||||
testOutput<typeof validator._TYPE, number>()(null)
|
||||
})
|
||||
test("optional number", async () => {
|
||||
test('optional number', async () => {
|
||||
const value = await Value.number({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: null,
|
||||
integer: false,
|
||||
@@ -184,45 +184,45 @@ describe("values", () => {
|
||||
validator.unsafeCast(2)
|
||||
testOutput<typeof validator._TYPE, number | null>()(null)
|
||||
})
|
||||
test("select", async () => {
|
||||
test('select', async () => {
|
||||
const value = await Value.select({
|
||||
name: "Testing",
|
||||
default: "a",
|
||||
name: 'Testing',
|
||||
default: 'a',
|
||||
values: {
|
||||
a: "A",
|
||||
b: "B",
|
||||
a: 'A',
|
||||
b: 'B',
|
||||
},
|
||||
description: null,
|
||||
warning: null,
|
||||
}).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast("a")
|
||||
validator.unsafeCast("b")
|
||||
expect(() => validator.unsafeCast("c")).toThrowError()
|
||||
testOutput<typeof validator._TYPE, "a" | "b">()(null)
|
||||
validator.unsafeCast('a')
|
||||
validator.unsafeCast('b')
|
||||
expect(() => validator.unsafeCast('c')).toThrowError()
|
||||
testOutput<typeof validator._TYPE, 'a' | 'b'>()(null)
|
||||
})
|
||||
test("nullable select", async () => {
|
||||
test('nullable select', async () => {
|
||||
const value = await Value.select({
|
||||
name: "Testing",
|
||||
default: "a",
|
||||
name: 'Testing',
|
||||
default: 'a',
|
||||
values: {
|
||||
a: "A",
|
||||
b: "B",
|
||||
a: 'A',
|
||||
b: 'B',
|
||||
},
|
||||
description: null,
|
||||
warning: null,
|
||||
}).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast("a")
|
||||
validator.unsafeCast("b")
|
||||
testOutput<typeof validator._TYPE, "a" | "b">()(null)
|
||||
validator.unsafeCast('a')
|
||||
validator.unsafeCast('b')
|
||||
testOutput<typeof validator._TYPE, 'a' | 'b'>()(null)
|
||||
})
|
||||
test("multiselect", async () => {
|
||||
test('multiselect', async () => {
|
||||
const value = await Value.multiselect({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
values: {
|
||||
a: "A",
|
||||
b: "B",
|
||||
a: 'A',
|
||||
b: 'B',
|
||||
},
|
||||
default: [],
|
||||
description: null,
|
||||
@@ -232,21 +232,21 @@ describe("values", () => {
|
||||
}).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast([])
|
||||
validator.unsafeCast(["a", "b"])
|
||||
validator.unsafeCast(['a', 'b'])
|
||||
|
||||
expect(() => validator.unsafeCast(["e"])).toThrowError()
|
||||
expect(() => validator.unsafeCast(['e'])).toThrowError()
|
||||
expect(() => validator.unsafeCast([4])).toThrowError()
|
||||
testOutput<typeof validator._TYPE, Array<"a" | "b">>()(null)
|
||||
testOutput<typeof validator._TYPE, Array<'a' | 'b'>>()(null)
|
||||
})
|
||||
test("object", async () => {
|
||||
test('object', async () => {
|
||||
const value = await Value.object(
|
||||
{
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
description: null,
|
||||
},
|
||||
InputSpec.of({
|
||||
a: Value.toggle({
|
||||
name: "test",
|
||||
name: 'test',
|
||||
description: null,
|
||||
warning: null,
|
||||
default: false,
|
||||
@@ -257,18 +257,18 @@ describe("values", () => {
|
||||
validator.unsafeCast({ a: true })
|
||||
testOutput<typeof validator._TYPE, { a: boolean }>()(null)
|
||||
})
|
||||
test("union", async () => {
|
||||
test('union', async () => {
|
||||
const value = await Value.union({
|
||||
name: "Testing",
|
||||
default: "a",
|
||||
name: 'Testing',
|
||||
default: 'a',
|
||||
description: null,
|
||||
warning: null,
|
||||
variants: Variants.of({
|
||||
a: {
|
||||
name: "a",
|
||||
name: 'a',
|
||||
spec: InputSpec.of({
|
||||
b: Value.toggle({
|
||||
name: "b",
|
||||
name: 'b',
|
||||
description: null,
|
||||
warning: null,
|
||||
default: false,
|
||||
@@ -278,12 +278,12 @@ describe("values", () => {
|
||||
}),
|
||||
}).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast({ selection: "a", value: { b: false } })
|
||||
validator.unsafeCast({ selection: 'a', value: { b: false } })
|
||||
type Test = typeof validator._TYPE
|
||||
testOutput<
|
||||
Test,
|
||||
{
|
||||
selection: "a"
|
||||
selection: 'a'
|
||||
value: {
|
||||
b: boolean
|
||||
}
|
||||
@@ -292,15 +292,15 @@ describe("values", () => {
|
||||
>()(null)
|
||||
})
|
||||
|
||||
describe("dynamic", () => {
|
||||
describe('dynamic', () => {
|
||||
const fakeOptions = {
|
||||
inputSpec: "inputSpec",
|
||||
effects: "effects",
|
||||
utils: "utils",
|
||||
inputSpec: 'inputSpec',
|
||||
effects: 'effects',
|
||||
utils: 'utils',
|
||||
} as any
|
||||
test("toggle", async () => {
|
||||
test('toggle', async () => {
|
||||
const value = await Value.dynamicToggle(async () => ({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
description: null,
|
||||
warning: null,
|
||||
default: false,
|
||||
@@ -310,98 +310,98 @@ describe("values", () => {
|
||||
expect(() => validator.unsafeCast(null)).toThrowError()
|
||||
testOutput<typeof validator._TYPE, boolean>()(null)
|
||||
expect(value.spec).toMatchObject({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
description: null,
|
||||
warning: null,
|
||||
default: false,
|
||||
})
|
||||
})
|
||||
test("text", async () => {
|
||||
test('text', async () => {
|
||||
const value = await Value.dynamicText(async () => ({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: null,
|
||||
})).build({} as any)
|
||||
const validator = value.validator
|
||||
const rawIs = value.spec
|
||||
validator.unsafeCast("test text")
|
||||
validator.unsafeCast('test text')
|
||||
validator.unsafeCast(null)
|
||||
testOutput<typeof validator._TYPE, string | null>()(null)
|
||||
expect(value.spec).toMatchObject({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: null,
|
||||
})
|
||||
})
|
||||
test("text with default", async () => {
|
||||
test('text with default', async () => {
|
||||
const value = await Value.dynamicText(async () => ({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: "this is a default value",
|
||||
default: 'this is a default value',
|
||||
})).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast("test text")
|
||||
validator.unsafeCast('test text')
|
||||
validator.unsafeCast(null)
|
||||
testOutput<typeof validator._TYPE, string | null>()(null)
|
||||
expect(value.spec).toMatchObject({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: "this is a default value",
|
||||
default: 'this is a default value',
|
||||
})
|
||||
})
|
||||
test("optional text", async () => {
|
||||
test('optional text', async () => {
|
||||
const value = await Value.dynamicText(async () => ({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: null,
|
||||
})).build({} as any)
|
||||
const validator = value.validator
|
||||
const rawIs = value.spec
|
||||
validator.unsafeCast("test text")
|
||||
validator.unsafeCast('test text')
|
||||
validator.unsafeCast(null)
|
||||
testOutput<typeof validator._TYPE, string | null>()(null)
|
||||
expect(value.spec).toMatchObject({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: null,
|
||||
})
|
||||
})
|
||||
test("color", async () => {
|
||||
test('color', async () => {
|
||||
const value = await Value.dynamicColor(async () => ({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: null,
|
||||
description: null,
|
||||
warning: null,
|
||||
})).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast("#000000")
|
||||
validator.unsafeCast('#000000')
|
||||
validator.unsafeCast(null)
|
||||
testOutput<typeof validator._TYPE, string | null>()(null)
|
||||
expect(value.spec).toMatchObject({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: null,
|
||||
description: null,
|
||||
warning: null,
|
||||
})
|
||||
})
|
||||
test("datetime", async () => {
|
||||
test('datetime', async () => {
|
||||
const sdk = StartSdk.of()
|
||||
.withManifest(
|
||||
setupManifest({
|
||||
id: "testOutput",
|
||||
title: "",
|
||||
license: "",
|
||||
wrapperRepo: "",
|
||||
upstreamRepo: "",
|
||||
supportSite: "",
|
||||
marketingSite: "",
|
||||
id: 'testOutput',
|
||||
title: '',
|
||||
license: '',
|
||||
wrapperRepo: '',
|
||||
upstreamRepo: '',
|
||||
supportSite: '',
|
||||
marketingSite: '',
|
||||
donationUrl: null,
|
||||
docsUrl: "",
|
||||
docsUrl: '',
|
||||
description: {
|
||||
short: "",
|
||||
long: "",
|
||||
short: '',
|
||||
long: '',
|
||||
},
|
||||
images: {},
|
||||
volumes: [],
|
||||
@@ -414,10 +414,10 @@ describe("values", () => {
|
||||
stop: null,
|
||||
},
|
||||
dependencies: {
|
||||
"remote-test": {
|
||||
description: "",
|
||||
'remote-test': {
|
||||
description: '',
|
||||
optional: true,
|
||||
s9pk: "https://example.com/remote-test.s9pk",
|
||||
s9pk: 'https://example.com/remote-test.s9pk',
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -426,28 +426,28 @@ describe("values", () => {
|
||||
|
||||
const value = await Value.dynamicDatetime(async ({ effects }) => {
|
||||
return {
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: null,
|
||||
inputmode: "date",
|
||||
inputmode: 'date',
|
||||
}
|
||||
}).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast("2021-01-01")
|
||||
validator.unsafeCast('2021-01-01')
|
||||
validator.unsafeCast(null)
|
||||
testOutput<typeof validator._TYPE, string | null>()(null)
|
||||
expect(value.spec).toMatchObject({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: null,
|
||||
description: null,
|
||||
warning: null,
|
||||
inputmode: "date",
|
||||
inputmode: 'date',
|
||||
})
|
||||
})
|
||||
test("textarea", async () => {
|
||||
test('textarea', async () => {
|
||||
const value = await Value.dynamicTextarea(async () => ({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: null,
|
||||
description: null,
|
||||
@@ -459,16 +459,16 @@ describe("values", () => {
|
||||
placeholder: null,
|
||||
})).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast("test text")
|
||||
validator.unsafeCast('test text')
|
||||
testOutput<typeof validator._TYPE, string | null>()(null)
|
||||
expect(value.spec).toMatchObject({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
})
|
||||
})
|
||||
test("number", async () => {
|
||||
test('number', async () => {
|
||||
const value = await Value.dynamicNumber(() => ({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
default: null,
|
||||
integer: false,
|
||||
@@ -483,38 +483,38 @@ describe("values", () => {
|
||||
const validator = value.validator
|
||||
validator.unsafeCast(2)
|
||||
validator.unsafeCast(null)
|
||||
expect(() => validator.unsafeCast("null")).toThrowError()
|
||||
expect(() => validator.unsafeCast('null')).toThrowError()
|
||||
testOutput<typeof validator._TYPE, number | null>()(null)
|
||||
expect(value.spec).toMatchObject({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
required: false,
|
||||
})
|
||||
})
|
||||
test("select", async () => {
|
||||
test('select', async () => {
|
||||
const value = await Value.dynamicSelect(() => ({
|
||||
name: "Testing",
|
||||
default: "a",
|
||||
name: 'Testing',
|
||||
default: 'a',
|
||||
values: {
|
||||
a: "A",
|
||||
b: "B",
|
||||
a: 'A',
|
||||
b: 'B',
|
||||
},
|
||||
description: null,
|
||||
warning: null,
|
||||
})).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast("a")
|
||||
validator.unsafeCast("b")
|
||||
testOutput<typeof validator._TYPE, "a" | "b">()(null)
|
||||
validator.unsafeCast('a')
|
||||
validator.unsafeCast('b')
|
||||
testOutput<typeof validator._TYPE, 'a' | 'b'>()(null)
|
||||
expect(value.spec).toMatchObject({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
})
|
||||
})
|
||||
test("multiselect", async () => {
|
||||
test('multiselect', async () => {
|
||||
const value = await Value.dynamicMultiselect(() => ({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
values: {
|
||||
a: "A",
|
||||
b: "B",
|
||||
a: 'A',
|
||||
b: 'B',
|
||||
},
|
||||
default: [],
|
||||
description: null,
|
||||
@@ -524,31 +524,31 @@ describe("values", () => {
|
||||
})).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast([])
|
||||
validator.unsafeCast(["a", "b"])
|
||||
validator.unsafeCast(['a', 'b'])
|
||||
|
||||
expect(() => validator.unsafeCast([4])).toThrowError()
|
||||
expect(() => validator.unsafeCast(null)).toThrowError()
|
||||
testOutput<typeof validator._TYPE, Array<"a" | "b">>()(null)
|
||||
testOutput<typeof validator._TYPE, Array<'a' | 'b'>>()(null)
|
||||
expect(value.spec).toMatchObject({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
default: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
describe("filtering", () => {
|
||||
test("union", async () => {
|
||||
describe('filtering', () => {
|
||||
test('union', async () => {
|
||||
const value = await Value.dynamicUnion(() => ({
|
||||
name: "Testing",
|
||||
default: "a",
|
||||
name: 'Testing',
|
||||
default: 'a',
|
||||
description: null,
|
||||
warning: null,
|
||||
disabled: ["a", "c"],
|
||||
disabled: ['a', 'c'],
|
||||
variants: Variants.of({
|
||||
a: {
|
||||
name: "a",
|
||||
name: 'a',
|
||||
spec: InputSpec.of({
|
||||
b: Value.toggle({
|
||||
name: "b",
|
||||
name: 'b',
|
||||
description: null,
|
||||
warning: null,
|
||||
default: false,
|
||||
@@ -556,10 +556,10 @@ describe("values", () => {
|
||||
}),
|
||||
},
|
||||
b: {
|
||||
name: "b",
|
||||
name: 'b',
|
||||
spec: InputSpec.of({
|
||||
b: Value.toggle({
|
||||
name: "b",
|
||||
name: 'b',
|
||||
description: null,
|
||||
warning: null,
|
||||
default: false,
|
||||
@@ -569,12 +569,12 @@ describe("values", () => {
|
||||
}),
|
||||
})).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast({ selection: "a", value: { b: false } })
|
||||
validator.unsafeCast({ selection: 'a', value: { b: false } })
|
||||
type Test = typeof validator._TYPE
|
||||
testOutput<
|
||||
Test,
|
||||
| {
|
||||
selection: "a"
|
||||
selection: 'a'
|
||||
value: {
|
||||
b: boolean
|
||||
}
|
||||
@@ -585,7 +585,7 @@ describe("values", () => {
|
||||
}
|
||||
}
|
||||
| {
|
||||
selection: "b"
|
||||
selection: 'b'
|
||||
value: {
|
||||
b: boolean
|
||||
}
|
||||
@@ -599,41 +599,41 @@ describe("values", () => {
|
||||
|
||||
const built = value.spec
|
||||
expect(built).toMatchObject({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
variants: {
|
||||
b: {},
|
||||
},
|
||||
})
|
||||
expect(built).toMatchObject({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
variants: {
|
||||
a: {},
|
||||
b: {},
|
||||
},
|
||||
})
|
||||
expect(built).toMatchObject({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
variants: {
|
||||
a: {},
|
||||
b: {},
|
||||
},
|
||||
disabled: ["a", "c"],
|
||||
disabled: ['a', 'c'],
|
||||
})
|
||||
})
|
||||
})
|
||||
test("dynamic union", async () => {
|
||||
test('dynamic union', async () => {
|
||||
const value = await Value.dynamicUnion(() => ({
|
||||
disabled: ["a", "c"],
|
||||
name: "Testing",
|
||||
default: "b",
|
||||
disabled: ['a', 'c'],
|
||||
name: 'Testing',
|
||||
default: 'b',
|
||||
description: null,
|
||||
warning: null,
|
||||
variants: Variants.of({
|
||||
a: {
|
||||
name: "a",
|
||||
name: 'a',
|
||||
spec: InputSpec.of({
|
||||
b: Value.toggle({
|
||||
name: "b",
|
||||
name: 'b',
|
||||
description: null,
|
||||
warning: null,
|
||||
default: false,
|
||||
@@ -641,10 +641,10 @@ describe("values", () => {
|
||||
}),
|
||||
},
|
||||
b: {
|
||||
name: "b",
|
||||
name: 'b',
|
||||
spec: InputSpec.of({
|
||||
b: Value.toggle({
|
||||
name: "b",
|
||||
name: 'b',
|
||||
description: null,
|
||||
warning: null,
|
||||
default: false,
|
||||
@@ -654,12 +654,12 @@ describe("values", () => {
|
||||
}),
|
||||
})).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast({ selection: "a", value: { b: false } })
|
||||
validator.unsafeCast({ selection: 'a', value: { b: false } })
|
||||
type Test = typeof validator._TYPE
|
||||
testOutput<
|
||||
Test,
|
||||
| {
|
||||
selection: "a"
|
||||
selection: 'a'
|
||||
value: {
|
||||
b: boolean
|
||||
}
|
||||
@@ -670,7 +670,7 @@ describe("values", () => {
|
||||
}
|
||||
}
|
||||
| {
|
||||
selection: "b"
|
||||
selection: 'b'
|
||||
value: {
|
||||
b: boolean
|
||||
}
|
||||
@@ -684,40 +684,40 @@ describe("values", () => {
|
||||
|
||||
const built = value.spec
|
||||
expect(built).toMatchObject({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
variants: {
|
||||
b: {},
|
||||
},
|
||||
})
|
||||
expect(built).toMatchObject({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
variants: {
|
||||
a: {},
|
||||
b: {},
|
||||
},
|
||||
})
|
||||
expect(built).toMatchObject({
|
||||
name: "Testing",
|
||||
name: 'Testing',
|
||||
variants: {
|
||||
a: {},
|
||||
b: {},
|
||||
},
|
||||
disabled: ["a", "c"],
|
||||
disabled: ['a', 'c'],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Builder List", () => {
|
||||
test("obj", async () => {
|
||||
describe('Builder List', () => {
|
||||
test('obj', async () => {
|
||||
const value = await Value.list(
|
||||
List.obj(
|
||||
{
|
||||
name: "test",
|
||||
name: 'test',
|
||||
},
|
||||
{
|
||||
spec: InputSpec.of({
|
||||
test: Value.toggle({
|
||||
name: "test",
|
||||
name: 'test',
|
||||
description: null,
|
||||
warning: null,
|
||||
default: false,
|
||||
@@ -730,11 +730,11 @@ describe("Builder List", () => {
|
||||
validator.unsafeCast([{ test: true }])
|
||||
testOutput<typeof validator._TYPE, { test: boolean }[]>()(null)
|
||||
})
|
||||
test("text", async () => {
|
||||
test('text', async () => {
|
||||
const value = await Value.list(
|
||||
List.text(
|
||||
{
|
||||
name: "test",
|
||||
name: 'test',
|
||||
},
|
||||
{
|
||||
patterns: [],
|
||||
@@ -742,53 +742,53 @@ describe("Builder List", () => {
|
||||
),
|
||||
).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast(["test", "text"])
|
||||
validator.unsafeCast(['test', 'text'])
|
||||
testOutput<typeof validator._TYPE, string[]>()(null)
|
||||
})
|
||||
describe("dynamic", () => {
|
||||
test("text", async () => {
|
||||
describe('dynamic', () => {
|
||||
test('text', async () => {
|
||||
const value = await Value.list(
|
||||
List.dynamicText(() => ({
|
||||
name: "test",
|
||||
name: 'test',
|
||||
spec: { patterns: [] },
|
||||
})),
|
||||
).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast(["test", "text"])
|
||||
validator.unsafeCast(['test', 'text'])
|
||||
expect(() => validator.unsafeCast([3, 4])).toThrowError()
|
||||
expect(() => validator.unsafeCast(null)).toThrowError()
|
||||
testOutput<typeof validator._TYPE, string[]>()(null)
|
||||
expect(value.spec).toMatchObject({
|
||||
name: "test",
|
||||
name: 'test',
|
||||
spec: { patterns: [] },
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Nested nullable values", () => {
|
||||
test("Testing text", async () => {
|
||||
describe('Nested nullable values', () => {
|
||||
test('Testing text', async () => {
|
||||
const value = await InputSpec.of({
|
||||
a: Value.text({
|
||||
name: "Temp Name",
|
||||
name: 'Temp Name',
|
||||
description:
|
||||
"If no name is provided, the name from inputSpec will be used",
|
||||
'If no name is provided, the name from inputSpec will be used',
|
||||
required: false,
|
||||
default: null,
|
||||
}),
|
||||
}).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast({ a: null })
|
||||
validator.unsafeCast({ a: "test" })
|
||||
validator.unsafeCast({ a: 'test' })
|
||||
expect(() => validator.unsafeCast({ a: 4 })).toThrowError()
|
||||
testOutput<typeof validator._TYPE, { a: string | null }>()(null)
|
||||
})
|
||||
test("Testing number", async () => {
|
||||
test('Testing number', async () => {
|
||||
const value = await InputSpec.of({
|
||||
a: Value.number({
|
||||
name: "Temp Name",
|
||||
name: 'Temp Name',
|
||||
description:
|
||||
"If no name is provided, the name from inputSpec will be used",
|
||||
'If no name is provided, the name from inputSpec will be used',
|
||||
required: false,
|
||||
default: null,
|
||||
warning: null,
|
||||
@@ -803,15 +803,15 @@ describe("Nested nullable values", () => {
|
||||
const validator = value.validator
|
||||
validator.unsafeCast({ a: null })
|
||||
validator.unsafeCast({ a: 5 })
|
||||
expect(() => validator.unsafeCast({ a: "4" })).toThrowError()
|
||||
expect(() => validator.unsafeCast({ a: '4' })).toThrowError()
|
||||
testOutput<typeof validator._TYPE, { a: number | null }>()(null)
|
||||
})
|
||||
test("Testing color", async () => {
|
||||
test('Testing color', async () => {
|
||||
const value = await InputSpec.of({
|
||||
a: Value.color({
|
||||
name: "Temp Name",
|
||||
name: 'Temp Name',
|
||||
description:
|
||||
"If no name is provided, the name from inputSpec will be used",
|
||||
'If no name is provided, the name from inputSpec will be used',
|
||||
required: false,
|
||||
default: null,
|
||||
warning: null,
|
||||
@@ -819,50 +819,50 @@ describe("Nested nullable values", () => {
|
||||
}).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast({ a: null })
|
||||
validator.unsafeCast({ a: "5" })
|
||||
validator.unsafeCast({ a: '5' })
|
||||
expect(() => validator.unsafeCast({ a: 4 })).toThrowError()
|
||||
testOutput<typeof validator._TYPE, { a: string | null }>()(null)
|
||||
})
|
||||
test("Testing select", async () => {
|
||||
test('Testing select', async () => {
|
||||
const value = await InputSpec.of({
|
||||
a: Value.select({
|
||||
name: "Temp Name",
|
||||
name: 'Temp Name',
|
||||
description:
|
||||
"If no name is provided, the name from inputSpec will be used",
|
||||
default: "a",
|
||||
'If no name is provided, the name from inputSpec will be used',
|
||||
default: 'a',
|
||||
warning: null,
|
||||
values: {
|
||||
a: "A",
|
||||
a: 'A',
|
||||
},
|
||||
}),
|
||||
}).build({} as any)
|
||||
const higher = await Value.select({
|
||||
name: "Temp Name",
|
||||
name: 'Temp Name',
|
||||
description:
|
||||
"If no name is provided, the name from inputSpec will be used",
|
||||
default: "a",
|
||||
'If no name is provided, the name from inputSpec will be used',
|
||||
default: 'a',
|
||||
warning: null,
|
||||
values: {
|
||||
a: "A",
|
||||
a: 'A',
|
||||
},
|
||||
}).build({} as any)
|
||||
|
||||
const validator = value.validator
|
||||
validator.unsafeCast({ a: "a" })
|
||||
expect(() => validator.unsafeCast({ a: "4" })).toThrowError()
|
||||
testOutput<typeof validator._TYPE, { a: "a" }>()(null)
|
||||
validator.unsafeCast({ a: 'a' })
|
||||
expect(() => validator.unsafeCast({ a: '4' })).toThrowError()
|
||||
testOutput<typeof validator._TYPE, { a: 'a' }>()(null)
|
||||
})
|
||||
test("Testing multiselect", async () => {
|
||||
test('Testing multiselect', async () => {
|
||||
const value = await InputSpec.of({
|
||||
a: Value.multiselect({
|
||||
name: "Temp Name",
|
||||
name: 'Temp Name',
|
||||
description:
|
||||
"If no name is provided, the name from inputSpec will be used",
|
||||
'If no name is provided, the name from inputSpec will be used',
|
||||
|
||||
warning: null,
|
||||
default: [],
|
||||
values: {
|
||||
a: "A",
|
||||
a: 'A',
|
||||
},
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
@@ -870,9 +870,9 @@ describe("Nested nullable values", () => {
|
||||
}).build({} as any)
|
||||
const validator = value.validator
|
||||
validator.unsafeCast({ a: [] })
|
||||
validator.unsafeCast({ a: ["a"] })
|
||||
expect(() => validator.unsafeCast({ a: ["4"] })).toThrowError()
|
||||
expect(() => validator.unsafeCast({ a: "4" })).toThrowError()
|
||||
testOutput<typeof validator._TYPE, { a: "a"[] }>()(null)
|
||||
validator.unsafeCast({ a: ['a'] })
|
||||
expect(() => validator.unsafeCast({ a: ['4'] })).toThrowError()
|
||||
expect(() => validator.unsafeCast({ a: '4' })).toThrowError()
|
||||
testOutput<typeof validator._TYPE, { a: 'a'[] }>()(null)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
import { oldSpecToBuilder } from "../../scripts/oldSpecToBuilder"
|
||||
import { oldSpecToBuilder } from '../../scripts/oldSpecToBuilder'
|
||||
|
||||
oldSpecToBuilder(
|
||||
// Make the location
|
||||
"./lib/test/output.ts",
|
||||
'./lib/test/output.ts',
|
||||
// Put the inputSpec here
|
||||
{
|
||||
mediasources: {
|
||||
type: "list",
|
||||
subtype: "enum",
|
||||
name: "Media Sources",
|
||||
description: "List of Media Sources to use with Jellyfin",
|
||||
range: "[1,*)",
|
||||
default: ["nextcloud"],
|
||||
type: 'list',
|
||||
subtype: 'enum',
|
||||
name: 'Media Sources',
|
||||
description: 'List of Media Sources to use with Jellyfin',
|
||||
range: '[1,*)',
|
||||
default: ['nextcloud'],
|
||||
spec: {
|
||||
values: ["nextcloud", "filebrowser"],
|
||||
"value-names": {
|
||||
nextcloud: "NextCloud",
|
||||
filebrowser: "File Browser",
|
||||
values: ['nextcloud', 'filebrowser'],
|
||||
'value-names': {
|
||||
nextcloud: 'NextCloud',
|
||||
filebrowser: 'File Browser',
|
||||
},
|
||||
},
|
||||
},
|
||||
testListUnion: {
|
||||
type: "list",
|
||||
subtype: "union",
|
||||
name: "Lightning Nodes",
|
||||
description: "List of Lightning Network node instances to manage",
|
||||
range: "[1,*)",
|
||||
default: ["lnd"],
|
||||
type: 'list',
|
||||
subtype: 'union',
|
||||
name: 'Lightning Nodes',
|
||||
description: 'List of Lightning Network node instances to manage',
|
||||
range: '[1,*)',
|
||||
default: ['lnd'],
|
||||
spec: {
|
||||
type: "string",
|
||||
"display-as": "{{name}}",
|
||||
"unique-by": "name",
|
||||
name: "Node Implementation",
|
||||
type: 'string',
|
||||
'display-as': '{{name}}',
|
||||
'unique-by': 'name',
|
||||
name: 'Node Implementation',
|
||||
tag: {
|
||||
id: "type",
|
||||
name: "Type",
|
||||
id: 'type',
|
||||
name: 'Type',
|
||||
description:
|
||||
"- LND: Lightning Network Daemon from Lightning Labs\n- CLN: Core Lightning from Blockstream\n",
|
||||
"variant-names": {
|
||||
lnd: "Lightning Network Daemon (LND)",
|
||||
"c-lightning": "Core Lightning (CLN)",
|
||||
'- LND: Lightning Network Daemon from Lightning Labs\n- CLN: Core Lightning from Blockstream\n',
|
||||
'variant-names': {
|
||||
lnd: 'Lightning Network Daemon (LND)',
|
||||
'c-lightning': 'Core Lightning (CLN)',
|
||||
},
|
||||
},
|
||||
default: "lnd",
|
||||
default: 'lnd',
|
||||
variants: {
|
||||
lnd: {
|
||||
name: {
|
||||
type: "string",
|
||||
name: "Node Name",
|
||||
description: "Name of this node in the list",
|
||||
default: "LND Wrapper",
|
||||
type: 'string',
|
||||
name: 'Node Name',
|
||||
description: 'Name of this node in the list',
|
||||
default: 'LND Wrapper',
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
@@ -57,262 +57,262 @@ oldSpecToBuilder(
|
||||
},
|
||||
},
|
||||
rpc: {
|
||||
type: "object",
|
||||
name: "RPC Settings",
|
||||
description: "RPC configuration options.",
|
||||
type: 'object',
|
||||
name: 'RPC Settings',
|
||||
description: 'RPC configuration options.',
|
||||
spec: {
|
||||
enable: {
|
||||
type: "boolean",
|
||||
name: "Enable",
|
||||
description: "Allow remote RPC requests.",
|
||||
type: 'boolean',
|
||||
name: 'Enable',
|
||||
description: 'Allow remote RPC requests.',
|
||||
default: true,
|
||||
},
|
||||
username: {
|
||||
type: "string",
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
name: "Username",
|
||||
description: "The username for connecting to Bitcoin over RPC.",
|
||||
default: "bitcoin",
|
||||
name: 'Username',
|
||||
description: 'The username for connecting to Bitcoin over RPC.',
|
||||
default: 'bitcoin',
|
||||
masked: true,
|
||||
pattern: "^[a-zA-Z0-9_]+$",
|
||||
"pattern-description":
|
||||
"Must be alphanumeric (can contain underscore).",
|
||||
pattern: '^[a-zA-Z0-9_]+$',
|
||||
'pattern-description':
|
||||
'Must be alphanumeric (can contain underscore).',
|
||||
},
|
||||
password: {
|
||||
type: "string",
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
name: "RPC Password",
|
||||
description: "The password for connecting to Bitcoin over RPC.",
|
||||
name: 'RPC Password',
|
||||
description: 'The password for connecting to Bitcoin over RPC.',
|
||||
default: {
|
||||
charset: "a-z,2-7",
|
||||
charset: 'a-z,2-7',
|
||||
len: 20,
|
||||
},
|
||||
pattern: '^[^\\n"]*$',
|
||||
"pattern-description":
|
||||
"Must not contain newline or quote characters.",
|
||||
'pattern-description':
|
||||
'Must not contain newline or quote characters.',
|
||||
copyable: true,
|
||||
masked: true,
|
||||
},
|
||||
bio: {
|
||||
type: "string",
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
name: "Username",
|
||||
description: "The username for connecting to Bitcoin over RPC.",
|
||||
default: "bitcoin",
|
||||
name: 'Username',
|
||||
description: 'The username for connecting to Bitcoin over RPC.',
|
||||
default: 'bitcoin',
|
||||
masked: true,
|
||||
pattern: "^[a-zA-Z0-9_]+$",
|
||||
"pattern-description":
|
||||
"Must be alphanumeric (can contain underscore).",
|
||||
pattern: '^[a-zA-Z0-9_]+$',
|
||||
'pattern-description':
|
||||
'Must be alphanumeric (can contain underscore).',
|
||||
textarea: true,
|
||||
},
|
||||
advanced: {
|
||||
type: "object",
|
||||
name: "Advanced",
|
||||
description: "Advanced RPC Settings",
|
||||
type: 'object',
|
||||
name: 'Advanced',
|
||||
description: 'Advanced RPC Settings',
|
||||
spec: {
|
||||
auth: {
|
||||
name: "Authorization",
|
||||
name: 'Authorization',
|
||||
description:
|
||||
"Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.",
|
||||
type: "list",
|
||||
subtype: "string",
|
||||
'Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.',
|
||||
type: 'list',
|
||||
subtype: 'string',
|
||||
default: [],
|
||||
spec: {
|
||||
pattern:
|
||||
"^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$",
|
||||
"pattern-description":
|
||||
'^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$',
|
||||
'pattern-description':
|
||||
'Each item must be of the form "<USERNAME>:<SALT>$<HASH>".',
|
||||
masked: false,
|
||||
},
|
||||
range: "[0,*)",
|
||||
range: '[0,*)',
|
||||
},
|
||||
serialversion: {
|
||||
name: "Serialization Version",
|
||||
name: 'Serialization Version',
|
||||
description:
|
||||
"Return raw transaction or block hex with Segwit or non-SegWit serialization.",
|
||||
type: "enum",
|
||||
values: ["non-segwit", "segwit"],
|
||||
"value-names": {},
|
||||
default: "segwit",
|
||||
'Return raw transaction or block hex with Segwit or non-SegWit serialization.',
|
||||
type: 'enum',
|
||||
values: ['non-segwit', 'segwit'],
|
||||
'value-names': {},
|
||||
default: 'segwit',
|
||||
},
|
||||
servertimeout: {
|
||||
name: "Rpc Server Timeout",
|
||||
name: 'Rpc Server Timeout',
|
||||
description:
|
||||
"Number of seconds after which an uncompleted RPC call will time out.",
|
||||
type: "number",
|
||||
'Number of seconds after which an uncompleted RPC call will time out.',
|
||||
type: 'number',
|
||||
nullable: false,
|
||||
range: "[5,300]",
|
||||
range: '[5,300]',
|
||||
integral: true,
|
||||
units: "seconds",
|
||||
units: 'seconds',
|
||||
default: 30,
|
||||
},
|
||||
threads: {
|
||||
name: "Threads",
|
||||
name: 'Threads',
|
||||
description:
|
||||
"Set the number of threads for handling RPC calls. You may wish to increase this if you are making lots of calls via an integration.",
|
||||
type: "number",
|
||||
'Set the number of threads for handling RPC calls. You may wish to increase this if you are making lots of calls via an integration.',
|
||||
type: 'number',
|
||||
nullable: false,
|
||||
default: 16,
|
||||
range: "[1,64]",
|
||||
range: '[1,64]',
|
||||
integral: true,
|
||||
},
|
||||
workqueue: {
|
||||
name: "Work Queue",
|
||||
name: 'Work Queue',
|
||||
description:
|
||||
"Set the depth of the work queue to service RPC calls. Determines how long the backlog of RPC requests can get before it just rejects new ones.",
|
||||
type: "number",
|
||||
'Set the depth of the work queue to service RPC calls. Determines how long the backlog of RPC requests can get before it just rejects new ones.',
|
||||
type: 'number',
|
||||
nullable: false,
|
||||
default: 128,
|
||||
range: "[8,256]",
|
||||
range: '[8,256]',
|
||||
integral: true,
|
||||
units: "requests",
|
||||
units: 'requests',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"zmq-enabled": {
|
||||
type: "boolean",
|
||||
name: "ZeroMQ Enabled",
|
||||
description: "Enable the ZeroMQ interface",
|
||||
'zmq-enabled': {
|
||||
type: 'boolean',
|
||||
name: 'ZeroMQ Enabled',
|
||||
description: 'Enable the ZeroMQ interface',
|
||||
default: true,
|
||||
},
|
||||
txindex: {
|
||||
type: "boolean",
|
||||
name: "Transaction Index",
|
||||
description: "Enable the Transaction Index (txindex)",
|
||||
type: 'boolean',
|
||||
name: 'Transaction Index',
|
||||
description: 'Enable the Transaction Index (txindex)',
|
||||
default: true,
|
||||
},
|
||||
wallet: {
|
||||
type: "object",
|
||||
name: "Wallet",
|
||||
description: "Wallet Settings",
|
||||
type: 'object',
|
||||
name: 'Wallet',
|
||||
description: 'Wallet Settings',
|
||||
spec: {
|
||||
enable: {
|
||||
name: "Enable Wallet",
|
||||
description: "Load the wallet and enable wallet RPC calls.",
|
||||
type: "boolean",
|
||||
name: 'Enable Wallet',
|
||||
description: 'Load the wallet and enable wallet RPC calls.',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
avoidpartialspends: {
|
||||
name: "Avoid Partial Spends",
|
||||
name: 'Avoid Partial Spends',
|
||||
description:
|
||||
"Group outputs by address, selecting all or none, instead of selecting on a per-output basis. This improves privacy at the expense of higher transaction fees.",
|
||||
type: "boolean",
|
||||
'Group outputs by address, selecting all or none, instead of selecting on a per-output basis. This improves privacy at the expense of higher transaction fees.',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
discardfee: {
|
||||
name: "Discard Change Tolerance",
|
||||
name: 'Discard Change Tolerance',
|
||||
description:
|
||||
"The fee rate (in BTC/kB) that indicates your tolerance for discarding change by adding it to the fee.",
|
||||
type: "number",
|
||||
'The fee rate (in BTC/kB) that indicates your tolerance for discarding change by adding it to the fee.',
|
||||
type: 'number',
|
||||
nullable: false,
|
||||
default: 0.0001,
|
||||
range: "[0,.01]",
|
||||
range: '[0,.01]',
|
||||
integral: false,
|
||||
units: "BTC/kB",
|
||||
units: 'BTC/kB',
|
||||
},
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
type: "object",
|
||||
name: "Advanced",
|
||||
description: "Advanced Settings",
|
||||
type: 'object',
|
||||
name: 'Advanced',
|
||||
description: 'Advanced Settings',
|
||||
spec: {
|
||||
mempool: {
|
||||
type: "object",
|
||||
name: "Mempool",
|
||||
description: "Mempool Settings",
|
||||
type: 'object',
|
||||
name: 'Mempool',
|
||||
description: 'Mempool Settings',
|
||||
spec: {
|
||||
mempoolfullrbf: {
|
||||
name: "Enable Full RBF",
|
||||
name: 'Enable Full RBF',
|
||||
description:
|
||||
"Policy for your node to use for relaying and mining unconfirmed transactions. For details, see https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-24.0.md#notice-of-new-option-for-transaction-replacement-policies",
|
||||
type: "boolean",
|
||||
'Policy for your node to use for relaying and mining unconfirmed transactions. For details, see https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-24.0.md#notice-of-new-option-for-transaction-replacement-policies',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
persistmempool: {
|
||||
type: "boolean",
|
||||
name: "Persist Mempool",
|
||||
description: "Save the mempool on shutdown and load on restart.",
|
||||
type: 'boolean',
|
||||
name: 'Persist Mempool',
|
||||
description: 'Save the mempool on shutdown and load on restart.',
|
||||
default: true,
|
||||
},
|
||||
maxmempool: {
|
||||
type: "number",
|
||||
type: 'number',
|
||||
nullable: false,
|
||||
name: "Max Mempool Size",
|
||||
name: 'Max Mempool Size',
|
||||
description:
|
||||
"Keep the transaction memory pool below <n> megabytes.",
|
||||
range: "[1,*)",
|
||||
'Keep the transaction memory pool below <n> megabytes.',
|
||||
range: '[1,*)',
|
||||
integral: true,
|
||||
units: "MiB",
|
||||
units: 'MiB',
|
||||
default: 300,
|
||||
},
|
||||
mempoolexpiry: {
|
||||
type: "number",
|
||||
type: 'number',
|
||||
nullable: false,
|
||||
name: "Mempool Expiration",
|
||||
name: 'Mempool Expiration',
|
||||
description:
|
||||
"Do not keep transactions in the mempool longer than <n> hours.",
|
||||
range: "[1,*)",
|
||||
'Do not keep transactions in the mempool longer than <n> hours.',
|
||||
range: '[1,*)',
|
||||
integral: true,
|
||||
units: "Hr",
|
||||
units: 'Hr',
|
||||
default: 336,
|
||||
},
|
||||
},
|
||||
},
|
||||
peers: {
|
||||
type: "object",
|
||||
name: "Peers",
|
||||
description: "Peer Connection Settings",
|
||||
type: 'object',
|
||||
name: 'Peers',
|
||||
description: 'Peer Connection Settings',
|
||||
spec: {
|
||||
listen: {
|
||||
type: "boolean",
|
||||
name: "Make Public",
|
||||
type: 'boolean',
|
||||
name: 'Make Public',
|
||||
description:
|
||||
"Allow other nodes to find your server on the network.",
|
||||
'Allow other nodes to find your server on the network.',
|
||||
default: true,
|
||||
},
|
||||
onlyconnect: {
|
||||
type: "boolean",
|
||||
name: "Disable Peer Discovery",
|
||||
description: "Only connect to specified peers.",
|
||||
type: 'boolean',
|
||||
name: 'Disable Peer Discovery',
|
||||
description: 'Only connect to specified peers.',
|
||||
default: false,
|
||||
},
|
||||
onlyonion: {
|
||||
type: "boolean",
|
||||
name: "Disable Clearnet",
|
||||
description: "Only connect to peers over Tor.",
|
||||
type: 'boolean',
|
||||
name: 'Disable Clearnet',
|
||||
description: 'Only connect to peers over Tor.',
|
||||
default: false,
|
||||
},
|
||||
addnode: {
|
||||
name: "Add Nodes",
|
||||
description: "Add addresses of nodes to connect to.",
|
||||
type: "list",
|
||||
subtype: "object",
|
||||
range: "[0,*)",
|
||||
name: 'Add Nodes',
|
||||
description: 'Add addresses of nodes to connect to.',
|
||||
type: 'list',
|
||||
subtype: 'object',
|
||||
range: '[0,*)',
|
||||
default: [],
|
||||
spec: {
|
||||
"unique-by": null,
|
||||
'unique-by': null,
|
||||
spec: {
|
||||
hostname: {
|
||||
type: "string",
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
name: "Hostname",
|
||||
description: "Domain or IP address of bitcoin peer",
|
||||
name: 'Hostname',
|
||||
description: 'Domain or IP address of bitcoin peer',
|
||||
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]$))",
|
||||
"pattern-description":
|
||||
'(^(?:(?: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]$))',
|
||||
'pattern-description':
|
||||
"Must be either a domain name, or an IPv4 or IPv6 address. Do not include protocol scheme (eg 'http://') or port.",
|
||||
masked: false,
|
||||
},
|
||||
port: {
|
||||
type: "number",
|
||||
type: 'number',
|
||||
nullable: true,
|
||||
name: "Port",
|
||||
name: 'Port',
|
||||
description:
|
||||
"Port that peer is listening on for inbound p2p connections",
|
||||
range: "[0,65535]",
|
||||
'Port that peer is listening on for inbound p2p connections',
|
||||
range: '[0,65535]',
|
||||
integral: true,
|
||||
},
|
||||
},
|
||||
@@ -321,81 +321,81 @@ oldSpecToBuilder(
|
||||
},
|
||||
},
|
||||
dbcache: {
|
||||
type: "number",
|
||||
type: 'number',
|
||||
nullable: true,
|
||||
name: "Database Cache",
|
||||
name: 'Database Cache',
|
||||
description:
|
||||
"How much RAM to allocate for caching the TXO set. Higher values improve syncing performance, but increase your chance of using up all your system's memory or corrupting your database in the event of an ungraceful shutdown. Set this high but comfortably below your system's total RAM during IBD, then turn down to 450 (or leave blank) once the sync completes.",
|
||||
warning:
|
||||
"WARNING: Increasing this value results in a higher chance of ungraceful shutdowns, which can leave your node unusable if it happens during the initial block download. Use this setting with caution. Be sure to set this back to the default (450 or leave blank) once your node is synced. DO NOT press the STOP button if your dbcache is large. Instead, set this number back to the default, hit save, and wait for bitcoind to restart on its own.",
|
||||
range: "(0,*)",
|
||||
'WARNING: Increasing this value results in a higher chance of ungraceful shutdowns, which can leave your node unusable if it happens during the initial block download. Use this setting with caution. Be sure to set this back to the default (450 or leave blank) once your node is synced. DO NOT press the STOP button if your dbcache is large. Instead, set this number back to the default, hit save, and wait for bitcoind to restart on its own.',
|
||||
range: '(0,*)',
|
||||
integral: true,
|
||||
units: "MiB",
|
||||
units: 'MiB',
|
||||
},
|
||||
pruning: {
|
||||
type: "union",
|
||||
name: "Pruning Settings",
|
||||
type: 'union',
|
||||
name: 'Pruning Settings',
|
||||
description:
|
||||
"Blockchain Pruning Options\nReduce the blockchain size on disk\n",
|
||||
'Blockchain Pruning Options\nReduce the blockchain size on disk\n',
|
||||
warning:
|
||||
"If you set pruning to Manual and your disk is smaller than the total size of the blockchain, you MUST have something running that prunes these blocks or you may overfill your disk!\nDisabling pruning will convert your node into a full archival node. This requires a resync of the entire blockchain, a process that may take several days. Make sure you have enough free disk space or you may fill up your disk.\n",
|
||||
'If you set pruning to Manual and your disk is smaller than the total size of the blockchain, you MUST have something running that prunes these blocks or you may overfill your disk!\nDisabling pruning will convert your node into a full archival node. This requires a resync of the entire blockchain, a process that may take several days. Make sure you have enough free disk space or you may fill up your disk.\n',
|
||||
tag: {
|
||||
id: "mode",
|
||||
name: "Pruning Mode",
|
||||
id: 'mode',
|
||||
name: 'Pruning Mode',
|
||||
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',
|
||||
"variant-names": {
|
||||
disabled: "Disabled",
|
||||
automatic: "Automatic",
|
||||
manual: "Manual",
|
||||
'variant-names': {
|
||||
disabled: 'Disabled',
|
||||
automatic: 'Automatic',
|
||||
manual: 'Manual',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
disabled: {},
|
||||
automatic: {
|
||||
size: {
|
||||
type: "number",
|
||||
type: 'number',
|
||||
nullable: false,
|
||||
name: "Max Chain Size",
|
||||
description: "Limit of blockchain size on disk.",
|
||||
name: 'Max Chain Size',
|
||||
description: 'Limit of blockchain size on disk.',
|
||||
warning:
|
||||
"Increasing this value will require re-syncing your node.",
|
||||
'Increasing this value will require re-syncing your node.',
|
||||
default: 550,
|
||||
range: "[550,1000000)",
|
||||
range: '[550,1000000)',
|
||||
integral: true,
|
||||
units: "MiB",
|
||||
units: 'MiB',
|
||||
},
|
||||
},
|
||||
manual: {
|
||||
size: {
|
||||
type: "number",
|
||||
type: 'number',
|
||||
nullable: false,
|
||||
name: "Failsafe Chain Size",
|
||||
description: "Prune blockchain if size expands beyond this.",
|
||||
name: 'Failsafe Chain Size',
|
||||
description: 'Prune blockchain if size expands beyond this.',
|
||||
default: 65536,
|
||||
range: "[550,1000000)",
|
||||
range: '[550,1000000)',
|
||||
integral: true,
|
||||
units: "MiB",
|
||||
units: 'MiB',
|
||||
},
|
||||
},
|
||||
},
|
||||
default: "disabled",
|
||||
default: 'disabled',
|
||||
},
|
||||
blockfilters: {
|
||||
type: "object",
|
||||
name: "Block Filters",
|
||||
description: "Settings for storing and serving compact block filters",
|
||||
type: 'object',
|
||||
name: 'Block Filters',
|
||||
description: 'Settings for storing and serving compact block filters',
|
||||
spec: {
|
||||
blockfilterindex: {
|
||||
type: "boolean",
|
||||
name: "Compute Compact Block Filters (BIP158)",
|
||||
type: 'boolean',
|
||||
name: 'Compute Compact Block Filters (BIP158)',
|
||||
description:
|
||||
"Generate Compact Block Filters during initial sync (IBD) to enable 'getblockfilter' RPC. This is useful if dependent services need block filters to efficiently scan for addresses/transactions etc.",
|
||||
default: true,
|
||||
},
|
||||
peerblockfilters: {
|
||||
type: "boolean",
|
||||
name: "Serve Compact Block Filters to Peers (BIP157)",
|
||||
type: 'boolean',
|
||||
name: 'Serve Compact Block Filters to Peers (BIP157)',
|
||||
description:
|
||||
"Serve Compact Block Filters as a peer service to other nodes on the network. This is useful if you wish to connect an SPV client to your node to make it efficient to scan transactions without having to download all block data. 'Compute Compact Block Filters (BIP158)' is required.",
|
||||
default: false,
|
||||
@@ -403,17 +403,17 @@ oldSpecToBuilder(
|
||||
},
|
||||
},
|
||||
bloomfilters: {
|
||||
type: "object",
|
||||
name: "Bloom Filters (BIP37)",
|
||||
description: "Setting for serving Bloom Filters",
|
||||
type: 'object',
|
||||
name: 'Bloom Filters (BIP37)',
|
||||
description: 'Setting for serving Bloom Filters',
|
||||
spec: {
|
||||
peerbloomfilters: {
|
||||
type: "boolean",
|
||||
name: "Serve Bloom Filters to Peers",
|
||||
type: 'boolean',
|
||||
name: 'Serve Bloom Filters to Peers',
|
||||
description:
|
||||
"Peers have the option of setting filters on each connection they make after the version handshake has completed. Bloom filters are for clients implementing SPV (Simplified Payment Verification) that want to check that block headers connect together correctly, without needing to verify the full blockchain. The client must trust that the transactions in the chain are in fact valid. It is highly recommended AGAINST using for anything except Bisq integration.",
|
||||
'Peers have the option of setting filters on each connection they make after the version handshake has completed. Bloom filters are for clients implementing SPV (Simplified Payment Verification) that want to check that block headers connect together correctly, without needing to verify the full blockchain. The client must trust that the transactions in the chain are in fact valid. It is highly recommended AGAINST using for anything except Bisq integration.',
|
||||
warning:
|
||||
"This is ONLY for use with Bisq integration, please use Block Filters for all other applications.",
|
||||
'This is ONLY for use with Bisq integration, please use Block Filters for all other applications.',
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
@@ -423,6 +423,6 @@ oldSpecToBuilder(
|
||||
},
|
||||
{
|
||||
// convert this to `start-sdk/lib` for conversions
|
||||
StartSdk: "./output.sdk",
|
||||
StartSdk: './output.sdk',
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import { StartSdk } from "../StartSdk"
|
||||
import { setupManifest } from "../manifest/setupManifest"
|
||||
import { VersionGraph } from "../version/VersionGraph"
|
||||
import { StartSdk } from '../StartSdk'
|
||||
import { setupManifest } from '../manifest/setupManifest'
|
||||
import { VersionGraph } from '../version/VersionGraph'
|
||||
|
||||
export type Manifest = any
|
||||
export const sdk = StartSdk.of()
|
||||
.withManifest(
|
||||
setupManifest({
|
||||
id: "testOutput",
|
||||
title: "",
|
||||
license: "",
|
||||
wrapperRepo: "",
|
||||
upstreamRepo: "",
|
||||
supportSite: "",
|
||||
marketingSite: "",
|
||||
id: 'testOutput',
|
||||
title: '',
|
||||
license: '',
|
||||
wrapperRepo: '',
|
||||
upstreamRepo: '',
|
||||
supportSite: '',
|
||||
marketingSite: '',
|
||||
donationUrl: null,
|
||||
docsUrl: "",
|
||||
docsUrl: '',
|
||||
description: {
|
||||
short: "",
|
||||
long: "",
|
||||
short: '',
|
||||
long: '',
|
||||
},
|
||||
images: {
|
||||
main: {
|
||||
source: {
|
||||
dockerTag: "start9/hello-world",
|
||||
dockerTag: 'start9/hello-world',
|
||||
},
|
||||
arch: ["aarch64", "x86_64"],
|
||||
emulateMissingAs: "aarch64",
|
||||
arch: ['aarch64', 'x86_64'],
|
||||
emulateMissingAs: 'aarch64',
|
||||
},
|
||||
},
|
||||
volumes: [],
|
||||
@@ -38,10 +38,10 @@ export const sdk = StartSdk.of()
|
||||
stop: null,
|
||||
},
|
||||
dependencies: {
|
||||
"remote-test": {
|
||||
description: "",
|
||||
'remote-test': {
|
||||
description: '',
|
||||
optional: false,
|
||||
s9pk: "https://example.com/remote-test.s9pk",
|
||||
s9pk: 'https://example.com/remote-test.s9pk',
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inputSpecSpec, InputSpecSpec } from "./output"
|
||||
import * as _I from "../index"
|
||||
import { camelCase } from "../../scripts/oldSpecToBuilder"
|
||||
import { deepMerge } from "../../../base/lib/util"
|
||||
import { inputSpecSpec, InputSpecSpec } from './output'
|
||||
import * as _I from '../index'
|
||||
import { camelCase } from '../../scripts/oldSpecToBuilder'
|
||||
import { deepMerge } from '../../../base/lib/util'
|
||||
|
||||
export type IfEquals<T, U, Y = unknown, N = never> =
|
||||
(<G>() => G extends T ? 1 : 2) extends <G>() => G extends U ? 1 : 2 ? Y : N
|
||||
@@ -10,60 +10,60 @@ export function testOutput<A, B>(): (c: IfEquals<A, B>) => null {
|
||||
}
|
||||
|
||||
/// Testing the types of the input spec
|
||||
testOutput<InputSpecSpec["rpc"]["enable"], boolean>()(null)
|
||||
testOutput<InputSpecSpec["rpc"]["username"], string>()(null)
|
||||
testOutput<InputSpecSpec["rpc"]["username"], string>()(null)
|
||||
testOutput<InputSpecSpec['rpc']['enable'], boolean>()(null)
|
||||
testOutput<InputSpecSpec['rpc']['username'], string>()(null)
|
||||
testOutput<InputSpecSpec['rpc']['username'], string>()(null)
|
||||
|
||||
testOutput<InputSpecSpec["rpc"]["advanced"]["auth"], string[]>()(null)
|
||||
testOutput<InputSpecSpec['rpc']['advanced']['auth'], string[]>()(null)
|
||||
testOutput<
|
||||
InputSpecSpec["rpc"]["advanced"]["serialversion"],
|
||||
"segwit" | "non-segwit"
|
||||
InputSpecSpec['rpc']['advanced']['serialversion'],
|
||||
'segwit' | 'non-segwit'
|
||||
>()(null)
|
||||
testOutput<InputSpecSpec["rpc"]["advanced"]["servertimeout"], number>()(null)
|
||||
testOutput<InputSpecSpec['rpc']['advanced']['servertimeout'], number>()(null)
|
||||
testOutput<
|
||||
InputSpecSpec["advanced"]["peers"]["addnode"][0]["hostname"],
|
||||
InputSpecSpec['advanced']['peers']['addnode'][0]['hostname'],
|
||||
string | null
|
||||
>()(null)
|
||||
testOutput<
|
||||
InputSpecSpec["testListUnion"][0]["union"]["value"]["name"],
|
||||
InputSpecSpec['testListUnion'][0]['union']['value']['name'],
|
||||
string
|
||||
>()(null)
|
||||
testOutput<InputSpecSpec["testListUnion"][0]["union"]["selection"], "lnd">()(
|
||||
testOutput<InputSpecSpec['testListUnion'][0]['union']['selection'], 'lnd'>()(
|
||||
null,
|
||||
)
|
||||
testOutput<InputSpecSpec["mediasources"], Array<"filebrowser" | "nextcloud">>()(
|
||||
testOutput<InputSpecSpec['mediasources'], Array<'filebrowser' | 'nextcloud'>>()(
|
||||
null,
|
||||
)
|
||||
|
||||
// @ts-expect-error Because enable should be a boolean
|
||||
testOutput<InputSpecSpec["rpc"]["enable"], string>()(null)
|
||||
testOutput<InputSpecSpec['rpc']['enable'], string>()(null)
|
||||
// prettier-ignore
|
||||
// @ts-expect-error Expect that the string is the one above
|
||||
testOutput<InputSpecSpec["testListUnion"][0]['selection']['selection'], "selection">()(null);
|
||||
|
||||
/// Here we test the output of the matchInputSpecSpec function
|
||||
describe("Inputs", () => {
|
||||
describe('Inputs', () => {
|
||||
const validInput: InputSpecSpec = {
|
||||
mediasources: ["filebrowser"],
|
||||
mediasources: ['filebrowser'],
|
||||
testListUnion: [
|
||||
{
|
||||
union: { selection: "lnd", value: { name: "string" } },
|
||||
union: { selection: 'lnd', value: { name: 'string' } },
|
||||
},
|
||||
],
|
||||
rpc: {
|
||||
enable: true,
|
||||
bio: "This is a bio",
|
||||
username: "test",
|
||||
password: "test",
|
||||
bio: 'This is a bio',
|
||||
username: 'test',
|
||||
password: 'test',
|
||||
advanced: {
|
||||
auth: ["test"],
|
||||
serialversion: "segwit",
|
||||
auth: ['test'],
|
||||
serialversion: 'segwit',
|
||||
servertimeout: 6,
|
||||
threads: 3,
|
||||
workqueue: 9,
|
||||
},
|
||||
},
|
||||
"zmq-enabled": false,
|
||||
'zmq-enabled': false,
|
||||
txindex: false,
|
||||
wallet: { enable: false, avoidpartialspends: false, discardfee: 0.0001 },
|
||||
advanced: {
|
||||
@@ -79,14 +79,14 @@ describe("Inputs", () => {
|
||||
onlyonion: true,
|
||||
addnode: [
|
||||
{
|
||||
hostname: "test",
|
||||
hostname: 'test',
|
||||
port: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
dbcache: 5,
|
||||
pruning: {
|
||||
selection: "disabled",
|
||||
selection: 'disabled',
|
||||
value: { disabled: {} },
|
||||
},
|
||||
blockfilters: {
|
||||
@@ -97,52 +97,52 @@ describe("Inputs", () => {
|
||||
},
|
||||
}
|
||||
|
||||
test("test valid input", async () => {
|
||||
test('test valid input', async () => {
|
||||
const { validator } = await inputSpecSpec.build({} as any)
|
||||
const output = validator.unsafeCast(validInput)
|
||||
expect(output).toEqual(validInput)
|
||||
})
|
||||
test("test no longer care about the conversion of min/max and validating", async () => {
|
||||
test('test no longer care about the conversion of min/max and validating', async () => {
|
||||
const { validator } = await inputSpecSpec.build({} as any)
|
||||
validator.unsafeCast(
|
||||
deepMerge({}, validInput, { rpc: { advanced: { threads: 0 } } }),
|
||||
)
|
||||
})
|
||||
test("test errors should throw for number in string", async () => {
|
||||
test('test errors should throw for number in string', async () => {
|
||||
const { validator } = await inputSpecSpec.build({} as any)
|
||||
expect(() =>
|
||||
validator.unsafeCast(deepMerge({}, validInput, { rpc: { enable: 2 } })),
|
||||
).toThrowError()
|
||||
})
|
||||
test("Test that we set serialversion to something not segwit or non-segwit", async () => {
|
||||
test('Test that we set serialversion to something not segwit or non-segwit', async () => {
|
||||
const { validator } = await inputSpecSpec.build({} as any)
|
||||
expect(() =>
|
||||
validator.unsafeCast(
|
||||
deepMerge({}, validInput, {
|
||||
rpc: { advanced: { serialversion: "testing" } },
|
||||
rpc: { advanced: { serialversion: 'testing' } },
|
||||
}),
|
||||
),
|
||||
).toThrowError()
|
||||
})
|
||||
})
|
||||
|
||||
describe("camelCase", () => {
|
||||
describe('camelCase', () => {
|
||||
test("'EquipmentClass name'", () => {
|
||||
expect(camelCase("EquipmentClass name")).toEqual("equipmentClassName")
|
||||
expect(camelCase('EquipmentClass name')).toEqual('equipmentClassName')
|
||||
})
|
||||
test("'Equipment className'", () => {
|
||||
expect(camelCase("Equipment className")).toEqual("equipmentClassName")
|
||||
expect(camelCase('Equipment className')).toEqual('equipmentClassName')
|
||||
})
|
||||
test("'equipment class name'", () => {
|
||||
expect(camelCase("equipment class name")).toEqual("equipmentClassName")
|
||||
expect(camelCase('equipment class name')).toEqual('equipmentClassName')
|
||||
})
|
||||
test("'Equipment Class Name'", () => {
|
||||
expect(camelCase("Equipment Class Name")).toEqual("equipmentClassName")
|
||||
expect(camelCase('Equipment Class Name')).toEqual('equipmentClassName')
|
||||
})
|
||||
test("'hyphen-name-format'", () => {
|
||||
expect(camelCase("hyphen-name-format")).toEqual("hyphenNameFormat")
|
||||
expect(camelCase('hyphen-name-format')).toEqual('hyphenNameFormat')
|
||||
})
|
||||
test("'underscore_name_format'", () => {
|
||||
expect(camelCase("underscore_name_format")).toEqual("underscoreNameFormat")
|
||||
expect(camelCase('underscore_name_format')).toEqual('underscoreNameFormat')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HealthStatus } from "../../../base/lib/types"
|
||||
import { HealthStatus } from '../../../base/lib/types'
|
||||
|
||||
export type TriggerInput = {
|
||||
lastResult?: HealthStatus
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Trigger } from "./index"
|
||||
import { Trigger } from './index'
|
||||
|
||||
export function changeOnFirstSuccess(o: {
|
||||
beforeFirstSuccess: Trigger
|
||||
@@ -13,7 +13,7 @@ export function changeOnFirstSuccess(o: {
|
||||
const beforeFirstSuccess = o.beforeFirstSuccess(getInput)
|
||||
for (
|
||||
let res = await beforeFirstSuccess.next();
|
||||
currentValue?.lastResult !== "success" && !res.done;
|
||||
currentValue?.lastResult !== 'success' && !res.done;
|
||||
res = await beforeFirstSuccess.next()
|
||||
) {
|
||||
yield
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cooldownTrigger } from "./cooldownTrigger"
|
||||
import { changeOnFirstSuccess } from "./changeOnFirstSuccess"
|
||||
import { cooldownTrigger } from './cooldownTrigger'
|
||||
import { changeOnFirstSuccess } from './changeOnFirstSuccess'
|
||||
|
||||
export const defaultTrigger = changeOnFirstSuccess({
|
||||
beforeFirstSuccess: cooldownTrigger(1000),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TriggerInput } from "./TriggerInput"
|
||||
export { changeOnFirstSuccess } from "./changeOnFirstSuccess"
|
||||
export { cooldownTrigger } from "./cooldownTrigger"
|
||||
import { TriggerInput } from './TriggerInput'
|
||||
export { changeOnFirstSuccess } from './changeOnFirstSuccess'
|
||||
export { cooldownTrigger } from './cooldownTrigger'
|
||||
|
||||
export type Trigger = (
|
||||
getInput: () => TriggerInput,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Trigger } from "."
|
||||
import { HealthStatus } from "../../../base/lib/types"
|
||||
import { Trigger } from '.'
|
||||
import { HealthStatus } from '../../../base/lib/types'
|
||||
|
||||
export type LastStatusTriggerParams = { [k in HealthStatus]?: Trigger } & {
|
||||
default: Trigger
|
||||
@@ -15,13 +15,13 @@ export function lastStatus(o: LastStatusTriggerParams): Trigger {
|
||||
}
|
||||
while (true) {
|
||||
let currentValue = getInput()
|
||||
let prev: HealthStatus | "default" | undefined = currentValue.lastResult
|
||||
let prev: HealthStatus | 'default' | undefined = currentValue.lastResult
|
||||
if (!prev) {
|
||||
yield
|
||||
continue
|
||||
}
|
||||
if (!(prev in o)) {
|
||||
prev = "default"
|
||||
prev = 'default'
|
||||
}
|
||||
if (!triggers[prev]) {
|
||||
triggers[prev] = o[prev]!(getInput)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Trigger } from "."
|
||||
import { lastStatus } from "./lastStatus"
|
||||
import { Trigger } from '.'
|
||||
import { lastStatus } from './lastStatus'
|
||||
|
||||
export const successFailure = (o: {
|
||||
duringSuccess: Trigger
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "../../base/lib/types"
|
||||
export { HealthCheck } from "./health"
|
||||
export * from '../../base/lib/types'
|
||||
export { HealthCheck } from './health'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effects } from "../../../base/lib/Effects"
|
||||
import { Manifest, PackageId } from "../../../base/lib/osBindings"
|
||||
import { DropGenerator, DropPromise } from "../../../base/lib/util/Drop"
|
||||
import { deepEqual } from "../../../base/lib/util/deepEqual"
|
||||
import { Effects } from '../../../base/lib/Effects'
|
||||
import { Manifest, PackageId } from '../../../base/lib/osBindings'
|
||||
import { DropGenerator, DropPromise } from '../../../base/lib/util/Drop'
|
||||
import { deepEqual } from '../../../base/lib/util/deepEqual'
|
||||
|
||||
export class GetServiceManifest<Mapped = Manifest> {
|
||||
constructor(
|
||||
@@ -45,7 +45,7 @@ export class GetServiceManifest<Mapped = Manifest> {
|
||||
this.effects.onLeaveContext(() => {
|
||||
resolveCell.resolve()
|
||||
})
|
||||
abort?.addEventListener("abort", () => resolveCell.resolve())
|
||||
abort?.addEventListener('abort', () => resolveCell.resolve())
|
||||
while (this.effects.isInContext && !abort?.aborted) {
|
||||
let callback: () => void = () => {}
|
||||
const waitForNext = new Promise<void>((resolve) => {
|
||||
@@ -64,7 +64,7 @@ export class GetServiceManifest<Mapped = Manifest> {
|
||||
}
|
||||
await waitForNext
|
||||
}
|
||||
return new Promise<never>((_, rej) => rej(new Error("aborted")))
|
||||
return new Promise<never>((_, rej) => rej(new Error('aborted')))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +72,7 @@ export class GetServiceManifest<Mapped = Manifest> {
|
||||
*/
|
||||
watch(abort?: AbortSignal): AsyncGenerator<Mapped, never, unknown> {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
abort?.addEventListener('abort', () => ctrl.abort())
|
||||
return DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort())
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export class GetServiceManifest<Mapped = Manifest> {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"callback function threw an error @ GetServiceManifest.onChange",
|
||||
'callback function threw an error @ GetServiceManifest.onChange',
|
||||
e,
|
||||
)
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export class GetServiceManifest<Mapped = Manifest> {
|
||||
.catch((e) => callback(null, e))
|
||||
.catch((e) =>
|
||||
console.error(
|
||||
"callback function threw an error @ GetServiceManifest.onChange",
|
||||
'callback function threw an error @ GetServiceManifest.onChange',
|
||||
e,
|
||||
),
|
||||
)
|
||||
@@ -123,7 +123,7 @@ export class GetServiceManifest<Mapped = Manifest> {
|
||||
return next
|
||||
}
|
||||
}
|
||||
throw new Error("context left before predicate passed")
|
||||
throw new Error('context left before predicate passed')
|
||||
}),
|
||||
() => ctrl.abort(),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { T } from ".."
|
||||
import { Effects } from "../../../base/lib/Effects"
|
||||
import { DropGenerator, DropPromise } from "../../../base/lib/util/Drop"
|
||||
import { T } from '..'
|
||||
import { Effects } from '../../../base/lib/Effects'
|
||||
import { DropGenerator, DropPromise } from '../../../base/lib/util/Drop'
|
||||
|
||||
export class GetSslCertificate {
|
||||
constructor(
|
||||
@@ -36,7 +36,7 @@ export class GetSslCertificate {
|
||||
this.effects.onLeaveContext(() => {
|
||||
resolveCell.resolve()
|
||||
})
|
||||
abort?.addEventListener("abort", () => resolveCell.resolve())
|
||||
abort?.addEventListener('abort', () => resolveCell.resolve())
|
||||
while (this.effects.isInContext && !abort?.aborted) {
|
||||
let callback: () => void = () => {}
|
||||
const waitForNext = new Promise<void>((resolve) => {
|
||||
@@ -50,7 +50,7 @@ export class GetSslCertificate {
|
||||
})
|
||||
await waitForNext
|
||||
}
|
||||
return new Promise<never>((_, rej) => rej(new Error("aborted")))
|
||||
return new Promise<never>((_, rej) => rej(new Error('aborted')))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,7 +60,7 @@ export class GetSslCertificate {
|
||||
abort?: AbortSignal,
|
||||
): AsyncGenerator<[string, string, string], never, unknown> {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
abort?.addEventListener('abort', () => ctrl.abort())
|
||||
return DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort())
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export class GetSslCertificate {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"callback function threw an error @ GetSslCertificate.onChange",
|
||||
'callback function threw an error @ GetSslCertificate.onChange',
|
||||
e,
|
||||
)
|
||||
}
|
||||
@@ -93,7 +93,7 @@ export class GetSslCertificate {
|
||||
.catch((e) => callback(null, e))
|
||||
.catch((e) =>
|
||||
console.error(
|
||||
"callback function threw an error @ GetSslCertificate.onChange",
|
||||
'callback function threw an error @ GetSslCertificate.onChange',
|
||||
e,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as T from "../../../base/lib/types"
|
||||
import * as cp from "child_process"
|
||||
import { promisify } from "util"
|
||||
import { Buffer } from "node:buffer"
|
||||
import { once } from "../../../base/lib/util/once"
|
||||
import { Drop } from "../../../base/lib/util/Drop"
|
||||
import { Mounts } from "../mainFn/Mounts"
|
||||
import { BackupEffects } from "../backup/Backups"
|
||||
import { PathBase } from "./Volume"
|
||||
import * as fs from 'fs/promises'
|
||||
import * as T from '../../../base/lib/types'
|
||||
import * as cp from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import { once } from '../../../base/lib/util/once'
|
||||
import { Drop } from '../../../base/lib/util/Drop'
|
||||
import { Mounts } from '../mainFn/Mounts'
|
||||
import { BackupEffects } from '../backup/Backups'
|
||||
import { PathBase } from './Volume'
|
||||
|
||||
export const execFile = promisify(cp.execFile)
|
||||
const False = () => false
|
||||
@@ -28,20 +28,20 @@ const TIMES_TO_WAIT_FOR_PROC = 100
|
||||
async function prepBind(
|
||||
from: string | null,
|
||||
to: string,
|
||||
type: "file" | "directory" | "infer",
|
||||
type: 'file' | 'directory' | 'infer',
|
||||
) {
|
||||
const fromMeta = from ? await fs.stat(from).catch((_) => null) : null
|
||||
const toMeta = await fs.stat(to).catch((_) => null)
|
||||
|
||||
if (type === "file" || (type === "infer" && from && fromMeta?.isFile())) {
|
||||
if (type === 'file' || (type === 'infer' && from && fromMeta?.isFile())) {
|
||||
if (toMeta && toMeta.isDirectory()) await fs.rmdir(to, { recursive: false })
|
||||
if (from && !fromMeta) {
|
||||
await fs.mkdir(from.replace(/\/[^\/]*\/?$/, ""), { recursive: true })
|
||||
await fs.writeFile(from, "")
|
||||
await fs.mkdir(from.replace(/\/[^\/]*\/?$/, ''), { recursive: true })
|
||||
await fs.writeFile(from, '')
|
||||
}
|
||||
if (!toMeta) {
|
||||
await fs.mkdir(to.replace(/\/[^\/]*\/?$/, ""), { recursive: true })
|
||||
await fs.writeFile(to, "")
|
||||
await fs.mkdir(to.replace(/\/[^\/]*\/?$/, ''), { recursive: true })
|
||||
await fs.writeFile(to, '')
|
||||
}
|
||||
} else {
|
||||
if (toMeta && toMeta.isFile() && !toMeta.size) await fs.rm(to)
|
||||
@@ -53,20 +53,20 @@ async function prepBind(
|
||||
async function bind(
|
||||
from: string,
|
||||
to: string,
|
||||
type: "file" | "directory" | "infer",
|
||||
type: 'file' | 'directory' | 'infer',
|
||||
idmap: IdMap[],
|
||||
) {
|
||||
await prepBind(from, to, type)
|
||||
|
||||
const args = ["--bind"]
|
||||
const args = ['--bind']
|
||||
|
||||
if (idmap.length) {
|
||||
args.push(
|
||||
`-oX-mount.idmap=${idmap.map((i) => `b:${i.fromId}:${i.toId}:${i.range}`).join(" ")}`,
|
||||
`-oX-mount.idmap=${idmap.map((i) => `b:${i.fromId}:${i.toId}:${i.range}`).join(' ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
await execFile("mount", [...args, from, to])
|
||||
await execFile('mount', [...args, from, to])
|
||||
}
|
||||
|
||||
export interface SubContainer<
|
||||
@@ -74,7 +74,7 @@ export interface SubContainer<
|
||||
Effects extends T.Effects = T.Effects,
|
||||
> extends Drop,
|
||||
PathBase {
|
||||
readonly imageId: keyof Manifest["images"] & T.ImageId
|
||||
readonly imageId: keyof Manifest['images'] & T.ImageId
|
||||
readonly rootfs: string
|
||||
readonly guid: T.Guid
|
||||
|
||||
@@ -185,21 +185,21 @@ export class SubContainerOwned<
|
||||
private waitProc: () => Promise<null>
|
||||
private constructor(
|
||||
readonly effects: Effects,
|
||||
readonly imageId: keyof Manifest["images"] & T.ImageId,
|
||||
readonly imageId: keyof Manifest['images'] & T.ImageId,
|
||||
readonly rootfs: string,
|
||||
readonly guid: T.Guid,
|
||||
) {
|
||||
super()
|
||||
this.leaderExited = false
|
||||
this.leader = cp.spawn(
|
||||
"start-container",
|
||||
["subcontainer", "launch", rootfs],
|
||||
'start-container',
|
||||
['subcontainer', 'launch', rootfs],
|
||||
{
|
||||
killSignal: "SIGKILL",
|
||||
stdio: "inherit",
|
||||
killSignal: 'SIGKILL',
|
||||
stdio: 'inherit',
|
||||
},
|
||||
)
|
||||
this.leader.on("exit", () => {
|
||||
this.leader.on('exit', () => {
|
||||
this.leaderExited = true
|
||||
})
|
||||
this.waitProc = once(
|
||||
@@ -210,7 +210,7 @@ export class SubContainerOwned<
|
||||
!(await fs.stat(`${this.rootfs}/proc/1`).then((x) => !!x, False))
|
||||
) {
|
||||
if (count++ > TIMES_TO_WAIT_FOR_PROC) {
|
||||
console.debug("Failed to start subcontainer", {
|
||||
console.debug('Failed to start subcontainer', {
|
||||
guid: this.guid,
|
||||
imageId: this.imageId,
|
||||
rootfs: this.rootfs,
|
||||
@@ -228,7 +228,7 @@ export class SubContainerOwned<
|
||||
static async of<Manifest extends T.SDKManifest, Effects extends T.Effects>(
|
||||
effects: Effects,
|
||||
image: {
|
||||
imageId: keyof Manifest["images"] & T.ImageId
|
||||
imageId: keyof Manifest['images'] & T.ImageId
|
||||
sharedRun?: boolean
|
||||
},
|
||||
mounts:
|
||||
@@ -256,20 +256,20 @@ export class SubContainerOwned<
|
||||
if (mounts) {
|
||||
await res.mount(mounts)
|
||||
}
|
||||
const shared = ["dev", "sys"]
|
||||
const shared = ['dev', 'sys']
|
||||
if (!!sharedRun) {
|
||||
shared.push("run")
|
||||
shared.push('run')
|
||||
}
|
||||
|
||||
await fs.mkdir(`${rootfs}/etc`, { recursive: true })
|
||||
await fs.copyFile("/etc/resolv.conf", `${rootfs}/etc/resolv.conf`)
|
||||
await fs.copyFile('/etc/resolv.conf', `${rootfs}/etc/resolv.conf`)
|
||||
|
||||
for (const dirPart of shared) {
|
||||
const from = `/${dirPart}`
|
||||
const to = `${rootfs}/${dirPart}`
|
||||
await fs.mkdir(from, { recursive: true })
|
||||
await fs.mkdir(to, { recursive: true })
|
||||
await execFile("mount", ["--rbind", from, to])
|
||||
await execFile('mount', ['--rbind', from, to])
|
||||
}
|
||||
|
||||
return res
|
||||
@@ -286,7 +286,7 @@ export class SubContainerOwned<
|
||||
>(
|
||||
effects: Effects,
|
||||
image: {
|
||||
imageId: keyof Manifest["images"] & T.ImageId
|
||||
imageId: keyof Manifest['images'] & T.ImageId
|
||||
sharedRun?: boolean
|
||||
},
|
||||
mounts:
|
||||
@@ -317,7 +317,7 @@ export class SubContainerOwned<
|
||||
}
|
||||
|
||||
subpath(path: string): string {
|
||||
return path.startsWith("/")
|
||||
return path.startsWith('/')
|
||||
? `${this.rootfs}${path}`
|
||||
: `${this.rootfs}/${path}`
|
||||
}
|
||||
@@ -335,36 +335,36 @@ export class SubContainerOwned<
|
||||
): Promise<this> {
|
||||
for (let mount of mounts.build()) {
|
||||
let { options, mountpoint } = mount
|
||||
const path = mountpoint.startsWith("/")
|
||||
const path = mountpoint.startsWith('/')
|
||||
? `${this.rootfs}${mountpoint}`
|
||||
: `${this.rootfs}/${mountpoint}`
|
||||
if (options.type === "volume") {
|
||||
if (options.type === 'volume') {
|
||||
const subpath = options.subpath
|
||||
? options.subpath.startsWith("/")
|
||||
? options.subpath.startsWith('/')
|
||||
? options.subpath
|
||||
: `/${options.subpath}`
|
||||
: "/"
|
||||
: '/'
|
||||
const from = `/media/startos/volumes/${options.volumeId}${subpath}`
|
||||
|
||||
await bind(from, path, options.filetype, options.idmap)
|
||||
} else if (options.type === "assets") {
|
||||
} else if (options.type === 'assets') {
|
||||
const subpath = options.subpath
|
||||
? options.subpath.startsWith("/")
|
||||
? options.subpath.startsWith('/')
|
||||
? options.subpath
|
||||
: `/${options.subpath}`
|
||||
: "/"
|
||||
: '/'
|
||||
const from = `/media/startos/assets/${subpath}`
|
||||
|
||||
await bind(from, path, options.filetype, options.idmap)
|
||||
} else if (options.type === "pointer") {
|
||||
await prepBind(null, path, "directory")
|
||||
} else if (options.type === 'pointer') {
|
||||
await prepBind(null, path, 'directory')
|
||||
await this.effects.mount({ location: path, target: options })
|
||||
} else if (options.type === "backup") {
|
||||
} else if (options.type === 'backup') {
|
||||
const subpath = options.subpath
|
||||
? options.subpath.startsWith("/")
|
||||
? options.subpath.startsWith('/')
|
||||
? options.subpath
|
||||
: `/${options.subpath}`
|
||||
: "/"
|
||||
: '/'
|
||||
const from = `/media/startos/backup${subpath}`
|
||||
|
||||
await bind(from, path, options.filetype, options.idmap)
|
||||
@@ -381,13 +381,13 @@ export class SubContainerOwned<
|
||||
}
|
||||
return new Promise<null>((resolve, reject) => {
|
||||
try {
|
||||
let timeout = setTimeout(() => this.leader.kill("SIGKILL"), 30000)
|
||||
this.leader.on("exit", () => {
|
||||
let timeout = setTimeout(() => this.leader.kill('SIGKILL'), 30000)
|
||||
this.leader.on('exit', () => {
|
||||
clearTimeout(timeout)
|
||||
resolve(null)
|
||||
})
|
||||
if (!this.leader.kill("SIGTERM")) {
|
||||
reject(new Error("kill(2) failed"))
|
||||
if (!this.leader.kill('SIGTERM')) {
|
||||
reject(new Error('kill(2) failed'))
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
@@ -435,17 +435,17 @@ export class SubContainerOwned<
|
||||
await this.waitProc()
|
||||
const imageMeta: T.ImageMetadata = await fs
|
||||
.readFile(`/media/startos/images/${this.imageId}.json`, {
|
||||
encoding: "utf8",
|
||||
encoding: 'utf8',
|
||||
})
|
||||
.catch(() => "{}")
|
||||
.catch(() => '{}')
|
||||
.then(JSON.parse)
|
||||
let extra: string[] = []
|
||||
let user = imageMeta.user || "root"
|
||||
let user = imageMeta.user || 'root'
|
||||
if (options?.user) {
|
||||
user = options.user
|
||||
delete options.user
|
||||
}
|
||||
let workdir = imageMeta.workdir || "/"
|
||||
let workdir = imageMeta.workdir || '/'
|
||||
if (options?.cwd) {
|
||||
workdir = options.cwd
|
||||
delete options.cwd
|
||||
@@ -456,10 +456,10 @@ export class SubContainerOwned<
|
||||
}
|
||||
}
|
||||
const child = cp.spawn(
|
||||
"start-container",
|
||||
'start-container',
|
||||
[
|
||||
"subcontainer",
|
||||
"exec",
|
||||
'subcontainer',
|
||||
'exec',
|
||||
`--env-file=/media/startos/images/${this.imageId}.env`,
|
||||
`--user=${user}`,
|
||||
`--workdir=${workdir}`,
|
||||
@@ -469,11 +469,11 @@ export class SubContainerOwned<
|
||||
],
|
||||
options || {},
|
||||
)
|
||||
abort?.signal.addEventListener("abort", () => child.kill("SIGKILL"))
|
||||
abort?.signal.addEventListener('abort', () => child.kill('SIGKILL'))
|
||||
if (options?.input) {
|
||||
await new Promise<null>((resolve, reject) => {
|
||||
try {
|
||||
child.stdin.on("error", (e) => reject(e))
|
||||
child.stdin.on('error', (e) => reject(e))
|
||||
child.stdin.write(options.input, (e) => {
|
||||
if (e) {
|
||||
reject(e)
|
||||
@@ -493,25 +493,25 @@ export class SubContainerOwned<
|
||||
}
|
||||
})
|
||||
}
|
||||
const stdout = { data: "" as string }
|
||||
const stderr = { data: "" as string }
|
||||
const stdout = { data: '' as string }
|
||||
const stderr = { data: '' as string }
|
||||
const appendData =
|
||||
(appendTo: { data: string }) => (chunk: string | Buffer | any) => {
|
||||
if (typeof chunk === "string" || chunk instanceof Buffer) {
|
||||
if (typeof chunk === 'string' || chunk instanceof Buffer) {
|
||||
appendTo.data += chunk.toString()
|
||||
} else {
|
||||
console.error("received unexpected chunk", chunk)
|
||||
console.error('received unexpected chunk', chunk)
|
||||
}
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
child.on("error", reject)
|
||||
child.on('error', reject)
|
||||
let killTimeout: NodeJS.Timeout | undefined
|
||||
if (timeoutMs !== null && child.pid) {
|
||||
killTimeout = setTimeout(() => child.kill("SIGKILL"), timeoutMs)
|
||||
killTimeout = setTimeout(() => child.kill('SIGKILL'), timeoutMs)
|
||||
}
|
||||
child.stdout.on("data", appendData(stdout))
|
||||
child.stderr.on("data", appendData(stderr))
|
||||
child.on("exit", (code, signal) => {
|
||||
child.stdout.on('data', appendData(stdout))
|
||||
child.stderr.on('data', appendData(stderr))
|
||||
child.on('exit', (code, signal) => {
|
||||
clearTimeout(killTimeout)
|
||||
const result = {
|
||||
exitCode: code,
|
||||
@@ -560,17 +560,17 @@ export class SubContainerOwned<
|
||||
await this.waitProc()
|
||||
const imageMeta: T.ImageMetadata = await fs
|
||||
.readFile(`/media/startos/images/${this.imageId}.json`, {
|
||||
encoding: "utf8",
|
||||
encoding: 'utf8',
|
||||
})
|
||||
.catch(() => "{}")
|
||||
.catch(() => '{}')
|
||||
.then(JSON.parse)
|
||||
let extra: string[] = []
|
||||
let user = imageMeta.user || "root"
|
||||
let user = imageMeta.user || 'root'
|
||||
if (options?.user) {
|
||||
user = options.user
|
||||
delete options.user
|
||||
}
|
||||
let workdir = imageMeta.workdir || "/"
|
||||
let workdir = imageMeta.workdir || '/'
|
||||
if (options?.cwd) {
|
||||
workdir = options.cwd
|
||||
delete options.cwd
|
||||
@@ -585,10 +585,10 @@ export class SubContainerOwned<
|
||||
await this.killLeader()
|
||||
this.leaderExited = false
|
||||
this.leader = cp.spawn(
|
||||
"start-container",
|
||||
'start-container',
|
||||
[
|
||||
"subcontainer",
|
||||
"launch",
|
||||
'subcontainer',
|
||||
'launch',
|
||||
`--env-file=/media/startos/images/${this.imageId}.env`,
|
||||
`--user=${user}`,
|
||||
`--workdir=${workdir}`,
|
||||
@@ -596,9 +596,9 @@ export class SubContainerOwned<
|
||||
this.rootfs,
|
||||
...command,
|
||||
],
|
||||
{ ...options, stdio: "inherit" },
|
||||
{ ...options, stdio: 'inherit' },
|
||||
)
|
||||
this.leader.on("exit", () => {
|
||||
this.leader.on('exit', () => {
|
||||
this.leaderExited = true
|
||||
})
|
||||
return this.leader as cp.ChildProcessWithoutNullStreams
|
||||
@@ -606,22 +606,22 @@ export class SubContainerOwned<
|
||||
|
||||
async spawn(
|
||||
command: string[],
|
||||
options: CommandOptions & StdioOptions = { stdio: "inherit" },
|
||||
options: CommandOptions & StdioOptions = { stdio: 'inherit' },
|
||||
): Promise<cp.ChildProcess> {
|
||||
await this.waitProc()
|
||||
const imageMeta: T.ImageMetadata = await fs
|
||||
.readFile(`/media/startos/images/${this.imageId}.json`, {
|
||||
encoding: "utf8",
|
||||
encoding: 'utf8',
|
||||
})
|
||||
.catch(() => "{}")
|
||||
.catch(() => '{}')
|
||||
.then(JSON.parse)
|
||||
let extra: string[] = []
|
||||
let user = imageMeta.user || "root"
|
||||
let user = imageMeta.user || 'root'
|
||||
if (options?.user) {
|
||||
user = options.user
|
||||
delete options.user
|
||||
}
|
||||
let workdir = imageMeta.workdir || "/"
|
||||
let workdir = imageMeta.workdir || '/'
|
||||
if (options.cwd) {
|
||||
workdir = options.cwd
|
||||
delete options.cwd
|
||||
@@ -634,10 +634,10 @@ export class SubContainerOwned<
|
||||
}
|
||||
}
|
||||
return cp.spawn(
|
||||
"start-container",
|
||||
'start-container',
|
||||
[
|
||||
"subcontainer",
|
||||
"exec",
|
||||
'subcontainer',
|
||||
'exec',
|
||||
`--env-file=/media/startos/images/${this.imageId}.env`,
|
||||
`--user=${user}`,
|
||||
`--workdir=${workdir}`,
|
||||
@@ -665,7 +665,7 @@ export class SubContainerOwned<
|
||||
options?: Parameters<typeof fs.writeFile>[2],
|
||||
): Promise<void> {
|
||||
const fullPath = this.subpath(path)
|
||||
const dir = fullPath.replace(/\/[^/]*\/?$/, "")
|
||||
const dir = fullPath.replace(/\/[^/]*\/?$/, '')
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
return fs.writeFile(fullPath, data, options)
|
||||
}
|
||||
@@ -709,7 +709,7 @@ export class SubContainerRc<
|
||||
static async of<Manifest extends T.SDKManifest, Effects extends T.Effects>(
|
||||
effects: Effects,
|
||||
image: {
|
||||
imageId: keyof Manifest["images"] & T.ImageId
|
||||
imageId: keyof Manifest['images'] & T.ImageId
|
||||
sharedRun?: boolean
|
||||
},
|
||||
mounts:
|
||||
@@ -737,7 +737,7 @@ export class SubContainerRc<
|
||||
>(
|
||||
effects: Effects,
|
||||
image: {
|
||||
imageId: keyof Manifest["images"] & T.ImageId
|
||||
imageId: keyof Manifest['images'] & T.ImageId
|
||||
sharedRun?: boolean
|
||||
},
|
||||
mounts:
|
||||
@@ -783,7 +783,7 @@ export class SubContainerRc<
|
||||
const rcs = --this.subcontainer.rcs
|
||||
if (rcs <= 0) {
|
||||
this.destroying = this.subcontainer.destroy()
|
||||
if (rcs < 0) console.error(new Error("UNREACHABLE: rcs < 0").stack)
|
||||
if (rcs < 0) console.error(new Error('UNREACHABLE: rcs < 0').stack)
|
||||
}
|
||||
}
|
||||
if (this.destroying) {
|
||||
@@ -850,7 +850,7 @@ export class SubContainerRc<
|
||||
|
||||
async spawn(
|
||||
command: string[],
|
||||
options: CommandOptions & StdioOptions = { stdio: "inherit" },
|
||||
options: CommandOptions & StdioOptions = { stdio: 'inherit' },
|
||||
): Promise<cp.ChildProcess> {
|
||||
return this.subcontainer.spawn(command, options)
|
||||
}
|
||||
@@ -910,23 +910,23 @@ export type MountOptions =
|
||||
| MountOptionsBackup
|
||||
|
||||
export type MountOptionsVolume = {
|
||||
type: "volume"
|
||||
type: 'volume'
|
||||
volumeId: string
|
||||
subpath: string | null
|
||||
readonly: boolean
|
||||
filetype: "file" | "directory" | "infer"
|
||||
filetype: 'file' | 'directory' | 'infer'
|
||||
idmap: IdMap[]
|
||||
}
|
||||
|
||||
export type MountOptionsAssets = {
|
||||
type: "assets"
|
||||
type: 'assets'
|
||||
subpath: string | null
|
||||
filetype: "file" | "directory" | "infer"
|
||||
filetype: 'file' | 'directory' | 'infer'
|
||||
idmap: { fromId: number; toId: number; range: number }[]
|
||||
}
|
||||
|
||||
export type MountOptionsPointer = {
|
||||
type: "pointer"
|
||||
type: 'pointer'
|
||||
packageId: string
|
||||
volumeId: string
|
||||
subpath: string | null
|
||||
@@ -935,9 +935,9 @@ export type MountOptionsPointer = {
|
||||
}
|
||||
|
||||
export type MountOptionsBackup = {
|
||||
type: "backup"
|
||||
type: 'backup'
|
||||
subpath: string | null
|
||||
filetype: "file" | "directory" | "infer"
|
||||
filetype: 'file' | 'directory' | 'infer'
|
||||
idmap: { fromId: number; toId: number; range: number }[]
|
||||
}
|
||||
function wait(time: number) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as fs from "node:fs/promises"
|
||||
import * as T from "../../../base/lib/types"
|
||||
import * as fs from 'node:fs/promises'
|
||||
import * as T from '../../../base/lib/types'
|
||||
|
||||
/**
|
||||
* Common interface for objects that have a subpath method (Volume, SubContainer, etc.)
|
||||
@@ -27,7 +27,7 @@ export class Volume<Id extends string = string> implements PathBase {
|
||||
* @param subpath Path relative to the volume root
|
||||
*/
|
||||
subpath(subpath: string): string {
|
||||
return subpath.startsWith("/")
|
||||
return subpath.startsWith('/')
|
||||
? `${this.path}${subpath}`
|
||||
: `${this.path}/${subpath}`
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export class Volume<Id extends string = string> implements PathBase {
|
||||
options?: Parameters<typeof fs.writeFile>[2],
|
||||
): Promise<void> {
|
||||
const fullPath = this.subpath(subpath)
|
||||
const dir = fullPath.replace(/\/[^/]*\/?$/, "")
|
||||
const dir = fullPath.replace(/\/[^/]*\/?$/, '')
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
return fs.writeFile(fullPath, data, options)
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export class Volume<Id extends string = string> implements PathBase {
|
||||
* Type-safe volumes object that provides Volume instances for each volume defined in the manifest
|
||||
*/
|
||||
export type Volumes<Manifest extends T.SDKManifest> = {
|
||||
[K in Manifest["volumes"][number]]: Volume<K>
|
||||
[K in Manifest['volumes'][number]]: Volume<K>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as matches from "ts-matches"
|
||||
import * as YAML from "yaml"
|
||||
import * as TOML from "@iarna/toml"
|
||||
import * as INI from "ini"
|
||||
import * as T from "../../../base/lib/types"
|
||||
import * as fs from "node:fs/promises"
|
||||
import { asError, deepEqual } from "../../../base/lib/util"
|
||||
import { DropGenerator, DropPromise } from "../../../base/lib/util/Drop"
|
||||
import { PathBase } from "./Volume"
|
||||
import * as matches from 'ts-matches'
|
||||
import * as YAML from 'yaml'
|
||||
import * as TOML from '@iarna/toml'
|
||||
import * as INI from 'ini'
|
||||
import * as T from '../../../base/lib/types'
|
||||
import * as fs from 'node:fs/promises'
|
||||
import { asError, deepEqual } from '../../../base/lib/util'
|
||||
import { DropGenerator, DropPromise } from '../../../base/lib/util/Drop'
|
||||
import { PathBase } from './Volume'
|
||||
|
||||
const previousPath = /(.+?)\/([^/]*)$/
|
||||
|
||||
@@ -17,14 +17,14 @@ const exists = (path: string) =>
|
||||
)
|
||||
|
||||
async function onCreated(path: string) {
|
||||
if (path === "/") return
|
||||
if (!path.startsWith("/")) path = `${process.cwd()}/${path}`
|
||||
if (path === '/') return
|
||||
if (!path.startsWith('/')) path = `${process.cwd()}/${path}`
|
||||
if (await exists(path)) {
|
||||
return
|
||||
}
|
||||
const split = path.split("/")
|
||||
const split = path.split('/')
|
||||
const filename = split.pop()
|
||||
const parent = split.join("/")
|
||||
const parent = split.join('/')
|
||||
await onCreated(parent)
|
||||
const ctrl = new AbortController()
|
||||
const watch = fs.watch(parent, { persistent: false, signal: ctrl.signal })
|
||||
@@ -43,7 +43,7 @@ async function onCreated(path: string) {
|
||||
}
|
||||
for await (let event of watch) {
|
||||
if (event.filename === filename) {
|
||||
ctrl.abort("finished")
|
||||
ctrl.abort('finished')
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -56,8 +56,8 @@ function fileMerge(...args: any[]): any {
|
||||
else if (
|
||||
res &&
|
||||
arg &&
|
||||
typeof res === "object" &&
|
||||
typeof arg === "object" &&
|
||||
typeof res === 'object' &&
|
||||
typeof arg === 'object' &&
|
||||
!Array.isArray(res) &&
|
||||
!Array.isArray(arg)
|
||||
) {
|
||||
@@ -70,7 +70,7 @@ function fileMerge(...args: any[]): any {
|
||||
}
|
||||
|
||||
function filterUndefined<A>(a: A): A {
|
||||
if (a && typeof a === "object") {
|
||||
if (a && typeof a === 'object') {
|
||||
if (Array.isArray(a)) {
|
||||
return a.map(filterUndefined) as A
|
||||
}
|
||||
@@ -91,7 +91,7 @@ export type Transformers<Raw = unknown, Transformed = unknown> = {
|
||||
|
||||
type ToPath = string | { base: PathBase; subpath: string }
|
||||
function toPath(path: ToPath): string {
|
||||
if (typeof path === "string") {
|
||||
if (typeof path === 'string') {
|
||||
return path
|
||||
}
|
||||
return path.base.subpath(path.subpath)
|
||||
@@ -195,7 +195,7 @@ export class FileHelper<A> {
|
||||
if (!(await exists(this.path))) {
|
||||
return null
|
||||
}
|
||||
return await fs.readFile(this.path).then((data) => data.toString("utf-8"))
|
||||
return await fs.readFile(this.path).then((data) => data.toString('utf-8'))
|
||||
}
|
||||
|
||||
private async readFile(): Promise<unknown> {
|
||||
@@ -251,7 +251,7 @@ export class FileHelper<A> {
|
||||
while (effects.isInContext && !abort?.aborted) {
|
||||
if (await exists(this.path)) {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
abort?.addEventListener('abort', () => ctrl.abort())
|
||||
const watch = fs.watch(this.path, {
|
||||
persistent: false,
|
||||
signal: ctrl.signal,
|
||||
@@ -266,7 +266,7 @@ export class FileHelper<A> {
|
||||
})
|
||||
.catch((e) => console.error(asError(e)))
|
||||
if (!prev || !eq(prev.value, newRes)) {
|
||||
console.error("yielding", JSON.stringify({ prev: prev, newRes }))
|
||||
console.error('yielding', JSON.stringify({ prev: prev, newRes }))
|
||||
yield newRes
|
||||
}
|
||||
prev = { value: newRes }
|
||||
@@ -276,7 +276,7 @@ export class FileHelper<A> {
|
||||
await onCreated(this.path).catch((e) => console.error(asError(e)))
|
||||
}
|
||||
}
|
||||
return new Promise<never>((_, rej) => rej(new Error("aborted")))
|
||||
return new Promise<never>((_, rej) => rej(new Error('aborted')))
|
||||
}
|
||||
|
||||
private readOnChange<B>(
|
||||
@@ -296,7 +296,7 @@ export class FileHelper<A> {
|
||||
if (res.cancel) ctrl.abort()
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"callback function threw an error @ FileHelper.read.onChange",
|
||||
'callback function threw an error @ FileHelper.read.onChange',
|
||||
e,
|
||||
)
|
||||
}
|
||||
@@ -305,7 +305,7 @@ export class FileHelper<A> {
|
||||
.catch((e) => callback(null, e))
|
||||
.catch((e) =>
|
||||
console.error(
|
||||
"callback function threw an error @ FileHelper.read.onChange",
|
||||
'callback function threw an error @ FileHelper.read.onChange',
|
||||
e,
|
||||
),
|
||||
)
|
||||
@@ -359,7 +359,7 @@ export class FileHelper<A> {
|
||||
const: (effects: T.Effects) => this.readConst(effects, map, eq),
|
||||
watch: (effects: T.Effects, abort?: AbortSignal) => {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
abort?.addEventListener('abort', () => ctrl.abort())
|
||||
return DropGenerator.of(
|
||||
this.readWatch(effects, map, eq, ctrl.signal),
|
||||
() => ctrl.abort(),
|
||||
@@ -620,15 +620,15 @@ export class FileHelper<A> {
|
||||
(inData) =>
|
||||
Object.entries(inData)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join("\n"),
|
||||
.join('\n'),
|
||||
(inString) =>
|
||||
Object.fromEntries(
|
||||
inString
|
||||
.split("\n")
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => !line.startsWith("#") && line.includes("="))
|
||||
.filter((line) => !line.startsWith('#') && line.includes('='))
|
||||
.map((line) => {
|
||||
const pos = line.indexOf("=")
|
||||
const pos = line.indexOf('=')
|
||||
return [line.slice(0, pos), line.slice(pos + 1)]
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from "../../../base/lib/util"
|
||||
export { GetSslCertificate } from "./GetSslCertificate"
|
||||
export { GetServiceManifest, getServiceManifest } from "./GetServiceManifest"
|
||||
export * from '../../../base/lib/util'
|
||||
export { GetSslCertificate } from './GetSslCertificate'
|
||||
export { GetServiceManifest, getServiceManifest } from './GetServiceManifest'
|
||||
|
||||
export { Drop } from "../../../base/lib/util/Drop"
|
||||
export { Volume, Volumes } from "./Volume"
|
||||
export { Drop } from '../../../base/lib/util/Drop'
|
||||
export { Volume, Volumes } from './Volume'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExtendedVersion, VersionRange } from "../../../base/lib/exver"
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { ExtendedVersion, VersionRange } from '../../../base/lib/exver'
|
||||
import * as T from '../../../base/lib/types'
|
||||
import {
|
||||
InitFn,
|
||||
InitKind,
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
UninitFn,
|
||||
UninitScript,
|
||||
UninitScriptOrFn,
|
||||
} from "../../../base/lib/inits"
|
||||
import { Graph, Vertex, once } from "../util"
|
||||
import { IMPOSSIBLE, VersionInfo } from "./VersionInfo"
|
||||
} from '../../../base/lib/inits'
|
||||
import { Graph, Vertex, once } from '../util'
|
||||
import { IMPOSSIBLE, VersionInfo } from './VersionInfo'
|
||||
|
||||
export async function getDataVersion(effects: T.Effects) {
|
||||
const versionStr = await effects.getDataVersion()
|
||||
@@ -30,11 +30,11 @@ export async function setDataVersion(
|
||||
}
|
||||
|
||||
function isExver(v: ExtendedVersion | VersionRange): v is ExtendedVersion {
|
||||
return "satisfies" in v
|
||||
return 'satisfies' in v
|
||||
}
|
||||
|
||||
function isRange(v: ExtendedVersion | VersionRange): v is VersionRange {
|
||||
return "satisfiedBy" in v
|
||||
return 'satisfiedBy' in v
|
||||
}
|
||||
|
||||
export function overlaps(
|
||||
@@ -64,7 +64,7 @@ export class VersionGraph<CurrentVersion extends string>
|
||||
private constructor(
|
||||
readonly current: VersionInfo<CurrentVersion>,
|
||||
versions: Array<VersionInfo<any>>,
|
||||
private readonly preInstall?: InitScriptOrFn<"install">,
|
||||
private readonly preInstall?: InitScriptOrFn<'install'>,
|
||||
private readonly uninstall?: UninitScript | UninitFn,
|
||||
) {
|
||||
this.graph = once(() => {
|
||||
@@ -86,7 +86,7 @@ export class VersionGraph<CurrentVersion extends string>
|
||||
for (let version of [current, ...versions]) {
|
||||
const v = ExtendedVersion.parse(version.options.version)
|
||||
const vertex = graph.addVertex(v, [], [])
|
||||
const flavor = v.flavor || ""
|
||||
const flavor = v.flavor || ''
|
||||
if (!flavorMap[flavor]) {
|
||||
flavorMap[flavor] = []
|
||||
}
|
||||
@@ -109,11 +109,11 @@ export class VersionGraph<CurrentVersion extends string>
|
||||
let range
|
||||
if (prev) {
|
||||
graph.addEdge(version.options.migrations.up, prev[2], vertex)
|
||||
range = VersionRange.anchor(">=", prev[0]).and(
|
||||
VersionRange.anchor("<", v),
|
||||
range = VersionRange.anchor('>=', prev[0]).and(
|
||||
VersionRange.anchor('<', v),
|
||||
)
|
||||
} else {
|
||||
range = VersionRange.anchor("<", v)
|
||||
range = VersionRange.anchor('<', v)
|
||||
}
|
||||
const vRange = graph.addVertex(range, [], [])
|
||||
graph.addEdge(version.options.migrations.up, vRange, vertex)
|
||||
@@ -123,11 +123,11 @@ export class VersionGraph<CurrentVersion extends string>
|
||||
let range
|
||||
if (prev) {
|
||||
graph.addEdge(version.options.migrations.down, vertex, prev[2])
|
||||
range = VersionRange.anchor(">=", prev[0]).and(
|
||||
VersionRange.anchor("<", v),
|
||||
range = VersionRange.anchor('>=', prev[0]).and(
|
||||
VersionRange.anchor('<', v),
|
||||
)
|
||||
} else {
|
||||
range = VersionRange.anchor("<", v)
|
||||
range = VersionRange.anchor('<', v)
|
||||
}
|
||||
const vRange = graph.addVertex(range, [], [])
|
||||
graph.addEdge(version.options.migrations.down, vertex, vRange)
|
||||
@@ -173,7 +173,7 @@ export class VersionGraph<CurrentVersion extends string>
|
||||
/**
|
||||
* A script to run only on fresh install
|
||||
*/
|
||||
preInstall?: InitScriptOrFn<"install">
|
||||
preInstall?: InitScriptOrFn<'install'>
|
||||
/**
|
||||
* A script to run only on uninstall
|
||||
*/
|
||||
@@ -211,8 +211,8 @@ export class VersionGraph<CurrentVersion extends string>
|
||||
acc +
|
||||
(prev && prev != x.from.metadata.toString()
|
||||
? ` (as ${prev})`
|
||||
: "") +
|
||||
" -> " +
|
||||
: '') +
|
||||
' -> ' +
|
||||
x.to.metadata.toString(),
|
||||
prev: x.to.metadata.toString(),
|
||||
}),
|
||||
@@ -246,7 +246,7 @@ export class VersionGraph<CurrentVersion extends string>
|
||||
acc.or(
|
||||
isRange(x.metadata)
|
||||
? x.metadata
|
||||
: VersionRange.anchor("=", x.metadata),
|
||||
: VersionRange.anchor('=', x.metadata),
|
||||
),
|
||||
VersionRange.none(),
|
||||
)
|
||||
@@ -263,7 +263,7 @@ export class VersionGraph<CurrentVersion extends string>
|
||||
acc.or(
|
||||
isRange(x.metadata)
|
||||
? x.metadata
|
||||
: VersionRange.anchor("=", x.metadata),
|
||||
: VersionRange.anchor('=', x.metadata),
|
||||
),
|
||||
VersionRange.none(),
|
||||
)
|
||||
@@ -279,9 +279,9 @@ export class VersionGraph<CurrentVersion extends string>
|
||||
to: this.currentVersion(),
|
||||
})
|
||||
} else {
|
||||
kind = "install" // implied by !dataVersion
|
||||
kind = 'install' // implied by !dataVersion
|
||||
if (this.preInstall)
|
||||
if ("init" in this.preInstall) await this.preInstall.init(effects, kind)
|
||||
if ('init' in this.preInstall) await this.preInstall.init(effects, kind)
|
||||
else await this.preInstall(effects, kind)
|
||||
await effects.setDataVersion({ version: this.current.options.version })
|
||||
}
|
||||
@@ -302,7 +302,7 @@ export class VersionGraph<CurrentVersion extends string>
|
||||
}
|
||||
} else {
|
||||
if (this.uninstall)
|
||||
if ("uninit" in this.uninstall)
|
||||
if ('uninit' in this.uninstall)
|
||||
await this.uninstall.uninit(effects, target)
|
||||
else await this.uninstall(effects, target)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ValidateExVer } from "../../../base/lib/exver"
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { ValidateExVer } from '../../../base/lib/exver'
|
||||
import * as T from '../../../base/lib/types'
|
||||
|
||||
export const IMPOSSIBLE: unique symbol = Symbol("IMPOSSIBLE")
|
||||
export const IMPOSSIBLE: unique symbol = Symbol('IMPOSSIBLE')
|
||||
|
||||
export type VersionOptions<Version extends string> = {
|
||||
/** The exver-compliant version number */
|
||||
@@ -60,30 +60,30 @@ export class VersionInfo<Version extends string> {
|
||||
}
|
||||
|
||||
function __type_tests() {
|
||||
const version: VersionInfo<"1.0.0:0"> = VersionInfo.of({
|
||||
version: "1.0.0:0",
|
||||
releaseNotes: "",
|
||||
const version: VersionInfo<'1.0.0:0'> = VersionInfo.of({
|
||||
version: '1.0.0:0',
|
||||
releaseNotes: '',
|
||||
migrations: {},
|
||||
})
|
||||
.satisfies("#other:1.0.0:0")
|
||||
.satisfies("#other:2.0.0:0")
|
||||
.satisfies('#other:1.0.0:0')
|
||||
.satisfies('#other:2.0.0:0')
|
||||
// @ts-expect-error
|
||||
.satisfies("#other:2.f.0:0")
|
||||
.satisfies('#other:2.f.0:0')
|
||||
|
||||
let a: VersionInfo<"1.0.0:0"> = version
|
||||
let a: VersionInfo<'1.0.0:0'> = version
|
||||
// @ts-expect-error
|
||||
let b: VersionInfo<"1.0.0:3"> = version
|
||||
let b: VersionInfo<'1.0.0:3'> = version
|
||||
|
||||
VersionInfo.of({
|
||||
// @ts-expect-error
|
||||
version: "test",
|
||||
releaseNotes: "",
|
||||
version: 'test',
|
||||
releaseNotes: '',
|
||||
migrations: {},
|
||||
})
|
||||
VersionInfo.of({
|
||||
// @ts-expect-error
|
||||
version: "test" as string,
|
||||
releaseNotes: "",
|
||||
version: 'test' as string,
|
||||
releaseNotes: '',
|
||||
migrations: {},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./VersionGraph"
|
||||
export * from "./VersionInfo"
|
||||
export * from './VersionGraph'
|
||||
export * from './VersionInfo'
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": false
|
||||
"singleQuote": true
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.4.0",
|
||||
|
||||
Reference in New Issue
Block a user