Merge branch 'integration/new-container-runtime' of github.com:Start9Labs/start-os into integration/new-container-runtime

This commit is contained in:
Aiden McClelland
2024-03-25 15:47:24 -06:00
13 changed files with 131 additions and 439 deletions

View File

@@ -147,11 +147,6 @@ export class HostSystemStartOs implements Effects {
T.Effects["exposeForDependents"]
>
}
exposeUi(...[options]: Parameters<T.Effects["exposeUi"]>) {
return this.rpcRound("exposeUi", options) as ReturnType<
T.Effects["exposeUi"]
>
}
getConfigured(...[]: Parameters<T.Effects["getConfigured"]>) {
return this.rpcRound("getConfigured", null) as ReturnType<
T.Effects["getConfigured"]

View File

@@ -25,15 +25,12 @@ export class MainLoop {
wait: Promise<unknown>
}>
| undefined
private propertiesEvent: NodeJS.Timeout | undefined
constructor(
readonly system: SystemForEmbassy,
readonly effects: HostSystemStartOs,
readonly runProperties: () => Promise<void>,
) {
this.healthLoops = this.constructHealthLoops()
this.mainEvent = this.constructMainEvent()
this.propertiesEvent = this.constructPropertiesEvent()
}
private async constructMainEvent() {
@@ -85,23 +82,14 @@ export class MainLoop {
}
public async clean(options?: { timeout?: number }) {
const { mainEvent, healthLoops, propertiesEvent } = this
const { mainEvent, healthLoops } = this
const main = await mainEvent
delete this.mainEvent
delete this.healthLoops
delete this.propertiesEvent
if (mainEvent) await main?.daemon.term()
clearInterval(propertiesEvent)
if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval))
}
private constructPropertiesEvent() {
const { runProperties } = this
return setInterval(() => {
runProperties()
}, EMBASSY_PROPERTIES_LOOP)
}
private constructHealthLoops() {
const { manifest } = this.system
const effects = this.effects

View File

@@ -59,6 +59,33 @@ export type PackagePropertyObject = {
type: "object"
description: string
}
const asProperty_ = (
x: PackagePropertyString | PackagePropertyObject,
): T.PropertiesValue => {
if (x.type === "object") {
return {
...x,
value: Object.fromEntries(
Object.entries(x.value).map(([key, value]) => [
key,
asProperty_(value),
]),
),
}
}
return {
masked: false,
description: null,
qr: null,
copyable: null,
...x,
}
}
const asProperty = (x: PackagePropertiesV2): T.PropertiesReturn =>
Object.fromEntries(
Object.entries(x).map(([key, value]) => [key, asProperty_(value)]),
)
const [matchPackageProperties, setMatchPackageProperties] =
deferred<PackagePropertiesV2>()
const matchPackagePropertyObject: Parser<unknown, PackagePropertyObject> =
@@ -92,44 +119,6 @@ const matchProperties = object({
data: matchPackageProperties,
})
type ExportUi = {
values: { [key: string]: any }
expose: { [key: string]: T.ExposeUiPathsAll }
}
function propertiesToExportUi(
properties: PackagePropertiesV2,
previousPath = "",
): ExportUi {
const exportUi: ExportUi = {
values: {},
expose: {},
}
for (const [key, value] of Object.entries(properties)) {
const path = `${previousPath}/${key}`
if (value.type === "object") {
const { values, expose } = propertiesToExportUi(value.value, path)
exportUi.values[key] = values
exportUi.expose[key] = {
type: "object",
value: expose,
description: value.description,
}
continue
}
exportUi.values[key] = value.value
exportUi.expose[key] = {
type: "string",
path,
description: value.description ?? null,
masked: value.masked ?? false,
copyable: value.copyable ?? null,
qr: value.qr ?? null,
}
}
return exportUi
}
export class SystemForEmbassy implements System {
currentRunning: MainLoop | undefined
static async of(manifestLocation: string = MANIFEST_LOCATION) {
@@ -236,6 +225,8 @@ export class SystemForEmbassy implements System {
return this.getConfig(effects)
case "/config/set":
return this.setConfig(effects, input)
case "/properties":
return this.properties(effects)
case "/actions/metadata":
return todo()
case "/init":
@@ -279,9 +270,7 @@ export class SystemForEmbassy implements System {
private async mainStart(effects: HostSystemStartOs): Promise<void> {
if (!!this.currentRunning) return
this.currentRunning = new MainLoop(this, effects, () =>
this.properties(effects),
)
this.currentRunning = new MainLoop(this, effects)
}
private async mainStop(
effects: HostSystemStartOs,
@@ -472,51 +461,44 @@ export class SystemForEmbassy implements System {
}
return { configured: true }
}
private async properties(effects: HostSystemStartOs): Promise<undefined> {
private async properties(
effects: HostSystemStartOs,
): Promise<ReturnType<T.ExpectedExports.Properties>> {
// TODO BLU-J set the properties ever so often
const setConfigValue = this.manifest.properties
if (!setConfigValue) return
if (!setConfigValue) throw new Error("There is no properties")
if (setConfigValue.type === "docker") {
const container = await DockerProcedureContainer.of(
effects,
setConfigValue,
this.manifest.volumes,
)
const properties = JSON.parse(
(
await container.exec([
setConfigValue.entrypoint,
...setConfigValue.args,
])
).stdout.toString(),
const properties = matchProperties.unsafeCast(
JSON.parse(
(
await container.exec([
setConfigValue.entrypoint,
...setConfigValue.args,
])
).stdout.toString(),
),
)
if (!matchProperties.test(properties)) return
const exposeUis = propertiesToExportUi(properties.data)
await effects.store.set<any, any>({
path: "/properties",
value: exposeUis.values,
})
await effects.exposeUi(exposeUis.expose)
return asProperty(properties.data)
} else if (setConfigValue.type === "script") {
const moduleCode = this.moduleCode
const method = moduleCode.properties
if (!method)
throw new Error("Expecting that the method properties exists")
const properties = await method(
new PolyfillEffects(effects, this.manifest),
).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])
})
if (!matchProperties.test(properties)) return
const exposeUis = propertiesToExportUi(properties.data)
await effects.store.set<any, any>({
path: "/properties",
value: exposeUis.values,
})
await effects.exposeUi(exposeUis.expose)
const properties = matchProperties.unsafeCast(
await method(new PolyfillEffects(effects, this.manifest)).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])
}),
)
return asProperty(properties.data)
}
throw new Error(`Unknown type in the fetch properties: ${setConfigValue}`)
}
private async health(
effects: HostSystemStartOs,
@@ -651,279 +633,6 @@ export class SystemForEmbassy implements System {
throw new Error("Error getting config: " + x["error-code"][1])
})) as any
}
// private async sandbox(
// effects: HostSystemStartOs,
// options: {
// procedure:
// | "/createBackup"
// | "/restoreBackup"
// | "/getConfig"
// | "/setConfig"
// | "migration"
// | "/properties"
// | `/action/${string}`
// | `/dependencies/${string}/check`
// | `/dependencies/${string}/autoConfigure`
// input: unknown
// timeout?: number | undefined
// },
// ): Promise<unknown> {
// const input = options.input
// switch (options.procedure) {
// case "/createBackup":
// return this.roCreateBackup(effects)
// case "/restoreBackup":
// return this.roRestoreBackup(effects)
// case "/getConfig":
// return this.roGetConfig(effects)
// case "/setConfig":
// return this.roSetConfig(effects, input)
// case "migration":
// return this.roMigration(effects, input)
// case "/properties":
// return this.roProperties(effects)
// default:
// const procedure = options.procedure.split("/")
// switch (true) {
// case options.procedure.startsWith("/action/"):
// return this.roAction(effects, procedure[2], input)
// case options.procedure.startsWith("/dependencies/") &&
// procedure[3] === "check":
// return this.roDependenciesCheck(effects, procedure[2], input)
// case options.procedure.startsWith("/dependencies/") &&
// procedure[3] === "autoConfigure":
// return this.roDependenciesAutoconfig(effects, procedure[2], input)
// }
// }
// }
// private async roCreateBackup(effects: HostSystemStartOs): Promise<void> {
// const backup = this.manifest.backup.create
// if (backup.type === "docker") {
// const container = await DockerProcedureContainer.readonlyOf(backup)
// await container.exec([backup.entrypoint, ...backup.args])
// } else {
// const moduleCode = await this.moduleCode
// await moduleCode.createBackup?.(new PolyfillEffects(effects))
// }
// }
// private async roRestoreBackup(effects: HostSystemStartOs): Promise<void> {
// const restoreBackup = this.manifest.backup.restore
// if (restoreBackup.type === "docker") {
// const container = await DockerProcedureContainer.readonlyOf(restoreBackup)
// await container.exec([restoreBackup.entrypoint, ...restoreBackup.args])
// } else {
// const moduleCode = await this.moduleCode
// await moduleCode.restoreBackup?.(new PolyfillEffects(effects))
// }
// }
// private async roGetConfig(effects: HostSystemStartOs): Promise<T.ConfigRes> {
// const config = this.manifest.config?.get
// if (!config) return { spec: {} }
// if (config.type === "docker") {
// const container = await DockerProcedureContainer.readonlyOf(config)
// return JSON.parse(
// (await container.exec([config.entrypoint, ...config.args])).stdout,
// )
// } else {
// const moduleCode = await this.moduleCode
// const method = moduleCode.getConfig
// if (!method) throw new Error("Expecting that the method getConfig exists")
// return (await method(new PolyfillEffects(effects)).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
// }
// }
// private async roSetConfig(
// effects: HostSystemStartOs,
// newConfig: unknown,
// ): Promise<T.SetResult> {
// const setConfigValue = this.manifest.config?.set
// if (!setConfigValue) return { signal: "SIGTERM", "depends-on": {} }
// if (setConfigValue.type === "docker") {
// const container = await DockerProcedureContainer.readonlyOf(
// setConfigValue,
// )
// return JSON.parse(
// (
// await container.exec([
// setConfigValue.entrypoint,
// ...setConfigValue.args,
// JSON.stringify(newConfig),
// ])
// ).stdout,
// )
// } else {
// const moduleCode = await this.moduleCode
// const method = moduleCode.setConfig
// if (!method) throw new Error("Expecting that the method setConfig exists")
// return await method(
// new PolyfillEffects(effects),
// newConfig as U.Config,
// ).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])
// })
// }
// }
// private async roMigration(
// effects: HostSystemStartOs,
// fromVersion: unknown,
// ): Promise<T.MigrationRes> {
// throw new Error("Migrations should never be ran in the sandbox mode")
// }
// private async roProperties(effects: HostSystemStartOs): Promise<unknown> {
// const setConfigValue = this.manifest.properties
// if (!setConfigValue) return {}
// if (setConfigValue.type === "docker") {
// const container = await DockerProcedureContainer.readonlyOf(
// setConfigValue,
// )
// return JSON.parse(
// (
// await container.exec([
// setConfigValue.entrypoint,
// ...setConfigValue.args,
// ])
// ).stdout,
// )
// } else {
// const moduleCode = await this.moduleCode
// const method = moduleCode.properties
// if (!method)
// throw new Error("Expecting that the method properties exists")
// return await method(new PolyfillEffects(effects)).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])
// })
// }
// }
// private async roHealth(
// effects: HostSystemStartOs,
// healthId: string,
// timeSinceStarted: unknown,
// ): Promise<void> {
// const healthProcedure = this.manifest["health-checks"][healthId]
// if (!healthProcedure) return
// if (healthProcedure.type === "docker") {
// const container = await DockerProcedureContainer.readonlyOf(
// healthProcedure,
// )
// return JSON.parse(
// (
// await container.exec([
// healthProcedure.entrypoint,
// ...healthProcedure.args,
// JSON.stringify(timeSinceStarted),
// ])
// ).stdout,
// )
// } else {
// const moduleCode = await this.moduleCode
// const method = moduleCode.health?.[healthId]
// if (!method) throw new Error("Expecting that the method health exists")
// await method(new PolyfillEffects(effects), Number(timeSinceStarted)).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])
// },
// )
// }
// }
// private async roAction(
// effects: HostSystemStartOs,
// actionId: string,
// formData: unknown,
// ): Promise<T.ActionResult> {
// const actionProcedure = this.manifest.actions?.[actionId]?.implementation
// if (!actionProcedure) return { message: "Action not found", value: null }
// if (actionProcedure.type === "docker") {
// const container = await DockerProcedureContainer.readonlyOf(
// actionProcedure,
// )
// return JSON.parse(
// (
// await container.exec([
// actionProcedure.entrypoint,
// ...actionProcedure.args,
// JSON.stringify(formData),
// ])
// ).stdout,
// )
// } else {
// const moduleCode = await this.moduleCode
// const method = moduleCode.action?.[actionId]
// if (!method) throw new Error("Expecting that the method action exists")
// return (await method(new PolyfillEffects(effects), formData as any).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
// }
// }
// private async roDependenciesCheck(
// effects: HostSystemStartOs,
// id: string,
// oldConfig: unknown,
// ): Promise<object> {
// const actionProcedure = this.manifest.dependencies?.[id]?.config?.check
// if (!actionProcedure) return { message: "Action not found", value: null }
// if (actionProcedure.type === "docker") {
// const container = await DockerProcedureContainer.readonlyOf(
// actionProcedure,
// )
// return JSON.parse(
// (
// await container.exec([
// actionProcedure.entrypoint,
// ...actionProcedure.args,
// JSON.stringify(oldConfig),
// ])
// ).stdout,
// )
// } else {
// const moduleCode = await this.moduleCode
// const method = moduleCode.dependencies?.[id]?.check
// if (!method)
// throw new Error(
// `Expecting that the method dependency check ${id} exists`,
// )
// return (await method(new PolyfillEffects(effects), oldConfig as any).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
// }
// }
// private async roDependenciesAutoconfig(
// effects: HostSystemStartOs,
// id: string,
// oldConfig: unknown,
// ): Promise<void> {
// const moduleCode = await this.moduleCode
// const method = moduleCode.dependencies?.[id]?.autoConfigure
// if (!method)
// throw new Error(
// `Expecting that the method dependency autoConfigure ${id} exists`,
// )
// return (await method(new PolyfillEffects(effects), oldConfig as any).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
// }
}
async function removePointers(value: T.ConfigRes): Promise<T.ConfigRes> {
const startingSpec = structuredClone(value.spec)

View File

@@ -35,6 +35,7 @@ export const jsonPath = some(
"/backup/create",
"/backup/restore",
"/actions/metadata",
"/properties",
),
string.refine(isNestedPath, "isNestedPath"),
)

View File

@@ -9,6 +9,7 @@ pub enum ProcedureName {
GetConfig,
SetConfig,
CreateBackup,
Properties,
RestoreBackup,
ActionMetadata,
RunAction(ActionId),
@@ -29,6 +30,7 @@ impl ProcedureName {
ProcedureName::SetConfig => "/config/set".to_string(),
ProcedureName::GetConfig => "/config/get".to_string(),
ProcedureName::CreateBackup => "/backup/create".to_string(),
ProcedureName::Properties => "/properties".to_string(),
ProcedureName::RestoreBackup => "/backup/restore".to_string(),
ProcedureName::ActionMetadata => "/actions/metadata".to_string(),
ProcedureName::RunAction(id) => format!("/actions/{}/run", id),

View File

@@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::{borrow::Borrow, collections::BTreeMap};
use clap::Parser;
use imbl_value::{json, InOMap, InternedString, Value};
@@ -15,48 +15,6 @@ pub fn display_properties(response: Value) {
println!("{}", response);
}
trait IntoProperties {
fn into_properties(self, store: &Value) -> Value;
}
impl IntoProperties for ExposedUI {
fn into_properties(self, store: &Value) -> Value {
match self {
ExposedUI::Object { value, description } => {
json!({
"type": "object",
"description": description,
"value": value.into_iter().map(|(k, v)| (k, v.into_properties(store))).collect::<BTreeMap<String,_>>()
})
}
ExposedUI::String {
path,
description,
masked,
copyable,
qr,
} => json!({
"type": "string",
"description": description,
"value": path.get(store).cloned().unwrap_or_default(),
"copyable": copyable,
"qr": qr,
"masked": masked
}),
}
}
}
impl IntoProperties for StoreExposedUI {
fn into_properties(self, store: &Value) -> Value {
Value::Object(
self.0
.into_iter()
.map(|(k, v)| (k, v.into_properties(store)))
.collect::<InOMap<InternedString, Value>>(),
)
}
}
#[derive(Deserialize, Serialize, Parser)]
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
@@ -68,19 +26,11 @@ pub async fn properties(
ctx: RpcContext,
PropertiesParam { id }: PropertiesParam,
) -> Result<Value, Error> {
let peeked = ctx.db.peek().await;
let data = peeked
.as_private()
.as_package_stores()
.as_idx(&id)
.map(|x| x.de())
.unwrap_or_else(|| Ok(json!({})))?;
Ok(peeked
.as_public()
.as_package_data()
.as_idx(&id)
.or_not_found(&id)?
.as_store_exposed_ui()
.de()?
.into_properties(&data))
match &*ctx.services.get(&id).await {
Some(service) => service.properties().await,
None => Err(Error::new(
eyre!("Could not find a service with id {id}"),
ErrorKind::NotFound,
)),
}
}

View File

@@ -325,6 +325,17 @@ impl Service {
.await
.with_kind(ErrorKind::Action)
}
pub async fn properties(&self) -> Result<Value, Error> {
let container = &self.seed.persistent_container;
container
.execute::<Value>(
ProcedureName::Properties,
Value::Null,
Some(Duration::from_secs(30)),
)
.await
.with_kind(ErrorKind::Unknown)
}
pub async fn shutdown(self) -> Result<(), Error> {
self.actor

14
sdk/lib/index.browser.ts Normal file
View File

@@ -0,0 +1,14 @@
export { EmVer } from "./emverLite/mod"
export { setupManifest } from "./manifest/setupManifest"
export { setupExposeStore } from "./store/setupExposeStore"
export * as config from "./config"
export * as CB from "./config/builder"
export * as CT from "./config/configTypes"
export * as dependencyConfig from "./dependencyConfig"
export * as manifest from "./manifest"
export * as types from "./types"
export * as T from "./types"
export * as yaml from "yaml"
export * as matches from "ts-matches"
export * as util from "./util/index.browser"

View File

@@ -5,6 +5,7 @@ export { StartSdk } from "./StartSdk"
export { setupManifest } from "./manifest/setupManifest"
export { FileHelper } from "./util/fileHelper"
export { setupExposeStore } from "./store/setupExposeStore"
export { pathBuilder } from "./store/PathBuilder"
export * as actions from "./actions"
export * as backup from "./backup"
export * as config from "./config"

View File

@@ -0,0 +1,25 @@
import * as T from "../types"
export { GetServiceInterface, getServiceInterface } from "./getServiceInterface"
export { getServiceInterfaces } from "./getServiceInterfaces"
// prettier-ignore
export type FlattenIntersection<T> =
T extends ArrayLike<any> ? T :
T extends object ? {} & {[P in keyof T]: T[P]} :
T;
export type _<T> = FlattenIntersection<T>
export const isKnownError = (e: unknown): e is T.KnownError =>
e instanceof Object && ("error" in e || "error-code" in e)
declare const affine: unique symbol
export type Affine<A> = { [affine]: A }
type NeverPossible = { [affine]: string }
export type NoAny<A> = NeverPossible extends A
? keyof NeverPossible extends keyof A
? never
: A
: A

12
sdk/package-lock.json generated
View File

@@ -9,19 +9,19 @@
"version": "0.4.0-rev0.lib0.rc8.beta10",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^2.2.5",
"isomorphic-fetch": "^3.0.0",
"ts-matches": "^5.4.1",
"yaml": "^2.2.2"
"ts-matches": "^5.4.1"
},
"devDependencies": {
"@iarna/toml": "^2.2.5",
"@types/jest": "^29.4.0",
"jest": "^29.4.3",
"prettier": "^3.2.5",
"ts-jest": "^29.0.5",
"ts-node": "^10.9.1",
"tsx": "^4.7.1",
"typescript": "^5.0.4"
"typescript": "^5.0.4",
"yaml": "^2.2.2"
}
},
"node_modules/@ampproject/remapping": {
@@ -651,7 +651,8 @@
"node_modules/@iarna/toml": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz",
"integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="
"integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==",
"dev": true
},
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
@@ -4235,6 +4236,7 @@
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz",
"integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==",
"dev": true,
"engines": {
"node": ">= 14"
}

View File

@@ -5,14 +5,8 @@
"main": "./cjs/sdk/lib/index.js",
"types": "./cjs/sdk/lib/index.d.ts",
"module": "./mjs/sdk/lib/index.js",
"browser": "./mjs/sdk/lib/index.browser.js",
"sideEffects": true,
"exports": {
".": {
"import": "./mjs/sdk/lib/index.js",
"require": "./cjs/sdk/lib/index.js",
"types": "./cjs/sdk/lib/index.d.ts"
}
},
"typesVersion": {
">=3.1": {
"*": [
@@ -36,10 +30,8 @@
},
"homepage": "https://github.com/Start9Labs/start-sdk#readme",
"dependencies": {
"@iarna/toml": "^2.2.5",
"isomorphic-fetch": "^3.0.0",
"ts-matches": "^5.4.1",
"yaml": "^2.2.2"
"ts-matches": "^5.4.1"
},
"prettier": {
"trailingComma": "all",
@@ -48,6 +40,8 @@
"singleQuote": false
},
"devDependencies": {
"@iarna/toml": "^2.2.5",
"yaml": "^2.2.2",
"@types/jest": "^29.4.0",
"jest": "^29.4.3",
"prettier": "^3.2.5",

8
web/package-lock.json generated
View File

@@ -1976,19 +1976,19 @@
"version": "0.4.0-rev0.lib0.rc8.beta10",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^2.2.5",
"isomorphic-fetch": "^3.0.0",
"ts-matches": "^5.4.1",
"yaml": "^2.2.2"
"ts-matches": "^5.4.1"
},
"devDependencies": {
"@iarna/toml": "^2.2.5",
"@types/jest": "^29.4.0",
"jest": "^29.4.3",
"prettier": "^3.2.5",
"ts-jest": "^29.0.5",
"ts-node": "^10.9.1",
"tsx": "^4.7.1",
"typescript": "^5.0.4"
"typescript": "^5.0.4",
"yaml": "^2.2.2"
}
},
"node_modules/@adobe/css-tools": {