feat: add static variable and resources support to ansible (#4435)

* Add static variable and resources to ansible

* Add wasm parser to cli

* Fix pulling of resources, rs and playbooks

* Remove unused import, add publish pkg-yaml

* Update package.json

* Update generate-metadata

* Fix null fields being serialized in script metadata

* npm install
This commit is contained in:
wendrul
2024-09-25 15:05:53 +02:00
committed by GitHub
parent d7f8e78d47
commit 08ae91e6dd
14 changed files with 365 additions and 89 deletions
@@ -21,3 +21,6 @@ popd
pushd "pkg-rust" && npm publish ${args}
popd
pushd "pkg-yaml" && npm publish ${args}
popd
+212 -11
View File
@@ -25,14 +25,19 @@ pub fn parse_ansible_sig(inner_content: &str) -> anyhow::Result<MainArgSignature
if let Yaml::Hash(v) = &value {
for (key, arg) in v {
if let Yaml::String(arg_name) = key {
args.push(Arg {
name: arg_name.to_string(),
otyp: None,
typ: parse_ansible_typ(&arg),
default: None,
has_default: false,
oidx: None,
})
// if not then it is a static resource or var, so skip
// it will be fetched by the worker
if let ArgTyp::Typ(typ) = parse_ansible_arg_typ(arg) {
let default = get_default_for_typ(&arg);
args.push(Arg {
name: arg_name.to_string(),
otyp: None,
typ,
has_default: default.is_some(),
default,
oidx: None,
})
}
}
}
}
@@ -65,6 +70,50 @@ pub fn parse_ansible_sig(inner_content: &str) -> anyhow::Result<MainArgSignature
})
}
fn get_default_for_typ(arg: &Yaml) -> Option<serde_json::Value> {
if let Yaml::Hash(arg) = arg {
if let Some(def) = arg.get(&Yaml::String("default".to_string())) {
if let Some(Yaml::String(typ)) = arg.get(&Yaml::String("type".to_string())) {
if let Yaml::String(path) = def {
if typ.as_str() == "windmill_resource" {
return Some(serde_json::Value::String(format!("$res:{}", path)));
}
}
}
return Some(yaml_to_json(def));
}
}
None
}
enum ArgTyp {
Typ(Typ),
StaticVar(String),
StaticResource(String),
}
fn parse_ansible_arg_typ(arg: &Yaml) -> ArgTyp {
if let Yaml::Hash(a) = arg {
if let Some(Yaml::String(typ)) = a.get(&Yaml::String("type".to_string())) {
if typ.as_str() == "windmill_variable" {
if let Some(Yaml::String(variable_path)) =
a.get(&Yaml::String("variable".to_string()))
{
return ArgTyp::StaticVar(variable_path.to_string());
}
}
if typ.as_str() == "windmill_resource" {
if let Some(Yaml::String(resource_path)) =
a.get(&Yaml::String("resource".to_string()))
{
return ArgTyp::StaticResource(resource_path.to_string());
}
}
}
}
ArgTyp::Typ(parse_ansible_typ(arg))
}
fn parse_ansible_typ(arg: &Yaml) -> Typ {
if let Yaml::Hash(arg) = arg {
if let Some(Yaml::String(typ)) = arg.get(&Yaml::String("type".to_string())) {
@@ -127,7 +176,19 @@ fn parse_ansible_typ(arg: &Yaml) -> Typ {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct AnsiblePlaybookOptions {
// There are a lot more options
// TODO: Add the options as customers require them
// Add it here and then on the executors cmd_options
pub verbosity: Option<String>,
pub forks: Option<i64>,
pub timeout: Option<i64>,
pub flush_cache: Option<()>,
pub force_handlers: Option<()>,
}
#[derive(Debug, Clone)]
pub struct FileResource {
pub resource_path: String,
pub target_path: String,
@@ -140,12 +201,15 @@ pub struct AnsibleInventory {
resource_type: Option<String>,
pub pinned_resource: Option<String>,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct AnsibleRequirements {
pub python_reqs: Vec<String>,
pub collections: Option<String>,
pub file_resources: Vec<FileResource>,
pub inventories: Vec<AnsibleInventory>,
pub vars: Vec<(String, String)>,
pub resources: Vec<(String, String)>,
pub options: AnsiblePlaybookOptions,
}
fn parse_inventories(inventory_yaml: &Yaml) -> anyhow::Result<Vec<AnsibleInventory>> {
@@ -204,11 +268,21 @@ pub fn parse_ansible_reqs(
return Ok((logs, None, inner_content.to_string()));
}
let opts = AnsiblePlaybookOptions {
verbosity: None,
forks: None,
timeout: None,
flush_cache: None,
force_handlers: None,
};
let mut ret = AnsibleRequirements {
python_reqs: vec![],
collections: None,
file_resources: vec![],
inventories: vec![],
vars: vec![],
resources: vec![],
options: opts,
};
if let Yaml::Hash(doc) = &docs[0] {
@@ -242,10 +316,33 @@ pub fn parse_ansible_reqs(
ret.file_resources.append(&mut resources?);
}
}
Yaml::String(key) if key == "extra_vars" => {}
Yaml::String(key) if key == "extra_vars" => {
if let Yaml::Hash(v) = &value {
for (key, arg) in v {
if let Yaml::String(arg_name) = key {
match parse_ansible_arg_typ(arg) {
ArgTyp::StaticVar(p) => {
ret.vars
.push((arg_name.to_string(), format!("$var:{}", p)));
}
ArgTyp::StaticResource(p) => {
ret.resources
.push((arg_name.to_string(), format!("$res:{}", p)));
}
ArgTyp::Typ(_) => (),
}
}
}
}
}
Yaml::String(key) if key == "inventory" => {
ret.inventories = parse_inventories(value)?;
}
Yaml::String(key) if key == "options" => {
if let Yaml::Array(opts) = &value {
ret.options = parse_ansible_options(opts);
}
}
Yaml::String(key) => logs.push_str(&format!("\nUnknown field `{}`. Ignoring", key)),
_ => (),
}
@@ -260,6 +357,82 @@ pub fn parse_ansible_reqs(
Ok((logs, Some(ret), out_str))
}
fn parse_ansible_options(opts: &Vec<Yaml>) -> AnsiblePlaybookOptions {
let mut ret = AnsiblePlaybookOptions {
verbosity: None,
forks: None,
timeout: None,
flush_cache: None,
force_handlers: None,
};
for opt in opts {
if let Yaml::String(o) = opt {
match o.as_str() {
"flush_cache" => {
ret.flush_cache = Some(());
}
"force_handlers" => {
ret.force_handlers = Some(());
}
s if count_consecutive_vs(s) > 0 => {
ret.verbosity = Some("v".repeat(count_consecutive_vs(s).min(6)));
}
_ => (),
}
}
if let Yaml::Hash(m) = opt {
if let Some((Yaml::String(name), value)) = m.iter().last() {
match name.as_str() {
"forks" => {
if let Yaml::Integer(count) = value {
ret.forks = Some(*count);
}
}
"timeout" => {
if let Yaml::Integer(timeout) = value {
ret.timeout = Some(*timeout);
}
}
"verbosity" => {
if let Yaml::String(verbosity) = value {
let c = count_consecutive_vs(verbosity);
if c > 0 && c <= 6 {
ret.verbosity = Some("v".repeat(c.min(6)));
}
}
}
_ => ()
}
}
}
}
ret
}
fn count_consecutive_vs(s: &str) -> usize {
let mut max_count = 0;
let mut current_count = 0;
for c in s.chars() {
if c == 'v' {
current_count += 1;
if current_count == 6 {
return 6; // Stop early if we reach 6
}
} else {
current_count = 0; // Reset count if the character is not 'v'
}
max_count = max_count.max(current_count);
}
max_count
}
fn parse_file_resource(yaml: &Yaml) -> anyhow::Result<FileResource> {
if let Yaml::Hash(f) = yaml {
if let Some(Yaml::String(resource_path)) = f.get(&Yaml::String("resource".to_string())) {
@@ -279,3 +452,31 @@ fn parse_file_resource(yaml: &Yaml) -> anyhow::Result<FileResource> {
}
return Err(anyhow!("Invalid file resource: Should be a dictionary."));
}
fn yaml_to_json(yaml: &Yaml) -> serde_json::Value {
match yaml {
Yaml::Array(arr) => {
let json_array: Vec<serde_json::Value> = arr.into_iter().map(yaml_to_json).collect();
serde_json::Value::Array(json_array)
}
Yaml::Hash(hash) => {
let json_object = hash
.into_iter()
.map(|(k, v)| {
let key = match k {
Yaml::String(s) => s.clone(),
_ => k.as_str().unwrap_or("").to_string(),
};
(key, yaml_to_json(v))
})
.collect();
serde_json::Value::Object(json_object)
}
Yaml::String(s) => serde_json::Value::String(s.to_string()),
Yaml::Integer(i) => serde_json::Value::Number(i.clone().into()),
Yaml::Real(r) => serde_json::Value::Number(r.parse().unwrap_or(0.into())),
Yaml::Boolean(b) => serde_json::Value::Bool(*b),
Yaml::Null => serde_json::Value::Null,
_ => serde_json::Value::Null,
}
}
+1 -1
View File
@@ -2577,7 +2577,7 @@ async fn tarball_workspace(
}
ScriptLang::Php => "php",
ScriptLang::Rust => "rs",
ScriptLang::Ansible => "yaml",
ScriptLang::Ansible => "playbook.yml",
};
archive
.write_to_archive(&script.content, &format!("{}.{}", script.path, ext))
+65 -12
View File
@@ -1,5 +1,8 @@
use std::{
collections::HashMap, os::unix::fs::PermissionsExt, path::{Path, PathBuf}, process::Stdio
collections::HashMap,
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
process::Stdio,
};
use anyhow::anyhow;
@@ -10,16 +13,20 @@ use uuid::Uuid;
use windmill_common::{
error,
jobs::QueuedJob,
worker::{write_file, write_file_at_user_defined_location, WORKER_CONFIG},
worker::{to_raw_value, write_file, write_file_at_user_defined_location, WORKER_CONFIG},
};
use windmill_parser_yaml::AnsibleRequirements;
use windmill_queue::{append_logs, CanceledBy};
use crate::{
bash_executor::BIN_BASH, common::{
bash_executor::BIN_BASH,
common::{
get_reserved_variables, handle_child, read_and_check_result, start_child_process,
transform_json,
}, python_executor::{create_dependencies_dir, handle_python_reqs, pip_compile}, AuthedClientBackgroundTask, DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV, NSJAIL_PATH, PATH_ENV, TZ_ENV
},
python_executor::{create_dependencies_dir, handle_python_reqs, pip_compile},
AuthedClientBackgroundTask, DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV, NSJAIL_PATH, PATH_ENV,
TZ_ENV,
};
lazy_static::lazy_static! {
@@ -198,7 +205,6 @@ pub async fn handle_ansible_job(
append_logs(&job.id, &job.workspace_id, logs, db).await;
write_file(job_dir, "main.yml", &playbook)?;
let additional_python_paths = handle_ansible_python_deps(
job_dir,
requirements_o,
@@ -215,7 +221,13 @@ pub async fn handle_ansible_job(
let interpolated_args;
if let Some(args) = &job.args {
if let Some(x) = transform_json(client, &job.workspace_id, &args.0, job, db).await? {
let mut args = args.0.clone();
if let Some(reqs) = reqs.clone() {
for (name, path) in reqs.resources.iter().chain(reqs.vars.iter()) {
args.insert(name.clone(), to_raw_value(path));
}
}
if let Some(x) = transform_json(client, &job.workspace_id, &args, job, db).await? {
write_file(
job_dir,
"args.json",
@@ -228,7 +240,7 @@ pub async fn handle_ansible_job(
"args.json",
&serde_json::to_string(&args).unwrap_or_else(|_| "{}".to_string()),
)?;
interpolated_args = Some(args.clone().0);
interpolated_args = Some(args);
}
} else {
interpolated_args = None;
@@ -236,6 +248,12 @@ pub async fn handle_ansible_job(
};
write_file(job_dir, "result.json", "")?;
let cmd_options: Vec<String> = reqs
.as_ref()
.map(|r| r.options.clone())
.map(|r| get_cmd_options(r))
.unwrap_or_default();
let inventories: Vec<String> = reqs
.as_ref()
.map(|x| {
@@ -282,14 +300,16 @@ pub async fn handle_ansible_job(
db,
)
.await;
let ansible_cfg_content = format!(r#"
let ansible_cfg_content = format!(
r#"
[defaults]
collections_path = ./
roles_path = ./roles
home={job_dir}/.ansible
local_tmp={job_dir}/.ansible/tmp
remote_tmp={job_dir}/.ansible/tmp
"#);
"#
);
write_file(job_dir, "ansible.cfg", &ansible_cfg_content)?;
let mut reserved_variables = get_reserved_variables(job, &authed_client.token, db).await?;
@@ -331,10 +351,11 @@ mount {{
let mut cmd_args = vec!["main.yml", "--extra-vars", "@args.json"];
cmd_args.extend(inventories.iter().map(|s| s.as_str()));
cmd_args.extend(cmd_options.iter().map(|s| s.as_str()));
let child = if !*DISABLE_NSJAIL {
let wrapper = format!(r#"set -eou pipefail
let wrapper = format!(
r#"set -eou pipefail
{0} "$@"
if [ -f "result" ]; then
cat result > result_nsjail_mount.json
@@ -342,7 +363,9 @@ fi
if [ -f "result.json" ]; then
cat result.json > result_nsjail_mount.json
fi
"#, ANSIBLE_PLAYBOOK_PATH.as_str());
"#,
ANSIBLE_PLAYBOOK_PATH.as_str()
);
let file = write_file(job_dir, "wrapper.sh", &wrapper)?;
@@ -406,6 +429,36 @@ fi
read_and_check_result(job_dir).await
}
fn get_cmd_options(r: windmill_parser_yaml::AnsiblePlaybookOptions) -> Vec<String> {
let mut ret = vec![];
if let Some(v) = r.verbosity {
if v.chars().all(|c| c == 'v') && v.len() <= 6 {
ret.push(format!("-{}", v));
}
}
if let Some(o) = r.timeout {
ret.push("--timeout".to_string());
ret.push(o.to_string());
}
if let Some(o) = r.forks {
ret.push("--forks".to_string());
ret.push(o.to_string());
}
if r.flush_cache.is_some() {
ret.push("--flush-cache".to_string());
}
if r.force_handlers.is_some() {
ret.push("--force-handlers".to_string());
}
ret
}
fn define_nsjail_mount(job_dir: &str, path: &PathBuf) -> anyhow::Result<String> {
Ok(format!(
r#"
+2 -1
View File
@@ -1,2 +1,3 @@
#!/bin/bash
./gen_wm_client.sh
deno run -A dnt.ts
deno run -A dnt.ts
+2 -1
View File
@@ -1,3 +1,4 @@
#!/bin/bash
set -eou pipefail
script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -19,4 +20,4 @@ sed -i "s/BASE: '\/api'/BASE: baseUrlApi/g" gen/core/OpenAPI.ts
find gen/ -name "*.ts" -exec sed -i -E "s/(import.*from[[:space:]]*['\"][^'\"]+)(['\"])/\1.ts\2/g" {} \;
find gen/ -name "*.ts" -exec sed -i -E "s/(export.*from[[:space:]]*['\"][^'\"]+)(['\"])/\1.ts\2/g" {} \;
find gen/ -name "*.ts" -exec sed -i -E "s/(export.*from[[:space:]]*['\"][^'\"]+)(['\"])/\1.ts\2/g" {} \;
+16 -3
View File
@@ -7,6 +7,7 @@ import {
} from "./bootstrap/script_bootstrap.ts";
import {
instantiate as instantiateWasm,
parse_ansible,
parse_bash,
parse_bigquery,
parse_deno,
@@ -14,8 +15,10 @@ import {
parse_graphql,
parse_mssql,
parse_mysql,
parse_php,
parse_powershell,
parse_python,
parse_rust,
parse_snowflake,
parse_sql,
} from "./wasm/windmill_parser_wasm.generated.js";
@@ -306,8 +309,10 @@ export async function updateScriptSchema(
path
);
metadataContent.schema = result.schema;
metadataContent.has_preprocessor = result.has_preprocessor;
metadataContent.no_main_func = result.no_main_func;
if (result.has_preprocessor != null)
metadataContent.has_preprocessor = result.has_preprocessor;
if (result.no_main_func != null)
metadataContent.no_main_func = result.no_main_func;
}
async function updateScriptLock(
@@ -324,7 +329,9 @@ async function updateScriptLock(
language == "python3" ||
language == "go" ||
language == "deno" ||
language == "php"
language == "php" ||
language == "rust" ||
language == "ansible"
)
) {
return;
@@ -482,6 +489,12 @@ export function inferSchema(
inferedSchema = JSON.parse(parse_bash(content));
} else if (language === "powershell") {
inferedSchema = JSON.parse(parse_powershell(content));
} else if (language === "php") {
inferedSchema = JSON.parse(parse_php(content));
} else if (language === "rust") {
inferedSchema = JSON.parse(parse_rust(content));
} else if (language === "ansible") {
inferedSchema = JSON.parse(parse_ansible(content));
} else {
throw new Error("Invalid language: " + language);
}
+2 -2
View File
@@ -48,9 +48,9 @@ export function inferContentTypeFromFilePath(
return "bash";
} else if (contentPath.endsWith(".ps1")) {
return "powershell";
} else if (contentPath.endsWith("php")) {
} else if (contentPath.endsWith(".php")) {
return "php";
} else if (contentPath.endsWith("rs")) {
} else if (contentPath.endsWith(".rs")) {
return "rust";
} else if (contentPath.endsWith(".playbook.yml")) {
return "ansible";
+14 -12
View File
@@ -504,18 +504,20 @@ function ZipFSElement(
if (formatExtension) {
const fileContent: string = parsed["value"]["content"];
r.push({
isDirectory: false,
path:
removeSuffix(finalPath, ".resource.json") +
".resource.file." +
formatExtension,
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return fileContent;
},
});
if (typeof(fileContent) === "string") {
r.push({
isDirectory: false,
path:
removeSuffix(finalPath, ".resource.json") +
".resource.file." +
formatExtension,
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return fileContent;
},
});
}
}
}
return r;
+3 -1
View File
@@ -200,7 +200,9 @@ export function getTypeStrFromPath(
parsed.ext == ".gql" ||
parsed.ext == ".ps1" ||
parsed.ext == ".js" ||
parsed.ext == ".php"
parsed.ext == ".php" ||
parsed.ext == ".rs" ||
(parsed.ext == ".yml" && parsed.name.split(".").pop() == "playbook")
) {
return "script";
}
+39 -39
View File
@@ -15,20 +15,6 @@ function getObject(idx) {
return heap[idx];
}
let heap_next = heap.length;
function dropObject(idx) {
if (idx < 132) return;
heap[idx] = heap_next;
heap_next = idx;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
let WASM_VECTOR_LEN = 0;
let cachedUint8Memory0 = null;
@@ -103,6 +89,38 @@ function getInt32Memory0() {
return cachedInt32Memory0;
}
let heap_next = heap.length;
function dropObject(idx) {
if (idx < 132) return;
heap[idx] = heap_next;
heap_next = idx;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
let cachedFloat64Memory0 = null;
function getFloat64Memory0() {
if (cachedFloat64Memory0 === null || cachedFloat64Memory0.byteLength === 0) {
cachedFloat64Memory0 = new Float64Array(wasm.memory.buffer);
}
return cachedFloat64Memory0;
}
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
heap[idx] = obj;
return idx;
}
const cachedTextDecoder = typeof TextDecoder !== "undefined"
? new TextDecoder("utf-8", { ignoreBOM: true, fatal: true })
: {
@@ -118,24 +136,6 @@ function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
heap[idx] = obj;
return idx;
}
let cachedFloat64Memory0 = null;
function getFloat64Memory0() {
if (cachedFloat64Memory0 === null || cachedFloat64Memory0.byteLength === 0) {
cachedFloat64Memory0 = new Float64Array(wasm.memory.buffer);
}
return cachedFloat64Memory0;
}
let cachedBigInt64Memory0 = null;
function getBigInt64Memory0() {
@@ -698,9 +698,6 @@ function handleError(f, args) {
const imports = {
__wbindgen_placeholder__: {
__wbindgen_object_drop_ref: function (arg0) {
takeObject(arg0);
},
__wbindgen_string_get: function (arg0, arg1) {
const obj = getObject(arg1);
const ret = typeof obj === "string" ? obj : undefined;
@@ -715,9 +712,8 @@ const imports = {
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
},
__wbindgen_error_new: function (arg0, arg1) {
const ret = new Error(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
__wbindgen_object_drop_ref: function (arg0) {
takeObject(arg0);
},
__wbindgen_boolean_get: function (arg0) {
const v = getObject(arg0);
@@ -755,7 +751,11 @@ const imports = {
const ret = BigInt.asUintN(64, arg0);
return addHeapObject(ret);
},
__wbg_eval_b7405fdd08ddef3d: function (arg0, arg1) {
__wbindgen_error_new: function (arg0, arg1) {
const ret = new Error(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
},
__wbg_eval_8243d86342a64a9b: function (arg0, arg1) {
const ret = eval(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
},
Binary file not shown.
+4 -4
View File
@@ -72,7 +72,7 @@
"windmill-parser-wasm-regex": "^1.397.2",
"windmill-parser-wasm-rust": "^1.397.2",
"windmill-parser-wasm-ts": "^1.397.2",
"windmill-parser-wasm-yaml": "^1.396.1",
"windmill-parser-wasm-yaml": "^1.398.1",
"windmill-sql-datatype-parser-wasm": "^1.318.0",
"y-monaco": "^0.1.4",
"y-websocket": "^1.5.4",
@@ -13525,9 +13525,9 @@
"integrity": "sha512-nspEC1zLfrvvh6ta8YSjKo99lDfFvsXh7rV2mmpTAmc2ZzzyuVDvW5Cm0Wmo8Ww3seO+oDi/v3jy+8QX8pIzwA=="
},
"node_modules/windmill-parser-wasm-yaml": {
"version": "1.396.1",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-yaml/-/windmill-parser-wasm-yaml-1.396.1.tgz",
"integrity": "sha512-ZRo5WIAwO21+ZweFPGe0Dx+PZbzJ9gd1qtAc9m0KeB7iNH1XY6lYKrmdQNaHJN5NqdwFiWTRgxIRgP62vvizBQ=="
"version": "1.398.1",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-yaml/-/windmill-parser-wasm-yaml-1.398.1.tgz",
"integrity": "sha512-f5MCJ9PVLrJQBcVuxnTlZNw561HIJkMqX/76HLgOqKehNpO7gboZyaHx0txUJP50MIDv/IMpn0SD6S7DZnynEg=="
},
"node_modules/windmill-sql-datatype-parser-wasm": {
"version": "1.318.0",
+2 -2
View File
@@ -145,7 +145,7 @@
"windmill-parser-wasm-regex": "^1.397.2",
"windmill-parser-wasm-rust": "^1.397.2",
"windmill-parser-wasm-ts": "^1.397.2",
"windmill-parser-wasm-yaml": "^1.396.1",
"windmill-parser-wasm-yaml": "^1.398.1",
"windmill-sql-datatype-parser-wasm": "^1.318.0",
"y-monaco": "^0.1.4",
"y-websocket": "^1.5.4",
@@ -459,4 +459,4 @@
"optionalDependencies": {
"fsevents": "^2.3.3"
}
}
}