misc sdk changes (#2934)

* misc sdk changes

* delete the store ☠️

* port comments

* fix build

* fix removing

* fix tests

* beta.20

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Aiden McClelland
2025-05-09 15:10:51 -06:00
committed by GitHub
parent d2c4741f0b
commit 7750e33f82
62 changed files with 1255 additions and 2130 deletions

View File

@@ -18,7 +18,7 @@
"jsonpath": "^1.1.1",
"lodash.merge": "^4.6.2",
"node-fetch": "^3.1.0",
"ts-matches": "^5.5.1",
"ts-matches": "^6.3.2",
"tslib": "^2.5.3",
"typescript": "^5.1.3",
"yaml": "^2.3.1"
@@ -37,12 +37,13 @@
},
"../sdk/dist": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.18",
"version": "0.4.0-beta.20",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",
"@noble/curves": "^1.8.2",
"@noble/hashes": "^1.7.2",
"@types/ini": "^4.1.1",
"deep-equality-data-structures": "^2.0.0",
"ini": "^5.0.0",
"isomorphic-fetch": "^3.0.0",
@@ -51,8 +52,6 @@
"yaml": "^2.7.1"
},
"devDependencies": {
"@iarna/toml": "^2.2.5",
"@types/ini": "^4.1.1",
"@types/jest": "^29.4.0",
"copyfiles": "^2.4.1",
"jest": "^29.4.3",
@@ -6476,9 +6475,9 @@
}
},
"node_modules/ts-matches": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.6.1.tgz",
"integrity": "sha512-1QXWQUa14MCgbz7vMg7i7eVPhMKB/5w8808nkN2sfnDkbG9nWYr9IwuTxX+h99yyawHYS53DewShA2RYCbSW4Q==",
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-6.3.2.tgz",
"integrity": "sha512-UhSgJymF8cLd4y0vV29qlKVCkQpUtekAaujXbQVc729FezS8HwqzepqvtjzQ3HboatIqN/Idor85O2RMwT7lIQ==",
"license": "MIT"
},
"node_modules/tslib": {

View File

@@ -27,7 +27,7 @@
"jsonpath": "^1.1.1",
"lodash.merge": "^4.6.2",
"node-fetch": "^3.1.0",
"ts-matches": "^5.5.1",
"ts-matches": "^6.3.2",
"tslib": "^2.5.3",
"typescript": "^5.1.3",
"yaml": "^2.3.1"

View File

@@ -6,23 +6,19 @@ import { Effects } from "../Models/Effects"
import { CallbackHolder } from "../Models/CallbackHolder"
import { asError } from "@start9labs/start-sdk/base/lib/util"
const matchRpcError = object({
error: object(
{
code: number,
message: string,
data: some(
string,
object(
{
details: string,
debug: string,
},
["debug"],
),
),
},
["data"],
),
error: object({
code: number,
message: string,
data: some(
string,
object({
details: string,
debug: string.nullable().optional(),
}),
)
.nullable()
.optional(),
}),
})
const testRpcError = matchRpcError.test
const testRpcResult = object({
@@ -197,13 +193,6 @@ export function makeEffects(context: EffectContext): Effects {
T.Effects["exportServiceInterface"]
>
}) as Effects["exportServiceInterface"],
exposeForDependents(
...[options]: Parameters<T.Effects["exposeForDependents"]>
) {
return rpcRound("expose-for-dependents", options) as ReturnType<
T.Effects["exposeForDependents"]
>
},
getContainerIp(...[options]: Parameters<T.Effects["getContainerIp"]>) {
return rpcRound("get-container-ip", options) as ReturnType<
T.Effects["getContainerIp"]
@@ -305,15 +294,6 @@ export function makeEffects(context: EffectContext): Effects {
shutdown(...[]: Parameters<T.Effects["shutdown"]>) {
return rpcRound("shutdown", {}) as ReturnType<T.Effects["shutdown"]>
},
store: {
get: async (options: any) =>
rpcRound("store.get", {
...options,
callback: context.callbacks?.addCallback(options.callback) || null,
}) as any,
set: async (options: any) =>
rpcRound("store.set", options) as ReturnType<T.Effects["store"]["set"]>,
} as T.Effects["store"],
getDataVersion() {
return rpcRound("get-data-version", {}) as ReturnType<
T.Effects["getDataVersion"]

View File

@@ -26,20 +26,16 @@ type MaybePromise<T> = T | Promise<T>
export const matchRpcResult = anyOf(
object({ result: any }),
object({
error: object(
{
code: number,
message: string,
data: object(
{
details: string,
debug: any,
},
["details", "debug"],
),
},
["data"],
),
error: object({
code: number,
message: string,
data: object({
details: string.optional(),
debug: any.optional(),
})
.nullable()
.optional(),
}),
}),
)
@@ -54,38 +50,26 @@ const isResult = object({ result: any }).test
const idType = some(string, number, literal(null))
type IdType = null | string | number | undefined
const runType = object(
{
id: idType,
method: literal("execute"),
params: object(
{
id: string,
procedure: string,
input: any,
timeout: number,
},
["timeout"],
),
},
["id"],
)
const sandboxRunType = object(
{
id: idType,
method: literal("sandbox"),
params: object(
{
id: string,
procedure: string,
input: any,
timeout: number,
},
["timeout"],
),
},
["id"],
)
const runType = object({
id: idType.optional(),
method: literal("execute"),
params: object({
id: string,
procedure: string,
input: any,
timeout: number.nullable().optional(),
}),
})
const sandboxRunType = object({
id: idType.optional(),
method: literal("sandbox"),
params: object({
id: string,
procedure: string,
input: any,
timeout: number.nullable().optional(),
}),
})
const callbackType = object({
method: literal("callback"),
params: object({
@@ -93,44 +77,29 @@ const callbackType = object({
args: array,
}),
})
const initType = object(
{
id: idType,
method: literal("init"),
},
["id"],
)
const startType = object(
{
id: idType,
method: literal("start"),
},
["id"],
)
const stopType = object(
{
id: idType,
method: literal("stop"),
},
["id"],
)
const exitType = object(
{
id: idType,
method: literal("exit"),
},
["id"],
)
const evalType = object(
{
id: idType,
method: literal("eval"),
params: object({
script: string,
}),
},
["id"],
)
const initType = object({
id: idType.optional(),
method: literal("init"),
})
const startType = object({
id: idType.optional(),
method: literal("start"),
})
const stopType = object({
id: idType.optional(),
method: literal("stop"),
})
const exitType = object({
id: idType.optional(),
method: literal("exit"),
})
const evalType = object({
id: idType.optional(),
method: literal("eval"),
params: object({
script: string,
}),
})
const jsonParse = (x: string) => JSON.parse(x)
@@ -365,7 +334,7 @@ export class RpcListener {
)
})
.when(
shape({ id: idType, method: string }, ["id"]),
shape({ id: idType.optional(), method: string }),
({ id, method }) => ({
jsonrpc,
id,
@@ -400,7 +369,7 @@ export class RpcListener {
procedure: typeof jsonPath._TYPE,
system: System,
procedureId: string,
timeout: number | undefined,
timeout: number | null | undefined,
input: any,
) {
const ensureResultTypeShape = (
@@ -449,14 +418,10 @@ export class RpcListener {
})().then(ensureResultTypeShape, (error) =>
matches(error)
.when(
object(
{
error: string,
code: number,
},
["code"],
{ code: 0 },
),
object({
error: string,
code: number.defaultTo(0),
}),
(error) => ({
error: {
code: error.code,

View File

@@ -64,7 +64,7 @@ export class DockerProcedureContainer extends Drop {
const volumeMount = volumes[mount]
if (volumeMount.type === "data") {
await subcontainer.mount(
Mounts.of().addVolume({
Mounts.of().mountVolume({
volumeId: mount,
subpath: null,
mountpoint: mounts[mount],
@@ -73,7 +73,7 @@ export class DockerProcedureContainer extends Drop {
)
} else if (volumeMount.type === "assets") {
await subcontainer.mount(
Mounts.of().addAssets({
Mounts.of().mountAssets({
subpath: mount,
mountpoint: mounts[mount],
}),
@@ -119,7 +119,7 @@ export class DockerProcedureContainer extends Drop {
})
} else if (volumeMount.type === "backup") {
await subcontainer.mount(
Mounts.of().addBackups({
Mounts.of().mountBackups({
subpath: null,
mountpoint: mounts[mount],
}),

View File

@@ -1,5 +1,6 @@
import {
ExtendedVersion,
FileHelper,
types as T,
utils,
VersionRange,
@@ -46,7 +47,7 @@ import {
transformNewConfigToOld,
transformOldConfigToNew,
} from "./transformConfigSpec"
import { partialDiff, StorePath } from "@start9labs/start-sdk/base/lib/util"
import { partialDiff } from "@start9labs/start-sdk/base/lib/util"
type Optional<A> = A | undefined | null
function todo(): never {
@@ -55,8 +56,21 @@ function todo(): never {
const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json"
export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js"
const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig" as utils.StorePath
const EMBASSY_DEPENDS_ON_PATH_PREFIX = "/embassyDependsOn" as utils.StorePath
const configFile = FileHelper.json(
{
volumeId: "embassy",
subpath: "config.json",
},
matches.any,
)
const dependsOnFile = FileHelper.json(
{
volumeId: "embassy",
subpath: "dependsOn.json",
},
dictionary([string, array(string)]),
)
const matchResult = object({
result: any,
@@ -94,47 +108,48 @@ const fromReturnType = <A>(a: U.ResultType<A>): A => {
return assertNever(a)
}
const matchSetResult = object(
{
"depends-on": dictionary([string, array(string)]),
dependsOn: dictionary([string, array(string)]),
signal: literals(
"SIGTERM",
"SIGHUP",
"SIGINT",
"SIGQUIT",
"SIGILL",
"SIGTRAP",
"SIGABRT",
"SIGBUS",
"SIGFPE",
"SIGKILL",
"SIGUSR1",
"SIGSEGV",
"SIGUSR2",
"SIGPIPE",
"SIGALRM",
"SIGSTKFLT",
"SIGCHLD",
"SIGCONT",
"SIGSTOP",
"SIGTSTP",
"SIGTTIN",
"SIGTTOU",
"SIGURG",
"SIGXCPU",
"SIGXFSZ",
"SIGVTALRM",
"SIGPROF",
"SIGWINCH",
"SIGIO",
"SIGPWR",
"SIGSYS",
"SIGINFO",
),
},
["depends-on", "dependsOn"],
)
const matchSetResult = object({
"depends-on": dictionary([string, array(string)])
.nullable()
.optional(),
dependsOn: dictionary([string, array(string)])
.nullable()
.optional(),
signal: literals(
"SIGTERM",
"SIGHUP",
"SIGINT",
"SIGQUIT",
"SIGILL",
"SIGTRAP",
"SIGABRT",
"SIGBUS",
"SIGFPE",
"SIGKILL",
"SIGUSR1",
"SIGSEGV",
"SIGUSR2",
"SIGPIPE",
"SIGALRM",
"SIGSTKFLT",
"SIGCHLD",
"SIGCONT",
"SIGSTOP",
"SIGTSTP",
"SIGTTIN",
"SIGTTOU",
"SIGURG",
"SIGXCPU",
"SIGXFSZ",
"SIGVTALRM",
"SIGPROF",
"SIGWINCH",
"SIGIO",
"SIGPWR",
"SIGSYS",
"SIGINFO",
),
})
type OldGetConfigRes = {
config?: null | Record<string, unknown>
@@ -174,14 +189,14 @@ export type PackagePropertiesV2 = {
}
export type PackagePropertyString = {
type: "string"
description?: string
description?: string | null
value: string
/** Let's the ui make this copyable button */
copyable?: boolean
copyable?: boolean | null
/** Let the ui create a qr for this field */
qr?: boolean
qr?: boolean | null
/** Hiding the value unless toggled off for field */
masked?: boolean
masked?: boolean | null
}
export type PackagePropertyObject = {
value: PackagePropertiesV2
@@ -225,17 +240,14 @@ const matchPackagePropertyObject: Parser<unknown, PackagePropertyObject> =
})
const matchPackagePropertyString: Parser<unknown, PackagePropertyString> =
object(
{
type: literal("string"),
description: string,
value: string,
copyable: boolean,
qr: boolean,
masked: boolean,
},
["copyable", "description", "qr", "masked"],
)
object({
type: literal("string"),
description: string.nullable().optional(),
value: string,
copyable: boolean.nullable().optional(),
qr: boolean.nullable().optional(),
masked: boolean.nullable().optional(),
})
setMatchPackageProperties(
dictionary([
string,
@@ -300,7 +312,7 @@ export class SystemForEmbassy implements System {
async containerInit(effects: Effects): Promise<void> {
for (let depId in this.manifest.dependencies) {
if (this.manifest.dependencies[depId].config) {
if (this.manifest.dependencies[depId]?.config) {
await this.dependenciesAutoconfig(effects, depId, null)
}
}
@@ -355,10 +367,7 @@ export class SystemForEmbassy implements System {
) {
await effects.action.clearRequests({ only: ["needs-config"] })
}
await effects.store.set({
path: EMBASSY_POINTER_PATH_PREFIX,
value: this.getConfig(effects, timeoutMs),
})
await configFile.write(effects, await this.getConfig(effects, timeoutMs))
} else if (this.manifest.config) {
await effects.action.request({
packageId: this.manifest.id,
@@ -587,11 +596,6 @@ export class SystemForEmbassy implements System {
const moduleCode = await this.moduleCode
await moduleCode.createBackup?.(polyfillEffects(effects, this.manifest))
}
await fs.writeFile(
"/media/startos/backup/store.json",
JSON.stringify(await effects.store.get({ path: "" as StorePath })),
{ encoding: "utf-8" },
)
const dataVersion = await effects.getDataVersion()
if (dataVersion)
await fs.writeFile("/media/startos/backup/dataVersion.txt", dataVersion, {
@@ -607,11 +611,6 @@ export class SystemForEmbassy implements System {
encoding: "utf-8",
})
.catch((_) => null)
if (store)
await effects.store.set({
path: "" as StorePath,
value: JSON.parse(store),
})
const restoreBackup = this.manifest.backup.restore
if (restoreBackup.type === "docker") {
const commands = [restoreBackup.entrypoint, ...restoreBackup.args]
@@ -686,10 +685,7 @@ export class SystemForEmbassy implements System {
structuredClone(newConfigWithoutPointers as Record<string, unknown>),
)
await updateConfig(effects, this.manifest, spec, newConfig)
await effects.store.set({
path: EMBASSY_POINTER_PATH_PREFIX,
value: newConfig,
})
await configFile.write(effects, newConfig)
const setConfigValue = this.manifest.config?.set
if (!setConfigValue) return
if (setConfigValue.type === "docker") {
@@ -743,15 +739,11 @@ export class SystemForEmbassy implements System {
rawDepends: { [x: string]: readonly string[] },
configuring: boolean,
) {
const storedDependsOn = (await effects.store.get({
packageId: this.manifest.id,
path: EMBASSY_DEPENDS_ON_PATH_PREFIX,
})) as Record<string, readonly string[]>
const storedDependsOn = await dependsOnFile.read().once()
const requiredDeps = {
...Object.fromEntries(
Object.entries(this.manifest.dependencies || {})
?.filter((x) => x[1].requirement.type === "required")
Object.entries(this.manifest.dependencies ?? {})
.filter(([k, v]) => v?.requirement.type === "required")
.map((x) => [x[0], []]) || [],
),
}
@@ -765,10 +757,7 @@ export class SystemForEmbassy implements System {
? storedDependsOn
: requiredDeps
await effects.store.set({
path: EMBASSY_DEPENDS_ON_PATH_PREFIX,
value: dependsOn,
})
await dependsOnFile.write(effects, dependsOn)
await effects.setDependencies({
dependencies: Object.entries(dependsOn).flatMap(
@@ -1006,43 +995,50 @@ export class SystemForEmbassy implements System {
timeoutMs: number | null,
): Promise<void> {
// TODO: docker
const oldConfig = (await effects.store.get({
packageId: id,
path: EMBASSY_POINTER_PATH_PREFIX,
callback: () => {
this.dependenciesAutoconfig(effects, id, timeoutMs)
},
})) as U.Config
if (!oldConfig) return
const moduleCode = await this.moduleCode
const method = moduleCode?.dependencies?.[id]?.autoConfigure
if (!method) return
const newConfig = (await method(
polyfillEffects(effects, this.manifest),
JSON.parse(JSON.stringify(oldConfig)),
).then((x) => {
if ("result" in x) return x.result
if ("error" in x) throw new Error("Error getting config: " + x.error)
throw new Error("Error getting config: " + x["error-code"][1])
})) as any
const diff = partialDiff(oldConfig, newConfig)
if (diff) {
await effects.action.request({
actionId: "config",
await effects.mount({
location: `/media/embassy/${id}`,
target: {
packageId: id,
replayId: `${id}/config`,
severity: "important",
reason: `Configure this dependency for the needs of ${this.manifest.title}`,
input: {
kind: "partial",
value: diff.diff,
},
when: {
condition: "input-not-matches",
once: false,
},
volumeId: "embassy",
subpath: null,
readonly: true,
},
})
configFile
.withPath(`/media/embassy/${id}/config.json`)
.read()
.onChange(effects, async (oldConfig: U.Config) => {
if (!oldConfig) return
const moduleCode = await this.moduleCode
const method = moduleCode?.dependencies?.[id]?.autoConfigure
if (!method) return
const newConfig = (await method(
polyfillEffects(effects, this.manifest),
JSON.parse(JSON.stringify(oldConfig)),
).then((x) => {
if ("result" in x) return x.result
if ("error" in x) throw new Error("Error getting config: " + x.error)
throw new Error("Error getting config: " + x["error-code"][1])
})) as any
const diff = partialDiff(oldConfig, newConfig)
if (diff) {
await effects.action.request({
actionId: "config",
packageId: id,
replayId: `${id}/config`,
severity: "important",
reason: `Configure this dependency for the needs of ${this.manifest.title}`,
input: {
kind: "partial",
value: diff.diff,
},
when: {
condition: "input-not-matches",
once: false,
},
})
}
})
}
}
}
@@ -1144,11 +1140,20 @@ async function updateConfig(
) {
if (specValue.target === "config") {
const jp = require("jsonpath")
const remoteConfig = await effects.store.get({
packageId: specValue["package-id"],
callback: () => effects.restart(),
path: EMBASSY_POINTER_PATH_PREFIX,
const depId = specValue["package-id"]
await effects.mount({
location: `/media/embassy/${depId}`,
target: {
packageId: depId,
volumeId: "embassy",
subpath: null,
readonly: true,
},
})
const remoteConfig = configFile
.withPath(`/media/embassy/${depId}/config.json`)
.read()
.once()
console.debug(remoteConfig)
const configValue = specValue.multi
? jp.query(remoteConfig, specValue.selector)

View File

@@ -14,123 +14,113 @@ import {
import { matchVolume } from "./matchVolume"
import { matchDockerProcedure } from "../../../Models/DockerProcedure"
const matchJsProcedure = object(
{
type: literal("script"),
args: array(unknown),
},
["args"],
{
args: [],
},
)
const matchJsProcedure = object({
type: literal("script"),
args: array(unknown).nullable().optional().defaultTo([]),
})
const matchProcedure = some(matchDockerProcedure, matchJsProcedure)
export type Procedure = typeof matchProcedure._TYPE
const matchAction = object(
{
name: string,
description: string,
warning: string,
implementation: matchProcedure,
"allowed-statuses": array(literals("running", "stopped")),
"input-spec": unknown,
},
["warning", "input-spec", "input-spec"],
)
export const matchManifest = object(
{
id: string,
title: string,
version: string,
main: matchDockerProcedure,
assets: object(
{
assets: string,
scripts: string,
},
["assets", "scripts"],
const matchAction = object({
name: string,
description: string,
warning: string.nullable().optional(),
implementation: matchProcedure,
"allowed-statuses": array(literals("running", "stopped")),
"input-spec": unknown.nullable().optional(),
})
export const matchManifest = object({
id: string,
title: string,
version: string,
main: matchDockerProcedure,
assets: object({
assets: string.nullable().optional(),
scripts: string.nullable().optional(),
})
.nullable()
.optional(),
"health-checks": dictionary([
string,
every(
matchProcedure,
object({
name: string,
["success-message"]: string.nullable().optional(),
}),
),
"health-checks": dictionary([
string,
every(
matchProcedure,
object(
{
name: string,
["success-message"]: string,
},
["success-message"],
),
),
]),
config: object({
get: matchProcedure,
set: matchProcedure,
]),
config: object({
get: matchProcedure,
set: matchProcedure,
})
.nullable()
.optional(),
properties: matchProcedure.nullable().optional(),
volumes: dictionary([string, matchVolume]),
interfaces: dictionary([
string,
object({
name: string,
description: string,
"tor-config": object({
"port-mapping": dictionary([string, string]),
})
.nullable()
.optional(),
"lan-config": dictionary([
string,
object({
ssl: boolean,
internal: number,
}),
])
.nullable()
.optional(),
ui: boolean,
protocols: array(string),
}),
properties: matchProcedure,
volumes: dictionary([string, matchVolume]),
interfaces: dictionary([
string,
object(
{
name: string,
description: string,
"tor-config": object({
"port-mapping": dictionary([string, string]),
}),
"lan-config": dictionary([
string,
object({
ssl: boolean,
internal: number,
}),
]),
ui: boolean,
protocols: array(string),
},
["lan-config", "tor-config"],
]),
backup: object({
create: matchProcedure,
restore: matchProcedure,
}),
migrations: object({
to: dictionary([string, matchProcedure]),
from: dictionary([string, matchProcedure]),
})
.nullable()
.optional(),
dependencies: dictionary([
string,
object({
version: string,
requirement: some(
object({
type: literal("opt-in"),
how: string,
}),
object({
type: literal("opt-out"),
how: string,
}),
object({
type: literal("required"),
}),
),
]),
backup: object({
create: matchProcedure,
restore: matchProcedure,
}),
migrations: object({
to: dictionary([string, matchProcedure]),
from: dictionary([string, matchProcedure]),
}),
dependencies: dictionary([
string,
object(
{
version: string,
requirement: some(
object({
type: literal("opt-in"),
how: string,
}),
object({
type: literal("opt-out"),
how: string,
}),
object({
type: literal("required"),
}),
),
description: string,
config: object({
check: matchProcedure,
"auto-configure": matchProcedure,
}),
},
["description", "config"],
),
]),
description: string.nullable().optional(),
config: object({
check: matchProcedure,
"auto-configure": matchProcedure,
})
.nullable()
.optional(),
})
.nullable()
.optional(),
]),
actions: dictionary([string, matchAction]),
},
["config", "actions", "properties", "migrations", "dependencies"],
)
actions: dictionary([string, matchAction]),
})
export type Manifest = typeof matchManifest._TYPE

View File

@@ -1,12 +1,9 @@
import { object, literal, string, boolean, some } from "ts-matches"
const matchDataVolume = object(
{
type: literal("data"),
readonly: boolean,
},
["readonly"],
)
const matchDataVolume = object({
type: literal("data"),
readonly: boolean.optional(),
})
const matchAssetVolume = object({
type: literal("assets"),
})

View File

@@ -169,7 +169,7 @@ export const polyfillEffects = (
{ imageId: manifest.main.image },
commands,
{
mounts: Mounts.of().addVolume({
mounts: Mounts.of().mountVolume({
volumeId: input.volumeId,
subpath: null,
mountpoint: "/drive",
@@ -206,7 +206,7 @@ export const polyfillEffects = (
{ imageId: manifest.main.image },
commands,
{
mounts: Mounts.of().addVolume({
mounts: Mounts.of().mountVolume({
volumeId: input.volumeId,
subpath: null,
mountpoint: "/drive",

View File

@@ -203,6 +203,7 @@ export function transformNewConfigToOld(
spec: OldConfigSpec,
config: Record<string, any>,
): Record<string, any> {
if (!config) return config
return Object.entries(spec).reduce((obj, [key, val]) => {
let newVal = config[key]
@@ -396,100 +397,71 @@ export const matchOldDefaultString = anyOf(
)
type OldDefaultString = typeof matchOldDefaultString._TYPE
export const matchOldValueSpecString = object(
{
type: literals("string"),
name: string,
masked: boolean,
copyable: boolean,
nullable: boolean,
placeholder: string,
pattern: string,
"pattern-description": string,
default: matchOldDefaultString,
textarea: boolean,
description: string,
warning: string,
},
[
"masked",
"copyable",
"nullable",
"placeholder",
"pattern",
"pattern-description",
"default",
"textarea",
"description",
"warning",
],
)
export const matchOldValueSpecString = object({
type: literals("string"),
name: string,
masked: boolean.nullable().optional(),
copyable: boolean.nullable().optional(),
nullable: boolean.nullable().optional(),
placeholder: string.nullable().optional(),
pattern: string.nullable().optional(),
"pattern-description": string.nullable().optional(),
default: matchOldDefaultString.nullable().optional(),
textarea: boolean.nullable().optional(),
description: string.nullable().optional(),
warning: string.nullable().optional(),
})
export const matchOldValueSpecNumber = object(
{
type: literals("number"),
nullable: boolean,
name: string,
range: string,
integral: boolean,
default: number,
description: string,
warning: string,
units: string,
placeholder: anyOf(number, string),
},
["default", "description", "warning", "units", "placeholder"],
)
export const matchOldValueSpecNumber = object({
type: literals("number"),
nullable: boolean,
name: string,
range: string,
integral: boolean,
default: number.nullable().optional(),
description: string.nullable().optional(),
warning: string.nullable().optional(),
units: string.nullable().optional(),
placeholder: anyOf(number, string).nullable().optional(),
})
type OldValueSpecNumber = typeof matchOldValueSpecNumber._TYPE
export const matchOldValueSpecBoolean = object(
{
type: literals("boolean"),
default: boolean,
name: string,
description: string,
warning: string,
},
["description", "warning"],
)
export const matchOldValueSpecBoolean = object({
type: literals("boolean"),
default: boolean,
name: string,
description: string.nullable().optional(),
warning: string.nullable().optional(),
})
type OldValueSpecBoolean = typeof matchOldValueSpecBoolean._TYPE
const matchOldValueSpecObject = object(
{
type: literals("object"),
spec: _matchOldConfigSpec,
name: string,
description: string,
warning: string,
},
["description", "warning"],
)
const matchOldValueSpecObject = object({
type: literals("object"),
spec: _matchOldConfigSpec,
name: string,
description: string.nullable().optional(),
warning: string.nullable().optional(),
})
type OldValueSpecObject = typeof matchOldValueSpecObject._TYPE
const matchOldValueSpecEnum = object(
{
values: array(string),
"value-names": dictionary([string, string]),
type: literals("enum"),
default: string,
name: string,
description: string,
warning: string,
},
["description", "warning"],
)
const matchOldValueSpecEnum = object({
values: array(string),
"value-names": dictionary([string, string]),
type: literals("enum"),
default: string,
name: string,
description: string.nullable().optional(),
warning: string.nullable().optional(),
})
type OldValueSpecEnum = typeof matchOldValueSpecEnum._TYPE
const matchOldUnionTagSpec = object(
{
id: string, // The name of the field containing one of the union variants
"variant-names": dictionary([string, string]), // The name of each variant
name: string,
description: string,
warning: string,
},
["description", "warning"],
)
const matchOldUnionTagSpec = object({
id: string, // The name of the field containing one of the union variants
"variant-names": dictionary([string, string]), // The name of each variant
name: string,
description: string.nullable().optional(),
warning: string.nullable().optional(),
})
const matchOldValueSpecUnion = object({
type: literals("union"),
tag: matchOldUnionTagSpec,
@@ -514,57 +486,45 @@ setOldUniqueBy(
),
)
const matchOldListValueSpecObject = object(
{
spec: _matchOldConfigSpec, // this is a mapped type of the config object at this level, replacing the object's values with specs on those values
"unique-by": matchOldUniqueBy, // indicates whether duplicates can be permitted in the list
"display-as": string, // this should be a handlebars template which can make use of the entire config which corresponds to 'spec'
},
["display-as", "unique-by"],
)
const matchOldListValueSpecString = object(
{
masked: boolean,
copyable: boolean,
pattern: string,
"pattern-description": string,
placeholder: string,
},
["pattern", "pattern-description", "placeholder", "copyable", "masked"],
)
const matchOldListValueSpecObject = object({
spec: _matchOldConfigSpec, // this is a mapped type of the config object at this level, replacing the object's values with specs on those values
"unique-by": matchOldUniqueBy.nullable().optional(), // indicates whether duplicates can be permitted in the list
"display-as": string.nullable().optional(), // this should be a handlebars template which can make use of the entire config which corresponds to 'spec'
})
const matchOldListValueSpecString = object({
masked: boolean.nullable().optional(),
copyable: boolean.nullable().optional(),
pattern: string.nullable().optional(),
"pattern-description": string.nullable().optional(),
placeholder: string.nullable().optional(),
})
const matchOldListValueSpecEnum = object({
values: array(string),
"value-names": dictionary([string, string]),
})
const matchOldListValueSpecNumber = object(
{
range: string,
integral: boolean,
units: string,
placeholder: anyOf(number, string),
},
["units", "placeholder"],
)
const matchOldListValueSpecNumber = object({
range: string,
integral: boolean,
units: string.nullable().optional(),
placeholder: anyOf(number, string).nullable().optional(),
})
// represents a spec for a list
const matchOldValueSpecList = every(
object(
{
type: literals("list"),
range: string, // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules
default: anyOf(
array(string),
array(number),
array(matchOldDefaultString),
array(object),
),
name: string,
description: string,
warning: string,
},
["description", "warning"],
),
object({
type: literals("list"),
range: string, // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules
default: anyOf(
array(string),
array(number),
array(matchOldDefaultString),
array(object),
),
name: string,
description: string.nullable().optional(),
warning: string.nullable().optional(),
}),
anyOf(
object({
subtype: literals("string"),

View File

@@ -1,7 +1,6 @@
import { System } from "../../Interfaces/System"
import { Effects } from "../../Models/Effects"
import { T, utils } from "@start9labs/start-sdk"
import { Optional } from "ts-matches/lib/parsers/interfaces"
export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js"
@@ -30,7 +29,7 @@ export class SystemForStartOs implements System {
}
async packageUninit(
effects: Effects,
nextVersion: Optional<string> = null,
nextVersion: string | null = null,
timeoutMs: number | null = null,
): Promise<void> {
return void (await this.abi.packageUninit({ effects, nextVersion }))

View File

@@ -17,31 +17,25 @@ const Path = string
export type VolumeId = string
export type Path = string
export const matchDockerProcedure = object(
{
type: literal("docker"),
image: string,
system: boolean,
entrypoint: string,
args: array(string),
mounts: dictionary([VolumeId, Path]),
"io-format": literals(
"json",
"json-pretty",
"yaml",
"cbor",
"toml",
"toml-pretty",
),
"sigterm-timeout": some(number, matchDuration),
inject: boolean,
},
["io-format", "sigterm-timeout", "system", "args", "inject", "mounts"],
{
"sigterm-timeout": 30,
inject: false,
args: [],
},
)
export const matchDockerProcedure = object({
type: literal("docker"),
image: string,
system: boolean.optional(),
entrypoint: string,
args: array(string).defaultTo([]),
mounts: dictionary([VolumeId, Path]).optional(),
"io-format": literals(
"json",
"json-pretty",
"yaml",
"cbor",
"toml",
"toml-pretty",
)
.nullable()
.optional(),
"sigterm-timeout": some(number, matchDuration).onMismatch(30),
inject: boolean.defaultTo(false),
})
export type DockerProcedure = typeof matchDockerProcedure._TYPE