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:
Aiden McClelland
2026-02-06 00:10:16 +01:00
committed by GitHub
parent 86ca23c093
commit f2142f0bb3
280 changed files with 6793 additions and 5515 deletions

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -1,2 +1,2 @@
import "./Backups"
import "./setupBackups"
import './Backups'
import './setupBackups'

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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'>

View File

@@ -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}`,
}),

View File

@@ -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 }
})
}

View File

@@ -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),
)

View File

@@ -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
}

View File

@@ -1,3 +1,3 @@
import "./checkFns"
import './checkFns'
export { HealthCheck } from "./HealthCheck"
export { HealthCheck } from './HealthCheck'

View File

@@ -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) {

View File

@@ -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'

View File

@@ -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,
),
),

View File

@@ -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) {

View File

@@ -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())
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
/**

View File

@@ -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

View File

@@ -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

View File

@@ -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,
})

View File

@@ -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)
})
})

View File

@@ -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',
},
)

View File

@@ -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',
},
},
}),

View File

@@ -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')
})
})

View File

@@ -1,4 +1,4 @@
import { HealthStatus } from "../../../base/lib/types"
import { HealthStatus } from '../../../base/lib/types'
export type TriggerInput = {
lastResult?: HealthStatus

View File

@@ -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

View File

@@ -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),

View File

@@ -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,

View File

@@ -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)

View File

@@ -1,5 +1,5 @@
import { Trigger } from "."
import { lastStatus } from "./lastStatus"
import { Trigger } from '.'
import { lastStatus } from './lastStatus'
export const successFailure = (o: {
duringSuccess: Trigger

View File

@@ -1,2 +1,2 @@
export * from "../../base/lib/types"
export { HealthCheck } from "./health"
export * from '../../base/lib/types'
export { HealthCheck } from './health'

View File

@@ -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(),
)

View File

@@ -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,
),
)

View File

@@ -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) {

View File

@@ -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>
}
/**

View File

@@ -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)]
}),
),

View File

@@ -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'

View File

@@ -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)
}

View File

@@ -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: {},
})
}

View File

@@ -1,2 +1,2 @@
export * from "./VersionGraph"
export * from "./VersionInfo"
export * from './VersionGraph'
export * from './VersionInfo'

View File

@@ -46,7 +46,7 @@
"trailingComma": "all",
"tabWidth": 2,
"semi": false,
"singleQuote": false
"singleQuote": true
},
"devDependencies": {
"@types/jest": "^29.4.0",